Merge branch 'dict-list-assert-trunc' of https://github.com/mberkowitz03/pytest into dict-list-assert-trunc
This commit is contained in:
commit
bf911a168e
|
@ -26,7 +26,7 @@ jobs:
|
|||
persist-credentials: false
|
||||
|
||||
- name: Build and Check Package
|
||||
uses: hynek/build-and-inspect-python-package@v2.0.2
|
||||
uses: hynek/build-and-inspect-python-package@v2.4.0
|
||||
|
||||
deploy:
|
||||
if: github.repository == 'pytest-dev/pytest'
|
||||
|
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
days-before-issue-close: 7
|
||||
only-labels: "status: needs information"
|
||||
stale-issue-label: "stale"
|
||||
stale-issue-message: "This issue is stale because it has been open for 14 days with no activity."
|
||||
close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale."
|
||||
stale-issue-message: "This issue is stale because it has the `status: needs information` label and requested follow-up information was not provided for 14 days."
|
||||
close-issue-message: "This issue was closed because it has the `status: needs information` label and follow-up information has not been provided for 7 days since being marked as stale."
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
|
|
|
@ -35,7 +35,7 @@ jobs:
|
|||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Build and Check Package
|
||||
uses: hynek/build-and-inspect-python-package@v2.0.2
|
||||
uses: hynek/build-and-inspect-python-package@v2.4.0
|
||||
|
||||
build:
|
||||
needs: [package]
|
||||
|
|
|
@ -46,7 +46,7 @@ jobs:
|
|||
run: python scripts/update-plugin-list.py
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@70a41aba780001da0a30141984ae2a0c95d8704e
|
||||
uses: peter-evans/create-pull-request@c55203cfde3e5c11a452d352b4393e68b85b4533
|
||||
with:
|
||||
commit-message: '[automated] Update plugin list'
|
||||
author: 'pytest bot <pytestbot@users.noreply.github.com>'
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: "v0.3.4"
|
||||
rev: "v0.3.7"
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: ["--fix"]
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
|
|
2
AUTHORS
2
AUTHORS
|
@ -137,6 +137,7 @@ Endre Galaczi
|
|||
Eric Hunsberger
|
||||
Eric Liu
|
||||
Eric Siegerman
|
||||
Eric Yuan
|
||||
Erik Aronesty
|
||||
Erik Hasse
|
||||
Erik M. Bray
|
||||
|
@ -432,6 +433,7 @@ Xixi Zhao
|
|||
Xuan Luong
|
||||
Xuecong Liao
|
||||
Yannick Péroux
|
||||
Yao Xiao
|
||||
Yoav Caspi
|
||||
Yuliang Shao
|
||||
Yusuke Kadowaki
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Text is no longer truncated in the ``short test summary info`` section when ``-vv`` is given.
|
|
@ -0,0 +1 @@
|
|||
Improve namespace packages detection when :confval:`consider_namespace_packages` is enabled, covering more situations (like editable installs).
|
|
@ -0,0 +1 @@
|
|||
cache: create cache directory supporting files (``CACHEDIR.TAG``, ``.gitignore``, etc.) in a temporary directory to provide atomic semantics.
|
|
@ -224,6 +224,7 @@ place the objects you want to appear in the doctest namespace:
|
|||
.. code-block:: python
|
||||
|
||||
# content of conftest.py
|
||||
import pytest
|
||||
import numpy
|
||||
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1279,8 +1279,7 @@ passed multiple times. The expected format is ``name=value``. For example::
|
|||
Controls if pytest should attempt to identify `namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages>`__
|
||||
when collecting Python modules. Default is ``False``.
|
||||
|
||||
Set to ``True`` if you are testing namespace packages installed into a virtual environment and it is important for
|
||||
your packages to be imported using their full namespace package name.
|
||||
Set to ``True`` if the package you are testing is part of a namespace package.
|
||||
|
||||
Only `native namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#native-namespace-packages>`__
|
||||
are supported, with no plans to support `legacy namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#legacy-namespace-packages>`__.
|
||||
|
|
|
@ -7,6 +7,7 @@ import dataclasses
|
|||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
from typing import Dict
|
||||
from typing import final
|
||||
from typing import Generator
|
||||
|
@ -123,6 +124,10 @@ class Cache:
|
|||
stacklevel=3,
|
||||
)
|
||||
|
||||
def _mkdir(self, path: Path) -> None:
|
||||
self._ensure_cache_dir_and_supporting_files()
|
||||
path.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
def mkdir(self, name: str) -> Path:
|
||||
"""Return a directory path object with the given name.
|
||||
|
||||
|
@ -141,7 +146,7 @@ class Cache:
|
|||
if len(path.parts) > 1:
|
||||
raise ValueError("name is not allowed to contain path separators")
|
||||
res = self._cachedir.joinpath(self._CACHE_PREFIX_DIRS, path)
|
||||
res.mkdir(exist_ok=True, parents=True)
|
||||
self._mkdir(res)
|
||||
return res
|
||||
|
||||
def _getvaluepath(self, key: str) -> Path:
|
||||
|
@ -178,19 +183,13 @@ class Cache:
|
|||
"""
|
||||
path = self._getvaluepath(key)
|
||||
try:
|
||||
if path.parent.is_dir():
|
||||
cache_dir_exists_already = True
|
||||
else:
|
||||
cache_dir_exists_already = self._cachedir.exists()
|
||||
path.parent.mkdir(exist_ok=True, parents=True)
|
||||
self._mkdir(path.parent)
|
||||
except OSError as exc:
|
||||
self.warn(
|
||||
f"could not create cache path {path}: {exc}",
|
||||
_ispytest=True,
|
||||
)
|
||||
return
|
||||
if not cache_dir_exists_already:
|
||||
self._ensure_supporting_files()
|
||||
data = json.dumps(value, ensure_ascii=False, indent=2)
|
||||
try:
|
||||
f = path.open("w", encoding="UTF-8")
|
||||
|
@ -203,17 +202,32 @@ class Cache:
|
|||
with f:
|
||||
f.write(data)
|
||||
|
||||
def _ensure_supporting_files(self) -> None:
|
||||
"""Create supporting files in the cache dir that are not really part of the cache."""
|
||||
readme_path = self._cachedir / "README.md"
|
||||
readme_path.write_text(README_CONTENT, encoding="UTF-8")
|
||||
def _ensure_cache_dir_and_supporting_files(self) -> None:
|
||||
"""Create the cache dir and its supporting files."""
|
||||
if self._cachedir.is_dir():
|
||||
return
|
||||
|
||||
gitignore_path = self._cachedir.joinpath(".gitignore")
|
||||
msg = "# Created by pytest automatically.\n*\n"
|
||||
gitignore_path.write_text(msg, encoding="UTF-8")
|
||||
self._cachedir.parent.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory(
|
||||
prefix="pytest-cache-files-",
|
||||
dir=self._cachedir.parent,
|
||||
) as newpath:
|
||||
path = Path(newpath)
|
||||
with open(path.joinpath("README.md"), "xt", encoding="UTF-8") as f:
|
||||
f.write(README_CONTENT)
|
||||
with open(path.joinpath(".gitignore"), "xt", encoding="UTF-8") as f:
|
||||
f.write("# Created by pytest automatically.\n*\n")
|
||||
with open(path.joinpath("CACHEDIR.TAG"), "xb") as f:
|
||||
f.write(CACHEDIR_TAG_CONTENT)
|
||||
|
||||
cachedir_tag_path = self._cachedir.joinpath("CACHEDIR.TAG")
|
||||
cachedir_tag_path.write_bytes(CACHEDIR_TAG_CONTENT)
|
||||
path.rename(self._cachedir)
|
||||
# Create a directory in place of the one we just moved so that `TemporaryDirectory`'s
|
||||
# cleanup doesn't complain.
|
||||
#
|
||||
# TODO: pass ignore_cleanup_errors=True when we no longer support python < 3.10. See
|
||||
# https://github.com/python/cpython/issues/74168. Note that passing delete=False would
|
||||
# do the wrong thing in case of errors and isn't supported until python 3.12.
|
||||
path.mkdir()
|
||||
|
||||
|
||||
class LFPluginCollWrapper:
|
||||
|
|
|
@ -51,6 +51,7 @@ from _pytest.compat import NotSetType
|
|||
from _pytest.compat import safe_getattr
|
||||
from _pytest.config import _PluggyPlugin
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ExitCode
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.deprecated import check_ispytest
|
||||
from _pytest.deprecated import MARKED_FIXTURE
|
||||
|
@ -1365,6 +1366,33 @@ def pytest_addoption(parser: Parser) -> None:
|
|||
default=[],
|
||||
help="List of default fixtures to be used with this project",
|
||||
)
|
||||
group = parser.getgroup("general")
|
||||
group.addoption(
|
||||
"--fixtures",
|
||||
"--funcargs",
|
||||
action="store_true",
|
||||
dest="showfixtures",
|
||||
default=False,
|
||||
help="Show available fixtures, sorted by plugin appearance "
|
||||
"(fixtures with leading '_' are only shown with '-v')",
|
||||
)
|
||||
group.addoption(
|
||||
"--fixtures-per-test",
|
||||
action="store_true",
|
||||
dest="show_fixtures_per_test",
|
||||
default=False,
|
||||
help="Show fixtures per test",
|
||||
)
|
||||
|
||||
|
||||
def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
|
||||
if config.option.showfixtures:
|
||||
showfixtures(config)
|
||||
return 0
|
||||
if config.option.show_fixtures_per_test:
|
||||
show_fixtures_per_test(config)
|
||||
return 0
|
||||
return None
|
||||
|
||||
|
||||
def _get_direct_parametrize_args(node: nodes.Node) -> Set[str]:
|
||||
|
@ -1761,3 +1789,137 @@ class FixtureManager:
|
|||
for fixturedef in fixturedefs:
|
||||
if fixturedef.baseid in parentnodeids:
|
||||
yield fixturedef
|
||||
|
||||
|
||||
def show_fixtures_per_test(config: Config) -> Union[int, ExitCode]:
|
||||
from _pytest.main import wrap_session
|
||||
|
||||
return wrap_session(config, _show_fixtures_per_test)
|
||||
|
||||
|
||||
_PYTEST_DIR = Path(_pytest.__file__).parent
|
||||
|
||||
|
||||
def _pretty_fixture_path(invocation_dir: Path, func) -> str:
|
||||
loc = Path(getlocation(func, invocation_dir))
|
||||
prefix = Path("...", "_pytest")
|
||||
try:
|
||||
return str(prefix / loc.relative_to(_PYTEST_DIR))
|
||||
except ValueError:
|
||||
return bestrelpath(invocation_dir, loc)
|
||||
|
||||
|
||||
def _show_fixtures_per_test(config: Config, session: "Session") -> None:
|
||||
import _pytest.config
|
||||
|
||||
session.perform_collect()
|
||||
invocation_dir = config.invocation_params.dir
|
||||
tw = _pytest.config.create_terminal_writer(config)
|
||||
verbose = config.getvalue("verbose")
|
||||
|
||||
def get_best_relpath(func) -> str:
|
||||
loc = getlocation(func, invocation_dir)
|
||||
return bestrelpath(invocation_dir, Path(loc))
|
||||
|
||||
def write_fixture(fixture_def: FixtureDef[object]) -> None:
|
||||
argname = fixture_def.argname
|
||||
if verbose <= 0 and argname.startswith("_"):
|
||||
return
|
||||
prettypath = _pretty_fixture_path(invocation_dir, fixture_def.func)
|
||||
tw.write(f"{argname}", green=True)
|
||||
tw.write(f" -- {prettypath}", yellow=True)
|
||||
tw.write("\n")
|
||||
fixture_doc = inspect.getdoc(fixture_def.func)
|
||||
if fixture_doc:
|
||||
write_docstring(
|
||||
tw, fixture_doc.split("\n\n")[0] if verbose <= 0 else fixture_doc
|
||||
)
|
||||
else:
|
||||
tw.line(" no docstring available", red=True)
|
||||
|
||||
def write_item(item: nodes.Item) -> None:
|
||||
# Not all items have _fixtureinfo attribute.
|
||||
info: Optional[FuncFixtureInfo] = getattr(item, "_fixtureinfo", None)
|
||||
if info is None or not info.name2fixturedefs:
|
||||
# This test item does not use any fixtures.
|
||||
return
|
||||
tw.line()
|
||||
tw.sep("-", f"fixtures used by {item.name}")
|
||||
# TODO: Fix this type ignore.
|
||||
tw.sep("-", f"({get_best_relpath(item.function)})") # type: ignore[attr-defined]
|
||||
# dict key not used in loop but needed for sorting.
|
||||
for _, fixturedefs in sorted(info.name2fixturedefs.items()):
|
||||
assert fixturedefs is not None
|
||||
if not fixturedefs:
|
||||
continue
|
||||
# Last item is expected to be the one used by the test item.
|
||||
write_fixture(fixturedefs[-1])
|
||||
|
||||
for session_item in session.items:
|
||||
write_item(session_item)
|
||||
|
||||
|
||||
def showfixtures(config: Config) -> Union[int, ExitCode]:
|
||||
from _pytest.main import wrap_session
|
||||
|
||||
return wrap_session(config, _showfixtures_main)
|
||||
|
||||
|
||||
def _showfixtures_main(config: Config, session: "Session") -> None:
|
||||
import _pytest.config
|
||||
|
||||
session.perform_collect()
|
||||
invocation_dir = config.invocation_params.dir
|
||||
tw = _pytest.config.create_terminal_writer(config)
|
||||
verbose = config.getvalue("verbose")
|
||||
|
||||
fm = session._fixturemanager
|
||||
|
||||
available = []
|
||||
seen: Set[Tuple[str, str]] = set()
|
||||
|
||||
for argname, fixturedefs in fm._arg2fixturedefs.items():
|
||||
assert fixturedefs is not None
|
||||
if not fixturedefs:
|
||||
continue
|
||||
for fixturedef in fixturedefs:
|
||||
loc = getlocation(fixturedef.func, invocation_dir)
|
||||
if (fixturedef.argname, loc) in seen:
|
||||
continue
|
||||
seen.add((fixturedef.argname, loc))
|
||||
available.append(
|
||||
(
|
||||
len(fixturedef.baseid),
|
||||
fixturedef.func.__module__,
|
||||
_pretty_fixture_path(invocation_dir, fixturedef.func),
|
||||
fixturedef.argname,
|
||||
fixturedef,
|
||||
)
|
||||
)
|
||||
|
||||
available.sort()
|
||||
currentmodule = None
|
||||
for baseid, module, prettypath, argname, fixturedef in available:
|
||||
if currentmodule != module:
|
||||
if not module.startswith("_pytest."):
|
||||
tw.line()
|
||||
tw.sep("-", f"fixtures defined from {module}")
|
||||
currentmodule = module
|
||||
if verbose <= 0 and argname.startswith("_"):
|
||||
continue
|
||||
tw.write(f"{argname}", green=True)
|
||||
if fixturedef.scope != "function":
|
||||
tw.write(" [%s scope]" % fixturedef.scope, cyan=True)
|
||||
tw.write(f" -- {prettypath}", yellow=True)
|
||||
tw.write("\n")
|
||||
doc = inspect.getdoc(fixturedef.func)
|
||||
if doc:
|
||||
write_docstring(tw, doc.split("\n\n")[0] if verbose <= 0 else doc)
|
||||
else:
|
||||
tw.line(" no docstring available", red=True)
|
||||
tw.line()
|
||||
|
||||
|
||||
def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None:
|
||||
for line in doc.split("\n"):
|
||||
tw.line(indent + line)
|
||||
|
|
|
@ -8,6 +8,7 @@ from errno import ENOENT
|
|||
from errno import ENOTDIR
|
||||
import fnmatch
|
||||
from functools import partial
|
||||
from importlib.machinery import ModuleSpec
|
||||
import importlib.util
|
||||
import itertools
|
||||
import os
|
||||
|
@ -628,11 +629,13 @@ def _import_module_using_spec(
|
|||
# such as our own assertion-rewrite hook.
|
||||
for meta_importer in sys.meta_path:
|
||||
spec = meta_importer.find_spec(module_name, [str(module_location)])
|
||||
if spec is not None:
|
||||
if spec_matches_module_path(spec, module_path):
|
||||
break
|
||||
else:
|
||||
spec = importlib.util.spec_from_file_location(module_name, str(module_path))
|
||||
if spec is not None:
|
||||
|
||||
if spec_matches_module_path(spec, module_path):
|
||||
assert spec is not None
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
sys.modules[module_name] = mod
|
||||
spec.loader.exec_module(mod) # type: ignore[union-attr]
|
||||
|
@ -643,6 +646,16 @@ def _import_module_using_spec(
|
|||
return None
|
||||
|
||||
|
||||
def spec_matches_module_path(
|
||||
module_spec: Optional[ModuleSpec], module_path: Path
|
||||
) -> bool:
|
||||
"""Return true if the given ModuleSpec can be used to import the given module path."""
|
||||
if module_spec is None or module_spec.origin is None:
|
||||
return False
|
||||
|
||||
return Path(module_spec.origin) == module_path
|
||||
|
||||
|
||||
# Implement a special _is_same function on Windows which returns True if the two filenames
|
||||
# compare equal, to circumvent os.path.samefile returning False for mounts in UNC (#7678).
|
||||
if sys.platform.startswith("win"):
|
||||
|
@ -762,39 +775,79 @@ def resolve_pkg_root_and_module_name(
|
|||
Passing the full path to `models.py` will yield Path("src") and "app.core.models".
|
||||
|
||||
If consider_namespace_packages is True, then we additionally check upwards in the hierarchy
|
||||
until we find a directory that is reachable from sys.path, which marks it as a namespace package:
|
||||
for namespace packages:
|
||||
|
||||
https://packaging.python.org/en/latest/guides/packaging-namespace-packages
|
||||
|
||||
Raises CouldNotResolvePathError if the given path does not belong to a package (missing any __init__.py files).
|
||||
"""
|
||||
pkg_root: Optional[Path] = None
|
||||
pkg_path = resolve_package_path(path)
|
||||
if pkg_path is not None:
|
||||
pkg_root = pkg_path.parent
|
||||
# https://packaging.python.org/en/latest/guides/packaging-namespace-packages/
|
||||
if consider_namespace_packages:
|
||||
# Go upwards in the hierarchy, if we find a parent path included
|
||||
# in sys.path, it means the package found by resolve_package_path()
|
||||
# actually belongs to a namespace package.
|
||||
for parent in pkg_root.parents:
|
||||
# If any of the parent paths has a __init__.py, it means it is not
|
||||
# a namespace package (see the docs linked above).
|
||||
if (parent / "__init__.py").is_file():
|
||||
break
|
||||
if str(parent) in sys.path:
|
||||
# Point the pkg_root to the root of the namespace package.
|
||||
pkg_root = parent
|
||||
break
|
||||
if consider_namespace_packages:
|
||||
start = pkg_root if pkg_root is not None else path.parent
|
||||
for candidate in (start, *start.parents):
|
||||
module_name = compute_module_name(candidate, path)
|
||||
if module_name and is_importable(module_name, path):
|
||||
# Point the pkg_root to the root of the namespace package.
|
||||
pkg_root = candidate
|
||||
break
|
||||
|
||||
names = list(path.with_suffix("").relative_to(pkg_root).parts)
|
||||
if names[-1] == "__init__":
|
||||
names.pop()
|
||||
module_name = ".".join(names)
|
||||
return pkg_root, module_name
|
||||
if pkg_root is not None:
|
||||
module_name = compute_module_name(pkg_root, path)
|
||||
if module_name:
|
||||
return pkg_root, module_name
|
||||
|
||||
raise CouldNotResolvePathError(f"Could not resolve for {path}")
|
||||
|
||||
|
||||
def is_importable(module_name: str, module_path: Path) -> bool:
|
||||
"""
|
||||
Return if the given module path could be imported normally by Python, akin to the user
|
||||
entering the REPL and importing the corresponding module name directly, and corresponds
|
||||
to the module_path specified.
|
||||
|
||||
:param module_name:
|
||||
Full module name that we want to check if is importable.
|
||||
For example, "app.models".
|
||||
|
||||
:param module_path:
|
||||
Full path to the python module/package we want to check if is importable.
|
||||
For example, "/projects/src/app/models.py".
|
||||
"""
|
||||
try:
|
||||
# Note this is different from what we do in ``_import_module_using_spec``, where we explicitly search through
|
||||
# sys.meta_path to be able to pass the path of the module that we want to import (``meta_importer.find_spec``).
|
||||
# Using importlib.util.find_spec() is different, it gives the same results as trying to import
|
||||
# the module normally in the REPL.
|
||||
spec = importlib.util.find_spec(module_name)
|
||||
except (ImportError, ValueError, ImportWarning):
|
||||
return False
|
||||
else:
|
||||
return spec_matches_module_path(spec, module_path)
|
||||
|
||||
|
||||
def compute_module_name(root: Path, module_path: Path) -> Optional[str]:
|
||||
"""Compute a module name based on a path and a root anchor."""
|
||||
try:
|
||||
path_without_suffix = module_path.with_suffix("")
|
||||
except ValueError:
|
||||
# Empty paths (such as Path.cwd()) might break meta_path hooks (like our own assertion rewriter).
|
||||
return None
|
||||
|
||||
try:
|
||||
relative = path_without_suffix.relative_to(root)
|
||||
except ValueError: # pragma: no cover
|
||||
return None
|
||||
names = list(relative.parts)
|
||||
if not names:
|
||||
return None
|
||||
if names[-1] == "__init__":
|
||||
names.pop()
|
||||
return ".".join(names)
|
||||
|
||||
|
||||
class CouldNotResolvePathError(Exception):
|
||||
"""Custom exception raised by resolve_pkg_root_and_module_name."""
|
||||
|
||||
|
|
|
@ -40,13 +40,11 @@ from _pytest._code import getfslineno
|
|||
from _pytest._code.code import ExceptionInfo
|
||||
from _pytest._code.code import TerminalRepr
|
||||
from _pytest._code.code import Traceback
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest._io.saferepr import saferepr
|
||||
from _pytest.compat import ascii_escaped
|
||||
from _pytest.compat import get_default_arg_names
|
||||
from _pytest.compat import get_real_func
|
||||
from _pytest.compat import getimfunc
|
||||
from _pytest.compat import getlocation
|
||||
from _pytest.compat import is_async_function
|
||||
from _pytest.compat import is_generator
|
||||
from _pytest.compat import LEGACY_PATH
|
||||
|
@ -54,7 +52,6 @@ from _pytest.compat import NOTSET
|
|||
from _pytest.compat import safe_getattr
|
||||
from _pytest.compat import safe_isclass
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ExitCode
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.deprecated import check_ispytest
|
||||
|
@ -71,7 +68,6 @@ from _pytest.mark.structures import MarkDecorator
|
|||
from _pytest.mark.structures import normalize_mark_list
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.outcomes import skip
|
||||
from _pytest.pathlib import bestrelpath
|
||||
from _pytest.pathlib import fnmatch_ex
|
||||
from _pytest.pathlib import import_path
|
||||
from _pytest.pathlib import ImportPathMismatchError
|
||||
|
@ -88,27 +84,7 @@ if TYPE_CHECKING:
|
|||
from typing import Self
|
||||
|
||||
|
||||
_PYTEST_DIR = Path(_pytest.__file__).parent
|
||||
|
||||
|
||||
def pytest_addoption(parser: Parser) -> None:
|
||||
group = parser.getgroup("general")
|
||||
group.addoption(
|
||||
"--fixtures",
|
||||
"--funcargs",
|
||||
action="store_true",
|
||||
dest="showfixtures",
|
||||
default=False,
|
||||
help="Show available fixtures, sorted by plugin appearance "
|
||||
"(fixtures with leading '_' are only shown with '-v')",
|
||||
)
|
||||
group.addoption(
|
||||
"--fixtures-per-test",
|
||||
action="store_true",
|
||||
dest="show_fixtures_per_test",
|
||||
default=False,
|
||||
help="Show fixtures per test",
|
||||
)
|
||||
parser.addini(
|
||||
"python_files",
|
||||
type="args",
|
||||
|
@ -137,16 +113,6 @@ def pytest_addoption(parser: Parser) -> None:
|
|||
)
|
||||
|
||||
|
||||
def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
|
||||
if config.option.showfixtures:
|
||||
showfixtures(config)
|
||||
return 0
|
||||
if config.option.show_fixtures_per_test:
|
||||
show_fixtures_per_test(config)
|
||||
return 0
|
||||
return None
|
||||
|
||||
|
||||
def pytest_generate_tests(metafunc: "Metafunc") -> None:
|
||||
for marker in metafunc.definition.iter_markers(name="parametrize"):
|
||||
metafunc.parametrize(*marker.args, **marker.kwargs, _param_mark=marker)
|
||||
|
@ -1525,137 +1491,6 @@ def _ascii_escaped_by_config(val: Union[str, bytes], config: Optional[Config]) -
|
|||
return val if escape_option else ascii_escaped(val) # type: ignore
|
||||
|
||||
|
||||
def _pretty_fixture_path(invocation_dir: Path, func) -> str:
|
||||
loc = Path(getlocation(func, invocation_dir))
|
||||
prefix = Path("...", "_pytest")
|
||||
try:
|
||||
return str(prefix / loc.relative_to(_PYTEST_DIR))
|
||||
except ValueError:
|
||||
return bestrelpath(invocation_dir, loc)
|
||||
|
||||
|
||||
def show_fixtures_per_test(config):
|
||||
from _pytest.main import wrap_session
|
||||
|
||||
return wrap_session(config, _show_fixtures_per_test)
|
||||
|
||||
|
||||
def _show_fixtures_per_test(config: Config, session: Session) -> None:
|
||||
import _pytest.config
|
||||
|
||||
session.perform_collect()
|
||||
invocation_dir = config.invocation_params.dir
|
||||
tw = _pytest.config.create_terminal_writer(config)
|
||||
verbose = config.getvalue("verbose")
|
||||
|
||||
def get_best_relpath(func) -> str:
|
||||
loc = getlocation(func, invocation_dir)
|
||||
return bestrelpath(invocation_dir, Path(loc))
|
||||
|
||||
def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None:
|
||||
argname = fixture_def.argname
|
||||
if verbose <= 0 and argname.startswith("_"):
|
||||
return
|
||||
prettypath = _pretty_fixture_path(invocation_dir, fixture_def.func)
|
||||
tw.write(f"{argname}", green=True)
|
||||
tw.write(f" -- {prettypath}", yellow=True)
|
||||
tw.write("\n")
|
||||
fixture_doc = inspect.getdoc(fixture_def.func)
|
||||
if fixture_doc:
|
||||
write_docstring(
|
||||
tw, fixture_doc.split("\n\n")[0] if verbose <= 0 else fixture_doc
|
||||
)
|
||||
else:
|
||||
tw.line(" no docstring available", red=True)
|
||||
|
||||
def write_item(item: nodes.Item) -> None:
|
||||
# Not all items have _fixtureinfo attribute.
|
||||
info: Optional[FuncFixtureInfo] = getattr(item, "_fixtureinfo", None)
|
||||
if info is None or not info.name2fixturedefs:
|
||||
# This test item does not use any fixtures.
|
||||
return
|
||||
tw.line()
|
||||
tw.sep("-", f"fixtures used by {item.name}")
|
||||
# TODO: Fix this type ignore.
|
||||
tw.sep("-", f"({get_best_relpath(item.function)})") # type: ignore[attr-defined]
|
||||
# dict key not used in loop but needed for sorting.
|
||||
for _, fixturedefs in sorted(info.name2fixturedefs.items()):
|
||||
assert fixturedefs is not None
|
||||
if not fixturedefs:
|
||||
continue
|
||||
# Last item is expected to be the one used by the test item.
|
||||
write_fixture(fixturedefs[-1])
|
||||
|
||||
for session_item in session.items:
|
||||
write_item(session_item)
|
||||
|
||||
|
||||
def showfixtures(config: Config) -> Union[int, ExitCode]:
|
||||
from _pytest.main import wrap_session
|
||||
|
||||
return wrap_session(config, _showfixtures_main)
|
||||
|
||||
|
||||
def _showfixtures_main(config: Config, session: Session) -> None:
|
||||
import _pytest.config
|
||||
|
||||
session.perform_collect()
|
||||
invocation_dir = config.invocation_params.dir
|
||||
tw = _pytest.config.create_terminal_writer(config)
|
||||
verbose = config.getvalue("verbose")
|
||||
|
||||
fm = session._fixturemanager
|
||||
|
||||
available = []
|
||||
seen: Set[Tuple[str, str]] = set()
|
||||
|
||||
for argname, fixturedefs in fm._arg2fixturedefs.items():
|
||||
assert fixturedefs is not None
|
||||
if not fixturedefs:
|
||||
continue
|
||||
for fixturedef in fixturedefs:
|
||||
loc = getlocation(fixturedef.func, invocation_dir)
|
||||
if (fixturedef.argname, loc) in seen:
|
||||
continue
|
||||
seen.add((fixturedef.argname, loc))
|
||||
available.append(
|
||||
(
|
||||
len(fixturedef.baseid),
|
||||
fixturedef.func.__module__,
|
||||
_pretty_fixture_path(invocation_dir, fixturedef.func),
|
||||
fixturedef.argname,
|
||||
fixturedef,
|
||||
)
|
||||
)
|
||||
|
||||
available.sort()
|
||||
currentmodule = None
|
||||
for baseid, module, prettypath, argname, fixturedef in available:
|
||||
if currentmodule != module:
|
||||
if not module.startswith("_pytest."):
|
||||
tw.line()
|
||||
tw.sep("-", f"fixtures defined from {module}")
|
||||
currentmodule = module
|
||||
if verbose <= 0 and argname.startswith("_"):
|
||||
continue
|
||||
tw.write(f"{argname}", green=True)
|
||||
if fixturedef.scope != "function":
|
||||
tw.write(" [%s scope]" % fixturedef.scope, cyan=True)
|
||||
tw.write(f" -- {prettypath}", yellow=True)
|
||||
tw.write("\n")
|
||||
doc = inspect.getdoc(fixturedef.func)
|
||||
if doc:
|
||||
write_docstring(tw, doc.split("\n\n")[0] if verbose <= 0 else doc)
|
||||
else:
|
||||
tw.line(" no docstring available", red=True)
|
||||
tw.line()
|
||||
|
||||
|
||||
def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None:
|
||||
for line in doc.split("\n"):
|
||||
tw.line(indent + line)
|
||||
|
||||
|
||||
class Function(PyobjMixin, nodes.Item):
|
||||
"""Item responsible for setting up and executing a Python test function.
|
||||
|
||||
|
|
|
@ -1408,11 +1408,11 @@ def _get_line_with_reprcrash_message(
|
|||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
if not running_on_ci():
|
||||
if running_on_ci() or config.option.verbose >= 2:
|
||||
msg = f" - {msg}"
|
||||
else:
|
||||
available_width = tw.fullwidth - line_width
|
||||
msg = _format_trimmed(" - {}", msg, available_width)
|
||||
else:
|
||||
msg = f" - {msg}"
|
||||
if msg is not None:
|
||||
line += msg
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
anyio[curio,trio]==4.3.0
|
||||
django==5.0.3
|
||||
django==5.0.4
|
||||
pytest-asyncio==0.23.6
|
||||
# Temporarily not installed until pytest-bdd is fixed:
|
||||
# https://github.com/pytest-dev/pytest/pull/11785
|
||||
# pytest-bdd==7.0.1
|
||||
pytest-cov==4.1.0
|
||||
pytest-cov==5.0.0
|
||||
pytest-django==4.8.0
|
||||
pytest-flakes==4.0.5
|
||||
pytest-html==4.1.1
|
||||
|
|
|
@ -1731,8 +1731,8 @@ class TestEarlyRewriteBailout:
|
|||
import os
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
os.chdir(d)
|
||||
with tempfile.TemporaryDirectory() as newpath:
|
||||
os.chdir(newpath)
|
||||
""",
|
||||
"test_test.py": """\
|
||||
def test():
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
# mypy: allow-untyped-defs
|
||||
from enum import auto
|
||||
from enum import Enum
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from typing import Any
|
||||
from typing import Generator
|
||||
from typing import List
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
|
||||
from _pytest.compat import assert_never
|
||||
from _pytest.config import ExitCode
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
from _pytest.pytester import Pytester
|
||||
|
@ -175,7 +180,9 @@ class TestNewAPI:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("env", ((), ("TOX_ENV_DIR", "/tox_env_dir")))
|
||||
def test_cache_reportheader(env, pytester: Pytester, monkeypatch: MonkeyPatch) -> None:
|
||||
def test_cache_reportheader(
|
||||
env: Sequence[str], pytester: Pytester, monkeypatch: MonkeyPatch
|
||||
) -> None:
|
||||
pytester.makepyfile("""def test_foo(): pass""")
|
||||
if env:
|
||||
monkeypatch.setenv(*env)
|
||||
|
@ -507,7 +514,7 @@ class TestLastFailed:
|
|||
"""
|
||||
)
|
||||
|
||||
def rlf(fail_import, fail_run):
|
||||
def rlf(fail_import: int, fail_run: int) -> Any:
|
||||
monkeypatch.setenv("FAILIMPORT", str(fail_import))
|
||||
monkeypatch.setenv("FAILTEST", str(fail_run))
|
||||
|
||||
|
@ -555,7 +562,9 @@ class TestLastFailed:
|
|||
"""
|
||||
)
|
||||
|
||||
def rlf(fail_import, fail_run, args=()):
|
||||
def rlf(
|
||||
fail_import: int, fail_run: int, args: Sequence[str] = ()
|
||||
) -> Tuple[Any, Any]:
|
||||
monkeypatch.setenv("FAILIMPORT", str(fail_import))
|
||||
monkeypatch.setenv("FAILTEST", str(fail_run))
|
||||
|
||||
|
@ -1254,20 +1263,41 @@ class TestReadme:
|
|||
assert self.check_readme(pytester) is True
|
||||
|
||||
|
||||
def test_gitignore(pytester: Pytester) -> None:
|
||||
class Action(Enum):
|
||||
"""Action to perform on the cache directory."""
|
||||
|
||||
MKDIR = auto()
|
||||
SET = auto()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("action", list(Action))
|
||||
def test_gitignore(
|
||||
pytester: Pytester,
|
||||
action: Action,
|
||||
) -> None:
|
||||
"""Ensure we automatically create .gitignore file in the pytest_cache directory (#3286)."""
|
||||
from _pytest.cacheprovider import Cache
|
||||
|
||||
config = pytester.parseconfig()
|
||||
cache = Cache.for_config(config, _ispytest=True)
|
||||
cache.set("foo", "bar")
|
||||
if action == Action.MKDIR:
|
||||
cache.mkdir("foo")
|
||||
elif action == Action.SET:
|
||||
cache.set("foo", "bar")
|
||||
else:
|
||||
assert_never(action)
|
||||
msg = "# Created by pytest automatically.\n*\n"
|
||||
gitignore_path = cache._cachedir.joinpath(".gitignore")
|
||||
assert gitignore_path.read_text(encoding="UTF-8") == msg
|
||||
|
||||
# Does not overwrite existing/custom one.
|
||||
gitignore_path.write_text("custom", encoding="utf-8")
|
||||
cache.set("something", "else")
|
||||
if action == Action.MKDIR:
|
||||
cache.mkdir("something")
|
||||
elif action == Action.SET:
|
||||
cache.set("something", "else")
|
||||
else:
|
||||
assert_never(action)
|
||||
assert gitignore_path.read_text(encoding="UTF-8") == "custom"
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# mypy: allow-untyped-defs
|
||||
import errno
|
||||
import importlib.abc
|
||||
import importlib.machinery
|
||||
import os.path
|
||||
from pathlib import Path
|
||||
import pickle
|
||||
|
@ -10,12 +12,15 @@ from types import ModuleType
|
|||
from typing import Any
|
||||
from typing import Generator
|
||||
from typing import Iterator
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
import unittest.mock
|
||||
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
from _pytest.pathlib import bestrelpath
|
||||
from _pytest.pathlib import commonpath
|
||||
from _pytest.pathlib import compute_module_name
|
||||
from _pytest.pathlib import CouldNotResolvePathError
|
||||
from _pytest.pathlib import ensure_deletable
|
||||
from _pytest.pathlib import fnmatch_ex
|
||||
|
@ -25,6 +30,7 @@ from _pytest.pathlib import import_path
|
|||
from _pytest.pathlib import ImportMode
|
||||
from _pytest.pathlib import ImportPathMismatchError
|
||||
from _pytest.pathlib import insert_missing_modules
|
||||
from _pytest.pathlib import is_importable
|
||||
from _pytest.pathlib import maybe_delete_a_numbered_dir
|
||||
from _pytest.pathlib import module_name_from_path
|
||||
from _pytest.pathlib import resolve_package_path
|
||||
|
@ -33,6 +39,7 @@ from _pytest.pathlib import safe_exists
|
|||
from _pytest.pathlib import symlink_or_skip
|
||||
from _pytest.pathlib import visit
|
||||
from _pytest.pytester import Pytester
|
||||
from _pytest.pytester import RunResult
|
||||
from _pytest.tmpdir import TempPathFactory
|
||||
import pytest
|
||||
|
||||
|
@ -717,12 +724,13 @@ class TestImportLibMode:
|
|||
assert result == "_env_310.tests.test_foo"
|
||||
|
||||
def test_resolve_pkg_root_and_module_name(
|
||||
self, tmp_path: Path, monkeypatch: MonkeyPatch
|
||||
self, tmp_path: Path, monkeypatch: MonkeyPatch, pytester: Pytester
|
||||
) -> None:
|
||||
# Create a directory structure first without __init__.py files.
|
||||
(tmp_path / "src/app/core").mkdir(parents=True)
|
||||
models_py = tmp_path / "src/app/core/models.py"
|
||||
models_py.touch()
|
||||
|
||||
with pytest.raises(CouldNotResolvePathError):
|
||||
_ = resolve_pkg_root_and_module_name(models_py)
|
||||
|
||||
|
@ -738,6 +746,8 @@ class TestImportLibMode:
|
|||
|
||||
# If we add tmp_path to sys.path, src becomes a namespace package.
|
||||
monkeypatch.syspath_prepend(tmp_path)
|
||||
validate_namespace_package(pytester, [tmp_path], ["src.app.core.models"])
|
||||
|
||||
assert resolve_pkg_root_and_module_name(
|
||||
models_py, consider_namespace_packages=True
|
||||
) == (
|
||||
|
@ -1119,39 +1129,56 @@ def test_safe_exists(tmp_path: Path) -> None:
|
|||
class TestNamespacePackages:
|
||||
"""Test import_path support when importing from properly namespace packages."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_imports_tracking(self, monkeypatch: MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(sys, "pytest_namespace_packages_test", [], raising=False)
|
||||
|
||||
def setup_directories(
|
||||
self, tmp_path: Path, monkeypatch: MonkeyPatch, pytester: Pytester
|
||||
self, tmp_path: Path, monkeypatch: Optional[MonkeyPatch], pytester: Pytester
|
||||
) -> Tuple[Path, Path]:
|
||||
# Use a code to guard against modules being imported more than once.
|
||||
# This is a safeguard in case future changes break this invariant.
|
||||
code = dedent(
|
||||
"""
|
||||
import sys
|
||||
imported = getattr(sys, "pytest_namespace_packages_test", [])
|
||||
assert __name__ not in imported, f"{__name__} already imported"
|
||||
imported.append(__name__)
|
||||
sys.pytest_namespace_packages_test = imported
|
||||
"""
|
||||
)
|
||||
|
||||
# Set up a namespace package "com.company", containing
|
||||
# two subpackages, "app" and "calc".
|
||||
(tmp_path / "src/dist1/com/company/app/core").mkdir(parents=True)
|
||||
(tmp_path / "src/dist1/com/company/app/__init__.py").touch()
|
||||
(tmp_path / "src/dist1/com/company/app/core/__init__.py").touch()
|
||||
(tmp_path / "src/dist1/com/company/app/__init__.py").write_text(
|
||||
code, encoding="UTF-8"
|
||||
)
|
||||
(tmp_path / "src/dist1/com/company/app/core/__init__.py").write_text(
|
||||
code, encoding="UTF-8"
|
||||
)
|
||||
models_py = tmp_path / "src/dist1/com/company/app/core/models.py"
|
||||
models_py.touch()
|
||||
|
||||
(tmp_path / "src/dist2/com/company/calc/algo").mkdir(parents=True)
|
||||
(tmp_path / "src/dist2/com/company/calc/__init__.py").touch()
|
||||
(tmp_path / "src/dist2/com/company/calc/algo/__init__.py").touch()
|
||||
(tmp_path / "src/dist2/com/company/calc/__init__.py").write_text(
|
||||
code, encoding="UTF-8"
|
||||
)
|
||||
(tmp_path / "src/dist2/com/company/calc/algo/__init__.py").write_text(
|
||||
code, encoding="UTF-8"
|
||||
)
|
||||
algorithms_py = tmp_path / "src/dist2/com/company/calc/algo/algorithms.py"
|
||||
algorithms_py.touch()
|
||||
algorithms_py.write_text(code, encoding="UTF-8")
|
||||
|
||||
# Validate the namespace package by importing it in a Python subprocess.
|
||||
r = pytester.runpython_c(
|
||||
dedent(
|
||||
f"""
|
||||
import sys
|
||||
sys.path.append(r{str(tmp_path / "src/dist1")!r})
|
||||
sys.path.append(r{str(tmp_path / "src/dist2")!r})
|
||||
import com.company.app.core.models
|
||||
import com.company.calc.algo.algorithms
|
||||
"""
|
||||
)
|
||||
r = validate_namespace_package(
|
||||
pytester,
|
||||
[tmp_path / "src/dist1", tmp_path / "src/dist2"],
|
||||
["com.company.app.core.models", "com.company.calc.algo.algorithms"],
|
||||
)
|
||||
assert r.ret == 0
|
||||
|
||||
monkeypatch.syspath_prepend(tmp_path / "src/dist1")
|
||||
monkeypatch.syspath_prepend(tmp_path / "src/dist2")
|
||||
if monkeypatch is not None:
|
||||
monkeypatch.syspath_prepend(tmp_path / "src/dist1")
|
||||
monkeypatch.syspath_prepend(tmp_path / "src/dist2")
|
||||
return models_py, algorithms_py
|
||||
|
||||
@pytest.mark.parametrize("import_mode", ["prepend", "append", "importlib"])
|
||||
|
@ -1223,11 +1250,76 @@ class TestNamespacePackages:
|
|||
models_py, algorithms_py = self.setup_directories(
|
||||
tmp_path, monkeypatch, pytester
|
||||
)
|
||||
# Namespace packages must not have an __init__.py at any of its
|
||||
# directories; if it does, we then fall back to importing just the
|
||||
# part of the package containing the __init__.py files.
|
||||
# Namespace packages must not have an __init__.py at its top-level
|
||||
# directory; if it does, it is no longer a namespace package, and we fall back
|
||||
# to importing just the part of the package containing the __init__.py files.
|
||||
(tmp_path / "src/dist1/com/__init__.py").touch()
|
||||
|
||||
# Because of the __init__ file, 'com' is no longer a namespace package:
|
||||
# 'com.company.app' is importable as a normal module.
|
||||
# 'com.company.calc' is no longer importable because 'com' is not a namespace package anymore.
|
||||
r = validate_namespace_package(
|
||||
pytester,
|
||||
[tmp_path / "src/dist1", tmp_path / "src/dist2"],
|
||||
["com.company.app.core.models", "com.company.calc.algo.algorithms"],
|
||||
)
|
||||
assert r.ret == 1
|
||||
r.stderr.fnmatch_lines("*No module named 'com.company.calc*")
|
||||
|
||||
pkg_root, module_name = resolve_pkg_root_and_module_name(
|
||||
models_py, consider_namespace_packages=True
|
||||
)
|
||||
assert (pkg_root, module_name) == (
|
||||
tmp_path / "src/dist1",
|
||||
"com.company.app.core.models",
|
||||
)
|
||||
|
||||
# dist2/com/company will contain a normal Python package.
|
||||
pkg_root, module_name = resolve_pkg_root_and_module_name(
|
||||
algorithms_py, consider_namespace_packages=True
|
||||
)
|
||||
assert (pkg_root, module_name) == (
|
||||
tmp_path / "src/dist2/com/company",
|
||||
"calc.algo.algorithms",
|
||||
)
|
||||
|
||||
def test_detect_meta_path(
|
||||
self,
|
||||
tmp_path: Path,
|
||||
monkeypatch: MonkeyPatch,
|
||||
pytester: Pytester,
|
||||
) -> None:
|
||||
"""
|
||||
resolve_pkg_root_and_module_name() considers sys.meta_path when importing namespace packages.
|
||||
|
||||
Regression test for #12112.
|
||||
"""
|
||||
|
||||
class CustomImporter(importlib.abc.MetaPathFinder):
|
||||
"""
|
||||
Imports the module name "com" as a namespace package.
|
||||
|
||||
This ensures our namespace detection considers sys.meta_path, which is important
|
||||
to support all possible ways a module can be imported (for example editable installs).
|
||||
"""
|
||||
|
||||
def find_spec(
|
||||
self, name: str, path: Any = None, target: Any = None
|
||||
) -> Optional[importlib.machinery.ModuleSpec]:
|
||||
if name == "com":
|
||||
spec = importlib.machinery.ModuleSpec("com", loader=None)
|
||||
spec.submodule_search_locations = [str(com_root_2), str(com_root_1)]
|
||||
return spec
|
||||
return None
|
||||
|
||||
# Setup directories without configuring sys.path.
|
||||
models_py, algorithms_py = self.setup_directories(
|
||||
tmp_path, monkeypatch=None, pytester=pytester
|
||||
)
|
||||
com_root_1 = tmp_path / "src/dist1/com"
|
||||
com_root_2 = tmp_path / "src/dist2/com"
|
||||
|
||||
# Because the namespace package is not setup correctly, we cannot resolve it as a namespace package.
|
||||
pkg_root, module_name = resolve_pkg_root_and_module_name(
|
||||
models_py, consider_namespace_packages=True
|
||||
)
|
||||
|
@ -1235,3 +1327,107 @@ class TestNamespacePackages:
|
|||
tmp_path / "src/dist1/com/company",
|
||||
"app.core.models",
|
||||
)
|
||||
|
||||
# Insert our custom importer, which will recognize the "com" directory as a namespace package.
|
||||
new_meta_path = [CustomImporter(), *sys.meta_path]
|
||||
monkeypatch.setattr(sys, "meta_path", new_meta_path)
|
||||
|
||||
# Now we should be able to resolve the path as namespace package.
|
||||
pkg_root, module_name = resolve_pkg_root_and_module_name(
|
||||
models_py, consider_namespace_packages=True
|
||||
)
|
||||
assert (pkg_root, module_name) == (
|
||||
tmp_path / "src/dist1",
|
||||
"com.company.app.core.models",
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("insert", [True, False])
|
||||
def test_full_ns_packages_without_init_files(
|
||||
self, pytester: Pytester, tmp_path: Path, monkeypatch: MonkeyPatch, insert: bool
|
||||
) -> None:
|
||||
(tmp_path / "src/dist1/ns/b/app/bar/test").mkdir(parents=True)
|
||||
(tmp_path / "src/dist1/ns/b/app/bar/m.py").touch()
|
||||
|
||||
if insert:
|
||||
# The presence of this __init__.py is not a problem, ns.b.app is still part of the namespace package.
|
||||
(tmp_path / "src/dist1/ns/b/app/__init__.py").touch()
|
||||
|
||||
(tmp_path / "src/dist2/ns/a/core/foo/test").mkdir(parents=True)
|
||||
(tmp_path / "src/dist2/ns/a/core/foo/m.py").touch()
|
||||
|
||||
# Validate the namespace package by importing it in a Python subprocess.
|
||||
r = validate_namespace_package(
|
||||
pytester,
|
||||
[tmp_path / "src/dist1", tmp_path / "src/dist2"],
|
||||
["ns.b.app.bar.m", "ns.a.core.foo.m"],
|
||||
)
|
||||
assert r.ret == 0
|
||||
monkeypatch.syspath_prepend(tmp_path / "src/dist1")
|
||||
monkeypatch.syspath_prepend(tmp_path / "src/dist2")
|
||||
|
||||
assert resolve_pkg_root_and_module_name(
|
||||
tmp_path / "src/dist1/ns/b/app/bar/m.py", consider_namespace_packages=True
|
||||
) == (tmp_path / "src/dist1", "ns.b.app.bar.m")
|
||||
assert resolve_pkg_root_and_module_name(
|
||||
tmp_path / "src/dist2/ns/a/core/foo/m.py", consider_namespace_packages=True
|
||||
) == (tmp_path / "src/dist2", "ns.a.core.foo.m")
|
||||
|
||||
|
||||
def test_is_importable(pytester: Pytester) -> None:
|
||||
pytester.syspathinsert()
|
||||
|
||||
path = pytester.path / "bar/foo.py"
|
||||
path.parent.mkdir()
|
||||
path.touch()
|
||||
assert is_importable("bar.foo", path) is True
|
||||
|
||||
# Ensure that the module that can be imported points to the path we expect.
|
||||
path = pytester.path / "some/other/path/bar/foo.py"
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
assert is_importable("bar.foo", path) is False
|
||||
|
||||
# Paths containing "." cannot be imported.
|
||||
path = pytester.path / "bar.x/__init__.py"
|
||||
path.parent.mkdir()
|
||||
path.touch()
|
||||
assert is_importable("bar.x", path) is False
|
||||
|
||||
# Pass starting with "." denote relative imports and cannot be checked using is_importable.
|
||||
path = pytester.path / ".bar.x/__init__.py"
|
||||
path.parent.mkdir()
|
||||
path.touch()
|
||||
assert is_importable(".bar.x", path) is False
|
||||
|
||||
|
||||
def test_compute_module_name(tmp_path: Path) -> None:
|
||||
assert compute_module_name(tmp_path, tmp_path) is None
|
||||
assert compute_module_name(Path(), Path()) is None
|
||||
|
||||
assert compute_module_name(tmp_path, tmp_path / "mod.py") == "mod"
|
||||
assert compute_module_name(tmp_path, tmp_path / "src/app/bar") == "src.app.bar"
|
||||
assert compute_module_name(tmp_path, tmp_path / "src/app/bar.py") == "src.app.bar"
|
||||
assert (
|
||||
compute_module_name(tmp_path, tmp_path / "src/app/bar/__init__.py")
|
||||
== "src.app.bar"
|
||||
)
|
||||
|
||||
|
||||
def validate_namespace_package(
|
||||
pytester: Pytester, paths: Sequence[Path], modules: Sequence[str]
|
||||
) -> RunResult:
|
||||
"""
|
||||
Validate that a Python namespace package is set up correctly.
|
||||
|
||||
In a sub interpreter, add 'paths' to sys.path and attempt to import the given modules.
|
||||
|
||||
In this module many tests configure a set of files as a namespace package, this function
|
||||
is used as sanity check that our files are configured correctly from the point of view of Python.
|
||||
"""
|
||||
lines = [
|
||||
"import sys",
|
||||
# Configure sys.path.
|
||||
*[f"sys.path.append(r{str(x)!r})" for x in paths],
|
||||
# Imports.
|
||||
*[f"import {x}" for x in modules],
|
||||
]
|
||||
return pytester.runpython_c("\n".join(lines))
|
||||
|
|
|
@ -2377,8 +2377,13 @@ def test_line_with_reprcrash(monkeypatch: MonkeyPatch) -> None:
|
|||
|
||||
monkeypatch.setattr(_pytest.terminal, "_get_node_id_with_markup", mock_get_pos)
|
||||
|
||||
class Namespace:
|
||||
def __init__(self, **kwargs):
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
class config:
|
||||
pass
|
||||
def __init__(self):
|
||||
self.option = Namespace(verbose=0)
|
||||
|
||||
class rep:
|
||||
def _get_verbose_word(self, *args):
|
||||
|
@ -2399,7 +2404,7 @@ def test_line_with_reprcrash(monkeypatch: MonkeyPatch) -> None:
|
|||
if msg:
|
||||
rep.longrepr.reprcrash.message = msg # type: ignore
|
||||
actual = _get_line_with_reprcrash_message(
|
||||
config, # type: ignore[arg-type]
|
||||
config(), # type: ignore[arg-type]
|
||||
rep(), # type: ignore[arg-type]
|
||||
DummyTerminalWriter(), # type: ignore[arg-type]
|
||||
{},
|
||||
|
@ -2443,6 +2448,43 @@ def test_line_with_reprcrash(monkeypatch: MonkeyPatch) -> None:
|
|||
check("🉐🉐🉐🉐🉐\n2nd line", 80, "FAILED nodeid::🉐::withunicode - 🉐🉐🉐🉐🉐")
|
||||
|
||||
|
||||
def test_short_summary_with_verbose(
|
||||
monkeypatch: MonkeyPatch, pytester: Pytester
|
||||
) -> None:
|
||||
"""With -vv do not truncate the summary info (#11777)."""
|
||||
# On CI we also do not truncate the summary info, monkeypatch it to ensure we
|
||||
# are testing against the -vv flag on CI.
|
||||
monkeypatch.setattr(_pytest.terminal, "running_on_ci", lambda: False)
|
||||
|
||||
string_length = 200
|
||||
pytester.makepyfile(
|
||||
f"""
|
||||
def test():
|
||||
s1 = "A" * {string_length}
|
||||
s2 = "B" * {string_length}
|
||||
assert s1 == s2
|
||||
"""
|
||||
)
|
||||
|
||||
# No -vv, summary info should be truncated.
|
||||
result = pytester.runpytest()
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*short test summary info*",
|
||||
"* assert 'AAA...",
|
||||
],
|
||||
)
|
||||
|
||||
# No truncation with -vv.
|
||||
result = pytester.runpytest("-vv")
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*short test summary info*",
|
||||
f"*{'A' * string_length}*{'B' * string_length}'",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"seconds, expected",
|
||||
[
|
||||
|
|
Loading…
Reference in New Issue