From c83c1c4bda7e92bcda00dad29798b8fc73028997 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 27 Feb 2024 10:51:31 +0200 Subject: [PATCH 01/47] fixtures: add `_iter_chain` helper method Will be reused in the next commit. --- src/_pytest/fixtures.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 99de03fe8..86a8eef04 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -540,6 +540,16 @@ class FixtureRequest(abc.ABC): ) return fixturedef.cached_result[0] + def _iter_chain(self) -> Iterator["SubRequest"]: + """Yield all SubRequests in the chain, from self up. + + Note: does *not* yield the TopRequest. + """ + current = self + while isinstance(current, SubRequest): + yield current + current = current._parent_request + def _get_active_fixturedef( self, argname: str ) -> Union["FixtureDef[object]", PseudoFixtureDef[object]]: @@ -557,11 +567,7 @@ class FixtureRequest(abc.ABC): return fixturedef def _get_fixturestack(self) -> List["FixtureDef[Any]"]: - current = self - values: List[FixtureDef[Any]] = [] - while isinstance(current, SubRequest): - values.append(current._fixturedef) # type: ignore[has-type] - current = current._parent_request + values = [request._fixturedef for request in self._iter_chain()] values.reverse() return values @@ -705,7 +711,7 @@ class SubRequest(FixtureRequest): ) self._parent_request: Final[FixtureRequest] = request self._scope_field: Final = scope - self._fixturedef: Final = fixturedef + self._fixturedef: Final[FixtureDef[object]] = fixturedef if param is not NOTSET: self.param = param self.param_index: Final = param_index From bd45ccd2ca399121fa91baa3fa0d25c2c36b6671 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 27 Feb 2024 10:37:40 +0200 Subject: [PATCH 02/47] fixtures: avoid mutable `arg2index` state in favor of looking up the request chain pytest allows a fixture to request its own name (directly or indirectly), in which case the fixture with the same name but one level up is used. To know which fixture should be used next, pytest keeps a mutable item-global dict `_arg2index` which maintains this state. This is not great: - Mutable state like this is hard to understand and reason about. - It is conceptually buggy; the indexing is global (e.g. if requesting `fix1` and `fix2`, the indexing is shared between them), but actually different branches of the subrequest tree should not affect each other. This is not an issue in practice because pytest keeps a cache of the fixturedefs it resolved anyway (`_fixture_defs`), but if the cache is removed it becomes evident. Instead of the `_arg2index` state, count how many `argname`s deep we are in the subrequest tree ("the fixture stack") and use that for the index. This way, no global mutable state and the logic is very localized and easier to understand. This is slower, however fixture stacks should not be so deep that this matters much, I hope. --- src/_pytest/fixtures.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 86a8eef04..fff955218 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -343,7 +343,6 @@ class FixtureRequest(abc.ABC): pyfuncitem: "Function", fixturename: Optional[str], arg2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]], - arg2index: Dict[str, int], fixture_defs: Dict[str, "FixtureDef[Any]"], *, _ispytest: bool = False, @@ -357,16 +356,6 @@ class FixtureRequest(abc.ABC): # collection. Dynamically requested fixtures (using # `request.getfixturevalue("foo")`) are added dynamically. self._arg2fixturedefs: Final = arg2fixturedefs - # A fixture may override another fixture with the same name, e.g. a fixture - # in a module can override a fixture in a conftest, a fixture in a class can - # override a fixture in the module, and so on. - # An overriding fixture can request its own name; in this case it gets - # the value of the fixture it overrides, one level up. - # The _arg2index state keeps the current depth in the overriding chain. - # The fixturedefs list in _arg2fixturedefs for a given name is ordered from - # furthest to closest, so we use negative indexing -1, -2, ... to go from - # last to first. - self._arg2index: Final = arg2index # The evaluated argnames so far, mapping to the FixtureDef they resolved # to. self._fixture_defs: Final = fixture_defs @@ -424,11 +413,24 @@ class FixtureRequest(abc.ABC): # The are no fixtures with this name applicable for the function. if not fixturedefs: raise FixtureLookupError(argname, self) - index = self._arg2index.get(argname, 0) - 1 - # The fixture requested its own name, but no remaining to override. + + # A fixture may override another fixture with the same name, e.g. a + # fixture in a module can override a fixture in a conftest, a fixture in + # a class can override a fixture in the module, and so on. + # An overriding fixture can request its own name (possibly indirectly); + # in this case it gets the value of the fixture it overrides, one level + # up. + # Check how many `argname`s deep we are, and take the next one. + # `fixturedefs` is sorted from furthest to closest, so use negative + # indexing to go in reverse. + index = -1 + for request in self._iter_chain(): + if request.fixturename == argname: + index -= 1 + # If already consumed all of the available levels, fail. if -index > len(fixturedefs): raise FixtureLookupError(argname, self) - self._arg2index[argname] = index + return fixturedefs[index] @property @@ -660,7 +662,6 @@ class TopRequest(FixtureRequest): fixturename=None, pyfuncitem=pyfuncitem, arg2fixturedefs=pyfuncitem._fixtureinfo.name2fixturedefs.copy(), - arg2index={}, fixture_defs={}, _ispytest=_ispytest, ) @@ -706,7 +707,6 @@ class SubRequest(FixtureRequest): fixturename=fixturedef.argname, fixture_defs=request._fixture_defs, arg2fixturedefs=request._arg2fixturedefs, - arg2index=request._arg2index, _ispytest=_ispytest, ) self._parent_request: Final[FixtureRequest] = request From 887e251abbea9d59be41972b3642ffd5748a587f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 27 Feb 2024 21:35:01 +0200 Subject: [PATCH 03/47] testing/test_pathlib: remove `test_issue131_on__init__` The test seems wrong, and we haven't been able to figure out what it's trying to test (the original issue is lost in time). --- testing/test_pathlib.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 075259009..b99b3e78f 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -259,20 +259,6 @@ class TestImportPath: assert orig == p assert issubclass(ImportPathMismatchError, ImportError) - def test_issue131_on__init__(self, tmp_path: Path) -> None: - # __init__.py files may be namespace packages, and thus the - # __file__ of an imported module may not be ourselves - # see issue - tmp_path.joinpath("proja").mkdir() - p1 = tmp_path.joinpath("proja", "__init__.py") - p1.touch() - tmp_path.joinpath("sub", "proja").mkdir(parents=True) - p2 = tmp_path.joinpath("sub", "proja", "__init__.py") - p2.touch() - m1 = import_path(p1, root=tmp_path) - m2 = import_path(p2, root=tmp_path) - assert m1 == m2 - def test_ensuresyspath_append(self, tmp_path: Path) -> None: root1 = tmp_path / "root1" root1.mkdir() From 300ceb435e9c5dc3f03698a281eb879eee610842 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 27 Feb 2024 21:33:42 +0200 Subject: [PATCH 04/47] testing/test_doctest: make `test_importmode` more realistic --- testing/test_doctest.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 32897a916..58fce244f 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -117,12 +117,12 @@ class TestDoctests: def test_importmode(self, pytester: Pytester): pytester.makepyfile( **{ - "namespacepkg/innerpkg/__init__.py": "", - "namespacepkg/innerpkg/a.py": """ + "src/namespacepkg/innerpkg/__init__.py": "", + "src/namespacepkg/innerpkg/a.py": """ def some_func(): return 42 """, - "namespacepkg/innerpkg/b.py": """ + "src/namespacepkg/innerpkg/b.py": """ from namespacepkg.innerpkg.a import some_func def my_func(): ''' @@ -133,6 +133,10 @@ class TestDoctests: """, } ) + # For 'namespacepkg' to be considered a namespace package, its containing directory + # needs to be reachable from sys.path: + # https://packaging.python.org/en/latest/guides/packaging-namespace-packages + pytester.syspathinsert(pytester.path / "src") reprec = pytester.inline_run("--doctest-modules", "--import-mode=importlib") reprec.assertoutcome(passed=1) From dcf01fd39ada7c6834467dde64fcab00910a4489 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 27 Feb 2024 21:59:02 +0200 Subject: [PATCH 05/47] testing/test_pathlib: add an importlib test Ensure the implementation isn't changed to trigger such a bug. --- testing/test_pathlib.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index b99b3e78f..d3ae00248 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -671,6 +671,36 @@ class TestImportLibMode: result = pytester.runpytest("--import-mode=importlib") result.stdout.fnmatch_lines("* 1 passed *") + def test_import_path_imports_correct_file(self, pytester: Pytester) -> None: + """ + Import the module by the given path, even if other module with the same name + is reachable from sys.path. + """ + pytester.syspathinsert() + # Create a 'x.py' module reachable from sys.path that raises AssertionError + # if imported. + x_at_root = pytester.path / "x.py" + x_at_root.write_text("raise AssertionError('x at root')", encoding="ascii") + + # Create another x.py module, but in some subdirectories to ensure it is not + # accessible from sys.path. + x_in_sub_folder = pytester.path / "a/b/x.py" + x_in_sub_folder.parent.mkdir(parents=True) + x_in_sub_folder.write_text("X = 'a/b/x'", encoding="ascii") + + # Import our x.py module from the subdirectories. + # The 'x.py' module from sys.path was not imported for sure because + # otherwise we would get an AssertionError. + mod = import_path( + x_in_sub_folder, mode=ImportMode.importlib, root=pytester.path + ) + assert mod.__file__ and Path(mod.__file__) == x_in_sub_folder + assert mod.X == "a/b/x" + + # Attempt to import root 'x.py'. + with pytest.raises(AssertionError, match="x at root"): + _ = import_path(x_at_root, mode=ImportMode.importlib, root=pytester.path) + def test_safe_exists(tmp_path: Path) -> None: d = tmp_path.joinpath("some_dir") From 7524e60d8f96f74deb6160a15beb0d20712cb901 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 27 Feb 2024 20:03:08 +0200 Subject: [PATCH 06/47] pathlib: extract a function `resolve_pkg_root_and_module_name` Will be reused. --- src/_pytest/pathlib.py | 46 +++++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 1e0891153..d64705154 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -541,16 +541,10 @@ def import_path( insert_missing_modules(sys.modules, module_name) return mod - pkg_path = resolve_package_path(path) - if pkg_path is not None: - pkg_root = pkg_path.parent - names = list(path.with_suffix("").relative_to(pkg_root).parts) - if names[-1] == "__init__": - names.pop() - module_name = ".".join(names) - else: - pkg_root = path.parent - module_name = path.stem + try: + pkg_root, module_name = resolve_pkg_root_and_module_name(path) + except CouldNotResolvePathError: + pkg_root, module_name = path.parent, path.stem # Change sys.path permanently: restoring it at the end of this function would cause surprising # problems because of delayed imports: for example, a conftest.py file imported by this function @@ -689,6 +683,38 @@ def resolve_package_path(path: Path) -> Optional[Path]: return result +def resolve_pkg_root_and_module_name(path: Path) -> Tuple[Path, str]: + """ + Return the path to the directory of the root package that contains the + given Python file, and its module name: + + src/ + app/ + __init__.py + core/ + __init__.py + models.py + + Passing the full path to `models.py` will yield Path("src") and "app.core.models". + + Raises CouldNotResolvePathError if the given path does not belong to a package (missing any __init__.py files). + """ + pkg_path = resolve_package_path(path) + if pkg_path is not None: + pkg_root = pkg_path.parent + 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 + + raise CouldNotResolvePathError(f"Could not resolve for {path}") + + +class CouldNotResolvePathError(Exception): + """Custom exception raised by resolve_pkg_root_and_module_name.""" + + def scandir( path: Union[str, "os.PathLike[str]"], sort_key: Callable[["os.DirEntry[str]"], object] = lambda entry: entry.name, From 4dea18308bafafb0be0b2d07e02328051c3095d5 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 27 Feb 2024 21:08:50 +0200 Subject: [PATCH 07/47] pathlib: extract a function `_import_module_using_spec` Will be reused. --- src/_pytest/pathlib.py | 50 ++++++++++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index d64705154..7e61a561d 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -526,19 +526,11 @@ def import_path( with contextlib.suppress(KeyError): return sys.modules[module_name] - for meta_importer in sys.meta_path: - spec = meta_importer.find_spec(module_name, [str(path.parent)]) - if spec is not None: - break - else: - spec = importlib.util.spec_from_file_location(module_name, str(path)) - - if spec is None: + mod = _import_module_using_spec( + module_name, path, path.parent, insert_modules=True + ) + if mod is None: raise ImportError(f"Can't find module {module_name} at location {path}") - mod = importlib.util.module_from_spec(spec) - sys.modules[module_name] = mod - spec.loader.exec_module(mod) # type: ignore[union-attr] - insert_missing_modules(sys.modules, module_name) return mod try: @@ -586,6 +578,40 @@ def import_path( return mod +def _import_module_using_spec( + module_name: str, module_path: Path, module_location: Path, *, insert_modules: bool +) -> Optional[ModuleType]: + """ + Tries to import a module by its canonical name, path to the .py file, and its + parent location. + + :param insert_modules: + If True, will call insert_missing_modules to create empty intermediate modules + for made-up module names (when importing test files not reachable from sys.path). + Note: we can probably drop insert_missing_modules altogether: instead of + generating module names such as "src.tests.test_foo", which require intermediate + empty modules, we might just as well generate unique module names like + "src_tests_test_foo". + """ + # Checking with sys.meta_path first in case one of its hooks can import this module, + # 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: + break + else: + spec = importlib.util.spec_from_file_location(module_name, str(module_path)) + if 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] + if insert_modules: + insert_missing_modules(sys.modules, module_name) + return mod + + return None + + # 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"): From 58674264553392e1d2c2e88ea7de5460e01cd6df Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 27 Feb 2024 20:08:15 +0200 Subject: [PATCH 08/47] pathlib: handle filenames starting with `.` in `module_name_from_path` --- src/_pytest/pathlib.py | 5 +++++ testing/test_pathlib.py | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 7e61a561d..e2fa4db12 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -648,6 +648,11 @@ def module_name_from_path(path: Path, root: Path) -> str: if len(path_parts) >= 2 and path_parts[-1] == "__init__": path_parts = path_parts[:-1] + # Module names cannot contain ".", normalize them to "_". This prevents + # a directory having a "." in the name (".env.310" for example) causing extra intermediate modules. + # Also, important to replace "." at the start of paths, as those are considered relative imports. + path_parts = [x.replace(".", "_") for x in path_parts] + return ".".join(path_parts) diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index d3ae00248..087090071 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -584,6 +584,18 @@ class TestImportLibMode: result = module_name_from_path(tmp_path / "__init__.py", tmp_path) assert result == "__init__" + # Modules which start with "." are considered relative and will not be imported + # unless part of a package, so we replace it with a "_" when generating the fake module name. + result = module_name_from_path(tmp_path / ".env/tests/test_foo.py", tmp_path) + assert result == "_env.tests.test_foo" + + # We want to avoid generating extra intermediate modules if some directory just happens + # to contain a "." in the name. + result = module_name_from_path( + tmp_path / ".env.310/tests/test_foo.py", tmp_path + ) + assert result == "_env_310.tests.test_foo" + def test_insert_missing_modules( self, monkeypatch: MonkeyPatch, tmp_path: Path ) -> None: From 067daf9f7d7072a4e199dd51b738193be370a1c7 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 27 Feb 2024 20:13:28 +0200 Subject: [PATCH 09/47] pathlib: consider namespace packages in `resolve_pkg_root_and_module_name` This applies to `append` and `prepend` import modes; support for `importlib` mode will be added in a separate change. --- changelog/11475.feature.rst | 3 + src/_pytest/pathlib.py | 28 ++++++- testing/test_pathlib.py | 143 ++++++++++++++++++++++++++++++++++++ 3 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 changelog/11475.feature.rst diff --git a/changelog/11475.feature.rst b/changelog/11475.feature.rst new file mode 100644 index 000000000..6b73c8158 --- /dev/null +++ b/changelog/11475.feature.rst @@ -0,0 +1,3 @@ +pytest now correctly identifies modules that are part of `namespace packages `__, for example when importing user-level modules for doctesting. + +Previously pytest was not aware of namespace packages, so running a doctest from a subpackage that is part of a namespace package would import just the subpackage (for example ``app.models``) instead of its full path (for example ``com.company.app.models``). diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index e2fa4db12..8097adb22 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -534,7 +534,9 @@ def import_path( return mod try: - pkg_root, module_name = resolve_pkg_root_and_module_name(path) + pkg_root, module_name = resolve_pkg_root_and_module_name( + path, consider_ns_packages=True + ) except CouldNotResolvePathError: pkg_root, module_name = path.parent, path.stem @@ -714,7 +716,9 @@ def resolve_package_path(path: Path) -> Optional[Path]: return result -def resolve_pkg_root_and_module_name(path: Path) -> Tuple[Path, str]: +def resolve_pkg_root_and_module_name( + path: Path, *, consider_ns_packages: bool = False +) -> Tuple[Path, str]: """ Return the path to the directory of the root package that contains the given Python file, and its module name: @@ -728,11 +732,31 @@ def resolve_pkg_root_and_module_name(path: Path) -> Tuple[Path, str]: Passing the full path to `models.py` will yield Path("src") and "app.core.models". + If consider_ns_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: + + 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_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_ns_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 + names = list(path.with_suffix("").relative_to(pkg_root).parts) if names[-1] == "__init__": names.pop() diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 087090071..d515cb5bd 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -9,11 +9,13 @@ from types import ModuleType from typing import Any from typing import Generator from typing import Iterator +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 CouldNotResolvePathError from _pytest.pathlib import ensure_deletable from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import get_extended_length_path_str @@ -25,6 +27,7 @@ from _pytest.pathlib import insert_missing_modules from _pytest.pathlib import maybe_delete_a_numbered_dir from _pytest.pathlib import module_name_from_path from _pytest.pathlib import resolve_package_path +from _pytest.pathlib import resolve_pkg_root_and_module_name from _pytest.pathlib import safe_exists from _pytest.pathlib import symlink_or_skip from _pytest.pathlib import visit @@ -33,6 +36,20 @@ from _pytest.tmpdir import TempPathFactory import pytest +@pytest.fixture(autouse=True) +def autouse_pytester(pytester: Pytester) -> None: + """ + Fixture to make pytester() being autouse for all tests in this module. + + pytester makes sure to restore sys.path to its previous state, and many tests in this module + import modules and change sys.path because of that, so common module names such as "test" or "test.conftest" + end up leaking to tests in other modules. + + Note: we might consider extracting the sys.path restoration aspect into its own fixture, and apply it + to the entire test suite always. + """ + + class TestFNMatcherPort: """Test our port of py.common.FNMatcher (fnmatch_ex).""" @@ -596,6 +613,33 @@ class TestImportLibMode: ) assert result == "_env_310.tests.test_foo" + def test_resolve_pkg_root_and_module_name( + self, tmp_path: Path, monkeypatch: MonkeyPatch + ) -> 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) + + # Create the __init__.py files, it should now resolve to a proper module name. + (tmp_path / "src/app/__init__.py").touch() + (tmp_path / "src/app/core/__init__.py").touch() + assert resolve_pkg_root_and_module_name(models_py) == ( + tmp_path / "src", + "app.core.models", + ) + + # If we add tmp_path to sys.path, src becomes a namespace package. + monkeypatch.syspath_prepend(tmp_path) + assert resolve_pkg_root_and_module_name( + models_py, consider_ns_packages=True + ) == ( + tmp_path, + "src.app.core.models", + ) + def test_insert_missing_modules( self, monkeypatch: MonkeyPatch, tmp_path: Path ) -> None: @@ -741,3 +785,102 @@ def test_safe_exists(tmp_path: Path) -> None: side_effect=ValueError("name too long"), ): assert safe_exists(p) is False + + +class TestNamespacePackages: + """Test import_path support when importing from properly namespace packages.""" + + def setup_directories( + self, tmp_path: Path, monkeypatch: MonkeyPatch, pytester: Pytester + ) -> Tuple[Path, Path]: + # 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() + 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() + algorithms_py = tmp_path / "src/dist2/com/company/calc/algo/algorithms.py" + algorithms_py.touch() + + # 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 + """ + ) + ) + assert r.ret == 0 + + 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"]) + def test_resolve_pkg_root_and_module_name_ns_multiple_levels( + self, + tmp_path: Path, + monkeypatch: MonkeyPatch, + pytester: Pytester, + import_mode: str, + ) -> None: + models_py, algorithms_py = self.setup_directories( + tmp_path, monkeypatch, pytester + ) + + pkg_root, module_name = resolve_pkg_root_and_module_name( + models_py, consider_ns_packages=True + ) + assert (pkg_root, module_name) == ( + tmp_path / "src/dist1", + "com.company.app.core.models", + ) + + mod = import_path(models_py, mode=import_mode, root=tmp_path) + assert mod.__name__ == "com.company.app.core.models" + assert mod.__file__ == str(models_py) + + pkg_root, module_name = resolve_pkg_root_and_module_name( + algorithms_py, consider_ns_packages=True + ) + assert (pkg_root, module_name) == ( + tmp_path / "src/dist2", + "com.company.calc.algo.algorithms", + ) + + mod = import_path(algorithms_py, mode=import_mode, root=tmp_path) + assert mod.__name__ == "com.company.calc.algo.algorithms" + assert mod.__file__ == str(algorithms_py) + + @pytest.mark.parametrize("import_mode", ["prepend", "append", "importlib"]) + def test_incorrect_namespace_package( + self, + tmp_path: Path, + monkeypatch: MonkeyPatch, + pytester: Pytester, + import_mode: str, + ) -> None: + 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. + (tmp_path / "src/dist1/com/__init__.py").touch() + + pkg_root, module_name = resolve_pkg_root_and_module_name( + models_py, consider_ns_packages=True + ) + assert (pkg_root, module_name) == ( + tmp_path / "src/dist1/com/company", + "app.core.models", + ) From c85fce39b60a6cc3537e9da3e7a4f4946cfe4d49 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 17 Feb 2024 10:39:15 -0300 Subject: [PATCH 10/47] Change importlib to first try to import modules using the standard mechanism As detailed in https://github.com/pytest-dev/pytest/issues/11475#issuecomment-1937043670, currently with `--import-mode=importlib` pytest will try to import every file by using a unique module name, regardless if that module could be imported using the normal import mechanism without touching `sys.path`. This has the consequence that non-test modules available in `sys.path` (via other mechanism, such as being installed into a virtualenv, PYTHONPATH, etc) would end up being imported as standalone modules, instead of imported with their expected module names. To illustrate: ``` .env/ lib/ site-packages/ anndata/ core.py ``` Given `anndata` is installed into the virtual environment, `python -c "import anndata.core"` works, but pytest with `importlib` mode would import that module as a standalone module named `".env.lib.site-packages.anndata.core"`, because importlib module was designed to import test files which are not reachable from `sys.path`, but now it is clear that normal modules should be imported using the standard mechanisms if possible. Now `imporlib` mode will first try to import the module normally, without changing `sys.path`, and if that fails it falls back to importing the module as a standalone module. This also makes `importlib` respect namespace packages. This supersedes #11931. Fix #11475 Close #11931 --- changelog/11475.improvement.rst | 1 + src/_pytest/pathlib.py | 17 ++++ testing/test_pathlib.py | 143 +++++++++++++++++++++++++++++++- 3 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 changelog/11475.improvement.rst diff --git a/changelog/11475.improvement.rst b/changelog/11475.improvement.rst new file mode 100644 index 000000000..fc6e8be3a --- /dev/null +++ b/changelog/11475.improvement.rst @@ -0,0 +1 @@ +:ref:`--import-mode=importlib ` now tries to import modules using the standard import mechanism (but still without changing :py:data:`sys.path`), falling back to importing modules directly only if that fails. diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 8097adb22..8c4e2fd87 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -522,6 +522,23 @@ def import_path( raise ImportError(path) if mode is ImportMode.importlib: + # Try to import this module using the standard import mechanisms, but + # without touching sys.path. + try: + pkg_root, module_name = resolve_pkg_root_and_module_name( + path, consider_ns_packages=True + ) + except CouldNotResolvePathError: + pass + else: + mod = _import_module_using_spec( + module_name, path, pkg_root, insert_modules=False + ) + if mod is not None: + return mod + + # Could not import the module with the current sys.path, so we fall back + # to importing the file as a single module, not being a part of a package. module_name = module_name_from_path(path, root) with contextlib.suppress(KeyError): return sys.modules[module_name] diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index d515cb5bd..13c22b800 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -3,6 +3,7 @@ import errno import os.path from pathlib import Path import pickle +import shutil import sys from textwrap import dedent from types import ModuleType @@ -727,6 +728,146 @@ class TestImportLibMode: result = pytester.runpytest("--import-mode=importlib") result.stdout.fnmatch_lines("* 1 passed *") + def create_installed_doctests_and_tests_dir( + self, path: Path, monkeypatch: MonkeyPatch + ) -> Tuple[Path, Path, Path]: + """ + Create a directory structure where the application code is installed in a virtual environment, + and the tests are in an outside ".tests" directory. + + Return the paths to the core module (installed in the virtualenv), and the test modules. + """ + app = path / "src/app" + app.mkdir(parents=True) + (app / "__init__.py").touch() + core_py = app / "core.py" + core_py.write_text( + dedent( + """ + def foo(): + ''' + >>> 1 + 1 + 2 + ''' + """ + ), + encoding="ascii", + ) + + # Install it into a site-packages directory, and add it to sys.path, mimicking what + # happens when installing into a virtualenv. + site_packages = path / ".env/lib/site-packages" + site_packages.mkdir(parents=True) + shutil.copytree(app, site_packages / "app") + assert (site_packages / "app/core.py").is_file() + + monkeypatch.syspath_prepend(site_packages) + + # Create the tests files, outside 'src' and the virtualenv. + # We use the same test name on purpose, but in different directories, to ensure + # this works as advertised. + conftest_path1 = path / ".tests/a/conftest.py" + conftest_path1.parent.mkdir(parents=True) + conftest_path1.write_text( + dedent( + """ + import pytest + @pytest.fixture + def a_fix(): return "a" + """ + ), + encoding="ascii", + ) + test_path1 = path / ".tests/a/test_core.py" + test_path1.write_text( + dedent( + """ + import app.core + def test(a_fix): + assert a_fix == "a" + """, + ), + encoding="ascii", + ) + + conftest_path2 = path / ".tests/b/conftest.py" + conftest_path2.parent.mkdir(parents=True) + conftest_path2.write_text( + dedent( + """ + import pytest + @pytest.fixture + def b_fix(): return "b" + """ + ), + encoding="ascii", + ) + + test_path2 = path / ".tests/b/test_core.py" + test_path2.write_text( + dedent( + """ + import app.core + def test(b_fix): + assert b_fix == "b" + """, + ), + encoding="ascii", + ) + return (site_packages / "app/core.py"), test_path1, test_path2 + + def test_import_using_normal_mechanism_first( + self, monkeypatch: MonkeyPatch, pytester: Pytester + ) -> None: + """ + Test import_path imports from the canonical location when possible first, only + falling back to its normal flow when the module being imported is not reachable via sys.path (#11475). + """ + core_py, test_path1, test_path2 = self.create_installed_doctests_and_tests_dir( + pytester.path, monkeypatch + ) + + # core_py is reached from sys.path, so should be imported normally. + mod = import_path(core_py, mode="importlib", root=pytester.path) + assert mod.__name__ == "app.core" + assert mod.__file__ and Path(mod.__file__) == core_py + + # tests are not reachable from sys.path, so they are imported as a standalone modules. + # Instead of '.tests.a.test_core', we import as "_tests.a.test_core" because + # importlib considers module names starting with '.' to be local imports. + mod = import_path(test_path1, mode="importlib", root=pytester.path) + assert mod.__name__ == "_tests.a.test_core" + mod = import_path(test_path2, mode="importlib", root=pytester.path) + assert mod.__name__ == "_tests.b.test_core" + + def test_import_using_normal_mechanism_first_integration( + self, monkeypatch: MonkeyPatch, pytester: Pytester + ) -> None: + """ + Same test as above, but verify the behavior calling pytest. + + We should not make this call in the same test as above, as the modules have already + been imported by separate import_path() calls. + """ + core_py, test_path1, test_path2 = self.create_installed_doctests_and_tests_dir( + pytester.path, monkeypatch + ) + result = pytester.runpytest( + "--import-mode=importlib", + "--doctest-modules", + "--pyargs", + "app", + "./.tests", + ) + result.stdout.fnmatch_lines( + [ + f"{core_py.relative_to(pytester.path)} . *", + f"{test_path1.relative_to(pytester.path)} . *", + f"{test_path2.relative_to(pytester.path)} . *", + "* 3 passed*", + ] + ) + def test_import_path_imports_correct_file(self, pytester: Pytester) -> None: """ Import the module by the given path, even if other module with the same name @@ -825,7 +966,7 @@ class TestNamespacePackages: monkeypatch.syspath_prepend(tmp_path / "src/dist2") return models_py, algorithms_py - @pytest.mark.parametrize("import_mode", ["prepend", "append"]) + @pytest.mark.parametrize("import_mode", ["prepend", "append", "importlib"]) def test_resolve_pkg_root_and_module_name_ns_multiple_levels( self, tmp_path: Path, From 5746b8e69620e39d69a04383cff2ca9d54a7e41f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 27 Feb 2024 21:29:59 +0200 Subject: [PATCH 11/47] doc: update and improve import mode docs --- doc/en/explanation/goodpractices.rst | 6 +- doc/en/explanation/pythonpath.rst | 82 ++++++++++++++++++++++------ 2 files changed, 69 insertions(+), 19 deletions(-) diff --git a/doc/en/explanation/goodpractices.rst b/doc/en/explanation/goodpractices.rst index efde420cd..1390ba4e8 100644 --- a/doc/en/explanation/goodpractices.rst +++ b/doc/en/explanation/goodpractices.rst @@ -60,8 +60,10 @@ Within Python modules, ``pytest`` also discovers tests using the standard :ref:`unittest.TestCase ` subclassing technique. -Choosing a test layout / import rules -------------------------------------- +.. _`test layout`: + +Choosing a test layout +---------------------- ``pytest`` supports two common test layouts: diff --git a/doc/en/explanation/pythonpath.rst b/doc/en/explanation/pythonpath.rst index 5b533f47f..6b1d3ae2c 100644 --- a/doc/en/explanation/pythonpath.rst +++ b/doc/en/explanation/pythonpath.rst @@ -15,14 +15,23 @@ changing :data:`sys.path`. Some aspects of the import process can be controlled through the ``--import-mode`` command-line flag, which can assume these values: -* ``prepend`` (default): the directory path containing each module will be inserted into the *beginning* - of :py:data:`sys.path` if not already there, and then imported with the :func:`importlib.import_module ` function. +.. _`import-mode-prepend`: - This requires test module names to be unique when the test directory tree is not arranged in - packages, because the modules will put in :py:data:`sys.modules` after importing. +* ``prepend`` (default): the directory path containing each module will be inserted into the *beginning* + of :py:data:`sys.path` if not already there, and then imported with + the :func:`importlib.import_module ` function. + + It is highly recommended to arrange your test modules as packages by adding ``__init__.py`` files to your directories + containing tests. This will make the tests part of a proper Python package, allowing pytest to resolve their full + name (for example ``tests.core.test_core`` for ``test_core.py`` inside the ``tests.core`` package). + + If the test directory tree is not arranged as packages, then each test file needs to have a unique name + compared to the other test files, otherwise pytest will raise an error if it finds two tests with the same name. This is the classic mechanism, dating back from the time Python 2 was still supported. +.. _`import-mode-append`: + * ``append``: the directory containing each module is appended to the end of :py:data:`sys.path` if not already there, and imported with :func:`importlib.import_module `. @@ -38,32 +47,71 @@ these values: the tests will run against the installed version of ``pkg_under_test`` when ``--import-mode=append`` is used whereas with ``prepend`` they would pick up the local version. This kind of confusion is why - we advocate for using :ref:`src ` layouts. + we advocate for using :ref:`src-layouts `. Same as ``prepend``, requires test module names to be unique when the test directory tree is not arranged in packages, because the modules will put in :py:data:`sys.modules` after importing. -* ``importlib``: new in pytest-6.0, this mode uses more fine control mechanisms provided by :mod:`importlib` to import test modules. This gives full control over the import process, and doesn't require changing :py:data:`sys.path`. +.. _`import-mode-importlib`: - For this reason this doesn't require test module names to be unique. +* ``importlib``: this mode uses more fine control mechanisms provided by :mod:`importlib` to import test modules, without changing :py:data:`sys.path`. - One drawback however is that test modules are non-importable by each other. Also, utility - modules in the tests directories are not automatically importable because the tests directory is no longer - added to :py:data:`sys.path`. + Advantages of this mode: - Initially we intended to make ``importlib`` the default in future releases, however it is clear now that - it has its own set of drawbacks so the default will remain ``prepend`` for the foreseeable future. + * pytest will not change :py:data:`sys.path` at all. + * Test module names do not need to be unique -- pytest will generate a unique name automatically based on the ``rootdir``. + + Disadvantages: + + * Test modules can't import each other. + * Testing utility modules in the tests directories (for example a ``tests.helpers`` module containing test-related functions/classes) + are not importable. The recommendation in this case it to place testing utility modules together with the application/library + code, for example ``app.testing.helpers``. + + Important: by "test utility modules" we mean functions/classes which are imported by + other tests directly; this does not include fixtures, which should be placed in ``conftest.py`` files, along + with the test modules, and are discovered automatically by pytest. + + It works like this: + + 1. Given a certain module path, for example ``tests/core/test_models.py``, derives a canonical name + like ``tests.core.test_models`` and tries to import it. + + For non-test modules this will work if they are accessible via :py:data:`sys.path`, so + for example ``.env/lib/site-packages/app/core.py`` will be importable as ``app.core``. + This is happens when plugins import non-test modules (for example doctesting). + + If this step succeeds, the module is returned. + + For test modules, unless they are reachable from :py:data:`sys.path`, this step will fail. + + 2. If the previous step fails, we import the module directly using ``importlib`` facilities, which lets us import it without + changing :py:data:`sys.path`. + + Because Python requires the module to also be available in :py:data:`sys.modules`, pytest derives a unique name for it based + on its relative location from the ``rootdir``, and adds the module to :py:data:`sys.modules`. + + For example, ``tests/core/test_models.py`` will end up being imported as the module ``tests.core.test_models``. + + .. versionadded:: 6.0 + +.. note:: + + Initially we intended to make ``importlib`` the default in future releases, however it is clear now that + it has its own set of drawbacks so the default will remain ``prepend`` for the foreseeable future. .. seealso:: The :confval:`pythonpath` configuration variable. + :ref:`test layout`. + ``prepend`` and ``append`` import modes scenarios ------------------------------------------------- Here's a list of scenarios when using ``prepend`` or ``append`` import modes where pytest needs to -change ``sys.path`` in order to import test modules or ``conftest.py`` files, and the issues users +change :py:data:`sys.path` in order to import test modules or ``conftest.py`` files, and the issues users might encounter because of that. Test modules / ``conftest.py`` files inside packages @@ -92,7 +140,7 @@ pytest will find ``foo/bar/tests/test_foo.py`` and realize it is part of a packa there's an ``__init__.py`` file in the same folder. It will then search upwards until it can find the last folder which still contains an ``__init__.py`` file in order to find the package *root* (in this case ``foo/``). To load the module, it will insert ``root/`` to the front of -``sys.path`` (if not there already) in order to load +:py:data:`sys.path` (if not there already) in order to load ``test_foo.py`` as the *module* ``foo.bar.tests.test_foo``. The same logic applies to the ``conftest.py`` file: it will be imported as ``foo.conftest`` module. @@ -122,8 +170,8 @@ When executing: pytest will find ``foo/bar/tests/test_foo.py`` and realize it is NOT part of a package given that there's no ``__init__.py`` file in the same folder. It will then add ``root/foo/bar/tests`` to -``sys.path`` in order to import ``test_foo.py`` as the *module* ``test_foo``. The same is done -with the ``conftest.py`` file by adding ``root/foo`` to ``sys.path`` to import it as ``conftest``. +:py:data:`sys.path` in order to import ``test_foo.py`` as the *module* ``test_foo``. The same is done +with the ``conftest.py`` file by adding ``root/foo`` to :py:data:`sys.path` to import it as ``conftest``. For this reason this layout cannot have test modules with the same name, as they all will be imported in the global import namespace. @@ -136,7 +184,7 @@ Invoking ``pytest`` versus ``python -m pytest`` ----------------------------------------------- Running pytest with ``pytest [...]`` instead of ``python -m pytest [...]`` yields nearly -equivalent behaviour, except that the latter will add the current directory to ``sys.path``, which +equivalent behaviour, except that the latter will add the current directory to :py:data:`sys.path`, which is standard ``python`` behavior. See also :ref:`invoke-python`. From 199d4e2b7387cb38f54fc578c1521b5b1eea5db2 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 2 Mar 2024 11:19:57 -0300 Subject: [PATCH 12/47] pathlib: import signature and docs for import_path --- src/_pytest/pathlib.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 8c4e2fd87..b4a0ceb22 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -484,26 +484,31 @@ class ImportPathMismatchError(ImportError): def import_path( - p: Union[str, "os.PathLike[str]"], + path: Union[str, "os.PathLike[str]"], *, mode: Union[str, ImportMode] = ImportMode.prepend, root: Path, ) -> ModuleType: - """Import and return a module from the given path, which can be a file (a module) or + """ + Import and return a module from the given path, which can be a file (a module) or a directory (a package). - The import mechanism used is controlled by the `mode` parameter: + :param path: + Path to the file to import. - * `mode == ImportMode.prepend`: the directory containing the module (or package, taking - `__init__.py` files into account) will be put at the *start* of `sys.path` before - being imported with `importlib.import_module`. + :param mode: + Controls the underlying import mechanism that will be used: - * `mode == ImportMode.append`: same as `prepend`, but the directory will be appended - to the end of `sys.path`, if not already in `sys.path`. + * ImportMode.prepend: the directory containing the module (or package, taking + `__init__.py` files into account) will be put at the *start* of `sys.path` before + being imported with `importlib.import_module`. - * `mode == ImportMode.importlib`: uses more fine control mechanisms provided by `importlib` - to import the module, which avoids having to muck with `sys.path` at all. It effectively - allows having same-named test modules in different places. + * ImportMode.append: same as `prepend`, but the directory will be appended + to the end of `sys.path`, if not already in `sys.path`. + + * ImportMode.importlib: uses more fine control mechanisms provided by `importlib` + to import the module, which avoids having to muck with `sys.path` at all. It effectively + allows having same-named test modules in different places. :param root: Used as an anchor when mode == ImportMode.importlib to obtain @@ -514,10 +519,9 @@ def import_path( If after importing the given `path` and the module `__file__` are different. Only raised in `prepend` and `append` modes. """ + path = Path(path) mode = ImportMode(mode) - path = Path(p) - if not path.exists(): raise ImportError(path) From 111c0d910e15afb9e1ed1f6862e1637594eaa076 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 2 Mar 2024 11:46:54 -0300 Subject: [PATCH 13/47] Add consider_namespace_packages ini option Fix #11475 --- src/_pytest/config/__init__.py | 68 ++++++++++-- src/_pytest/main.py | 6 ++ src/_pytest/pathlib.py | 16 +-- src/_pytest/python.py | 7 +- src/_pytest/runner.py | 3 + testing/code/test_excinfo.py | 6 +- testing/code/test_source.py | 2 +- testing/test_conftest.py | 76 +++++++++++--- testing/test_pathlib.py | 182 +++++++++++++++++++++++++-------- testing/test_pluginmanager.py | 26 ++++- 10 files changed, 315 insertions(+), 77 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 069e2196d..7ed79483c 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -547,6 +547,8 @@ class PytestPluginManager(PluginManager): confcutdir: Optional[Path], invocation_dir: Path, importmode: Union[ImportMode, str], + *, + consider_namespace_packages: bool, ) -> None: """Load initial conftest files given a preparsed "namespace". @@ -572,10 +574,20 @@ class PytestPluginManager(PluginManager): # Ensure we do not break if what appears to be an anchor # is in fact a very long option (#10169, #11394). if safe_exists(anchor): - self._try_load_conftest(anchor, importmode, rootpath) + self._try_load_conftest( + anchor, + importmode, + rootpath, + consider_namespace_packages=consider_namespace_packages, + ) foundanchor = True if not foundanchor: - self._try_load_conftest(invocation_dir, importmode, rootpath) + self._try_load_conftest( + invocation_dir, + importmode, + rootpath, + consider_namespace_packages=consider_namespace_packages, + ) def _is_in_confcutdir(self, path: Path) -> bool: """Whether to consider the given path to load conftests from.""" @@ -593,17 +605,37 @@ class PytestPluginManager(PluginManager): return path not in self._confcutdir.parents def _try_load_conftest( - self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path + self, + anchor: Path, + importmode: Union[str, ImportMode], + rootpath: Path, + *, + consider_namespace_packages: bool, ) -> None: - self._loadconftestmodules(anchor, importmode, rootpath) + self._loadconftestmodules( + anchor, + importmode, + rootpath, + consider_namespace_packages=consider_namespace_packages, + ) # let's also consider test* subdirs if anchor.is_dir(): for x in anchor.glob("test*"): if x.is_dir(): - self._loadconftestmodules(x, importmode, rootpath) + self._loadconftestmodules( + x, + importmode, + rootpath, + consider_namespace_packages=consider_namespace_packages, + ) def _loadconftestmodules( - self, path: Path, importmode: Union[str, ImportMode], rootpath: Path + self, + path: Path, + importmode: Union[str, ImportMode], + rootpath: Path, + *, + consider_namespace_packages: bool, ) -> None: if self._noconftest: return @@ -620,7 +652,12 @@ class PytestPluginManager(PluginManager): if self._is_in_confcutdir(parent): conftestpath = parent / "conftest.py" if conftestpath.is_file(): - mod = self._importconftest(conftestpath, importmode, rootpath) + mod = self._importconftest( + conftestpath, + importmode, + rootpath, + consider_namespace_packages=consider_namespace_packages, + ) clist.append(mod) self._dirpath2confmods[directory] = clist @@ -642,7 +679,12 @@ class PytestPluginManager(PluginManager): raise KeyError(name) def _importconftest( - self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path + self, + conftestpath: Path, + importmode: Union[str, ImportMode], + rootpath: Path, + *, + consider_namespace_packages: bool, ) -> types.ModuleType: conftestpath_plugin_name = str(conftestpath) existing = self.get_plugin(conftestpath_plugin_name) @@ -661,7 +703,12 @@ class PytestPluginManager(PluginManager): pass try: - mod = import_path(conftestpath, mode=importmode, root=rootpath) + mod = import_path( + conftestpath, + mode=importmode, + root=rootpath, + consider_namespace_packages=consider_namespace_packages, + ) except Exception as e: assert e.__traceback__ is not None raise ConftestImportFailure(conftestpath, cause=e) from e @@ -1177,6 +1224,9 @@ class Config: confcutdir=early_config.known_args_namespace.confcutdir, invocation_dir=early_config.invocation_params.dir, importmode=early_config.known_args_namespace.importmode, + consider_namespace_packages=early_config.getini( + "consider_namespace_packages" + ), ) def _initini(self, args: Sequence[str]) -> None: diff --git a/src/_pytest/main.py b/src/_pytest/main.py index b7ed72ddc..8e8d238ac 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -222,6 +222,12 @@ def pytest_addoption(parser: Parser) -> None: help="Prepend/append to sys.path when importing test modules and conftest " "files. Default: prepend.", ) + parser.addini( + "consider_namespace_packages", + type="bool", + default=False, + help="Consider namespace packages when resolving module names during import", + ) group = parser.getgroup("debugconfig", "test session debugging and configuration") group.addoption( diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index b4a0ceb22..a19e89aa1 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -488,6 +488,7 @@ def import_path( *, mode: Union[str, ImportMode] = ImportMode.prepend, root: Path, + consider_namespace_packages: bool, ) -> ModuleType: """ Import and return a module from the given path, which can be a file (a module) or @@ -515,6 +516,9 @@ def import_path( a unique name for the module being imported so it can safely be stored into ``sys.modules``. + :param consider_namespace_packages: + If True, consider namespace packages when resolving module names. + :raises ImportPathMismatchError: If after importing the given `path` and the module `__file__` are different. Only raised in `prepend` and `append` modes. @@ -530,7 +534,7 @@ def import_path( # without touching sys.path. try: pkg_root, module_name = resolve_pkg_root_and_module_name( - path, consider_ns_packages=True + path, consider_namespace_packages=consider_namespace_packages ) except CouldNotResolvePathError: pass @@ -556,7 +560,7 @@ def import_path( try: pkg_root, module_name = resolve_pkg_root_and_module_name( - path, consider_ns_packages=True + path, consider_namespace_packages=consider_namespace_packages ) except CouldNotResolvePathError: pkg_root, module_name = path.parent, path.stem @@ -674,7 +678,7 @@ def module_name_from_path(path: Path, root: Path) -> str: # Module names cannot contain ".", normalize them to "_". This prevents # a directory having a "." in the name (".env.310" for example) causing extra intermediate modules. # Also, important to replace "." at the start of paths, as those are considered relative imports. - path_parts = [x.replace(".", "_") for x in path_parts] + path_parts = tuple(x.replace(".", "_") for x in path_parts) return ".".join(path_parts) @@ -738,7 +742,7 @@ def resolve_package_path(path: Path) -> Optional[Path]: def resolve_pkg_root_and_module_name( - path: Path, *, consider_ns_packages: bool = False + path: Path, *, consider_namespace_packages: bool = False ) -> Tuple[Path, str]: """ Return the path to the directory of the root package that contains the @@ -753,7 +757,7 @@ def resolve_pkg_root_and_module_name( Passing the full path to `models.py` will yield Path("src") and "app.core.models". - If consider_ns_packages is True, then we additionally check upwards in the hierarchy + 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: https://packaging.python.org/en/latest/guides/packaging-namespace-packages @@ -764,7 +768,7 @@ def resolve_pkg_root_and_module_name( if pkg_path is not None: pkg_root = pkg_path.parent # https://packaging.python.org/en/latest/guides/packaging-namespace-packages/ - if consider_ns_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. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index ca64a877d..e1730b1a7 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -516,7 +516,12 @@ def importtestmodule( # We assume we are only called once per module. importmode = config.getoption("--import-mode") try: - mod = import_path(path, mode=importmode, root=config.rootpath) + mod = import_path( + path, + mode=importmode, + root=config.rootpath, + consider_namespace_packages=config.getini("consider_namespace_packages"), + ) except SyntaxError as e: raise nodes.Collector.CollectError( ExceptionInfo.from_current().getrepr(style="short") diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index b60af9dd3..16abb895d 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -380,6 +380,9 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport: collector.path, collector.config.getoption("importmode"), rootpath=collector.config.rootpath, + consider_namespace_packages=collector.config.getini( + "consider_namespace_packages" + ), ) return list(collector.collect()) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index cce23bf87..49c5dd371 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -180,7 +180,7 @@ class TestTraceback_f_g_h: def test_traceback_cut_excludepath(self, pytester: Pytester) -> None: p = pytester.makepyfile("def f(): raise ValueError") with pytest.raises(ValueError) as excinfo: - import_path(p, root=pytester.path).f() # type: ignore[attr-defined] + import_path(p, root=pytester.path, consider_namespace_packages=False).f() # type: ignore[attr-defined] basedir = Path(pytest.__file__).parent newtraceback = excinfo.traceback.cut(excludepath=basedir) for x in newtraceback: @@ -543,7 +543,9 @@ class TestFormattedExcinfo: tmp_path.joinpath("__init__.py").touch() modpath.write_text(source, encoding="utf-8") importlib.invalidate_caches() - return import_path(modpath, root=tmp_path) + return import_path( + modpath, root=tmp_path, consider_namespace_packages=False + ) return importasmod diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 9d0565380..12ea27b35 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -296,7 +296,7 @@ def test_source_of_class_at_eof_without_newline(_sys_snapshot, tmp_path: Path) - ) path = tmp_path.joinpath("a.py") path.write_text(str(source), encoding="utf-8") - mod: Any = import_path(path, root=tmp_path) + mod: Any = import_path(path, root=tmp_path, consider_namespace_packages=False) s2 = Source(mod.A) assert str(source).strip() == str(s2).strip() diff --git a/testing/test_conftest.py b/testing/test_conftest.py index bb74fa75d..3116dfe25 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -38,6 +38,7 @@ def conftest_setinitial( confcutdir=confcutdir, invocation_dir=Path.cwd(), importmode="prepend", + consider_namespace_packages=False, ) @@ -64,7 +65,9 @@ class TestConftestValueAccessGlobal: def test_basic_init(self, basedir: Path) -> None: conftest = PytestPluginManager() p = basedir / "adir" - conftest._loadconftestmodules(p, importmode="prepend", rootpath=basedir) + conftest._loadconftestmodules( + p, importmode="prepend", rootpath=basedir, consider_namespace_packages=False + ) assert conftest._rget_with_confmod("a", p)[1] == 1 def test_immediate_initialiation_and_incremental_are_the_same( @@ -72,15 +75,26 @@ class TestConftestValueAccessGlobal: ) -> None: conftest = PytestPluginManager() assert not len(conftest._dirpath2confmods) - conftest._loadconftestmodules(basedir, importmode="prepend", rootpath=basedir) + conftest._loadconftestmodules( + basedir, + importmode="prepend", + rootpath=basedir, + consider_namespace_packages=False, + ) snap1 = len(conftest._dirpath2confmods) assert snap1 == 1 conftest._loadconftestmodules( - basedir / "adir", importmode="prepend", rootpath=basedir + basedir / "adir", + importmode="prepend", + rootpath=basedir, + consider_namespace_packages=False, ) assert len(conftest._dirpath2confmods) == snap1 + 1 conftest._loadconftestmodules( - basedir / "b", importmode="prepend", rootpath=basedir + basedir / "b", + importmode="prepend", + rootpath=basedir, + consider_namespace_packages=False, ) assert len(conftest._dirpath2confmods) == snap1 + 2 @@ -92,10 +106,18 @@ class TestConftestValueAccessGlobal: def test_value_access_by_path(self, basedir: Path) -> None: conftest = ConftestWithSetinitial(basedir) adir = basedir / "adir" - conftest._loadconftestmodules(adir, importmode="prepend", rootpath=basedir) + conftest._loadconftestmodules( + adir, + importmode="prepend", + rootpath=basedir, + consider_namespace_packages=False, + ) assert conftest._rget_with_confmod("a", adir)[1] == 1 conftest._loadconftestmodules( - adir / "b", importmode="prepend", rootpath=basedir + adir / "b", + importmode="prepend", + rootpath=basedir, + consider_namespace_packages=False, ) assert conftest._rget_with_confmod("a", adir / "b")[1] == 1.5 @@ -152,7 +174,12 @@ def test_conftest_global_import(pytester: Pytester) -> None: import pytest from _pytest.config import PytestPluginManager conf = PytestPluginManager() - mod = conf._importconftest(Path("conftest.py"), importmode="prepend", rootpath=Path.cwd()) + mod = conf._importconftest( + Path("conftest.py"), + importmode="prepend", + rootpath=Path.cwd(), + consider_namespace_packages=False, + ) assert mod.x == 3 import conftest assert conftest is mod, (conftest, mod) @@ -160,7 +187,12 @@ def test_conftest_global_import(pytester: Pytester) -> None: sub.mkdir() subconf = sub / "conftest.py" subconf.write_text("y=4", encoding="utf-8") - mod2 = conf._importconftest(subconf, importmode="prepend", rootpath=Path.cwd()) + mod2 = conf._importconftest( + subconf, + importmode="prepend", + rootpath=Path.cwd(), + consider_namespace_packages=False, + ) assert mod != mod2 assert mod2.y == 4 import conftest @@ -176,17 +208,30 @@ def test_conftestcutdir(pytester: Pytester) -> None: p = pytester.mkdir("x") conftest = PytestPluginManager() conftest_setinitial(conftest, [pytester.path], confcutdir=p) - conftest._loadconftestmodules(p, importmode="prepend", rootpath=pytester.path) + conftest._loadconftestmodules( + p, + importmode="prepend", + rootpath=pytester.path, + consider_namespace_packages=False, + ) values = conftest._getconftestmodules(p) assert len(values) == 0 conftest._loadconftestmodules( - conf.parent, importmode="prepend", rootpath=pytester.path + conf.parent, + importmode="prepend", + rootpath=pytester.path, + consider_namespace_packages=False, ) values = conftest._getconftestmodules(conf.parent) assert len(values) == 0 assert not conftest.has_plugin(str(conf)) # but we can still import a conftest directly - conftest._importconftest(conf, importmode="prepend", rootpath=pytester.path) + conftest._importconftest( + conf, + importmode="prepend", + rootpath=pytester.path, + consider_namespace_packages=False, + ) values = conftest._getconftestmodules(conf.parent) assert values[0].__file__ is not None assert values[0].__file__.startswith(str(conf)) @@ -405,13 +450,18 @@ def test_conftest_import_order(pytester: Pytester, monkeypatch: MonkeyPatch) -> ct2 = sub / "conftest.py" ct2.write_text("", encoding="utf-8") - def impct(p, importmode, root): + def impct(p, importmode, root, consider_namespace_packages): return p conftest = PytestPluginManager() conftest._confcutdir = pytester.path monkeypatch.setattr(conftest, "_importconftest", impct) - conftest._loadconftestmodules(sub, importmode="prepend", rootpath=pytester.path) + conftest._loadconftestmodules( + sub, + importmode="prepend", + rootpath=pytester.path, + consider_namespace_packages=False, + ) mods = cast(List[Path], conftest._getconftestmodules(sub)) expected = [ct1, ct2] assert mods == expected diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 13c22b800..a5d582bc4 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -171,13 +171,17 @@ class TestImportPath: ) def test_smoke_test(self, path1: Path) -> None: - obj = import_path(path1 / "execfile.py", root=path1) + obj = import_path( + path1 / "execfile.py", root=path1, consider_namespace_packages=False + ) assert obj.x == 42 # type: ignore[attr-defined] assert obj.__name__ == "execfile" def test_import_path_missing_file(self, path1: Path) -> None: with pytest.raises(ImportPathMismatchError): - import_path(path1 / "sampledir", root=path1) + import_path( + path1 / "sampledir", root=path1, consider_namespace_packages=False + ) def test_renamed_dir_creates_mismatch( self, tmp_path: Path, monkeypatch: MonkeyPatch @@ -185,25 +189,37 @@ class TestImportPath: tmp_path.joinpath("a").mkdir() p = tmp_path.joinpath("a", "test_x123.py") p.touch() - import_path(p, root=tmp_path) + import_path(p, root=tmp_path, consider_namespace_packages=False) tmp_path.joinpath("a").rename(tmp_path.joinpath("b")) with pytest.raises(ImportPathMismatchError): - import_path(tmp_path.joinpath("b", "test_x123.py"), root=tmp_path) + import_path( + tmp_path.joinpath("b", "test_x123.py"), + root=tmp_path, + consider_namespace_packages=False, + ) # Errors can be ignored. monkeypatch.setenv("PY_IGNORE_IMPORTMISMATCH", "1") - import_path(tmp_path.joinpath("b", "test_x123.py"), root=tmp_path) + import_path( + tmp_path.joinpath("b", "test_x123.py"), + root=tmp_path, + consider_namespace_packages=False, + ) # PY_IGNORE_IMPORTMISMATCH=0 does not ignore error. monkeypatch.setenv("PY_IGNORE_IMPORTMISMATCH", "0") with pytest.raises(ImportPathMismatchError): - import_path(tmp_path.joinpath("b", "test_x123.py"), root=tmp_path) + import_path( + tmp_path.joinpath("b", "test_x123.py"), + root=tmp_path, + consider_namespace_packages=False, + ) def test_messy_name(self, tmp_path: Path) -> None: # https://bitbucket.org/hpk42/py-trunk/issue/129 path = tmp_path / "foo__init__.py" path.touch() - module = import_path(path, root=tmp_path) + module = import_path(path, root=tmp_path, consider_namespace_packages=False) assert module.__name__ == "foo__init__" def test_dir(self, tmp_path: Path) -> None: @@ -211,31 +227,39 @@ class TestImportPath: p.mkdir() p_init = p / "__init__.py" p_init.touch() - m = import_path(p, root=tmp_path) + m = import_path(p, root=tmp_path, consider_namespace_packages=False) assert m.__name__ == "hello_123" - m = import_path(p_init, root=tmp_path) + m = import_path(p_init, root=tmp_path, consider_namespace_packages=False) assert m.__name__ == "hello_123" def test_a(self, path1: Path) -> None: otherdir = path1 / "otherdir" - mod = import_path(otherdir / "a.py", root=path1) + mod = import_path( + otherdir / "a.py", root=path1, consider_namespace_packages=False + ) assert mod.result == "got it" # type: ignore[attr-defined] assert mod.__name__ == "otherdir.a" def test_b(self, path1: Path) -> None: otherdir = path1 / "otherdir" - mod = import_path(otherdir / "b.py", root=path1) + mod = import_path( + otherdir / "b.py", root=path1, consider_namespace_packages=False + ) assert mod.stuff == "got it" # type: ignore[attr-defined] assert mod.__name__ == "otherdir.b" def test_c(self, path1: Path) -> None: otherdir = path1 / "otherdir" - mod = import_path(otherdir / "c.py", root=path1) + mod = import_path( + otherdir / "c.py", root=path1, consider_namespace_packages=False + ) assert mod.value == "got it" # type: ignore[attr-defined] def test_d(self, path1: Path) -> None: otherdir = path1 / "otherdir" - mod = import_path(otherdir / "d.py", root=path1) + mod = import_path( + otherdir / "d.py", root=path1, consider_namespace_packages=False + ) assert mod.value2 == "got it" # type: ignore[attr-defined] def test_import_after(self, tmp_path: Path) -> None: @@ -243,7 +267,7 @@ class TestImportPath: tmp_path.joinpath("xxxpackage", "__init__.py").touch() mod1path = tmp_path.joinpath("xxxpackage", "module1.py") mod1path.touch() - mod1 = import_path(mod1path, root=tmp_path) + mod1 = import_path(mod1path, root=tmp_path, consider_namespace_packages=False) assert mod1.__name__ == "xxxpackage.module1" from xxxpackage import module1 @@ -262,7 +286,9 @@ class TestImportPath: pseudopath.touch() mod.__file__ = str(pseudopath) mp.setitem(sys.modules, name, mod) - newmod = import_path(p, root=tmp_path) + newmod = import_path( + p, root=tmp_path, consider_namespace_packages=False + ) assert mod == newmod mod = ModuleType(name) pseudopath = tmp_path.joinpath(name + "123.py") @@ -270,7 +296,7 @@ class TestImportPath: mod.__file__ = str(pseudopath) monkeypatch.setitem(sys.modules, name, mod) with pytest.raises(ImportPathMismatchError) as excinfo: - import_path(p, root=tmp_path) + import_path(p, root=tmp_path, consider_namespace_packages=False) modname, modfile, orig = excinfo.value.args assert modname == name assert modfile == str(pseudopath) @@ -283,13 +309,19 @@ class TestImportPath: file1 = root1 / "x123.py" file1.touch() assert str(root1) not in sys.path - import_path(file1, mode="append", root=tmp_path) + import_path( + file1, mode="append", root=tmp_path, consider_namespace_packages=False + ) assert str(root1) == sys.path[-1] assert str(root1) not in sys.path[:-1] def test_invalid_path(self, tmp_path: Path) -> None: with pytest.raises(ImportError): - import_path(tmp_path / "invalid.py", root=tmp_path) + import_path( + tmp_path / "invalid.py", + root=tmp_path, + consider_namespace_packages=False, + ) @pytest.fixture def simple_module( @@ -307,7 +339,12 @@ class TestImportPath: self, simple_module: Path, tmp_path: Path, request: pytest.FixtureRequest ) -> None: """`importlib` mode does not change sys.path.""" - module = import_path(simple_module, mode="importlib", root=tmp_path) + module = import_path( + simple_module, + mode="importlib", + root=tmp_path, + consider_namespace_packages=False, + ) assert module.foo(2) == 42 # type: ignore[attr-defined] assert str(simple_module.parent) not in sys.path assert module.__name__ in sys.modules @@ -319,8 +356,18 @@ class TestImportPath: self, simple_module: Path, tmp_path: Path ) -> None: """`importlib` mode called remembers previous module (#10341, #10811).""" - module1 = import_path(simple_module, mode="importlib", root=tmp_path) - module2 = import_path(simple_module, mode="importlib", root=tmp_path) + module1 = import_path( + simple_module, + mode="importlib", + root=tmp_path, + consider_namespace_packages=False, + ) + module2 = import_path( + simple_module, + mode="importlib", + root=tmp_path, + consider_namespace_packages=False, + ) assert module1 is module2 def test_no_meta_path_found( @@ -328,7 +375,12 @@ class TestImportPath: ) -> None: """Even without any meta_path should still import module.""" monkeypatch.setattr(sys, "meta_path", []) - module = import_path(simple_module, mode="importlib", root=tmp_path) + module = import_path( + simple_module, + mode="importlib", + root=tmp_path, + consider_namespace_packages=False, + ) assert module.foo(2) == 42 # type: ignore[attr-defined] # mode='importlib' fails if no spec is found to load the module @@ -341,7 +393,12 @@ class TestImportPath: importlib.util, "spec_from_file_location", lambda *args: None ) with pytest.raises(ImportError): - import_path(simple_module, mode="importlib", root=tmp_path) + import_path( + simple_module, + mode="importlib", + root=tmp_path, + consider_namespace_packages=False, + ) def test_resolve_package_path(tmp_path: Path) -> None: @@ -477,7 +534,9 @@ def test_samefile_false_negatives(tmp_path: Path, monkeypatch: MonkeyPatch) -> N # the paths too. Using a context to narrow the patch as much as possible given # this is an important system function. mp.setattr(os.path, "samefile", lambda x, y: False) - module = import_path(module_path, root=tmp_path) + module = import_path( + module_path, root=tmp_path, consider_namespace_packages=False + ) assert getattr(module, "foo")() == 42 @@ -499,7 +558,9 @@ class TestImportLibMode: encoding="utf-8", ) - module = import_path(fn, mode="importlib", root=tmp_path) + module = import_path( + fn, mode="importlib", root=tmp_path, consider_namespace_packages=False + ) Data: Any = getattr(module, "Data") data = Data(value="foo") assert data.value == "foo" @@ -525,7 +586,9 @@ class TestImportLibMode: encoding="utf-8", ) - module = import_path(fn, mode="importlib", root=tmp_path) + module = import_path( + fn, mode="importlib", root=tmp_path, consider_namespace_packages=False + ) round_trip = getattr(module, "round_trip") action = round_trip() assert action() == 42 @@ -575,10 +638,14 @@ class TestImportLibMode: s = pickle.dumps(obj) return pickle.loads(s) - module = import_path(fn1, mode="importlib", root=tmp_path) + module = import_path( + fn1, mode="importlib", root=tmp_path, consider_namespace_packages=False + ) Data1 = getattr(module, "Data") - module = import_path(fn2, mode="importlib", root=tmp_path) + module = import_path( + fn2, mode="importlib", root=tmp_path, consider_namespace_packages=False + ) Data2 = getattr(module, "Data") assert round_trip(Data1(20)) == Data1(20) @@ -635,7 +702,7 @@ class TestImportLibMode: # If we add tmp_path to sys.path, src becomes a namespace package. monkeypatch.syspath_prepend(tmp_path) assert resolve_pkg_root_and_module_name( - models_py, consider_ns_packages=True + models_py, consider_namespace_packages=True ) == ( tmp_path, "src.app.core.models", @@ -709,7 +776,12 @@ class TestImportLibMode: encoding="ascii", ) - mod = import_path(init, root=tmp_path, mode=ImportMode.importlib) + mod = import_path( + init, + root=tmp_path, + mode=ImportMode.importlib, + consider_namespace_packages=False, + ) assert len(mod.instance.INSTANCES) == 1 def test_importlib_root_is_package(self, pytester: Pytester) -> None: @@ -828,16 +900,31 @@ class TestImportLibMode: ) # core_py is reached from sys.path, so should be imported normally. - mod = import_path(core_py, mode="importlib", root=pytester.path) + mod = import_path( + core_py, + mode="importlib", + root=pytester.path, + consider_namespace_packages=False, + ) assert mod.__name__ == "app.core" assert mod.__file__ and Path(mod.__file__) == core_py # tests are not reachable from sys.path, so they are imported as a standalone modules. # Instead of '.tests.a.test_core', we import as "_tests.a.test_core" because # importlib considers module names starting with '.' to be local imports. - mod = import_path(test_path1, mode="importlib", root=pytester.path) + mod = import_path( + test_path1, + mode="importlib", + root=pytester.path, + consider_namespace_packages=False, + ) assert mod.__name__ == "_tests.a.test_core" - mod = import_path(test_path2, mode="importlib", root=pytester.path) + mod = import_path( + test_path2, + mode="importlib", + root=pytester.path, + consider_namespace_packages=False, + ) assert mod.__name__ == "_tests.b.test_core" def test_import_using_normal_mechanism_first_integration( @@ -889,14 +976,22 @@ class TestImportLibMode: # The 'x.py' module from sys.path was not imported for sure because # otherwise we would get an AssertionError. mod = import_path( - x_in_sub_folder, mode=ImportMode.importlib, root=pytester.path + x_in_sub_folder, + mode=ImportMode.importlib, + root=pytester.path, + consider_namespace_packages=False, ) assert mod.__file__ and Path(mod.__file__) == x_in_sub_folder assert mod.X == "a/b/x" # Attempt to import root 'x.py'. with pytest.raises(AssertionError, match="x at root"): - _ = import_path(x_at_root, mode=ImportMode.importlib, root=pytester.path) + _ = import_path( + x_at_root, + mode=ImportMode.importlib, + root=pytester.path, + consider_namespace_packages=False, + ) def test_safe_exists(tmp_path: Path) -> None: @@ -979,26 +1074,33 @@ class TestNamespacePackages: ) pkg_root, module_name = resolve_pkg_root_and_module_name( - models_py, consider_ns_packages=True + models_py, consider_namespace_packages=True ) assert (pkg_root, module_name) == ( tmp_path / "src/dist1", "com.company.app.core.models", ) - mod = import_path(models_py, mode=import_mode, root=tmp_path) + mod = import_path( + models_py, mode=import_mode, root=tmp_path, consider_namespace_packages=True + ) assert mod.__name__ == "com.company.app.core.models" assert mod.__file__ == str(models_py) pkg_root, module_name = resolve_pkg_root_and_module_name( - algorithms_py, consider_ns_packages=True + algorithms_py, consider_namespace_packages=True ) assert (pkg_root, module_name) == ( tmp_path / "src/dist2", "com.company.calc.algo.algorithms", ) - mod = import_path(algorithms_py, mode=import_mode, root=tmp_path) + mod = import_path( + algorithms_py, + mode=import_mode, + root=tmp_path, + consider_namespace_packages=True, + ) assert mod.__name__ == "com.company.calc.algo.algorithms" assert mod.__file__ == str(algorithms_py) @@ -1019,7 +1121,7 @@ class TestNamespacePackages: (tmp_path / "src/dist1/com/__init__.py").touch() pkg_root, module_name = resolve_pkg_root_and_module_name( - models_py, consider_ns_packages=True + models_py, consider_namespace_packages=True ) assert (pkg_root, module_name) == ( tmp_path / "src/dist1/com/company", diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index f68f143f4..da43364f6 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -46,7 +46,10 @@ class TestPytestPluginInteractions: kwargs=dict(pluginmanager=config.pluginmanager) ) config.pluginmanager._importconftest( - conf, importmode="prepend", rootpath=pytester.path + conf, + importmode="prepend", + rootpath=pytester.path, + consider_namespace_packages=False, ) # print(config.pluginmanager.get_plugins()) res = config.hook.pytest_myhook(xyz=10) @@ -75,7 +78,10 @@ class TestPytestPluginInteractions: """ ) config.pluginmanager._importconftest( - p, importmode="prepend", rootpath=pytester.path + p, + importmode="prepend", + rootpath=pytester.path, + consider_namespace_packages=False, ) assert config.option.test123 @@ -115,6 +121,7 @@ class TestPytestPluginInteractions: conftest, importmode="prepend", rootpath=pytester.path, + consider_namespace_packages=False, ) plugin = config.pluginmanager.get_plugin(str(conftest)) assert plugin is mod @@ -123,6 +130,7 @@ class TestPytestPluginInteractions: conftest_upper_case, importmode="prepend", rootpath=pytester.path, + consider_namespace_packages=False, ) plugin_uppercase = config.pluginmanager.get_plugin(str(conftest_upper_case)) assert plugin_uppercase is mod_uppercase @@ -174,12 +182,18 @@ class TestPytestPluginInteractions: conftest2 = pytester.path.joinpath("tests/subdir/conftest.py") config.pluginmanager._importconftest( - conftest1, importmode="prepend", rootpath=pytester.path + conftest1, + importmode="prepend", + rootpath=pytester.path, + consider_namespace_packages=False, ) ihook_a = session.gethookproxy(pytester.path / "tests") assert ihook_a is not None config.pluginmanager._importconftest( - conftest2, importmode="prepend", rootpath=pytester.path + conftest2, + importmode="prepend", + rootpath=pytester.path, + consider_namespace_packages=False, ) ihook_b = session.gethookproxy(pytester.path / "tests") assert ihook_a is not ihook_b @@ -398,7 +412,9 @@ class TestPytestPluginManager: pytestpm: PytestPluginManager, ) -> None: mod = import_path( - pytester.makepyfile("pytest_plugins='xyz'"), root=pytester.path + pytester.makepyfile("pytest_plugins='xyz'"), + root=pytester.path, + consider_namespace_packages=False, ) with pytest.raises(ImportError): pytestpm.consider_conftest(mod, registration_name="unused") From aac720abc900631ebd3d1807dcaf0c297d578113 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 2 Mar 2024 12:03:08 -0300 Subject: [PATCH 14/47] testing/test_pathlib: parametrize namespace package option Test with namespace packages support even when it will not find namespace packages to ensure it will at least not give weird results or crashes. --- testing/test_pathlib.py | 149 +++++++++++++++++++++++++--------------- 1 file changed, 93 insertions(+), 56 deletions(-) diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index a5d582bc4..357860563 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -100,6 +100,15 @@ class TestFNMatcherPort: assert not fnmatch_ex(pattern, path) +@pytest.fixture(params=[True, False]) +def ns_param(request: pytest.FixtureRequest) -> bool: + """ + Simple parametrized fixture for tests which call import_path() with consider_namespace_packages + using True and False. + """ + return bool(request.param) + + class TestImportPath: """ @@ -170,32 +179,32 @@ class TestImportPath: encoding="utf-8", ) - def test_smoke_test(self, path1: Path) -> None: + def test_smoke_test(self, path1: Path, ns_param: bool) -> None: obj = import_path( - path1 / "execfile.py", root=path1, consider_namespace_packages=False + path1 / "execfile.py", root=path1, consider_namespace_packages=ns_param ) assert obj.x == 42 # type: ignore[attr-defined] assert obj.__name__ == "execfile" - def test_import_path_missing_file(self, path1: Path) -> None: + def test_import_path_missing_file(self, path1: Path, ns_param: bool) -> None: with pytest.raises(ImportPathMismatchError): import_path( - path1 / "sampledir", root=path1, consider_namespace_packages=False + path1 / "sampledir", root=path1, consider_namespace_packages=ns_param ) def test_renamed_dir_creates_mismatch( - self, tmp_path: Path, monkeypatch: MonkeyPatch + self, tmp_path: Path, monkeypatch: MonkeyPatch, ns_param: bool ) -> None: tmp_path.joinpath("a").mkdir() p = tmp_path.joinpath("a", "test_x123.py") p.touch() - import_path(p, root=tmp_path, consider_namespace_packages=False) + import_path(p, root=tmp_path, consider_namespace_packages=ns_param) tmp_path.joinpath("a").rename(tmp_path.joinpath("b")) with pytest.raises(ImportPathMismatchError): import_path( tmp_path.joinpath("b", "test_x123.py"), root=tmp_path, - consider_namespace_packages=False, + consider_namespace_packages=ns_param, ) # Errors can be ignored. @@ -203,7 +212,7 @@ class TestImportPath: import_path( tmp_path.joinpath("b", "test_x123.py"), root=tmp_path, - consider_namespace_packages=False, + consider_namespace_packages=ns_param, ) # PY_IGNORE_IMPORTMISMATCH=0 does not ignore error. @@ -212,69 +221,71 @@ class TestImportPath: import_path( tmp_path.joinpath("b", "test_x123.py"), root=tmp_path, - consider_namespace_packages=False, + consider_namespace_packages=ns_param, ) - def test_messy_name(self, tmp_path: Path) -> None: + def test_messy_name(self, tmp_path: Path, ns_param: bool) -> None: # https://bitbucket.org/hpk42/py-trunk/issue/129 path = tmp_path / "foo__init__.py" path.touch() - module = import_path(path, root=tmp_path, consider_namespace_packages=False) + module = import_path(path, root=tmp_path, consider_namespace_packages=ns_param) assert module.__name__ == "foo__init__" - def test_dir(self, tmp_path: Path) -> None: + def test_dir(self, tmp_path: Path, ns_param: bool) -> None: p = tmp_path / "hello_123" p.mkdir() p_init = p / "__init__.py" p_init.touch() - m = import_path(p, root=tmp_path, consider_namespace_packages=False) + m = import_path(p, root=tmp_path, consider_namespace_packages=ns_param) assert m.__name__ == "hello_123" - m = import_path(p_init, root=tmp_path, consider_namespace_packages=False) + m = import_path(p_init, root=tmp_path, consider_namespace_packages=ns_param) assert m.__name__ == "hello_123" - def test_a(self, path1: Path) -> None: + def test_a(self, path1: Path, ns_param: bool) -> None: otherdir = path1 / "otherdir" mod = import_path( - otherdir / "a.py", root=path1, consider_namespace_packages=False + otherdir / "a.py", root=path1, consider_namespace_packages=ns_param ) assert mod.result == "got it" # type: ignore[attr-defined] assert mod.__name__ == "otherdir.a" - def test_b(self, path1: Path) -> None: + def test_b(self, path1: Path, ns_param: bool) -> None: otherdir = path1 / "otherdir" mod = import_path( - otherdir / "b.py", root=path1, consider_namespace_packages=False + otherdir / "b.py", root=path1, consider_namespace_packages=ns_param ) assert mod.stuff == "got it" # type: ignore[attr-defined] assert mod.__name__ == "otherdir.b" - def test_c(self, path1: Path) -> None: + def test_c(self, path1: Path, ns_param: bool) -> None: otherdir = path1 / "otherdir" mod = import_path( - otherdir / "c.py", root=path1, consider_namespace_packages=False + otherdir / "c.py", root=path1, consider_namespace_packages=ns_param ) assert mod.value == "got it" # type: ignore[attr-defined] - def test_d(self, path1: Path) -> None: + def test_d(self, path1: Path, ns_param: bool) -> None: otherdir = path1 / "otherdir" mod = import_path( - otherdir / "d.py", root=path1, consider_namespace_packages=False + otherdir / "d.py", root=path1, consider_namespace_packages=ns_param ) assert mod.value2 == "got it" # type: ignore[attr-defined] - def test_import_after(self, tmp_path: Path) -> None: + def test_import_after(self, tmp_path: Path, ns_param: bool) -> None: tmp_path.joinpath("xxxpackage").mkdir() tmp_path.joinpath("xxxpackage", "__init__.py").touch() mod1path = tmp_path.joinpath("xxxpackage", "module1.py") mod1path.touch() - mod1 = import_path(mod1path, root=tmp_path, consider_namespace_packages=False) + mod1 = import_path( + mod1path, root=tmp_path, consider_namespace_packages=ns_param + ) assert mod1.__name__ == "xxxpackage.module1" from xxxpackage import module1 assert module1 is mod1 def test_check_filepath_consistency( - self, monkeypatch: MonkeyPatch, tmp_path: Path + self, monkeypatch: MonkeyPatch, tmp_path: Path, ns_param: bool ) -> None: name = "pointsback123" p = tmp_path.joinpath(name + ".py") @@ -287,7 +298,7 @@ class TestImportPath: mod.__file__ = str(pseudopath) mp.setitem(sys.modules, name, mod) newmod = import_path( - p, root=tmp_path, consider_namespace_packages=False + p, root=tmp_path, consider_namespace_packages=ns_param ) assert mod == newmod mod = ModuleType(name) @@ -296,31 +307,31 @@ class TestImportPath: mod.__file__ = str(pseudopath) monkeypatch.setitem(sys.modules, name, mod) with pytest.raises(ImportPathMismatchError) as excinfo: - import_path(p, root=tmp_path, consider_namespace_packages=False) + import_path(p, root=tmp_path, consider_namespace_packages=ns_param) modname, modfile, orig = excinfo.value.args assert modname == name assert modfile == str(pseudopath) assert orig == p assert issubclass(ImportPathMismatchError, ImportError) - def test_ensuresyspath_append(self, tmp_path: Path) -> None: + def test_ensuresyspath_append(self, tmp_path: Path, ns_param: bool) -> None: root1 = tmp_path / "root1" root1.mkdir() file1 = root1 / "x123.py" file1.touch() assert str(root1) not in sys.path import_path( - file1, mode="append", root=tmp_path, consider_namespace_packages=False + file1, mode="append", root=tmp_path, consider_namespace_packages=ns_param ) assert str(root1) == sys.path[-1] assert str(root1) not in sys.path[:-1] - def test_invalid_path(self, tmp_path: Path) -> None: + def test_invalid_path(self, tmp_path: Path, ns_param: bool) -> None: with pytest.raises(ImportError): import_path( tmp_path / "invalid.py", root=tmp_path, - consider_namespace_packages=False, + consider_namespace_packages=ns_param, ) @pytest.fixture @@ -336,14 +347,18 @@ class TestImportPath: sys.modules.pop(module_name, None) def test_importmode_importlib( - self, simple_module: Path, tmp_path: Path, request: pytest.FixtureRequest + self, + simple_module: Path, + tmp_path: Path, + request: pytest.FixtureRequest, + ns_param: bool, ) -> None: """`importlib` mode does not change sys.path.""" module = import_path( simple_module, mode="importlib", root=tmp_path, - consider_namespace_packages=False, + consider_namespace_packages=ns_param, ) assert module.foo(2) == 42 # type: ignore[attr-defined] assert str(simple_module.parent) not in sys.path @@ -353,25 +368,29 @@ class TestImportPath: assert "_src.tests" in sys.modules def test_remembers_previous_imports( - self, simple_module: Path, tmp_path: Path + self, simple_module: Path, tmp_path: Path, ns_param: bool ) -> None: """`importlib` mode called remembers previous module (#10341, #10811).""" module1 = import_path( simple_module, mode="importlib", root=tmp_path, - consider_namespace_packages=False, + consider_namespace_packages=ns_param, ) module2 = import_path( simple_module, mode="importlib", root=tmp_path, - consider_namespace_packages=False, + consider_namespace_packages=ns_param, ) assert module1 is module2 def test_no_meta_path_found( - self, simple_module: Path, monkeypatch: MonkeyPatch, tmp_path: Path + self, + simple_module: Path, + monkeypatch: MonkeyPatch, + tmp_path: Path, + ns_param: bool, ) -> None: """Even without any meta_path should still import module.""" monkeypatch.setattr(sys, "meta_path", []) @@ -379,7 +398,7 @@ class TestImportPath: simple_module, mode="importlib", root=tmp_path, - consider_namespace_packages=False, + consider_namespace_packages=ns_param, ) assert module.foo(2) == 42 # type: ignore[attr-defined] @@ -541,7 +560,9 @@ def test_samefile_false_negatives(tmp_path: Path, monkeypatch: MonkeyPatch) -> N class TestImportLibMode: - def test_importmode_importlib_with_dataclass(self, tmp_path: Path) -> None: + def test_importmode_importlib_with_dataclass( + self, tmp_path: Path, ns_param: bool + ) -> None: """Ensure that importlib mode works with a module containing dataclasses (#7856).""" fn = tmp_path.joinpath("_src/tests/test_dataclass.py") fn.parent.mkdir(parents=True) @@ -559,14 +580,16 @@ class TestImportLibMode: ) module = import_path( - fn, mode="importlib", root=tmp_path, consider_namespace_packages=False + fn, mode="importlib", root=tmp_path, consider_namespace_packages=ns_param ) Data: Any = getattr(module, "Data") data = Data(value="foo") assert data.value == "foo" assert data.__module__ == "_src.tests.test_dataclass" - def test_importmode_importlib_with_pickle(self, tmp_path: Path) -> None: + def test_importmode_importlib_with_pickle( + self, tmp_path: Path, ns_param: bool + ) -> None: """Ensure that importlib mode works with pickle (#7859).""" fn = tmp_path.joinpath("_src/tests/test_pickle.py") fn.parent.mkdir(parents=True) @@ -587,14 +610,14 @@ class TestImportLibMode: ) module = import_path( - fn, mode="importlib", root=tmp_path, consider_namespace_packages=False + fn, mode="importlib", root=tmp_path, consider_namespace_packages=ns_param ) round_trip = getattr(module, "round_trip") action = round_trip() assert action() == 42 def test_importmode_importlib_with_pickle_separate_modules( - self, tmp_path: Path + self, tmp_path: Path, ns_param: bool ) -> None: """ Ensure that importlib mode works can load pickles that look similar but are @@ -639,12 +662,12 @@ class TestImportLibMode: return pickle.loads(s) module = import_path( - fn1, mode="importlib", root=tmp_path, consider_namespace_packages=False + fn1, mode="importlib", root=tmp_path, consider_namespace_packages=ns_param ) Data1 = getattr(module, "Data") module = import_path( - fn2, mode="importlib", root=tmp_path, consider_namespace_packages=False + fn2, mode="importlib", root=tmp_path, consider_namespace_packages=ns_param ) Data2 = getattr(module, "Data") @@ -694,7 +717,9 @@ class TestImportLibMode: # Create the __init__.py files, it should now resolve to a proper module name. (tmp_path / "src/app/__init__.py").touch() (tmp_path / "src/app/core/__init__.py").touch() - assert resolve_pkg_root_and_module_name(models_py) == ( + assert resolve_pkg_root_and_module_name( + models_py, consider_namespace_packages=True + ) == ( tmp_path / "src", "app.core.models", ) @@ -707,6 +732,12 @@ class TestImportLibMode: tmp_path, "src.app.core.models", ) + assert resolve_pkg_root_and_module_name( + models_py, consider_namespace_packages=False + ) == ( + tmp_path / "src", + "app.core.models", + ) def test_insert_missing_modules( self, monkeypatch: MonkeyPatch, tmp_path: Path @@ -739,7 +770,9 @@ class TestImportLibMode: assert modules["xxx"].tests is modules["xxx.tests"] assert modules["xxx.tests"].foo is modules["xxx.tests.foo"] - def test_importlib_package(self, monkeypatch: MonkeyPatch, tmp_path: Path): + def test_importlib_package( + self, monkeypatch: MonkeyPatch, tmp_path: Path, ns_param: bool + ): """ Importing a package using --importmode=importlib should not import the package's __init__.py file more than once (#11306). @@ -780,7 +813,7 @@ class TestImportLibMode: init, root=tmp_path, mode=ImportMode.importlib, - consider_namespace_packages=False, + consider_namespace_packages=ns_param, ) assert len(mod.instance.INSTANCES) == 1 @@ -889,7 +922,7 @@ class TestImportLibMode: return (site_packages / "app/core.py"), test_path1, test_path2 def test_import_using_normal_mechanism_first( - self, monkeypatch: MonkeyPatch, pytester: Pytester + self, monkeypatch: MonkeyPatch, pytester: Pytester, ns_param: bool ) -> None: """ Test import_path imports from the canonical location when possible first, only @@ -904,7 +937,7 @@ class TestImportLibMode: core_py, mode="importlib", root=pytester.path, - consider_namespace_packages=False, + consider_namespace_packages=ns_param, ) assert mod.__name__ == "app.core" assert mod.__file__ and Path(mod.__file__) == core_py @@ -916,19 +949,19 @@ class TestImportLibMode: test_path1, mode="importlib", root=pytester.path, - consider_namespace_packages=False, + consider_namespace_packages=ns_param, ) assert mod.__name__ == "_tests.a.test_core" mod = import_path( test_path2, mode="importlib", root=pytester.path, - consider_namespace_packages=False, + consider_namespace_packages=ns_param, ) assert mod.__name__ == "_tests.b.test_core" def test_import_using_normal_mechanism_first_integration( - self, monkeypatch: MonkeyPatch, pytester: Pytester + self, monkeypatch: MonkeyPatch, pytester: Pytester, ns_param: bool ) -> None: """ Same test as above, but verify the behavior calling pytest. @@ -941,6 +974,8 @@ class TestImportLibMode: ) result = pytester.runpytest( "--import-mode=importlib", + "-o", + f"consider_namespace_packages={ns_param}", "--doctest-modules", "--pyargs", "app", @@ -955,7 +990,9 @@ class TestImportLibMode: ] ) - def test_import_path_imports_correct_file(self, pytester: Pytester) -> None: + def test_import_path_imports_correct_file( + self, pytester: Pytester, ns_param: bool + ) -> None: """ Import the module by the given path, even if other module with the same name is reachable from sys.path. @@ -979,7 +1016,7 @@ class TestImportLibMode: x_in_sub_folder, mode=ImportMode.importlib, root=pytester.path, - consider_namespace_packages=False, + consider_namespace_packages=ns_param, ) assert mod.__file__ and Path(mod.__file__) == x_in_sub_folder assert mod.X == "a/b/x" @@ -990,7 +1027,7 @@ class TestImportLibMode: x_at_root, mode=ImportMode.importlib, root=pytester.path, - consider_namespace_packages=False, + consider_namespace_packages=ns_param, ) From d6134bc21e27efee7a2e264bd089e6c223515904 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 2 Mar 2024 12:16:12 -0300 Subject: [PATCH 15/47] doc: document consider_namespace_packages option --- changelog/11475.feature.rst | 4 ++-- changelog/11475.improvement.rst | 2 ++ doc/en/explanation/pythonpath.rst | 10 ++++++++-- doc/en/reference/reference.rst | 13 +++++++++++++ 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/changelog/11475.feature.rst b/changelog/11475.feature.rst index 6b73c8158..42550235d 100644 --- a/changelog/11475.feature.rst +++ b/changelog/11475.feature.rst @@ -1,3 +1,3 @@ -pytest now correctly identifies modules that are part of `namespace packages `__, for example when importing user-level modules for doctesting. +Added the new :confval:`consider_namespace_packages` configuration option, defaulting to ``False``. -Previously pytest was not aware of namespace packages, so running a doctest from a subpackage that is part of a namespace package would import just the subpackage (for example ``app.models``) instead of its full path (for example ``com.company.app.models``). +If set to ``True``, pytest will attempt to identify modules that are part of `namespace packages `__ when importing modules. diff --git a/changelog/11475.improvement.rst b/changelog/11475.improvement.rst index fc6e8be3a..4f6a4bffa 100644 --- a/changelog/11475.improvement.rst +++ b/changelog/11475.improvement.rst @@ -1 +1,3 @@ :ref:`--import-mode=importlib ` now tries to import modules using the standard import mechanism (but still without changing :py:data:`sys.path`), falling back to importing modules directly only if that fails. + +This means that installed packages will be imported under their canonical name if possible first, for example ``app.core.models``, instead of having the module name always be derived from their path (for example ``.env310.lib.site_packages.app.core.models``). diff --git a/doc/en/explanation/pythonpath.rst b/doc/en/explanation/pythonpath.rst index 6b1d3ae2c..33eba86b5 100644 --- a/doc/en/explanation/pythonpath.rst +++ b/doc/en/explanation/pythonpath.rst @@ -10,8 +10,7 @@ Import modes pytest as a testing framework needs to import test modules and ``conftest.py`` files for execution. -Importing files in Python (at least until recently) is a non-trivial processes, often requiring -changing :data:`sys.path`. Some aspects of the +Importing files in Python is a non-trivial processes, so aspects of the import process can be controlled through the ``--import-mode`` command-line flag, which can assume these values: @@ -100,10 +99,17 @@ these values: Initially we intended to make ``importlib`` the default in future releases, however it is clear now that it has its own set of drawbacks so the default will remain ``prepend`` for the foreseeable future. +.. note:: + + By default, pytest will not attempt to resolve namespace packages automatically, but that can + be changed via the :confval:`consider_namespace_packages` configuration variable. + .. seealso:: The :confval:`pythonpath` configuration variable. + The :confval:`consider_namespace_packages` configuration variable. + :ref:`test layout`. diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index bba4a399c..f84b7ea48 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -1274,6 +1274,19 @@ passed multiple times. The expected format is ``name=value``. For example:: variables, that will be expanded. For more information about cache plugin please refer to :ref:`cache_provider`. +.. confval:: consider_namespace_packages + + Controls if pytest should attempt to identify `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. + + Only `native namespace packages `__ + are supported, with no plans to support `legacy namespace packages `__. + + .. versionadded:: 8.1 + .. confval:: console_output_style Sets the console output style while running tests: From 434282e17f5f1f4fcc1464a0a0921cf19804bdd7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 2 Mar 2024 22:43:56 +0200 Subject: [PATCH 16/47] fixtures: use exception group when multiple finalizers raise in fixture teardown Previously, if more than one fixture finalizer raised, only the first was reported, and the other errors were lost. Use an exception group to report them all. This is similar to the change we made in node teardowns (in `SetupState`). --- changelog/12047.improvement.rst | 2 ++ src/_pytest/fixtures.py | 45 ++++++++++++++++++--------------- testing/python/fixtures.py | 15 ++++++++--- 3 files changed, 38 insertions(+), 24 deletions(-) create mode 100644 changelog/12047.improvement.rst diff --git a/changelog/12047.improvement.rst b/changelog/12047.improvement.rst new file mode 100644 index 000000000..e9ad5eddc --- /dev/null +++ b/changelog/12047.improvement.rst @@ -0,0 +1,2 @@ +When multiple finalizers of a fixture raise an exception, now all exceptions are reported as an exception group. +Previously, only the first exception was reported. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 0a505d65a..b619dc358 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -7,6 +7,7 @@ import functools import inspect import os from pathlib import Path +import sys from typing import AbstractSet from typing import Any from typing import Callable @@ -67,6 +68,10 @@ from _pytest.scope import HIGH_SCOPES from _pytest.scope import Scope +if sys.version_info[:2] < (3, 11): + from exceptiongroup import BaseExceptionGroup + + if TYPE_CHECKING: from typing import Deque @@ -1017,27 +1022,25 @@ class FixtureDef(Generic[FixtureValue]): self._finalizers.append(finalizer) def finish(self, request: SubRequest) -> None: - exc = None - try: - while self._finalizers: - try: - func = self._finalizers.pop() - func() - except BaseException as e: - # XXX Only first exception will be seen by user, - # ideally all should be reported. - if exc is None: - exc = e - if exc: - raise exc - finally: - ihook = request.node.ihook - ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request) - # Even if finalization fails, we invalidate the cached fixture - # value and remove all finalizers because they may be bound methods - # which will keep instances alive. - self.cached_result = None - self._finalizers.clear() + exceptions: List[BaseException] = [] + while self._finalizers: + fin = self._finalizers.pop() + try: + fin() + except BaseException as e: + exceptions.append(e) + node = request.node + node.ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request) + # Even if finalization fails, we invalidate the cached fixture + # value and remove all finalizers because they may be bound methods + # which will keep instances alive. + self.cached_result = None + self._finalizers.clear() + if len(exceptions) == 1: + raise exceptions[0] + elif len(exceptions) > 1: + msg = f'errors while tearing down fixture "{self.argname}" of {node}' + raise BaseExceptionGroup(msg, exceptions[::-1]) def execute(self, request: SubRequest) -> FixtureValue: # Get required arguments and register our own finish() diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 299e411a6..6edff6ecd 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -932,8 +932,9 @@ class TestRequestBasic: self, pytester: Pytester ) -> None: """ - Ensure exceptions raised during teardown by a finalizer are suppressed - until all finalizers are called, re-raising the first exception (#2440) + Ensure exceptions raised during teardown by finalizers are suppressed + until all finalizers are called, then re-reaised together in an + exception group (#2440) """ pytester.makepyfile( """ @@ -960,8 +961,16 @@ class TestRequestBasic: """ ) result = pytester.runpytest() + result.assert_outcomes(passed=2, errors=1) result.stdout.fnmatch_lines( - ["*Exception: Error in excepts fixture", "* 2 passed, 1 error in *"] + [ + ' | *ExceptionGroup: errors while tearing down fixture "subrequest" of (2 sub-exceptions)', # noqa: E501 + " +-+---------------- 1 ----------------", + " | Exception: Error in something fixture", + " +---------------- 2 ----------------", + " | Exception: Error in excepts fixture", + " +------------------------------------", + ], ) def test_request_getmodulepath(self, pytester: Pytester) -> None: From 8248946a552635f5751a58c7a6dfd24e98db7404 Mon Sep 17 00:00:00 2001 From: mrbean-bremen Date: Sun, 3 Mar 2024 13:41:31 +0100 Subject: [PATCH 17/47] Do not collect symlinked tests under Windows (#12050) The check for short paths under Windows via os.path.samefile, introduced in #11936, also found similar tests in symlinked tests in the GH Actions CI. Fixes #12039. Co-authored-by: Bruno Oliveira --- AUTHORS | 1 + changelog/12039.bugfix.rst | 1 + src/_pytest/main.py | 9 ++++++++- testing/test_collection.py | 27 ++++++++++++++++++++++++++- 4 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 changelog/12039.bugfix.rst diff --git a/AUTHORS b/AUTHORS index f78c4b3f9..4c4d68df1 100644 --- a/AUTHORS +++ b/AUTHORS @@ -283,6 +283,7 @@ Mike Hoyle (hoylemd) Mike Lundy Milan Lesnek Miro Hrončok +mrbean-bremen Nathaniel Compton Nathaniel Waisbrot Ned Batchelder diff --git a/changelog/12039.bugfix.rst b/changelog/12039.bugfix.rst new file mode 100644 index 000000000..267eae6b8 --- /dev/null +++ b/changelog/12039.bugfix.rst @@ -0,0 +1 @@ +Fixed a regression in ``8.0.2`` where tests created using :fixture:`tmp_path` have been collected multiple times in CI under Windows. diff --git a/src/_pytest/main.py b/src/_pytest/main.py index b7ed72ddc..d8cd023cc 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -924,7 +924,14 @@ class Session(nodes.Collector): if sys.platform == "win32" and not is_match: # In case the file paths do not match, fallback to samefile() to # account for short-paths on Windows (#11895). - is_match = os.path.samefile(node.path, matchparts[0]) + same_file = os.path.samefile(node.path, matchparts[0]) + # We don't want to match links to the current node, + # otherwise we would match the same file more than once (#12039). + is_match = same_file and ( + os.path.islink(node.path) + == os.path.islink(matchparts[0]) + ) + # Name part e.g. `TestIt` in `/a/b/test_file.py::TestIt::test_it`. else: # TODO: Remove parametrized workaround once collection structure contains diff --git a/testing/test_collection.py b/testing/test_collection.py index fbc8543e9..1491ec859 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1765,7 +1765,7 @@ def test_does_not_crash_on_recursive_symlink(pytester: Pytester) -> None: @pytest.mark.skipif(not sys.platform.startswith("win"), reason="Windows only") def test_collect_short_file_windows(pytester: Pytester) -> None: - """Reproducer for #11895: short paths not colleced on Windows.""" + """Reproducer for #11895: short paths not collected on Windows.""" short_path = tempfile.mkdtemp() if "~" not in short_path: # pragma: no cover if running_on_ci(): @@ -1832,3 +1832,28 @@ def test_pyargs_collection_tree(pytester: Pytester, monkeypatch: MonkeyPatch) -> ], consecutive=True, ) + + +def test_do_not_collect_symlink_siblings( + pytester: Pytester, tmp_path: Path, request: pytest.FixtureRequest +) -> None: + """ + Regression test for #12039: Do not collect from directories that are symlinks to other directories in the same path. + + The check for short paths under Windows via os.path.samefile, introduced in #11936, also finds the symlinked + directory created by tmp_path/tmpdir. + """ + # Use tmp_path because it creates a symlink with the name "current" next to the directory it creates. + symlink_path = tmp_path.parent / (tmp_path.name[:-1] + "current") + assert symlink_path.is_symlink() is True + + # Create test file. + tmp_path.joinpath("test_foo.py").write_text("def test(): pass", encoding="UTF-8") + + # Ensure we collect it only once if we pass the tmp_path. + result = pytester.runpytest(tmp_path, "-sv") + result.assert_outcomes(passed=1) + + # Ensure we collect it only once if we pass the symlinked directory. + result = pytester.runpytest(symlink_path, "-sv") + result.assert_outcomes(passed=1) From 5e2ee7175c145f84ff9882be9496abb56e6e56f2 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Sun, 3 Mar 2024 13:48:29 +0100 Subject: [PATCH 18/47] monkeypatch.delenv PYTHONBREAKPOINT in two tests that previously failed/skipped --- testing/test_debugging.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/testing/test_debugging.py b/testing/test_debugging.py index 02ad700a6..91a0be481 100644 --- a/testing/test_debugging.py +++ b/testing/test_debugging.py @@ -1,5 +1,4 @@ # mypy: allow-untyped-defs -import os import sys from typing import List @@ -10,9 +9,6 @@ from _pytest.pytester import Pytester import pytest -_ENVIRON_PYTHONBREAKPOINT = os.environ.get("PYTHONBREAKPOINT", "") - - @pytest.fixture(autouse=True) def pdb_env(request): if "pytester" in request.fixturenames: @@ -959,7 +955,10 @@ class TestDebuggingBreakpoints: result = pytester.runpytest_subprocess(*args) result.stdout.fnmatch_lines(["*1 passed in *"]) - def test_pdb_custom_cls(self, pytester: Pytester, custom_debugger_hook) -> None: + def test_pdb_custom_cls( + self, pytester: Pytester, custom_debugger_hook, monkeypatch: MonkeyPatch + ) -> None: + monkeypatch.delenv("PYTHONBREAKPOINT", raising=False) p1 = pytester.makepyfile( """ def test_nothing(): @@ -1003,11 +1002,10 @@ class TestDebuggingBreakpoints: result = pytester.runpytest_subprocess(*args) result.stdout.fnmatch_lines(["*1 passed in *"]) - @pytest.mark.skipif( - not _ENVIRON_PYTHONBREAKPOINT == "", - reason="Requires breakpoint() default value", - ) - def test_sys_breakpoint_interception(self, pytester: Pytester) -> None: + def test_sys_breakpoint_interception( + self, pytester: Pytester, monkeypatch: MonkeyPatch + ) -> None: + monkeypatch.delenv("PYTHONBREAKPOINT", raising=False) p1 = pytester.makepyfile( """ def test_1(): From 82fe28dae4eec900123175cee87245f37b964e5c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 3 Mar 2024 12:50:42 +0000 Subject: [PATCH 19/47] [automated] Update plugin list (#12049) Co-authored-by: pytest bot --- doc/en/reference/plugin_list.rst | 238 +++++++++++++++++++++---------- 1 file changed, 163 insertions(+), 75 deletions(-) diff --git a/doc/en/reference/plugin_list.rst b/doc/en/reference/plugin_list.rst index e02080eb2..fb3ff912c 100644 --- a/doc/en/reference/plugin_list.rst +++ b/doc/en/reference/plugin_list.rst @@ -27,7 +27,7 @@ please refer to `the update script =7,<9) ; extra == "pytest" + :pypi:`logot` Test whether your code is logging correctly 🪵 Feb 29, 2024 5 - Production/Stable pytest (>=7,<9) ; extra == "pytest" :pypi:`nuts` Network Unit Testing System Aug 11, 2023 N/A pytest (>=7.3.0,<8.0.0) :pypi:`pytest-abq` Pytest integration for the ABQ universal test runner. Apr 07, 2023 N/A N/A :pypi:`pytest-abstracts` A contextmanager pytest fixture for handling multiple mock abstracts May 25, 2022 N/A N/A @@ -108,7 +108,7 @@ This list contains 1397 plugins. :pypi:`pytest_async` pytest-async - Run your coroutine in event loop without decorator Feb 26, 2020 N/A N/A :pypi:`pytest-async-generators` Pytest fixtures for async generators Jul 05, 2023 N/A N/A :pypi:`pytest-asyncio` Pytest support for asyncio Feb 09, 2024 4 - Beta pytest <9,>=7.0.0 - :pypi:`pytest-asyncio-cooperative` Run all your asynchronous tests cooperatively. Feb 12, 2024 N/A N/A + :pypi:`pytest-asyncio-cooperative` Run all your asynchronous tests cooperatively. Feb 25, 2024 N/A N/A :pypi:`pytest-asyncio-network-simulator` pytest-asyncio-network-simulator: Plugin for pytest for simulator the network in tests Jul 31, 2018 3 - Alpha pytest (<3.7.0,>=3.3.2) :pypi:`pytest-async-mongodb` pytest plugin for async MongoDB Oct 18, 2017 5 - Production/Stable pytest (>=2.5.2) :pypi:`pytest-async-sqlalchemy` Database testing fixtures using the SQLAlchemy asyncio API Oct 07, 2021 4 - Beta pytest (>=6.0.0) @@ -149,6 +149,7 @@ This list contains 1397 plugins. :pypi:`pytest-bench` Benchmark utility that plugs into pytest. Jul 21, 2014 3 - Alpha N/A :pypi:`pytest-benchmark` A \`\`pytest\`\` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer. Oct 25, 2022 5 - Production/Stable pytest (>=3.8) :pypi:`pytest-better-datadir` A small example package Mar 13, 2023 N/A N/A + :pypi:`pytest-better-parametrize` Better description of parametrized test cases Feb 26, 2024 4 - Beta pytest >=6.2.0 :pypi:`pytest-bg-process` Pytest plugin to initialize background process Jan 24, 2022 4 - Beta pytest (>=3.5.0) :pypi:`pytest-bigchaindb` A BigchainDB plugin for pytest. Jan 24, 2022 4 - Beta N/A :pypi:`pytest-bigquery-mock` Provides a mock fixture for python bigquery client Dec 28, 2022 N/A pytest (>=5.0) @@ -181,7 +182,7 @@ This list contains 1397 plugins. :pypi:`pytest-bugzilla-notifier` A plugin that allows you to execute create, update, and read information from BugZilla bugs Jun 15, 2018 4 - Beta pytest (>=2.9.2) :pypi:`pytest-buildkite` Plugin for pytest that automatically publishes coverage and pytest report annotations to Buildkite. Jul 13, 2019 4 - Beta pytest (>=3.5.0) :pypi:`pytest-builtin-types` Nov 17, 2021 N/A pytest - :pypi:`pytest-bwrap` Run your tests in Bubblewrap sandboxes Oct 26, 2018 3 - Alpha N/A + :pypi:`pytest-bwrap` Run your tests in Bubblewrap sandboxes Feb 25, 2024 3 - Alpha N/A :pypi:`pytest-cache` pytest plugin with mechanisms for caching across test runs Jun 04, 2013 3 - Alpha N/A :pypi:`pytest-cache-assert` Cache assertion data to simplify regression testing of complex serializable data Aug 14, 2023 5 - Production/Stable pytest (>=6.0.0) :pypi:`pytest-cagoule` Pytest plugin to only run tests affected by changes Jan 01, 2020 3 - Alpha N/A @@ -197,6 +198,7 @@ This list contains 1397 plugins. :pypi:`pytest-catchlog` py.test plugin to catch log messages. This is a fork of pytest-capturelog. Jan 24, 2016 4 - Beta pytest (>=2.6) :pypi:`pytest-catch-server` Pytest plugin with server for catching HTTP requests. Dec 12, 2019 5 - Production/Stable N/A :pypi:`pytest-celery` pytest-celery a shim pytest plugin to enable celery.contrib.pytest Feb 12, 2024 N/A N/A + :pypi:`pytest-cfg-fetcher` Pass config options to your unit tests. Feb 26, 2024 N/A N/A :pypi:`pytest-chainmaker` pytest plugin for chainmaker Oct 15, 2021 N/A N/A :pypi:`pytest-chalice` A set of py.test fixtures for AWS Chalice Jul 01, 2020 4 - Beta N/A :pypi:`pytest-change-assert` 修改报错中文为英文 Oct 19, 2022 N/A N/A @@ -291,7 +293,7 @@ This list contains 1397 plugins. :pypi:`pytest-custom-scheduling` Custom grouping for pytest-xdist, rename test cases name and test cases nodeid, support allure report Mar 01, 2021 N/A N/A :pypi:`pytest-cython` A plugin for testing Cython extension modules Feb 16, 2023 5 - Production/Stable pytest (>=4.6.0) :pypi:`pytest-cython-collect` Jun 17, 2022 N/A pytest - :pypi:`pytest-darker` A pytest plugin for checking of modified code using Darker Feb 24, 2024 N/A pytest <8,>=6.0.1 + :pypi:`pytest-darker` A pytest plugin for checking of modified code using Darker Feb 25, 2024 N/A pytest <7,>=6.0.1 :pypi:`pytest-dash` pytest fixtures to run dash applications. Mar 18, 2019 N/A N/A :pypi:`pytest-data` Useful functions for managing data for pytest fixtures Nov 01, 2016 5 - Production/Stable N/A :pypi:`pytest-databricks` Pytest plugin for remote Databricks notebooks testing Jul 29, 2020 N/A pytest @@ -373,6 +375,7 @@ This list contains 1397 plugins. :pypi:`pytest-docker-butla` Jun 16, 2019 3 - Alpha N/A :pypi:`pytest-dockerc` Run, manage and stop Docker Compose project from Docker API Oct 09, 2020 5 - Production/Stable pytest (>=3.0) :pypi:`pytest-docker-compose` Manages Docker containers during your integration tests Jan 26, 2021 5 - Production/Stable pytest (>=3.3) + :pypi:`pytest-docker-compose-v2` Manages Docker containers during your integration tests Feb 28, 2024 4 - Beta pytest<8,>=7.2.2 :pypi:`pytest-docker-db` A plugin to use docker databases for pytests Mar 20, 2021 5 - Production/Stable pytest (>=3.1.1) :pypi:`pytest-docker-fixtures` pytest docker fixtures Nov 17, 2023 3 - Alpha N/A :pypi:`pytest-docker-git-fixtures` Pytest fixtures for testing with git scm. Feb 09, 2022 4 - Beta pytest @@ -389,6 +392,7 @@ This list contains 1397 plugins. :pypi:`pytest-doctest-custom` A py.test plugin for customizing string representations of doctest results. Jul 25, 2016 4 - Beta N/A :pypi:`pytest-doctest-ellipsis-markers` Setup additional values for ELLIPSIS_MARKER for doctests Jan 12, 2018 4 - Beta N/A :pypi:`pytest-doctest-import` A simple pytest plugin to import names and add them to the doctest namespace. Nov 13, 2018 4 - Beta pytest (>=3.3.0) + :pypi:`pytest-doctest-mkdocstrings` Run pytest --doctest-modules with markdown docstrings in code blocks (\`\`\`) Mar 02, 2024 N/A pytest :pypi:`pytest-doctestplus` Pytest plugin with advanced doctest features. Dec 13, 2023 5 - Production/Stable pytest >=4.6 :pypi:`pytest-dogu-report` pytest plugin for dogu report Jul 07, 2023 N/A N/A :pypi:`pytest-dogu-sdk` pytest plugin for the Dogu Dec 14, 2023 N/A N/A @@ -422,14 +426,14 @@ This list contains 1397 plugins. :pypi:`pytest-eliot` An eliot plugin for pytest. Aug 31, 2022 1 - Planning pytest (>=5.4.0) :pypi:`pytest-elk-reporter` A simple plugin to use with pytest Jan 24, 2021 4 - Beta pytest (>=3.5.0) :pypi:`pytest-email` Send execution result email Jul 08, 2020 N/A pytest - :pypi:`pytest-embedded` A pytest plugin that designed for embedded testing. Feb 23, 2024 5 - Production/Stable pytest>=7.0 - :pypi:`pytest-embedded-arduino` Make pytest-embedded plugin work with Arduino. Feb 23, 2024 5 - Production/Stable N/A - :pypi:`pytest-embedded-idf` Make pytest-embedded plugin work with ESP-IDF. Feb 23, 2024 5 - Production/Stable N/A - :pypi:`pytest-embedded-jtag` Make pytest-embedded plugin work with JTAG. Feb 23, 2024 5 - Production/Stable N/A - :pypi:`pytest-embedded-qemu` Make pytest-embedded plugin work with QEMU. Feb 23, 2024 5 - Production/Stable N/A - :pypi:`pytest-embedded-serial` Make pytest-embedded plugin work with Serial. Feb 23, 2024 5 - Production/Stable N/A - :pypi:`pytest-embedded-serial-esp` Make pytest-embedded plugin work with Espressif target boards. Feb 23, 2024 5 - Production/Stable N/A - :pypi:`pytest-embedded-wokwi` Make pytest-embedded plugin work with the Wokwi CLI. Feb 23, 2024 5 - Production/Stable N/A + :pypi:`pytest-embedded` A pytest plugin that designed for embedded testing. Mar 01, 2024 5 - Production/Stable pytest>=7.0 + :pypi:`pytest-embedded-arduino` Make pytest-embedded plugin work with Arduino. Mar 01, 2024 5 - Production/Stable N/A + :pypi:`pytest-embedded-idf` Make pytest-embedded plugin work with ESP-IDF. Mar 01, 2024 5 - Production/Stable N/A + :pypi:`pytest-embedded-jtag` Make pytest-embedded plugin work with JTAG. Mar 01, 2024 5 - Production/Stable N/A + :pypi:`pytest-embedded-qemu` Make pytest-embedded plugin work with QEMU. Mar 01, 2024 5 - Production/Stable N/A + :pypi:`pytest-embedded-serial` Make pytest-embedded plugin work with Serial. Mar 01, 2024 5 - Production/Stable N/A + :pypi:`pytest-embedded-serial-esp` Make pytest-embedded plugin work with Espressif target boards. Mar 01, 2024 5 - Production/Stable N/A + :pypi:`pytest-embedded-wokwi` Make pytest-embedded plugin work with the Wokwi CLI. Mar 01, 2024 5 - Production/Stable N/A :pypi:`pytest-embrace` 💝 Dataclasses-as-tests. Describe the runtime once and multiply coverage with no boilerplate. Mar 25, 2023 N/A pytest (>=7.0,<8.0) :pypi:`pytest-emoji` A pytest plugin that adds emojis to your test result report Feb 19, 2019 4 - Beta pytest (>=4.2.1) :pypi:`pytest-emoji-output` Pytest plugin to represent test output with emoji support Apr 09, 2023 4 - Beta pytest (==7.0.1) @@ -551,7 +555,7 @@ This list contains 1397 plugins. :pypi:`pytest-gather-fixtures` set up asynchronous pytest fixtures concurrently Apr 12, 2022 N/A pytest (>=6.0.0) :pypi:`pytest-gc` The garbage collector plugin for py.test Feb 01, 2018 N/A N/A :pypi:`pytest-gcov` Uses gcov to measure test coverage of a C library Feb 01, 2018 3 - Alpha N/A - :pypi:`pytest-gcs` GCS fixtures and fixture factories for Pytest. Feb 18, 2024 5 - Production/Stable pytest >=6.2 + :pypi:`pytest-gcs` GCS fixtures and fixture factories for Pytest. Mar 01, 2024 5 - Production/Stable pytest >=6.2 :pypi:`pytest-gee` The Python plugin for your GEE based packages. Feb 15, 2024 3 - Alpha pytest :pypi:`pytest-gevent` Ensure that gevent is properly patched when invoking pytest Feb 25, 2020 N/A pytest :pypi:`pytest-gherkin` A flexible framework for executing BDD gherkin tests Jul 27, 2019 3 - Alpha pytest (>=5.0.0) @@ -596,7 +600,7 @@ This list contains 1397 plugins. :pypi:`pytest-history` Pytest plugin to keep a history of your pytest runs Jan 14, 2024 N/A pytest (>=7.4.3,<8.0.0) :pypi:`pytest-home` Home directory fixtures Oct 09, 2023 5 - Production/Stable pytest :pypi:`pytest-homeassistant` A pytest plugin for use with homeassistant custom components. Aug 12, 2020 4 - Beta N/A - :pypi:`pytest-homeassistant-custom-component` Experimental package to automatically extract test plugins for Home Assistant custom components Feb 24, 2024 3 - Alpha pytest ==7.4.4 + :pypi:`pytest-homeassistant-custom-component` Experimental package to automatically extract test plugins for Home Assistant custom components Mar 01, 2024 3 - Alpha pytest ==8.0.2 :pypi:`pytest-honey` A simple plugin to use with pytest Jan 07, 2022 4 - Beta pytest (>=3.5.0) :pypi:`pytest-honors` Report on tests that honor constraints, and guard against regressions Mar 06, 2020 4 - Beta N/A :pypi:`pytest-hot-reloading` Jan 06, 2024 N/A N/A @@ -619,7 +623,7 @@ This list contains 1397 plugins. :pypi:`pytest-httpdbg` A pytest plugin to record HTTP(S) requests with stack trace Jan 10, 2024 3 - Alpha pytest >=7.0.0 :pypi:`pytest-http-mocker` Pytest plugin for http mocking (via https://github.com/vilus/mocker) Oct 20, 2019 N/A N/A :pypi:`pytest-httpretty` A thin wrapper of HTTPretty for pytest Feb 16, 2014 3 - Alpha N/A - :pypi:`pytest_httpserver` pytest-httpserver is a httpserver for pytest Feb 13, 2024 3 - Alpha N/A + :pypi:`pytest_httpserver` pytest-httpserver is a httpserver for pytest Feb 24, 2024 3 - Alpha N/A :pypi:`pytest-httptesting` http_testing framework on top of pytest Jul 24, 2023 N/A pytest (>=7.2.0,<8.0.0) :pypi:`pytest-httpx` Send responses to httpx. Feb 21, 2024 5 - Production/Stable pytest <9,>=7 :pypi:`pytest-httpx-blockage` Disable httpx requests during a test run Feb 16, 2023 N/A pytest (>=7.2.1) @@ -650,6 +654,7 @@ This list contains 1397 plugins. :pypi:`pytest-inmanta-lsm` Common fixtures for inmanta LSM related modules Feb 20, 2024 5 - Production/Stable N/A :pypi:`pytest-inmanta-yang` Common fixtures used in inmanta yang related modules Feb 22, 2024 4 - Beta pytest :pypi:`pytest-Inomaly` A simple image diff plugin for pytest Feb 13, 2018 4 - Beta N/A + :pypi:`pytest-in-robotframework` The extension enables easy execution of pytest tests within the Robot Framework environment. Mar 02, 2024 N/A pytest :pypi:`pytest-insper` Pytest plugin for courses at Insper Feb 01, 2024 N/A pytest :pypi:`pytest-insta` A practical snapshot testing plugin for pytest Feb 19, 2024 N/A pytest (>=7.2.0,<9.0.0) :pypi:`pytest-instafail` pytest plugin to show failures instantly Mar 31, 2023 4 - Beta pytest (>=5) @@ -659,7 +664,7 @@ This list contains 1397 plugins. :pypi:`pytest-interactive` A pytest plugin for console based interactive test selection just after the collection phase Nov 30, 2017 3 - Alpha N/A :pypi:`pytest-intercept-remote` Pytest plugin for intercepting outgoing connection requests during pytest run. May 24, 2021 4 - Beta pytest (>=4.6) :pypi:`pytest-interface-tester` Pytest plugin for checking charm relation interface protocol compliance. Feb 09, 2024 4 - Beta pytest - :pypi:`pytest-invenio` Pytest fixtures for Invenio. Jan 29, 2024 5 - Production/Stable pytest <7.2.0,>=6 + :pypi:`pytest-invenio` Pytest fixtures for Invenio. Feb 28, 2024 5 - Production/Stable pytest <7.2.0,>=6 :pypi:`pytest-involve` Run tests covering a specific file or changeset Feb 02, 2020 4 - Beta pytest (>=3.5.0) :pypi:`pytest-ipdb` A py.test plug-in to enable drop to ipdb debugger on test failure. Mar 20, 2013 2 - Pre-Alpha N/A :pypi:`pytest-ipynb` THIS PROJECT IS ABANDONED Jan 29, 2019 3 - Alpha N/A @@ -700,6 +705,7 @@ This list contains 1397 plugins. :pypi:`pytest-koopmans` A plugin for testing the koopmans package Nov 21, 2022 4 - Beta pytest (>=3.5.0) :pypi:`pytest-krtech-common` pytest krtech common library Nov 28, 2016 4 - Beta N/A :pypi:`pytest-kubernetes` Sep 14, 2023 N/A pytest (>=7.2.1,<8.0.0) + :pypi:`pytest-kuunda` pytest plugin to help with test data setup for PySpark tests Feb 25, 2024 4 - Beta pytest >=6.2.0 :pypi:`pytest-kwparametrize` Alternate syntax for @pytest.mark.parametrize with test cases as dictionaries and default value fallbacks Jan 22, 2021 N/A pytest (>=6) :pypi:`pytest-lambda` Define pytest fixtures with lambda functions. Aug 20, 2022 3 - Alpha pytest (>=3.6,<8) :pypi:`pytest-lamp` Jan 06, 2017 3 - Alpha N/A @@ -712,6 +718,7 @@ This list contains 1397 plugins. :pypi:`pytest-ldap` python-ldap fixtures for pytest Aug 18, 2020 N/A pytest :pypi:`pytest-leak-finder` Find the test that's leaking before the one that fails Feb 15, 2023 4 - Beta pytest (>=3.5.0) :pypi:`pytest-leaks` A pytest plugin to trace resource leaks. Nov 27, 2019 1 - Planning N/A + :pypi:`pytest-leaping` Coming soon! Mar 02, 2024 N/A N/A :pypi:`pytest-level` Select tests of a given level or lower Oct 21, 2019 N/A pytest :pypi:`pytest-libfaketime` A python-libfaketime plugin for pytest. Dec 22, 2018 4 - Beta pytest (>=3.0.0) :pypi:`pytest-libiio` A pytest plugin to manage interfacing with libiio contexts Dec 22, 2023 4 - Beta N/A @@ -753,7 +760,7 @@ This list contains 1397 plugins. :pypi:`pytest-markfiltration` UNKNOWN Nov 08, 2011 3 - Alpha N/A :pypi:`pytest-mark-no-py3` pytest plugin and bowler codemod to help migrate tests to Python 3 May 17, 2019 N/A pytest :pypi:`pytest-marks` UNKNOWN Nov 23, 2012 3 - Alpha N/A - :pypi:`pytest-matcher` Keep a ChangeLog Jan 15, 2024 5 - Production/Stable pytest + :pypi:`pytest-matcher` Keep a ChangeLog Feb 29, 2024 5 - Production/Stable pytest :pypi:`pytest-match-skip` Skip matching marks. Matches partial marks using wildcards. May 15, 2019 4 - Beta pytest (>=4.4.1) :pypi:`pytest-mat-report` this is report Jan 20, 2021 N/A N/A :pypi:`pytest-matrix` Provide tools for generating tests from combinations of fixtures. Jun 24, 2020 5 - Production/Stable pytest (>=5.4.3,<6.0.0) @@ -780,6 +787,7 @@ This list contains 1397 plugins. :pypi:`pytest-mini` A plugin to test mp Feb 06, 2023 N/A pytest (>=7.2.0,<8.0.0) :pypi:`pytest-minio-mock` A pytest plugin for mocking Minio S3 interactions Jan 04, 2024 N/A pytest >=5.0.0 :pypi:`pytest-missing-fixtures` Pytest plugin that creates missing fixtures Oct 14, 2020 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-mitmproxy` pytest plugin for mitmproxy tests Feb 28, 2024 N/A pytest >=7.0 :pypi:`pytest-ml` Test your machine learning! May 04, 2019 4 - Beta N/A :pypi:`pytest-mocha` pytest plugin to display test execution output like a mochajs Apr 02, 2020 4 - Beta pytest (>=5.4.0) :pypi:`pytest-mock` Thin-wrapper around the mock package for easier use with pytest Oct 19, 2023 5 - Production/Stable pytest >=5.0 @@ -792,6 +800,7 @@ This list contains 1397 plugins. :pypi:`pytest-mock-server` Mock server plugin for pytest Jan 09, 2022 4 - Beta pytest (>=3.5.0) :pypi:`pytest-mockservers` A set of fixtures to test your requests to HTTP/UDP servers Mar 31, 2020 N/A pytest (>=4.3.0) :pypi:`pytest-mocktcp` A pytest plugin for testing TCP clients Oct 11, 2022 N/A pytest + :pypi:`pytest-modalt` Massively distributed pytest runs using modal.com Feb 27, 2024 4 - Beta pytest >=6.2.0 :pypi:`pytest-modified-env` Pytest plugin to fail a test if it leaves modified \`os.environ\` afterwards. Jan 29, 2022 4 - Beta N/A :pypi:`pytest-modifyjunit` Utility for adding additional properties to junit xml for IDM QE Jan 10, 2019 N/A N/A :pypi:`pytest-modifyscope` pytest plugin to modify fixture scope Apr 12, 2020 N/A pytest @@ -818,9 +827,9 @@ This list contains 1397 plugins. :pypi:`pytest-my-cool-lib` Nov 02, 2023 N/A pytest (>=7.1.3,<8.0.0) :pypi:`pytest-mypy` Mypy static type checker plugin for Pytest Dec 18, 2022 4 - Beta pytest (>=6.2) ; python_version >= "3.10" :pypi:`pytest-mypyd` Mypy static type checker plugin for Pytest Aug 20, 2019 4 - Beta pytest (<4.7,>=2.8) ; python_version < "3.5" - :pypi:`pytest-mypy-plugins` pytest plugin for writing tests for mypy plugins Jul 25, 2023 4 - Beta pytest (>=7.0.0) + :pypi:`pytest-mypy-plugins` pytest plugin for writing tests for mypy plugins Feb 29, 2024 4 - Beta pytest >=7.0.0 :pypi:`pytest-mypy-plugins-shim` Substitute for "pytest-mypy-plugins" for Python implementations which aren't supported by mypy. Apr 12, 2021 N/A pytest>=6.0.0 - :pypi:`pytest-mypy-testing` Pytest plugin to check mypy output. Feb 25, 2023 N/A pytest>=7,<8 + :pypi:`pytest-mypy-testing` Pytest plugin to check mypy output. Feb 26, 2024 N/A pytest>=7,<9 :pypi:`pytest-mysql` MySQL process and client fixtures for pytest Oct 30, 2023 5 - Production/Stable pytest >=6.2 :pypi:`pytest-ndb` pytest notebook debugger Oct 15, 2023 N/A pytest :pypi:`pytest-needle` pytest plugin for visual testing websites using selenium Dec 10, 2018 4 - Beta pytest (<5.0.0,>=3.0.0) @@ -848,7 +857,7 @@ This list contains 1397 plugins. :pypi:`pytest_notify` Get notifications when your tests ends Jul 05, 2017 N/A pytest>=3.0.0 :pypi:`pytest-notimplemented` Pytest markers for not implemented features and tests. Aug 27, 2019 N/A pytest (>=5.1,<6.0) :pypi:`pytest-notion` A PyTest Reporter to send test runs to Notion.so Aug 07, 2019 N/A N/A - :pypi:`pytest-nunit` A pytest plugin for generating NUnit3 test result XML output Feb 13, 2024 5 - Production/Stable N/A + :pypi:`pytest-nunit` A pytest plugin for generating NUnit3 test result XML output Feb 26, 2024 5 - Production/Stable N/A :pypi:`pytest-oar` PyTest plugin for the OAR testing framework May 02, 2023 N/A pytest>=6.0.1 :pypi:`pytest-object-getter` Import any object from a 3rd party module while mocking its namespace on demand. Jul 31, 2022 5 - Production/Stable pytest :pypi:`pytest-ochrus` pytest results data-base and HTML reporter Feb 21, 2018 4 - Beta N/A @@ -923,9 +932,9 @@ This list contains 1397 plugins. :pypi:`pytest-play` pytest plugin that let you automate actions and assertions with test metrics reporting executing plain YAML files Jun 12, 2019 5 - Production/Stable N/A :pypi:`pytest-playbook` Pytest plugin for reading playbooks. Jan 21, 2021 3 - Alpha pytest (>=6.1.2,<7.0.0) :pypi:`pytest-playwright` A pytest wrapper with fixtures for Playwright to automate web browsers Feb 02, 2024 N/A pytest (<9.0.0,>=6.2.4) - :pypi:`pytest-playwright-async` ASYNC Pytest plugin for Playwright Feb 06, 2024 N/A N/A + :pypi:`pytest_playwright_async` ASYNC Pytest plugin for Playwright Feb 25, 2024 N/A N/A :pypi:`pytest-playwright-asyncio` Aug 29, 2023 N/A N/A - :pypi:`pytest-playwright-enhanced` A pytest plugin for playwright python Feb 24, 2024 N/A pytest (>=8.0.0,<9.0.0) + :pypi:`pytest-playwright-enhanced` A pytest plugin for playwright python Mar 02, 2024 N/A pytest (>=8.0.0,<9.0.0) :pypi:`pytest-playwrights` A pytest wrapper with fixtures for Playwright to automate web browsers Dec 02, 2021 N/A N/A :pypi:`pytest-playwright-snapshot` A pytest wrapper for snapshot testing with playwright Aug 19, 2021 N/A N/A :pypi:`pytest-playwright-visual` A pytest fixture for visual testing with Playwright Apr 28, 2022 N/A N/A @@ -988,9 +997,10 @@ This list contains 1397 plugins. :pypi:`pytest-pyspec` A plugin that transforms the pytest output into a result similar to the RSpec. It enables the use of docstrings to display results and also enables the use of the prefixes "describe", "with" and "it". Jan 02, 2024 N/A pytest (>=7.2.1,<8.0.0) :pypi:`pytest-pystack` Plugin to run pystack after a timeout for a test suite. Jan 04, 2024 N/A pytest >=3.5.0 :pypi:`pytest-pytestrail` Pytest plugin for interaction with TestRail Aug 27, 2020 4 - Beta pytest (>=3.8.0) - :pypi:`pytest-pythonhashseed` Pytest plugin to set PYTHONHASHSEED env var. Feb 18, 2024 4 - Beta pytest>=3.0.0 + :pypi:`pytest-pythonhashseed` Pytest plugin to set PYTHONHASHSEED env var. Feb 25, 2024 4 - Beta pytest>=3.0.0 :pypi:`pytest-pythonpath` pytest plugin for adding to the PYTHONPATH from command line or configs. Feb 10, 2022 5 - Production/Stable pytest (<7,>=2.5.2) :pypi:`pytest-pytorch` pytest plugin for a better developer experience when working with the PyTorch test suite May 25, 2021 4 - Beta pytest + :pypi:`pytest-pyvenv` A package for create venv in tests Feb 27, 2024 N/A pytest ; extra == 'test' :pypi:`pytest-pyvista` Pytest-pyvista package Sep 29, 2023 4 - Beta pytest>=3.5.0 :pypi:`pytest-qaseio` Pytest plugin for Qase.io integration Sep 12, 2023 4 - Beta pytest (>=7.2.2,<8.0.0) :pypi:`pytest-qasync` Pytest support for qasync. Jul 12, 2021 4 - Beta pytest (>=5.4.0) @@ -1016,7 +1026,7 @@ This list contains 1397 plugins. :pypi:`pytest-randomness` Pytest plugin about random seed management May 30, 2019 3 - Alpha N/A :pypi:`pytest-random-num` Randomise the order in which pytest tests are run with some control over the randomness Oct 19, 2020 5 - Production/Stable N/A :pypi:`pytest-random-order` Randomise the order in which pytest tests are run with some control over the randomness Jan 20, 2024 5 - Production/Stable pytest >=3.0.0 - :pypi:`pytest-ranking` A Pytest plugin for automatically prioritizing/ranking tests to speed up failure detection Feb 17, 2024 4 - Beta pytest >=7.4.3 + :pypi:`pytest-ranking` A Pytest plugin for automatically prioritizing/ranking tests to speed up failure detection Mar 01, 2024 4 - Beta pytest >=7.4.3 :pypi:`pytest-readme` Test your README.md file Sep 02, 2022 5 - Production/Stable N/A :pypi:`pytest-reana` Pytest fixtures for REANA. Nov 30, 2023 3 - Alpha N/A :pypi:`pytest-recorder` Pytest plugin, meant to facilitate unit tests writing for tools consumming Web APIs. Nov 21, 2023 N/A N/A @@ -1030,7 +1040,7 @@ This list contains 1397 plugins. :pypi:`pytest-regex` Select pytest tests with regular expressions May 29, 2023 4 - Beta pytest (>=3.5.0) :pypi:`pytest-regex-dependency` Management of Pytest dependencies via regex patterns Jun 12, 2022 N/A pytest :pypi:`pytest-regressions` Easy to use fixtures to write regression tests. Aug 31, 2023 5 - Production/Stable pytest >=6.2.0 - :pypi:`pytest-regtest` "pytest plugin for snapshot regression testing" Jan 22, 2024 N/A pytest>7.2 + :pypi:`pytest-regtest` pytest plugin for snapshot regression testing Feb 26, 2024 N/A pytest>7.2 :pypi:`pytest-relative-order` a pytest plugin that sorts tests using "before" and "after" markers May 17, 2021 4 - Beta N/A :pypi:`pytest-relaxed` Relaxed test discovery/organization for pytest May 23, 2023 5 - Production/Stable pytest (>=7) :pypi:`pytest-remfiles` Pytest plugin to create a temporary directory with remote files Jul 01, 2019 5 - Production/Stable N/A @@ -1043,15 +1053,15 @@ This list contains 1397 plugins. :pypi:`pytest-replay` Saves previous test runs and allow re-execute previous pytest runs to reproduce crashes or flaky tests Jan 11, 2024 5 - Production/Stable pytest :pypi:`pytest-repo-health` A pytest plugin to report on repository standards conformance Apr 17, 2023 3 - Alpha pytest :pypi:`pytest-report` Creates json report that is compatible with atom.io's linter message format May 11, 2016 4 - Beta N/A - :pypi:`pytest-reporter` Generate Pytest reports with templates Jul 22, 2021 4 - Beta pytest - :pypi:`pytest-reporter-html1` A basic HTML report template for Pytest Jun 05, 2023 4 - Beta N/A + :pypi:`pytest-reporter` Generate Pytest reports with templates Feb 28, 2024 4 - Beta pytest + :pypi:`pytest-reporter-html1` A basic HTML report template for Pytest Feb 28, 2024 4 - Beta N/A :pypi:`pytest-reporter-html-dots` A basic HTML report for pytest using Jinja2 template engine. Jan 22, 2023 N/A N/A :pypi:`pytest-reportinfra` Pytest plugin for reportinfra Aug 11, 2019 3 - Alpha N/A :pypi:`pytest-reporting` A plugin to report summarized results in a table format Oct 25, 2019 4 - Beta pytest (>=3.5.0) :pypi:`pytest-reportlog` Replacement for the --resultlog option, focused in simplicity and extensibility May 22, 2023 3 - Alpha pytest :pypi:`pytest-report-me` A pytest plugin to generate report. Dec 31, 2020 N/A pytest :pypi:`pytest-report-parameters` pytest plugin for adding tests' parameters to junit report Jun 18, 2020 3 - Alpha pytest (>=2.4.2) - :pypi:`pytest-reportportal` Agent for Reporting results of tests to the Report Portal Feb 05, 2024 N/A pytest <8.0.0,>=3.8.0 + :pypi:`pytest-reportportal` Agent for Reporting results of tests to the Report Portal Mar 01, 2024 N/A pytest >=3.8.0 :pypi:`pytest-report-stream` A pytest plugin which allows to stream test reports at runtime Oct 22, 2023 4 - Beta N/A :pypi:`pytest-reqs` pytest plugin to check pinned requirements May 12, 2019 N/A pytest (>=2.4.2) :pypi:`pytest-requests` A simple plugin to use with pytest Jun 24, 2019 4 - Beta pytest (>=3.5.0) @@ -1072,7 +1082,7 @@ This list contains 1397 plugins. :pypi:`pytest-responses` py.test integration for responses Oct 11, 2022 N/A pytest (>=2.5) :pypi:`pytest-rest-api` Aug 08, 2022 N/A pytest (>=7.1.2,<8.0.0) :pypi:`pytest-restrict` Pytest plugin to restrict the test types allowed Jul 10, 2023 5 - Production/Stable pytest - :pypi:`pytest-result-log` A pytest plugin that records the start, end, and result information of each use case in a log file Jan 10, 2024 N/A pytest>=7.2.0 + :pypi:`pytest-result-log` A pytest plugin that records the start, end, and result information of each use case in a log file Feb 27, 2024 N/A pytest>=7.2.0 :pypi:`pytest-result-sender` Apr 20, 2023 N/A pytest>=7.3.1 :pypi:`pytest-resume` A Pytest plugin to resuming from the last run test Apr 22, 2023 4 - Beta pytest (>=7.0) :pypi:`pytest-rethinkdb` A RethinkDB plugin for pytest. Jul 24, 2016 4 - Beta N/A @@ -1088,7 +1098,7 @@ This list contains 1397 plugins. :pypi:`pytest-rmsis` Sycronise pytest results to Jira RMsis Aug 10, 2022 N/A pytest (>=5.3.5) :pypi:`pytest-rng` Fixtures for seeding tests and making randomness reproducible Aug 08, 2019 5 - Production/Stable pytest :pypi:`pytest-roast` pytest plugin for ROAST configuration override and fixtures Nov 09, 2022 5 - Production/Stable pytest - :pypi:`pytest_robotframework` a pytest plugin that can run both python and robotframework tests while generating robot reports for them Feb 22, 2024 N/A pytest<9,>=7 + :pypi:`pytest_robotframework` a pytest plugin that can run both python and robotframework tests while generating robot reports for them Feb 27, 2024 N/A pytest<9,>=7 :pypi:`pytest-rocketchat` Pytest to Rocket.Chat reporting plugin Apr 18, 2021 5 - Production/Stable N/A :pypi:`pytest-rotest` Pytest integration with rotest Sep 08, 2019 N/A pytest (>=3.5.0) :pypi:`pytest-rpc` Extend py.test for RPC OpenStack testing. Feb 22, 2019 4 - Beta pytest (~=3.6) @@ -1112,7 +1122,7 @@ This list contains 1397 plugins. :pypi:`pytest-sanity` Dec 07, 2020 N/A N/A :pypi:`pytest-sa-pg` May 14, 2019 N/A N/A :pypi:`pytest_sauce` pytest_sauce provides sane and helpful methods worked out in clearcode to run py.test tests with selenium/saucelabs Jul 14, 2014 3 - Alpha N/A - :pypi:`pytest-sbase` A complete web automation framework for end-to-end testing. Feb 23, 2024 5 - Production/Stable N/A + :pypi:`pytest-sbase` A complete web automation framework for end-to-end testing. Mar 01, 2024 5 - Production/Stable N/A :pypi:`pytest-scenario` pytest plugin for test scenarios Feb 06, 2017 3 - Alpha N/A :pypi:`pytest-schedule` The job of test scheduling for humans. Jan 07, 2023 5 - Production/Stable N/A :pypi:`pytest-schema` 👍 Validate return values against a schema-like object in testing Feb 16, 2024 5 - Production/Stable pytest >=3.5.0 @@ -1121,7 +1131,7 @@ This list contains 1397 plugins. :pypi:`pytest-select` A pytest plugin which allows to (de-)select tests from a file. Jan 18, 2019 3 - Alpha pytest (>=3.0) :pypi:`pytest-selenium` pytest plugin for Selenium Feb 01, 2024 5 - Production/Stable pytest>=6.0.0 :pypi:`pytest-selenium-auto` pytest plugin to automatically capture screenshots upon selenium webdriver events Nov 07, 2023 N/A pytest >= 7.0.0 - :pypi:`pytest-seleniumbase` A complete web automation framework for end-to-end testing. Feb 23, 2024 5 - Production/Stable N/A + :pypi:`pytest-seleniumbase` A complete web automation framework for end-to-end testing. Mar 01, 2024 5 - Production/Stable N/A :pypi:`pytest-selenium-enhancer` pytest plugin for Selenium Apr 29, 2022 5 - Production/Stable N/A :pypi:`pytest-selenium-pdiff` A pytest package implementing perceptualdiff for Selenium tests. Apr 06, 2017 2 - Pre-Alpha N/A :pypi:`pytest-send-email` Send pytest execution result email Dec 04, 2019 N/A N/A @@ -1264,7 +1274,7 @@ This list contains 1397 plugins. :pypi:`pytest-testinfra-jpic` Test infrastructures Sep 21, 2023 5 - Production/Stable N/A :pypi:`pytest-testinfra-winrm-transport` Test infrastructures Sep 21, 2023 5 - Production/Stable N/A :pypi:`pytest-testlink-adaptor` pytest reporting plugin for testlink Dec 20, 2018 4 - Beta pytest (>=2.6) - :pypi:`pytest-testmon` selects tests affected by changed files and methods Nov 23, 2023 4 - Beta pytest <8,>=5 + :pypi:`pytest-testmon` selects tests affected by changed files and methods Feb 27, 2024 4 - Beta pytest <9,>=5 :pypi:`pytest-testmon-dev` selects tests affected by changed files and methods Mar 30, 2023 4 - Beta pytest (<8,>=5) :pypi:`pytest-testmon-oc` nOly selects tests affected by changed files and methods Jun 01, 2022 4 - Beta pytest (<8,>=5) :pypi:`pytest-testmon-skip-libraries` selects tests affected by changed files and methods Mar 03, 2023 4 - Beta pytest (<8,>=5) @@ -1280,6 +1290,7 @@ This list contains 1397 plugins. :pypi:`pytest-testrail-ns` pytest plugin for creating TestRail runs and adding results Aug 12, 2022 N/A N/A :pypi:`pytest-testrail-plugin` PyTest plugin for TestRail Apr 21, 2020 3 - Alpha pytest :pypi:`pytest-testrail-reporter` Sep 10, 2018 N/A N/A + :pypi:`pytest-testrail-results` A pytest plugin to upload results to TestRail. Mar 01, 2024 N/A pytest >=7.2.0 :pypi:`pytest-testreport` Dec 01, 2022 4 - Beta pytest (>=3.5.0) :pypi:`pytest-testreport-new` Oct 07, 2023 4 - Beta pytest >=3.5.0 :pypi:`pytest-testslide` TestSlide fixture for pytest Jan 07, 2021 5 - Production/Stable pytest (~=6.2) @@ -1444,7 +1455,7 @@ This list contains 1397 plugins. Simple but powerful assertion and verification of logged lines. :pypi:`logot` - *last release*: Feb 19, 2024, + *last release*: Feb 29, 2024, *status*: 5 - Production/Stable, *requires*: pytest (>=7,<9) ; extra == "pytest" @@ -1955,7 +1966,7 @@ This list contains 1397 plugins. Pytest support for asyncio :pypi:`pytest-asyncio-cooperative` - *last release*: Feb 12, 2024, + *last release*: Feb 25, 2024, *status*: N/A, *requires*: N/A @@ -2241,6 +2252,13 @@ This list contains 1397 plugins. A small example package + :pypi:`pytest-better-parametrize` + *last release*: Feb 26, 2024, + *status*: 4 - Beta, + *requires*: pytest >=6.2.0 + + Better description of parametrized test cases + :pypi:`pytest-bg-process` *last release*: Jan 24, 2022, *status*: 4 - Beta, @@ -2466,7 +2484,7 @@ This list contains 1397 plugins. :pypi:`pytest-bwrap` - *last release*: Oct 26, 2018, + *last release*: Feb 25, 2024, *status*: 3 - Alpha, *requires*: N/A @@ -2577,6 +2595,13 @@ This list contains 1397 plugins. pytest-celery a shim pytest plugin to enable celery.contrib.pytest + :pypi:`pytest-cfg-fetcher` + *last release*: Feb 26, 2024, + *status*: N/A, + *requires*: N/A + + Pass config options to your unit tests. + :pypi:`pytest-chainmaker` *last release*: Oct 15, 2021, *status*: N/A, @@ -3236,9 +3261,9 @@ This list contains 1397 plugins. :pypi:`pytest-darker` - *last release*: Feb 24, 2024, + *last release*: Feb 25, 2024, *status*: N/A, - *requires*: pytest <8,>=6.0.1 + *requires*: pytest <7,>=6.0.1 A pytest plugin for checking of modified code using Darker @@ -3809,6 +3834,13 @@ This list contains 1397 plugins. Manages Docker containers during your integration tests + :pypi:`pytest-docker-compose-v2` + *last release*: Feb 28, 2024, + *status*: 4 - Beta, + *requires*: pytest<8,>=7.2.2 + + Manages Docker containers during your integration tests + :pypi:`pytest-docker-db` *last release*: Mar 20, 2021, *status*: 5 - Production/Stable, @@ -3921,6 +3953,13 @@ This list contains 1397 plugins. A simple pytest plugin to import names and add them to the doctest namespace. + :pypi:`pytest-doctest-mkdocstrings` + *last release*: Mar 02, 2024, + *status*: N/A, + *requires*: pytest + + Run pytest --doctest-modules with markdown docstrings in code blocks (\`\`\`) + :pypi:`pytest-doctestplus` *last release*: Dec 13, 2023, *status*: 5 - Production/Stable, @@ -4153,56 +4192,56 @@ This list contains 1397 plugins. Send execution result email :pypi:`pytest-embedded` - *last release*: Feb 23, 2024, + *last release*: Mar 01, 2024, *status*: 5 - Production/Stable, *requires*: pytest>=7.0 A pytest plugin that designed for embedded testing. :pypi:`pytest-embedded-arduino` - *last release*: Feb 23, 2024, + *last release*: Mar 01, 2024, *status*: 5 - Production/Stable, *requires*: N/A Make pytest-embedded plugin work with Arduino. :pypi:`pytest-embedded-idf` - *last release*: Feb 23, 2024, + *last release*: Mar 01, 2024, *status*: 5 - Production/Stable, *requires*: N/A Make pytest-embedded plugin work with ESP-IDF. :pypi:`pytest-embedded-jtag` - *last release*: Feb 23, 2024, + *last release*: Mar 01, 2024, *status*: 5 - Production/Stable, *requires*: N/A Make pytest-embedded plugin work with JTAG. :pypi:`pytest-embedded-qemu` - *last release*: Feb 23, 2024, + *last release*: Mar 01, 2024, *status*: 5 - Production/Stable, *requires*: N/A Make pytest-embedded plugin work with QEMU. :pypi:`pytest-embedded-serial` - *last release*: Feb 23, 2024, + *last release*: Mar 01, 2024, *status*: 5 - Production/Stable, *requires*: N/A Make pytest-embedded plugin work with Serial. :pypi:`pytest-embedded-serial-esp` - *last release*: Feb 23, 2024, + *last release*: Mar 01, 2024, *status*: 5 - Production/Stable, *requires*: N/A Make pytest-embedded plugin work with Espressif target boards. :pypi:`pytest-embedded-wokwi` - *last release*: Feb 23, 2024, + *last release*: Mar 01, 2024, *status*: 5 - Production/Stable, *requires*: N/A @@ -5056,7 +5095,7 @@ This list contains 1397 plugins. Uses gcov to measure test coverage of a C library :pypi:`pytest-gcs` - *last release*: Feb 18, 2024, + *last release*: Mar 01, 2024, *status*: 5 - Production/Stable, *requires*: pytest >=6.2 @@ -5371,9 +5410,9 @@ This list contains 1397 plugins. A pytest plugin for use with homeassistant custom components. :pypi:`pytest-homeassistant-custom-component` - *last release*: Feb 24, 2024, + *last release*: Mar 01, 2024, *status*: 3 - Alpha, - *requires*: pytest ==7.4.4 + *requires*: pytest ==8.0.2 Experimental package to automatically extract test plugins for Home Assistant custom components @@ -5532,7 +5571,7 @@ This list contains 1397 plugins. A thin wrapper of HTTPretty for pytest :pypi:`pytest_httpserver` - *last release*: Feb 13, 2024, + *last release*: Feb 24, 2024, *status*: 3 - Alpha, *requires*: N/A @@ -5748,6 +5787,13 @@ This list contains 1397 plugins. A simple image diff plugin for pytest + :pypi:`pytest-in-robotframework` + *last release*: Mar 02, 2024, + *status*: N/A, + *requires*: pytest + + The extension enables easy execution of pytest tests within the Robot Framework environment. + :pypi:`pytest-insper` *last release*: Feb 01, 2024, *status*: N/A, @@ -5812,7 +5858,7 @@ This list contains 1397 plugins. Pytest plugin for checking charm relation interface protocol compliance. :pypi:`pytest-invenio` - *last release*: Jan 29, 2024, + *last release*: Feb 28, 2024, *status*: 5 - Production/Stable, *requires*: pytest <7.2.0,>=6 @@ -6098,6 +6144,13 @@ This list contains 1397 plugins. + :pypi:`pytest-kuunda` + *last release*: Feb 25, 2024, + *status*: 4 - Beta, + *requires*: pytest >=6.2.0 + + pytest plugin to help with test data setup for PySpark tests + :pypi:`pytest-kwparametrize` *last release*: Jan 22, 2021, *status*: N/A, @@ -6182,6 +6235,13 @@ This list contains 1397 plugins. A pytest plugin to trace resource leaks. + :pypi:`pytest-leaping` + *last release*: Mar 02, 2024, + *status*: N/A, + *requires*: N/A + + Coming soon! + :pypi:`pytest-level` *last release*: Oct 21, 2019, *status*: N/A, @@ -6470,7 +6530,7 @@ This list contains 1397 plugins. UNKNOWN :pypi:`pytest-matcher` - *last release*: Jan 15, 2024, + *last release*: Feb 29, 2024, *status*: 5 - Production/Stable, *requires*: pytest @@ -6658,6 +6718,13 @@ This list contains 1397 plugins. Pytest plugin that creates missing fixtures + :pypi:`pytest-mitmproxy` + *last release*: Feb 28, 2024, + *status*: N/A, + *requires*: pytest >=7.0 + + pytest plugin for mitmproxy tests + :pypi:`pytest-ml` *last release*: May 04, 2019, *status*: 4 - Beta, @@ -6742,6 +6809,13 @@ This list contains 1397 plugins. A pytest plugin for testing TCP clients + :pypi:`pytest-modalt` + *last release*: Feb 27, 2024, + *status*: 4 - Beta, + *requires*: pytest >=6.2.0 + + Massively distributed pytest runs using modal.com + :pypi:`pytest-modified-env` *last release*: Jan 29, 2022, *status*: 4 - Beta, @@ -6925,9 +6999,9 @@ This list contains 1397 plugins. Mypy static type checker plugin for Pytest :pypi:`pytest-mypy-plugins` - *last release*: Jul 25, 2023, + *last release*: Feb 29, 2024, *status*: 4 - Beta, - *requires*: pytest (>=7.0.0) + *requires*: pytest >=7.0.0 pytest plugin for writing tests for mypy plugins @@ -6939,9 +7013,9 @@ This list contains 1397 plugins. Substitute for "pytest-mypy-plugins" for Python implementations which aren't supported by mypy. :pypi:`pytest-mypy-testing` - *last release*: Feb 25, 2023, + *last release*: Feb 26, 2024, *status*: N/A, - *requires*: pytest>=7,<8 + *requires*: pytest>=7,<9 Pytest plugin to check mypy output. @@ -7135,7 +7209,7 @@ This list contains 1397 plugins. A PyTest Reporter to send test runs to Notion.so :pypi:`pytest-nunit` - *last release*: Feb 13, 2024, + *last release*: Feb 26, 2024, *status*: 5 - Production/Stable, *requires*: N/A @@ -7659,8 +7733,8 @@ This list contains 1397 plugins. A pytest wrapper with fixtures for Playwright to automate web browsers - :pypi:`pytest-playwright-async` - *last release*: Feb 06, 2024, + :pypi:`pytest_playwright_async` + *last release*: Feb 25, 2024, *status*: N/A, *requires*: N/A @@ -7674,7 +7748,7 @@ This list contains 1397 plugins. :pypi:`pytest-playwright-enhanced` - *last release*: Feb 24, 2024, + *last release*: Mar 02, 2024, *status*: N/A, *requires*: pytest (>=8.0.0,<9.0.0) @@ -8115,7 +8189,7 @@ This list contains 1397 plugins. Pytest plugin for interaction with TestRail :pypi:`pytest-pythonhashseed` - *last release*: Feb 18, 2024, + *last release*: Feb 25, 2024, *status*: 4 - Beta, *requires*: pytest>=3.0.0 @@ -8135,6 +8209,13 @@ This list contains 1397 plugins. pytest plugin for a better developer experience when working with the PyTorch test suite + :pypi:`pytest-pyvenv` + *last release*: Feb 27, 2024, + *status*: N/A, + *requires*: pytest ; extra == 'test' + + A package for create venv in tests + :pypi:`pytest-pyvista` *last release*: Sep 29, 2023, *status*: 4 - Beta, @@ -8311,7 +8392,7 @@ This list contains 1397 plugins. Randomise the order in which pytest tests are run with some control over the randomness :pypi:`pytest-ranking` - *last release*: Feb 17, 2024, + *last release*: Mar 01, 2024, *status*: 4 - Beta, *requires*: pytest >=7.4.3 @@ -8409,11 +8490,11 @@ This list contains 1397 plugins. Easy to use fixtures to write regression tests. :pypi:`pytest-regtest` - *last release*: Jan 22, 2024, + *last release*: Feb 26, 2024, *status*: N/A, *requires*: pytest>7.2 - "pytest plugin for snapshot regression testing" + pytest plugin for snapshot regression testing :pypi:`pytest-relative-order` *last release*: May 17, 2021, @@ -8500,14 +8581,14 @@ This list contains 1397 plugins. Creates json report that is compatible with atom.io's linter message format :pypi:`pytest-reporter` - *last release*: Jul 22, 2021, + *last release*: Feb 28, 2024, *status*: 4 - Beta, *requires*: pytest Generate Pytest reports with templates :pypi:`pytest-reporter-html1` - *last release*: Jun 05, 2023, + *last release*: Feb 28, 2024, *status*: 4 - Beta, *requires*: N/A @@ -8556,9 +8637,9 @@ This list contains 1397 plugins. pytest plugin for adding tests' parameters to junit report :pypi:`pytest-reportportal` - *last release*: Feb 05, 2024, + *last release*: Mar 01, 2024, *status*: N/A, - *requires*: pytest <8.0.0,>=3.8.0 + *requires*: pytest >=3.8.0 Agent for Reporting results of tests to the Report Portal @@ -8703,7 +8784,7 @@ This list contains 1397 plugins. Pytest plugin to restrict the test types allowed :pypi:`pytest-result-log` - *last release*: Jan 10, 2024, + *last release*: Feb 27, 2024, *status*: N/A, *requires*: pytest>=7.2.0 @@ -8815,7 +8896,7 @@ This list contains 1397 plugins. pytest plugin for ROAST configuration override and fixtures :pypi:`pytest_robotframework` - *last release*: Feb 22, 2024, + *last release*: Feb 27, 2024, *status*: N/A, *requires*: pytest<9,>=7 @@ -8983,7 +9064,7 @@ This list contains 1397 plugins. pytest_sauce provides sane and helpful methods worked out in clearcode to run py.test tests with selenium/saucelabs :pypi:`pytest-sbase` - *last release*: Feb 23, 2024, + *last release*: Mar 01, 2024, *status*: 5 - Production/Stable, *requires*: N/A @@ -9046,7 +9127,7 @@ This list contains 1397 plugins. pytest plugin to automatically capture screenshots upon selenium webdriver events :pypi:`pytest-seleniumbase` - *last release*: Feb 23, 2024, + *last release*: Mar 01, 2024, *status*: 5 - Production/Stable, *requires*: N/A @@ -10047,9 +10128,9 @@ This list contains 1397 plugins. pytest reporting plugin for testlink :pypi:`pytest-testmon` - *last release*: Nov 23, 2023, + *last release*: Feb 27, 2024, *status*: 4 - Beta, - *requires*: pytest <8,>=5 + *requires*: pytest <9,>=5 selects tests affected by changed files and methods @@ -10158,6 +10239,13 @@ This list contains 1397 plugins. + :pypi:`pytest-testrail-results` + *last release*: Mar 01, 2024, + *status*: N/A, + *requires*: pytest >=7.2.0 + + A pytest plugin to upload results to TestRail. + :pypi:`pytest-testreport` *last release*: Dec 01, 2022, *status*: 4 - Beta, From e410705561673380d535a2ae32a4be42e115cf2c Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 3 Mar 2024 23:25:09 +0200 Subject: [PATCH 20/47] Cherry-pick 8.1.0 release notes (cherry picked from commit 0a536810dc5f51dac99bdb90dde06704b5aa034e) --- changelog/10865.improvement.rst | 3 - changelog/11311.improvement.rst | 4 -- changelog/11475.feature.rst | 3 - changelog/11475.improvement.rst | 3 - changelog/11653.feature.rst | 2 - changelog/11785.trivial.rst | 7 --- changelog/11790.doc.rst | 1 - changelog/11801.improvement.rst | 2 - changelog/11850.improvement.rst | 1 - changelog/11904.bugfix.rst | 3 - changelog/11962.improvement.rst | 1 - changelog/11978.improvement.rst | 3 - changelog/12011.bugfix.rst | 1 - changelog/12014.bugfix.rst | 1 - changelog/12039.bugfix.rst | 1 - changelog/12047.improvement.rst | 2 - doc/en/announce/index.rst | 1 + doc/en/announce/release-8.1.0.rst | 54 +++++++++++++++++ doc/en/builtin.rst | 28 ++++----- doc/en/changelog.rst | 92 +++++++++++++++++++++++++++++ doc/en/example/parametrize.rst | 6 +- doc/en/example/pythoncollection.rst | 4 +- doc/en/example/reportingdemo.rst | 4 +- doc/en/getting-started.rst | 2 +- doc/en/how-to/fixtures.rst | 2 +- doc/en/reference/reference.rst | 12 ++++ 26 files changed, 182 insertions(+), 61 deletions(-) delete mode 100644 changelog/10865.improvement.rst delete mode 100644 changelog/11311.improvement.rst delete mode 100644 changelog/11475.feature.rst delete mode 100644 changelog/11475.improvement.rst delete mode 100644 changelog/11653.feature.rst delete mode 100644 changelog/11785.trivial.rst delete mode 100644 changelog/11790.doc.rst delete mode 100644 changelog/11801.improvement.rst delete mode 100644 changelog/11850.improvement.rst delete mode 100644 changelog/11904.bugfix.rst delete mode 100644 changelog/11962.improvement.rst delete mode 100644 changelog/11978.improvement.rst delete mode 100644 changelog/12011.bugfix.rst delete mode 100644 changelog/12014.bugfix.rst delete mode 100644 changelog/12039.bugfix.rst delete mode 100644 changelog/12047.improvement.rst create mode 100644 doc/en/announce/release-8.1.0.rst diff --git a/changelog/10865.improvement.rst b/changelog/10865.improvement.rst deleted file mode 100644 index a5ced8e9a..000000000 --- a/changelog/10865.improvement.rst +++ /dev/null @@ -1,3 +0,0 @@ -:func:`pytest.warns` now validates that :func:`warnings.warn` was called with a `str` or a `Warning`. -Currently in Python it is possible to use other types, however this causes an exception when :func:`warnings.filterwarnings` is used to filter those warnings (see `CPython #103577 `__ for a discussion). -While this can be considered a bug in CPython, we decided to put guards in pytest as the error message produced without this check in place is confusing. diff --git a/changelog/11311.improvement.rst b/changelog/11311.improvement.rst deleted file mode 100644 index 0072f3974..000000000 --- a/changelog/11311.improvement.rst +++ /dev/null @@ -1,4 +0,0 @@ -When using ``--override-ini`` for paths in invocations without a configuration file defined, the current working directory is used -as the relative directory. - -Previoulsy this would raise an :class:`AssertionError`. diff --git a/changelog/11475.feature.rst b/changelog/11475.feature.rst deleted file mode 100644 index 42550235d..000000000 --- a/changelog/11475.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -Added the new :confval:`consider_namespace_packages` configuration option, defaulting to ``False``. - -If set to ``True``, pytest will attempt to identify modules that are part of `namespace packages `__ when importing modules. diff --git a/changelog/11475.improvement.rst b/changelog/11475.improvement.rst deleted file mode 100644 index 4f6a4bffa..000000000 --- a/changelog/11475.improvement.rst +++ /dev/null @@ -1,3 +0,0 @@ -:ref:`--import-mode=importlib ` now tries to import modules using the standard import mechanism (but still without changing :py:data:`sys.path`), falling back to importing modules directly only if that fails. - -This means that installed packages will be imported under their canonical name if possible first, for example ``app.core.models``, instead of having the module name always be derived from their path (for example ``.env310.lib.site_packages.app.core.models``). diff --git a/changelog/11653.feature.rst b/changelog/11653.feature.rst deleted file mode 100644 index f165c3f8e..000000000 --- a/changelog/11653.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -Added the new :confval:`verbosity_test_cases` configuration option for fine-grained control of test execution verbosity. -See :ref:`Fine-grained verbosity ` for more details. diff --git a/changelog/11785.trivial.rst b/changelog/11785.trivial.rst deleted file mode 100644 index b6b74d0da..000000000 --- a/changelog/11785.trivial.rst +++ /dev/null @@ -1,7 +0,0 @@ -Some changes were made to private functions which may affect plugins which access them: - -- ``FixtureManager._getautousenames()`` now takes a ``Node`` itself instead of the nodeid. -- ``FixtureManager.getfixturedefs()`` now takes the ``Node`` itself instead of the nodeid. -- The ``_pytest.nodes.iterparentnodeids()`` function is removed without replacement. - Prefer to traverse the node hierarchy itself instead. - If you really need to, copy the function from the previous pytest release. diff --git a/changelog/11790.doc.rst b/changelog/11790.doc.rst deleted file mode 100644 index 648b20b96..000000000 --- a/changelog/11790.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Documented the retention of temporary directories created using the ``tmp_path`` fixture in more detail. diff --git a/changelog/11801.improvement.rst b/changelog/11801.improvement.rst deleted file mode 100644 index d9e5f8483..000000000 --- a/changelog/11801.improvement.rst +++ /dev/null @@ -1,2 +0,0 @@ -Added the :func:`iter_parents() <_pytest.nodes.Node.iter_parents>` helper method on nodes. -It is similar to :func:`listchain <_pytest.nodes.Node.listchain>`, but goes from bottom to top, and returns an iterator, not a list. diff --git a/changelog/11850.improvement.rst b/changelog/11850.improvement.rst deleted file mode 100644 index 87fc0953c..000000000 --- a/changelog/11850.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Added support for :data:`sys.last_exc` for post-mortem debugging on Python>=3.12. diff --git a/changelog/11904.bugfix.rst b/changelog/11904.bugfix.rst deleted file mode 100644 index 2aed9bcb0..000000000 --- a/changelog/11904.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Fixed a regression in pytest 8.0.0 that would cause test collection to fail due to permission errors when using ``--pyargs``. - -This change improves the collection tree for tests specified using ``--pyargs``, see :pull:`12043` for a comparison with pytest 8.0 and <8. diff --git a/changelog/11962.improvement.rst b/changelog/11962.improvement.rst deleted file mode 100644 index 453b99d33..000000000 --- a/changelog/11962.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -In case no other suitable candidates for configuration file are found, a ``pyproject.toml`` (even without a ``[tool.pytest.ini_options]`` table) will be considered as the configuration file and define the ``rootdir``. diff --git a/changelog/11978.improvement.rst b/changelog/11978.improvement.rst deleted file mode 100644 index 1f1143dac..000000000 --- a/changelog/11978.improvement.rst +++ /dev/null @@ -1,3 +0,0 @@ -Add ``--log-file-mode`` option to the logging plugin, enabling appending to log-files. This option accepts either ``"w"`` or ``"a"`` and defaults to ``"w"``. - -Previously, the mode was hard-coded to be ``"w"`` which truncates the file before logging. diff --git a/changelog/12011.bugfix.rst b/changelog/12011.bugfix.rst deleted file mode 100644 index 5b755ade3..000000000 --- a/changelog/12011.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed a regression in 8.0.1 whereby ``setup_module`` xunit-style fixtures are not executed when ``--doctest-modules`` is passed. diff --git a/changelog/12014.bugfix.rst b/changelog/12014.bugfix.rst deleted file mode 100644 index 344bf8b7e..000000000 --- a/changelog/12014.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix the ``stacklevel`` used when warning about marks used on fixtures. diff --git a/changelog/12039.bugfix.rst b/changelog/12039.bugfix.rst deleted file mode 100644 index 267eae6b8..000000000 --- a/changelog/12039.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed a regression in ``8.0.2`` where tests created using :fixture:`tmp_path` have been collected multiple times in CI under Windows. diff --git a/changelog/12047.improvement.rst b/changelog/12047.improvement.rst deleted file mode 100644 index e9ad5eddc..000000000 --- a/changelog/12047.improvement.rst +++ /dev/null @@ -1,2 +0,0 @@ -When multiple finalizers of a fixture raise an exception, now all exceptions are reported as an exception group. -Previously, only the first exception was reported. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 5374e8c75..68cae83d7 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-8.1.0 release-8.0.2 release-8.0.1 release-8.0.0 diff --git a/doc/en/announce/release-8.1.0.rst b/doc/en/announce/release-8.1.0.rst new file mode 100644 index 000000000..62cafdd78 --- /dev/null +++ b/doc/en/announce/release-8.1.0.rst @@ -0,0 +1,54 @@ +pytest-8.1.0 +======================================= + +The pytest team is proud to announce the 8.1.0 release! + +This release contains new features, improvements, and bug fixes, +the full list of changes is available in the changelog: + + https://docs.pytest.org/en/stable/changelog.html + +For complete documentation, please visit: + + https://docs.pytest.org/en/stable/ + +As usual, you can upgrade from PyPI via: + + pip install -U pytest + +Thanks to all of the contributors to this release: + +* Ben Brown +* Ben Leith +* Bruno Oliveira +* Clément Robert +* Dave Hall +* Dương Quốc Khánh +* Eero Vaher +* Eric Larson +* Fabian Sturm +* Faisal Fawad +* Florian Bruhin +* Franck Charras +* Joachim B Haga +* John Litborn +* Loïc Estève +* Marc Bresson +* Patrick Lannigan +* Pierre Sassoulas +* Ran Benita +* Reagan Lee +* Ronny Pfannschmidt +* Russell Martin +* clee2000 +* donghui +* faph +* jakkdl +* mrbean-bremen +* robotherapist +* whysage +* woutdenolf + + +Happy testing, +The pytest Development Team diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst index e9e42b9e8..1e1210648 100644 --- a/doc/en/builtin.rst +++ b/doc/en/builtin.rst @@ -33,7 +33,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a Values can be any object handled by the json stdlib module. - capsysbinary -- .../_pytest/capture.py:1007 + capsysbinary -- .../_pytest/capture.py:1008 Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. The captured output is made available via ``capsysbinary.readouterr()`` @@ -50,7 +50,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a captured = capsysbinary.readouterr() assert captured.out == b"hello\n" - capfd -- .../_pytest/capture.py:1034 + capfd -- .../_pytest/capture.py:1035 Enable text capturing of writes to file descriptors ``1`` and ``2``. The captured output is made available via ``capfd.readouterr()`` method @@ -67,7 +67,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a captured = capfd.readouterr() assert captured.out == "hello\n" - capfdbinary -- .../_pytest/capture.py:1061 + capfdbinary -- .../_pytest/capture.py:1062 Enable bytes capturing of writes to file descriptors ``1`` and ``2``. The captured output is made available via ``capfd.readouterr()`` method @@ -84,7 +84,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a captured = capfdbinary.readouterr() assert captured.out == b"hello\n" - capsys -- .../_pytest/capture.py:980 + capsys -- .../_pytest/capture.py:981 Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``. The captured output is made available via ``capsys.readouterr()`` method @@ -101,7 +101,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a captured = capsys.readouterr() assert captured.out == "hello\n" - doctest_namespace [session scope] -- .../_pytest/doctest.py:745 + doctest_namespace [session scope] -- .../_pytest/doctest.py:737 Fixture that returns a :py:class:`dict` that will be injected into the namespace of doctests. @@ -115,7 +115,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a For more details: :ref:`doctest_namespace`. - pytestconfig [session scope] -- .../_pytest/fixtures.py:1354 + pytestconfig [session scope] -- .../_pytest/fixtures.py:1346 Session-scoped fixture that returns the session's :class:`pytest.Config` object. @@ -170,18 +170,18 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a `pytest-xdist `__ plugin. See :issue:`7767` for details. - tmpdir_factory [session scope] -- .../_pytest/legacypath.py:302 + tmpdir_factory [session scope] -- .../_pytest/legacypath.py:317 Return a :class:`pytest.TempdirFactory` instance for the test session. - tmpdir -- .../_pytest/legacypath.py:309 + tmpdir -- .../_pytest/legacypath.py:324 Return a temporary directory path object which is unique to each test function invocation, created as a sub directory of the base temporary directory. By default, a new base temporary directory is created each test session, and old bases are removed after 3 sessions, to aid in debugging. If - ``--basetemp`` is used then it is cleared each session. See :ref:`base - temporary directory`. + ``--basetemp`` is used then it is cleared each session. See + :ref:`temporary directory location and retention`. The returned object is a `legacy_path`_ object. @@ -192,7 +192,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a .. _legacy_path: https://py.readthedocs.io/en/latest/path.html - caplog -- .../_pytest/logging.py:594 + caplog -- .../_pytest/logging.py:601 Access and control log capturing. Captured logs are available through the following properties/methods:: @@ -227,7 +227,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a To undo modifications done by the fixture in a contained scope, use :meth:`context() `. - recwarn -- .../_pytest/recwarn.py:32 + recwarn -- .../_pytest/recwarn.py:31 Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions. See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information @@ -245,8 +245,8 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a and old bases are removed after 3 sessions, to aid in debugging. This behavior can be configured with :confval:`tmp_path_retention_count` and :confval:`tmp_path_retention_policy`. - If ``--basetemp`` is used then it is cleared each session. See :ref:`base - temporary directory`. + If ``--basetemp`` is used then it is cleared each session. See + :ref:`temporary directory location and retention`. The returned object is a :class:`pathlib.Path` object. diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index bdf5a1550..1c2ef95f5 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -28,6 +28,98 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 8.1.0 (2024-03-03) +========================= + +Features +-------- + +- `#11475 `_: Added the new :confval:`consider_namespace_packages` configuration option, defaulting to ``False``. + + If set to ``True``, pytest will attempt to identify modules that are part of `namespace packages `__ when importing modules. + + +- `#11653 `_: Added the new :confval:`verbosity_test_cases` configuration option for fine-grained control of test execution verbosity. + See :ref:`Fine-grained verbosity ` for more details. + + + +Improvements +------------ + +- `#10865 `_: :func:`pytest.warns` now validates that :func:`warnings.warn` was called with a `str` or a `Warning`. + Currently in Python it is possible to use other types, however this causes an exception when :func:`warnings.filterwarnings` is used to filter those warnings (see `CPython #103577 `__ for a discussion). + While this can be considered a bug in CPython, we decided to put guards in pytest as the error message produced without this check in place is confusing. + + +- `#11311 `_: When using ``--override-ini`` for paths in invocations without a configuration file defined, the current working directory is used + as the relative directory. + + Previoulsy this would raise an :class:`AssertionError`. + + +- `#11475 `_: :ref:`--import-mode=importlib ` now tries to import modules using the standard import mechanism (but still without changing :py:data:`sys.path`), falling back to importing modules directly only if that fails. + + This means that installed packages will be imported under their canonical name if possible first, for example ``app.core.models``, instead of having the module name always be derived from their path (for example ``.env310.lib.site_packages.app.core.models``). + + +- `#11801 `_: Added the :func:`iter_parents() <_pytest.nodes.Node.iter_parents>` helper method on nodes. + It is similar to :func:`listchain <_pytest.nodes.Node.listchain>`, but goes from bottom to top, and returns an iterator, not a list. + + +- `#11850 `_: Added support for :data:`sys.last_exc` for post-mortem debugging on Python>=3.12. + + +- `#11962 `_: In case no other suitable candidates for configuration file are found, a ``pyproject.toml`` (even without a ``[tool.pytest.ini_options]`` table) will be considered as the configuration file and define the ``rootdir``. + + +- `#11978 `_: Add ``--log-file-mode`` option to the logging plugin, enabling appending to log-files. This option accepts either ``"w"`` or ``"a"`` and defaults to ``"w"``. + + Previously, the mode was hard-coded to be ``"w"`` which truncates the file before logging. + + +- `#12047 `_: When multiple finalizers of a fixture raise an exception, now all exceptions are reported as an exception group. + Previously, only the first exception was reported. + + + +Bug Fixes +--------- + +- `#11904 `_: Fixed a regression in pytest 8.0.0 that would cause test collection to fail due to permission errors when using ``--pyargs``. + + This change improves the collection tree for tests specified using ``--pyargs``, see :pull:`12043` for a comparison with pytest 8.0 and <8. + + +- `#12011 `_: Fixed a regression in 8.0.1 whereby ``setup_module`` xunit-style fixtures are not executed when ``--doctest-modules`` is passed. + + +- `#12014 `_: Fix the ``stacklevel`` used when warning about marks used on fixtures. + + +- `#12039 `_: Fixed a regression in ``8.0.2`` where tests created using :fixture:`tmp_path` have been collected multiple times in CI under Windows. + + + +Improved Documentation +---------------------- + +- `#11790 `_: Documented the retention of temporary directories created using the ``tmp_path`` fixture in more detail. + + + +Trivial/Internal Changes +------------------------ + +- `#11785 `_: Some changes were made to private functions which may affect plugins which access them: + + - ``FixtureManager._getautousenames()`` now takes a ``Node`` itself instead of the nodeid. + - ``FixtureManager.getfixturedefs()`` now takes the ``Node`` itself instead of the nodeid. + - The ``_pytest.nodes.iterparentnodeids()`` function is removed without replacement. + Prefer to traverse the node hierarchy itself instead. + If you really need to, copy the function from the previous pytest release. + + pytest 8.0.2 (2024-02-24) ========================= diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index c6ac64899..85c683679 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -162,7 +162,7 @@ objects, they are still using the default pytest representation: rootdir: /home/sweet/project collected 8 items - + @@ -239,7 +239,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia rootdir: /home/sweet/project collected 4 items - + @@ -318,7 +318,7 @@ Let's first see how it looks like at collection time: rootdir: /home/sweet/project collected 2 items - + diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index 7207ca2ae..6822aa68e 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -152,7 +152,7 @@ The test collection would look like this: configfile: pytest.ini collected 2 items - + @@ -215,7 +215,7 @@ You can always peek at the collection tree without running tests like this: configfile: pytest.ini collected 3 items - + diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index 2e8d4824c..2c34cc2b0 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -445,7 +445,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: self = def test_tupleerror(self): - > a, b = [1] # NOQA + > a, b = [1] # noqa: F841 E ValueError: not enough values to unpack (expected 2, got 1) failure_demo.py:175: ValueError @@ -467,7 +467,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: self = def test_some_error(self): - > if namenotexi: # NOQA + > if namenotexi: # noqa: F821 E NameError: name 'namenotexi' is not defined failure_demo.py:183: NameError diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index f19198864..89381c8c7 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -22,7 +22,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - pytest 8.0.2 + pytest 8.1.0 .. _`simpletest`: diff --git a/doc/en/how-to/fixtures.rst b/doc/en/how-to/fixtures.rst index 329c568c0..c32de1610 100644 --- a/doc/en/how-to/fixtures.rst +++ b/doc/en/how-to/fixtures.rst @@ -1418,7 +1418,7 @@ Running the above tests results in the following test IDs being used: rootdir: /home/sweet/project collected 12 items - + diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index f84b7ea48..358f371e5 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -2103,6 +2103,8 @@ All the command-line flags can be obtained by running ``pytest --help``:: --log-cli-date-format=LOG_CLI_DATE_FORMAT Log date format used by the logging module --log-file=LOG_FILE Path to a file when logging will be written to + --log-file-mode={w,a} + Log file open mode --log-file-level=LOG_FILE_LEVEL Log file logging level --log-file-format=LOG_FILE_FORMAT @@ -2128,6 +2130,9 @@ All the command-line flags can be obtained by running ``pytest --help``:: Each line specifies a pattern for warnings.filterwarnings. Processed after -W/--pythonwarnings. + consider_namespace_packages (bool): + Consider namespace packages when resolving module + names during import usefixtures (args): List of default fixtures to be used with this project python_files (args): Glob-style file patterns for Python test module @@ -2146,6 +2151,11 @@ All the command-line flags can be obtained by running ``pytest --help``:: progress information ("progress" (percentage) | "count" | "progress-even-when-capture-no" (forces progress even when capture=no) + verbosity_test_cases (string): + Specify a verbosity level for test case execution, + overriding the main level. Higher levels will + provide more detailed information about each test + case executed. xfail_strict (bool): Default for the strict parameter of xfail markers when not given explicitly (default: False) tmp_path_retention_count (string): @@ -2193,6 +2203,8 @@ All the command-line flags can be obtained by running ``pytest --help``:: log_cli_date_format (string): Default value for --log-cli-date-format log_file (string): Default value for --log-file + log_file_mode (string): + Default value for --log-file-mode log_file_level (string): Default value for --log-file-level log_file_format (string): From 23bdc643c9bd5ced8f09024f8c5882f078647f93 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 03:25:34 +0000 Subject: [PATCH 21/47] build(deps): Bump twisted in /testing/plugins_integration Bumps [twisted](https://github.com/twisted/twisted) from 23.10.0 to 24.3.0. - [Release notes](https://github.com/twisted/twisted/releases) - [Changelog](https://github.com/twisted/twisted/blob/trunk/NEWS.rst) - [Commits](https://github.com/twisted/twisted/compare/twisted-23.10.0...twisted-24.3.0) --- updated-dependencies: - dependency-name: twisted dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- testing/plugins_integration/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/plugins_integration/requirements.txt b/testing/plugins_integration/requirements.txt index 42ab2af99..d2f3fad17 100644 --- a/testing/plugins_integration/requirements.txt +++ b/testing/plugins_integration/requirements.txt @@ -13,5 +13,5 @@ pytest-rerunfailures==13.0 pytest-sugar==1.0.0 pytest-trio==0.7.0 pytest-twisted==1.14.0 -twisted==23.10.0 +twisted==24.3.0 pytest-xvfb==3.0.0 From 13558b9f5398595b36ca4b400bbfd57fb839684c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 03:47:21 +0000 Subject: [PATCH 22/47] build(deps): Bump pypa/gh-action-pypi-publish from 1.8.11 to 1.8.12 Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.11 to 1.8.12. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.11...v1.8.12) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0d48982d6..d46f1f91d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -47,7 +47,7 @@ jobs: path: dist - name: Publish package to PyPI - uses: pypa/gh-action-pypi-publish@v1.8.11 + uses: pypa/gh-action-pypi-publish@v1.8.12 - name: Push tag run: | From 37b340109130c679cddcf2bc3eb1e813827d6acb Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 4 Mar 2024 12:59:54 +0100 Subject: [PATCH 23/47] add myself to tidelift --- TIDELIFT.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TIDELIFT.rst b/TIDELIFT.rst index 6c7ad9177..1ba246bd8 100644 --- a/TIDELIFT.rst +++ b/TIDELIFT.rst @@ -25,6 +25,7 @@ The current list of contributors receiving funding are: * `@nicoddemus`_ * `@The-Compiler`_ +* `@RonnyPfannschmidt`_ Contributors interested in receiving a part of the funds just need to submit a PR adding their name to the list. Contributors that want to stop receiving the funds should also submit a PR @@ -56,3 +57,4 @@ funds. Just drop a line to one of the `@pytest-dev/tidelift-admins`_ or use the .. _`@nicoddemus`: https://github.com/nicoddemus .. _`@The-Compiler`: https://github.com/The-Compiler +.. _`@RonnyPfannschmidt`: https://github.com/RonnyPfannschmidt From 6ee02a3e6ca184288bd8fbe5f6d2e76e09facb9c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 4 Mar 2024 12:17:36 -0300 Subject: [PATCH 24/47] Yank version 8.1.0 Related to #12069 --- doc/en/changelog.rst | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 1c2ef95f5..344230594 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -28,8 +28,16 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start -pytest 8.1.0 (2024-03-03) -========================= +pytest 8.1.0 (YANKED) +===================== + + +.. note:: + + This release has been **yanked**: it broke some plugins without the proper warning period, due to + some warnings not showing up as expected. + + See `#12069 `__. Features -------- From 03e54712dd123138aa4c1035a027e17d0dd9a86c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 4 Mar 2024 12:44:56 -0300 Subject: [PATCH 25/47] Do not import duplicated modules with --importmode=importlib (#12074) Regression brought up by #11475. --- changelog/11475.bugfix.rst | 1 + src/_pytest/pathlib.py | 4 +++ testing/test_pathlib.py | 71 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 changelog/11475.bugfix.rst diff --git a/changelog/11475.bugfix.rst b/changelog/11475.bugfix.rst new file mode 100644 index 000000000..bef8f4f76 --- /dev/null +++ b/changelog/11475.bugfix.rst @@ -0,0 +1 @@ +Fixed regression where ``--importmode=importlib`` would import non-test modules more than once. diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index a19e89aa1..e39f47723 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -539,6 +539,10 @@ def import_path( except CouldNotResolvePathError: pass else: + # If the given module name is already in sys.modules, do not import it again. + with contextlib.suppress(KeyError): + return sys.modules[module_name] + mod = _import_module_using_spec( module_name, path, pkg_root, insert_modules=False ) diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 357860563..a4bccb1b2 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -587,6 +587,12 @@ class TestImportLibMode: assert data.value == "foo" assert data.__module__ == "_src.tests.test_dataclass" + # Ensure we do not import the same module again (#11475). + module2 = import_path( + fn, mode="importlib", root=tmp_path, consider_namespace_packages=ns_param + ) + assert module is module2 + def test_importmode_importlib_with_pickle( self, tmp_path: Path, ns_param: bool ) -> None: @@ -616,6 +622,12 @@ class TestImportLibMode: action = round_trip() assert action() == 42 + # Ensure we do not import the same module again (#11475). + module2 = import_path( + fn, mode="importlib", root=tmp_path, consider_namespace_packages=ns_param + ) + assert module is module2 + def test_importmode_importlib_with_pickle_separate_modules( self, tmp_path: Path, ns_param: bool ) -> None: @@ -816,6 +828,14 @@ class TestImportLibMode: consider_namespace_packages=ns_param, ) assert len(mod.instance.INSTANCES) == 1 + # Ensure we do not import the same module again (#11475). + mod2 = import_path( + init, + root=tmp_path, + mode=ImportMode.importlib, + consider_namespace_packages=ns_param, + ) + assert mod is mod2 def test_importlib_root_is_package(self, pytester: Pytester) -> None: """ @@ -942,6 +962,15 @@ class TestImportLibMode: assert mod.__name__ == "app.core" assert mod.__file__ and Path(mod.__file__) == core_py + # Ensure we do not import the same module again (#11475). + mod2 = import_path( + core_py, + mode="importlib", + root=pytester.path, + consider_namespace_packages=ns_param, + ) + assert mod is mod2 + # tests are not reachable from sys.path, so they are imported as a standalone modules. # Instead of '.tests.a.test_core', we import as "_tests.a.test_core" because # importlib considers module names starting with '.' to be local imports. @@ -952,6 +981,16 @@ class TestImportLibMode: consider_namespace_packages=ns_param, ) assert mod.__name__ == "_tests.a.test_core" + + # Ensure we do not import the same module again (#11475). + mod2 = import_path( + test_path1, + mode="importlib", + root=pytester.path, + consider_namespace_packages=ns_param, + ) + assert mod is mod2 + mod = import_path( test_path2, mode="importlib", @@ -960,6 +999,15 @@ class TestImportLibMode: ) assert mod.__name__ == "_tests.b.test_core" + # Ensure we do not import the same module again (#11475). + mod2 = import_path( + test_path2, + mode="importlib", + root=pytester.path, + consider_namespace_packages=ns_param, + ) + assert mod is mod2 + def test_import_using_normal_mechanism_first_integration( self, monkeypatch: MonkeyPatch, pytester: Pytester, ns_param: bool ) -> None: @@ -1021,6 +1069,14 @@ class TestImportLibMode: assert mod.__file__ and Path(mod.__file__) == x_in_sub_folder assert mod.X == "a/b/x" + mod2 = import_path( + x_in_sub_folder, + mode=ImportMode.importlib, + root=pytester.path, + consider_namespace_packages=ns_param, + ) + assert mod is mod2 + # Attempt to import root 'x.py'. with pytest.raises(AssertionError, match="x at root"): _ = import_path( @@ -1124,6 +1180,12 @@ class TestNamespacePackages: assert mod.__name__ == "com.company.app.core.models" assert mod.__file__ == str(models_py) + # Ensure we do not import the same module again (#11475). + mod2 = import_path( + models_py, mode=import_mode, root=tmp_path, consider_namespace_packages=True + ) + assert mod is mod2 + pkg_root, module_name = resolve_pkg_root_and_module_name( algorithms_py, consider_namespace_packages=True ) @@ -1141,6 +1203,15 @@ class TestNamespacePackages: assert mod.__name__ == "com.company.calc.algo.algorithms" assert mod.__file__ == str(algorithms_py) + # Ensure we do not import the same module again (#11475). + mod2 = import_path( + algorithms_py, + mode=import_mode, + root=tmp_path, + consider_namespace_packages=True, + ) + assert mod is mod2 + @pytest.mark.parametrize("import_mode", ["prepend", "append", "importlib"]) def test_incorrect_namespace_package( self, From 86945f9a1f7cae10a9ac9ccf4b41ce9ddfabe14c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 7 Mar 2024 19:12:19 -0300 Subject: [PATCH 26/47] Rename 'testing' extra to 'dev' (#12052) Minor, but seems `dev` is more standard for the development extras than `testing`, being the default for tools like `poetry`. --- CONTRIBUTING.rst | 4 ++-- pyproject.toml | 2 +- tox.ini | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 6f55c230c..d7da59c81 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -297,12 +297,12 @@ Here is a simple overview, with pytest-specific bits: When committing, ``pre-commit`` will re-format the files if necessary. #. If instead of using ``tox`` you prefer to run the tests directly, then we suggest to create a virtual environment and use - an editable install with the ``testing`` extra:: + an editable install with the ``dev`` extra:: $ python3 -m venv .venv $ source .venv/bin/activate # Linux $ .venv/Scripts/activate.bat # Windows - $ pip install -e ".[testing]" + $ pip install -e ".[dev]" Afterwards, you can edit the files and run pytest normally:: diff --git a/pyproject.toml b/pyproject.toml index 72988e233..e14556f2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dependencies = [ 'tomli>=1; python_version < "3.11"', ] [project.optional-dependencies] -testing = [ +dev = [ "argcomplete", "attrs>=19.2", "hypothesis>=3.56", diff --git a/tox.ini b/tox.ini index 0ac2ff2dd..cb3ca4b83 100644 --- a/tox.ini +++ b/tox.ini @@ -56,7 +56,7 @@ setenv = lsof: _PYTEST_TOX_POSARGS_LSOF=--lsof xdist: _PYTEST_TOX_POSARGS_XDIST=-n auto -extras = testing +extras = dev deps = doctesting: PyYAML exceptiongroup: exceptiongroup>=1.0.0rc8 From 303cd0d48a66bbeba7e2e3eaf4752c6ef4d38a2c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 7 Mar 2024 19:19:14 -0300 Subject: [PATCH 27/47] Revert "Remove deprecated py.path (`fspath`) node constructor arguments" This reverts commit 6c89f9261c6f5bde93bd116ef56b7ac96fc0ef21. --- doc/en/conf.py | 1 + doc/en/deprecations.rst | 79 ++++++++++++++++++------------------ src/_pytest/compat.py | 17 ++++++++ src/_pytest/config/compat.py | 13 ++++++ src/_pytest/deprecated.py | 8 ++++ src/_pytest/legacypath.py | 20 ++------- src/_pytest/main.py | 1 + src/_pytest/nodes.py | 46 ++++++++++++++++++--- src/_pytest/python.py | 3 ++ testing/deprecated_test.py | 22 ++++++++++ testing/test_legacypath.py | 4 +- testing/test_nodes.py | 9 ++-- 12 files changed, 156 insertions(+), 67 deletions(-) create mode 100644 src/_pytest/config/compat.py diff --git a/doc/en/conf.py b/doc/en/conf.py index cf889eb7a..8059c359f 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -200,6 +200,7 @@ nitpick_ignore = [ ("py:class", "_tracing.TagTracerSub"), ("py:class", "warnings.WarningMessage"), # Undocumented type aliases + ("py:class", "LEGACY_PATH"), ("py:class", "_PluggyPlugin"), # TypeVars ("py:class", "_pytest._code.code.E"), diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index b9a59d791..b7ddd8b01 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -19,6 +19,45 @@ Below is a complete list of all pytest features which are considered deprecated. :class:`~pytest.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters `. +.. _node-ctor-fspath-deprecation: + +``fspath`` argument for Node constructors replaced with ``pathlib.Path`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 7.0 + +In order to support the transition from ``py.path.local`` to :mod:`pathlib`, +the ``fspath`` argument to :class:`~_pytest.nodes.Node` constructors like +:func:`pytest.Function.from_parent()` and :func:`pytest.Class.from_parent()` +is now deprecated. + +Plugins which construct nodes should pass the ``path`` argument, of type +:class:`pathlib.Path`, instead of the ``fspath`` argument. + +Plugins which implement custom items and collectors are encouraged to replace +``fspath`` parameters (``py.path.local``) with ``path`` parameters +(``pathlib.Path``), and drop any other usage of the ``py`` library if possible. + +If possible, plugins with custom items should use :ref:`cooperative +constructors ` to avoid hardcoding +arguments they only pass on to the superclass. + +.. note:: + The name of the :class:`~_pytest.nodes.Node` arguments and attributes (the + new attribute being ``path``) is **the opposite** of the situation for + hooks, :ref:`outlined below ` (the old + argument being ``path``). + + This is an unfortunate artifact due to historical reasons, which should be + resolved in future versions as we slowly get rid of the :pypi:`py` + dependency (see :issue:`9283` for a longer discussion). + +Due to the ongoing migration of methods like :meth:`~pytest.Item.reportinfo` +which still is expected to return a ``py.path.local`` object, nodes still have +both ``fspath`` (``py.path.local``) and ``path`` (``pathlib.Path``) attributes, +no matter what argument was used in the constructor. We expect to deprecate the +``fspath`` attribute in a future release. + .. _legacy-path-hooks-deprecated: Configuring hook specs/impls using markers @@ -208,46 +247,6 @@ an appropriate period of deprecation has passed. Some breaking changes which could not be deprecated are also listed. -.. _node-ctor-fspath-deprecation: - -``fspath`` argument for Node constructors replaced with ``pathlib.Path`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 7.0 - -In order to support the transition from ``py.path.local`` to :mod:`pathlib`, -the ``fspath`` argument to :class:`~_pytest.nodes.Node` constructors like -:func:`pytest.Function.from_parent()` and :func:`pytest.Class.from_parent()` -is now deprecated. - -Plugins which construct nodes should pass the ``path`` argument, of type -:class:`pathlib.Path`, instead of the ``fspath`` argument. - -Plugins which implement custom items and collectors are encouraged to replace -``fspath`` parameters (``py.path.local``) with ``path`` parameters -(``pathlib.Path``), and drop any other usage of the ``py`` library if possible. - -If possible, plugins with custom items should use :ref:`cooperative -constructors ` to avoid hardcoding -arguments they only pass on to the superclass. - -.. note:: - The name of the :class:`~_pytest.nodes.Node` arguments and attributes (the - new attribute being ``path``) is **the opposite** of the situation for - hooks, :ref:`outlined below ` (the old - argument being ``path``). - - This is an unfortunate artifact due to historical reasons, which should be - resolved in future versions as we slowly get rid of the :pypi:`py` - dependency (see :issue:`9283` for a longer discussion). - -Due to the ongoing migration of methods like :meth:`~pytest.Item.reportinfo` -which still is expected to return a ``py.path.local`` object, nodes still have -both ``fspath`` (``py.path.local``) and ``path`` (``pathlib.Path``) attributes, -no matter what argument was used in the constructor. We expect to deprecate the -``fspath`` attribute in a future release. - - ``py.path.local`` arguments for hooks replaced with ``pathlib.Path`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index fa387f6db..121b1f9f6 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs """Python version compatibility code.""" + from __future__ import annotations import dataclasses @@ -16,6 +17,22 @@ from typing import Callable from typing import Final from typing import NoReturn +import py + + +#: constant to prepare valuing pylib path replacements/lazy proxies later on +# intended for removal in pytest 8.0 or 9.0 + +# fmt: off +# intentional space to create a fake difference for the verification +LEGACY_PATH = py.path. local +# fmt: on + + +def legacy_path(path: str | os.PathLike[str]) -> LEGACY_PATH: + """Internal wrapper to prepare lazy proxies for legacy_path instances""" + return LEGACY_PATH(path) + # fmt: off # Singleton type for NOTSET, as described in: diff --git a/src/_pytest/config/compat.py b/src/_pytest/config/compat.py new file mode 100644 index 000000000..9c61b4dac --- /dev/null +++ b/src/_pytest/config/compat.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from pathlib import Path + +from ..compat import LEGACY_PATH + + +def _check_path(path: Path, fspath: LEGACY_PATH) -> None: + if Path(fspath) != path: + raise ValueError( + f"Path({fspath!r}) != {path!r}\n" + "if both path and fspath are given they need to be equal" + ) diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 56271c957..508ea1184 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -36,6 +36,14 @@ YIELD_FIXTURE = PytestDeprecationWarning( PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.") +NODE_CTOR_FSPATH_ARG = UnformattedWarning( + PytestRemovedIn9Warning, + "The (fspath: py.path.local) argument to {node_type_name} is deprecated. " + "Please use the (path: pathlib.Path) argument instead.\n" + "See https://docs.pytest.org/en/latest/deprecations.html" + "#fspath-argument-for-node-constructors-replaced-with-pathlib-path", +) + HOOK_LEGACY_MARKING = UnformattedWarning( PytestDeprecationWarning, "The hook{type} {fullname} uses old-style configuration options (marks or attributes).\n" diff --git a/src/_pytest/legacypath.py b/src/_pytest/legacypath.py index b56f3a6fb..b28c89767 100644 --- a/src/_pytest/legacypath.py +++ b/src/_pytest/legacypath.py @@ -1,7 +1,7 @@ # mypy: allow-untyped-defs """Add backward compatibility support for the legacy py path type.""" + import dataclasses -import os from pathlib import Path import shlex import subprocess @@ -14,9 +14,9 @@ from typing import Union from iniconfig import SectionWrapper -import py - from _pytest.cacheprovider import Cache +from _pytest.compat import LEGACY_PATH +from _pytest.compat import legacy_path from _pytest.config import Config from _pytest.config import hookimpl from _pytest.config import PytestPluginManager @@ -39,20 +39,6 @@ if TYPE_CHECKING: import pexpect -#: constant to prepare valuing pylib path replacements/lazy proxies later on -# intended for removal in pytest 8.0 or 9.0 - -# fmt: off -# intentional space to create a fake difference for the verification -LEGACY_PATH = py.path. local -# fmt: on - - -def legacy_path(path: Union[str, "os.PathLike[str]"]) -> LEGACY_PATH: - """Internal wrapper to prepare lazy proxies for legacy_path instances""" - return LEGACY_PATH(path) - - @final class Testdir: """ diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 1de86be86..145722c25 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -557,6 +557,7 @@ class Session(nodes.Collector): super().__init__( name="", path=config.rootpath, + fspath=None, parent=None, config=config, session=self, diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 2381b65ea..cff15001c 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -3,6 +3,7 @@ import abc from functools import cached_property from inspect import signature import os +import pathlib from pathlib import Path from typing import Any from typing import Callable @@ -29,8 +30,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.compat import LEGACY_PATH from _pytest.config import Config from _pytest.config import ConftestImportFailure +from _pytest.config.compat import _check_path +from _pytest.deprecated import NODE_CTOR_FSPATH_ARG from _pytest.mark.structures import Mark from _pytest.mark.structures import MarkDecorator from _pytest.mark.structures import NodeKeywords @@ -55,6 +59,29 @@ tracebackcutdir = Path(_pytest.__file__).parent _T = TypeVar("_T") + + +def _imply_path( + node_type: Type["Node"], + path: Optional[Path], + fspath: Optional[LEGACY_PATH], +) -> Path: + if fspath is not None: + warnings.warn( + NODE_CTOR_FSPATH_ARG.format( + node_type_name=node_type.__name__, + ), + stacklevel=6, + ) + if path is not None: + if fspath is not None: + _check_path(path, fspath) + return path + else: + assert fspath is not None + return Path(fspath) + + _NodeType = TypeVar("_NodeType", bound="Node") @@ -110,6 +137,13 @@ class Node(abc.ABC, metaclass=NodeMeta): leaf nodes. """ + # Implemented in the legacypath plugin. + #: A ``LEGACY_PATH`` copy of the :attr:`path` attribute. Intended for usage + #: for methods not migrated to ``pathlib.Path`` yet, such as + #: :meth:`Item.reportinfo `. Will be deprecated in + #: a future release, prefer using :attr:`path` instead. + fspath: LEGACY_PATH + # Use __slots__ to make attribute access faster. # Note that __dict__ is still available. __slots__ = ( @@ -129,6 +163,7 @@ class Node(abc.ABC, metaclass=NodeMeta): parent: "Optional[Node]" = None, config: Optional[Config] = None, session: "Optional[Session]" = None, + fspath: Optional[LEGACY_PATH] = None, path: Optional[Path] = None, nodeid: Optional[str] = None, ) -> None: @@ -154,11 +189,10 @@ class Node(abc.ABC, metaclass=NodeMeta): raise TypeError("session or parent must be provided") self.session = parent.session - if path is None: + if path is None and fspath is None: path = getattr(parent, "path", None) - assert path is not None #: Filesystem path where this node was collected from (can be None). - self.path = path + self.path: pathlib.Path = _imply_path(type(self), path, fspath=fspath) # The explicit annotation is to avoid publicly exposing NodeKeywords. #: Keywords/markers collected from all scopes. @@ -529,6 +563,7 @@ class FSCollector(Collector, abc.ABC): def __init__( self, + fspath: Optional[LEGACY_PATH] = None, path_or_parent: Optional[Union[Path, Node]] = None, path: Optional[Path] = None, name: Optional[str] = None, @@ -544,8 +579,8 @@ class FSCollector(Collector, abc.ABC): elif isinstance(path_or_parent, Path): assert path is None path = path_or_parent - assert path is not None + path = _imply_path(type(self), path, fspath=fspath) if name is None: name = path.name if parent is not None and parent.path != path: @@ -585,11 +620,12 @@ class FSCollector(Collector, abc.ABC): cls, parent, *, + fspath: Optional[LEGACY_PATH] = None, path: Optional[Path] = None, **kw, ) -> "Self": """The public constructor.""" - return super().from_parent(parent=parent, path=path, **kw) + return super().from_parent(parent=parent, fspath=fspath, path=path, **kw) class File(FSCollector, abc.ABC): diff --git a/src/_pytest/python.py b/src/_pytest/python.py index e1730b1a7..1bbe96004 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -48,6 +48,7 @@ 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 from _pytest.compat import NOTSET from _pytest.compat import safe_getattr from _pytest.compat import safe_isclass @@ -665,6 +666,7 @@ class Package(nodes.Directory): def __init__( self, + fspath: Optional[LEGACY_PATH], parent: nodes.Collector, # NOTE: following args are unused: config=None, @@ -676,6 +678,7 @@ class Package(nodes.Directory): # super().__init__(self, fspath, parent=parent) session = parent.session super().__init__( + fspath=fspath, path=path, parent=parent, config=config, diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index a5f513063..6a230a9fd 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -1,5 +1,8 @@ # mypy: allow-untyped-defs +import re + from _pytest import deprecated +from _pytest.compat import legacy_path from _pytest.pytester import Pytester import pytest from pytest import PytestDeprecationWarning @@ -85,6 +88,25 @@ def test_private_is_deprecated() -> None: PrivateInit(10, _ispytest=True) +def test_node_ctor_fspath_argument_is_deprecated(pytester: Pytester) -> None: + mod = pytester.getmodulecol("") + + class MyFile(pytest.File): + def collect(self): + raise NotImplementedError() + + with pytest.warns( + pytest.PytestDeprecationWarning, + match=re.escape( + "The (fspath: py.path.local) argument to MyFile is deprecated." + ), + ): + MyFile.from_parent( + parent=mod.parent, + fspath=legacy_path("bla"), + ) + + def test_fixture_disallow_on_marked_functions(): """Test that applying @pytest.fixture to a marked function warns (#3364).""" with pytest.warns( diff --git a/testing/test_legacypath.py b/testing/test_legacypath.py index 850f14c58..49e620c11 100644 --- a/testing/test_legacypath.py +++ b/testing/test_legacypath.py @@ -1,8 +1,8 @@ # mypy: allow-untyped-defs from pathlib import Path +from _pytest.compat import LEGACY_PATH from _pytest.fixtures import TopRequest -from _pytest.legacypath import LEGACY_PATH from _pytest.legacypath import TempdirFactory from _pytest.legacypath import Testdir import pytest @@ -16,7 +16,7 @@ def test_item_fspath(pytester: pytest.Pytester) -> None: items2, hookrec = pytester.inline_genitems(item.nodeid) (item2,) = items2 assert item2.name == item.name - assert item2.fspath == item.fspath # type: ignore[attr-defined] + assert item2.fspath == item.fspath assert item2.path == item.path diff --git a/testing/test_nodes.py b/testing/test_nodes.py index e019f163c..a3caf471f 100644 --- a/testing/test_nodes.py +++ b/testing/test_nodes.py @@ -6,6 +6,7 @@ from typing import Type import warnings from _pytest import nodes +from _pytest.compat import legacy_path from _pytest.outcomes import OutcomeException from _pytest.pytester import Pytester from _pytest.warning_types import PytestWarning @@ -44,9 +45,9 @@ def test_subclassing_both_item_and_collector_deprecated( warnings.simplefilter("error") class SoWrong(nodes.Item, nodes.File): - def __init__(self, path, parent): + def __init__(self, fspath, parent): """Legacy ctor with legacy call # don't wana see""" - super().__init__(parent, path) + super().__init__(fspath, parent) def collect(self): raise NotImplementedError() @@ -55,7 +56,9 @@ def test_subclassing_both_item_and_collector_deprecated( raise NotImplementedError() with pytest.warns(PytestWarning) as rec: - SoWrong.from_parent(request.session, path=tmp_path / "broken.txt", wrong=10) + SoWrong.from_parent( + request.session, fspath=legacy_path(tmp_path / "broken.txt") + ) messages = [str(x.message) for x in rec] assert any( re.search(".*SoWrong.* not using a cooperative constructor.*", x) From dacee1f11d7495347fa2cfabeb33e998c95c8c05 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 7 Mar 2024 19:30:51 -0300 Subject: [PATCH 28/47] Revert "Remove deprecated py.path hook arguments" This reverts commit a98f02d4238793300f1be01f75bf0fff5609a241. --- doc/en/deprecations.rst | 55 +++++++++++++------------- src/_pytest/config/__init__.py | 4 +- src/_pytest/config/compat.py | 72 ++++++++++++++++++++++++++++++++++ src/_pytest/deprecated.py | 7 ++++ src/_pytest/hookspec.py | 43 ++++++++++---------- src/_pytest/main.py | 3 +- testing/deprecated_test.py | 33 ++++++++++++++++ 7 files changed, 165 insertions(+), 52 deletions(-) diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index b7ddd8b01..cd6d1e60a 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -58,7 +58,6 @@ both ``fspath`` (``py.path.local``) and ``path`` (``pathlib.Path``) attributes, no matter what argument was used in the constructor. We expect to deprecate the ``fspath`` attribute in a future release. -.. _legacy-path-hooks-deprecated: Configuring hook specs/impls using markers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -101,6 +100,33 @@ Changed ``hookwrapper`` attributes: * ``historic`` +.. _legacy-path-hooks-deprecated: + +``py.path.local`` arguments for hooks replaced with ``pathlib.Path`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 7.0 + +In order to support the transition from ``py.path.local`` to :mod:`pathlib`, the following hooks now receive additional arguments: + +* :hook:`pytest_ignore_collect(collection_path: pathlib.Path) ` as equivalent to ``path`` +* :hook:`pytest_collect_file(file_path: pathlib.Path) ` as equivalent to ``path`` +* :hook:`pytest_pycollect_makemodule(module_path: pathlib.Path) ` as equivalent to ``path`` +* :hook:`pytest_report_header(start_path: pathlib.Path) ` as equivalent to ``startdir`` +* :hook:`pytest_report_collectionfinish(start_path: pathlib.Path) ` as equivalent to ``startdir`` + +The accompanying ``py.path.local`` based paths have been deprecated: plugins which manually invoke those hooks should only pass the new ``pathlib.Path`` arguments, and users should change their hook implementations to use the new ``pathlib.Path`` arguments. + +.. note:: + The name of the :class:`~_pytest.nodes.Node` arguments and attributes, + :ref:`outlined above ` (the new attribute + being ``path``) is **the opposite** of the situation for hooks (the old + argument being ``path``). + + This is an unfortunate artifact due to historical reasons, which should be + resolved in future versions as we slowly get rid of the :pypi:`py` + dependency (see :issue:`9283` for a longer discussion). + Directly constructing internal classes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -247,33 +273,6 @@ an appropriate period of deprecation has passed. Some breaking changes which could not be deprecated are also listed. -``py.path.local`` arguments for hooks replaced with ``pathlib.Path`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 7.0 -.. versionremoved:: 8.0 - -In order to support the transition from ``py.path.local`` to :mod:`pathlib`, the following hooks now receive additional arguments: - -* :hook:`pytest_ignore_collect(collection_path: pathlib.Path) ` as equivalent to ``path`` -* :hook:`pytest_collect_file(file_path: pathlib.Path) ` as equivalent to ``path`` -* :hook:`pytest_pycollect_makemodule(module_path: pathlib.Path) ` as equivalent to ``path`` -* :hook:`pytest_report_header(start_path: pathlib.Path) ` as equivalent to ``startdir`` -* :hook:`pytest_report_collectionfinish(start_path: pathlib.Path) ` as equivalent to ``startdir`` - -The accompanying ``py.path.local`` based paths have been deprecated: plugins which manually invoke those hooks should only pass the new ``pathlib.Path`` arguments, and users should change their hook implementations to use the new ``pathlib.Path`` arguments. - -.. note:: - The name of the :class:`~_pytest.nodes.Node` arguments and attributes, - :ref:`outlined above ` (the new attribute - being ``path``) is **the opposite** of the situation for hooks (the old - argument being ``path``). - - This is an unfortunate artifact due to historical reasons, which should be - resolved in future versions as we slowly get rid of the :pypi:`py` - dependency (see :issue:`9283` for a longer discussion). - - .. _nose-deprecation: Support for tests written for nose diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 7ed79483c..bf2cfc399 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -38,12 +38,14 @@ from typing import TYPE_CHECKING from typing import Union import warnings +import pluggy from pluggy import HookimplMarker from pluggy import HookimplOpts from pluggy import HookspecMarker from pluggy import HookspecOpts from pluggy import PluginManager +from .compat import PathAwareHookProxy from .exceptions import PrintHelp as PrintHelp from .exceptions import UsageError as UsageError from .findpaths import determine_setup @@ -1068,7 +1070,7 @@ class Config: self._store = self.stash self.trace = self.pluginmanager.trace.root.get("config") - self.hook = self.pluginmanager.hook # type: ignore[assignment] + self.hook: pluggy.HookRelay = PathAwareHookProxy(self.pluginmanager.hook) # type: ignore[assignment] self._inicache: Dict[str, Any] = {} self._override_ini: Sequence[str] = () self._opt2dest: Dict[str, str] = {} diff --git a/src/_pytest/config/compat.py b/src/_pytest/config/compat.py index 9c61b4dac..2856d85d1 100644 --- a/src/_pytest/config/compat.py +++ b/src/_pytest/config/compat.py @@ -1,8 +1,26 @@ from __future__ import annotations +import functools from pathlib import Path +from typing import Any +from typing import Mapping +import warnings + +import pluggy from ..compat import LEGACY_PATH +from ..compat import legacy_path +from ..deprecated import HOOK_LEGACY_PATH_ARG + + +# hookname: (Path, LEGACY_PATH) +imply_paths_hooks: Mapping[str, tuple[str, str]] = { + "pytest_ignore_collect": ("collection_path", "path"), + "pytest_collect_file": ("file_path", "path"), + "pytest_pycollect_makemodule": ("module_path", "path"), + "pytest_report_header": ("start_path", "startdir"), + "pytest_report_collectionfinish": ("start_path", "startdir"), +} def _check_path(path: Path, fspath: LEGACY_PATH) -> None: @@ -11,3 +29,57 @@ def _check_path(path: Path, fspath: LEGACY_PATH) -> None: f"Path({fspath!r}) != {path!r}\n" "if both path and fspath are given they need to be equal" ) + + +class PathAwareHookProxy: + """ + this helper wraps around hook callers + until pluggy supports fixingcalls, this one will do + + it currently doesn't return full hook caller proxies for fixed hooks, + this may have to be changed later depending on bugs + """ + + def __init__(self, hook_relay: pluggy.HookRelay) -> None: + self._hook_relay = hook_relay + + def __dir__(self) -> list[str]: + return dir(self._hook_relay) + + def __getattr__(self, key: str) -> pluggy.HookCaller: + hook: pluggy.HookCaller = getattr(self._hook_relay, key) + if key not in imply_paths_hooks: + self.__dict__[key] = hook + return hook + else: + path_var, fspath_var = imply_paths_hooks[key] + + @functools.wraps(hook) + def fixed_hook(**kw: Any) -> Any: + path_value: Path | None = kw.pop(path_var, None) + fspath_value: LEGACY_PATH | None = kw.pop(fspath_var, None) + if fspath_value is not None: + warnings.warn( + HOOK_LEGACY_PATH_ARG.format( + pylib_path_arg=fspath_var, pathlib_path_arg=path_var + ), + stacklevel=2, + ) + if path_value is not None: + if fspath_value is not None: + _check_path(path_value, fspath_value) + else: + fspath_value = legacy_path(path_value) + else: + assert fspath_value is not None + path_value = Path(fspath_value) + + kw[path_var] = path_value + kw[fspath_var] = fspath_value + return hook(**kw) + + fixed_hook.name = hook.name # type: ignore[attr-defined] + fixed_hook.spec = hook.spec # type: ignore[attr-defined] + fixed_hook.__name__ = key + self.__dict__[key] = fixed_hook + return fixed_hook # type: ignore[return-value] diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 508ea1184..10811d158 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -36,6 +36,13 @@ YIELD_FIXTURE = PytestDeprecationWarning( PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.") +HOOK_LEGACY_PATH_ARG = UnformattedWarning( + PytestRemovedIn9Warning, + "The ({pylib_path_arg}: py.path.local) argument is deprecated, please use ({pathlib_path_arg}: pathlib.Path)\n" + "see https://docs.pytest.org/en/latest/deprecations.html" + "#py-path-local-arguments-for-hooks-replaced-with-pathlib-path", +) + NODE_CTOR_FSPATH_ARG = UnformattedWarning( PytestRemovedIn9Warning, "The (fspath: py.path.local) argument to {node_type_name} is deprecated. " diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 58f4986ec..4bee76f1e 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -22,6 +22,7 @@ if TYPE_CHECKING: from _pytest._code.code import ExceptionInfo from _pytest._code.code import ExceptionRepr + from _pytest.compat import LEGACY_PATH from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config import ExitCode @@ -296,7 +297,9 @@ def pytest_collection_finish(session: "Session") -> None: @hookspec(firstresult=True) -def pytest_ignore_collect(collection_path: Path, config: "Config") -> Optional[bool]: +def pytest_ignore_collect( + collection_path: Path, path: "LEGACY_PATH", config: "Config" +) -> Optional[bool]: """Return True to prevent considering this path for collection. This hook is consulted for all files and directories prior to calling @@ -310,10 +313,8 @@ def pytest_ignore_collect(collection_path: Path, config: "Config") -> Optional[b .. versionchanged:: 7.0.0 The ``collection_path`` parameter was added as a :class:`pathlib.Path` - equivalent of the ``path`` parameter. - - .. versionchanged:: 8.0.0 - The ``path`` parameter has been removed. + equivalent of the ``path`` parameter. The ``path`` parameter + has been deprecated. Use in conftest plugins ======================= @@ -354,7 +355,9 @@ def pytest_collect_directory(path: Path, parent: "Collector") -> "Optional[Colle """ -def pytest_collect_file(file_path: Path, parent: "Collector") -> "Optional[Collector]": +def pytest_collect_file( + file_path: Path, path: "LEGACY_PATH", parent: "Collector" +) -> "Optional[Collector]": """Create a :class:`~pytest.Collector` for the given path, or None if not relevant. For best results, the returned collector should be a subclass of @@ -367,10 +370,8 @@ def pytest_collect_file(file_path: Path, parent: "Collector") -> "Optional[Colle .. versionchanged:: 7.0.0 The ``file_path`` parameter was added as a :class:`pathlib.Path` - equivalent of the ``path`` parameter. - - .. versionchanged:: 8.0.0 - The ``path`` parameter was removed. + equivalent of the ``path`` parameter. The ``path`` parameter + has been deprecated. Use in conftest plugins ======================= @@ -467,7 +468,9 @@ def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectRepor @hookspec(firstresult=True) -def pytest_pycollect_makemodule(module_path: Path, parent) -> Optional["Module"]: +def pytest_pycollect_makemodule( + module_path: Path, path: "LEGACY_PATH", parent +) -> Optional["Module"]: """Return a :class:`pytest.Module` collector or None for the given path. This hook will be called for each matching test module path. @@ -483,8 +486,7 @@ def pytest_pycollect_makemodule(module_path: Path, parent) -> Optional["Module"] The ``module_path`` parameter was added as a :class:`pathlib.Path` equivalent of the ``path`` parameter. - .. versionchanged:: 8.0.0 - The ``path`` parameter has been removed in favor of ``module_path``. + The ``path`` parameter has been deprecated in favor of ``fspath``. Use in conftest plugins ======================= @@ -992,7 +994,7 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No def pytest_report_header( # type:ignore[empty-body] - config: "Config", start_path: Path + config: "Config", start_path: Path, startdir: "LEGACY_PATH" ) -> Union[str, List[str]]: """Return a string or list of strings to be displayed as header info for terminal reporting. @@ -1009,10 +1011,8 @@ def pytest_report_header( # type:ignore[empty-body] .. versionchanged:: 7.0.0 The ``start_path`` parameter was added as a :class:`pathlib.Path` - equivalent of the ``startdir`` parameter. - - .. versionchanged:: 8.0.0 - The ``startdir`` parameter has been removed. + equivalent of the ``startdir`` parameter. The ``startdir`` parameter + has been deprecated. Use in conftest plugins ======================= @@ -1024,6 +1024,7 @@ def pytest_report_header( # type:ignore[empty-body] def pytest_report_collectionfinish( # type:ignore[empty-body] config: "Config", start_path: Path, + startdir: "LEGACY_PATH", items: Sequence["Item"], ) -> Union[str, List[str]]: """Return a string or list of strings to be displayed after collection @@ -1047,10 +1048,8 @@ def pytest_report_collectionfinish( # type:ignore[empty-body] .. versionchanged:: 7.0.0 The ``start_path`` parameter was added as a :class:`pathlib.Path` - equivalent of the ``startdir`` parameter. - - .. versionchanged:: 8.0.0 - The ``startdir`` parameter has been removed. + equivalent of the ``startdir`` parameter. The ``startdir`` parameter + has been deprecated. Use in conftest plugins ======================= diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 145722c25..3b9ac93cf 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -37,6 +37,7 @@ from _pytest.config import hookimpl from _pytest.config import PytestPluginManager from _pytest.config import UsageError from _pytest.config.argparsing import Parser +from _pytest.config.compat import PathAwareHookProxy from _pytest.fixtures import FixtureManager from _pytest.outcomes import exit from _pytest.pathlib import absolutepath @@ -695,7 +696,7 @@ class Session(nodes.Collector): proxy: pluggy.HookRelay if remove_mods: # One or more conftests are not in use at this path. - proxy = FSHookProxy(pm, remove_mods) # type: ignore[arg-type,assignment] + proxy = PathAwareHookProxy(FSHookProxy(pm, remove_mods)) # type: ignore[arg-type,assignment] else: # All plugins are active for this fspath. proxy = self.config.hook diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 6a230a9fd..2be4d6dfc 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -1,5 +1,7 @@ # mypy: allow-untyped-defs +from pathlib import Path import re +import sys from _pytest import deprecated from _pytest.compat import legacy_path @@ -88,6 +90,37 @@ def test_private_is_deprecated() -> None: PrivateInit(10, _ispytest=True) +@pytest.mark.parametrize("hooktype", ["hook", "ihook"]) +def test_hookproxy_warnings_for_pathlib(tmp_path, hooktype, request): + path = legacy_path(tmp_path) + + PATH_WARN_MATCH = r".*path: py\.path\.local\) argument is deprecated, please use \(collection_path: pathlib\.Path.*" + if hooktype == "ihook": + hooks = request.node.ihook + else: + hooks = request.config.hook + + with pytest.warns(PytestDeprecationWarning, match=PATH_WARN_MATCH) as r: + l1 = sys._getframe().f_lineno + hooks.pytest_ignore_collect( + config=request.config, path=path, collection_path=tmp_path + ) + l2 = sys._getframe().f_lineno + + (record,) = r + assert record.filename == __file__ + assert l1 < record.lineno < l2 + + hooks.pytest_ignore_collect(config=request.config, collection_path=tmp_path) + + # Passing entirely *different* paths is an outright error. + with pytest.raises(ValueError, match=r"path.*fspath.*need to be equal"): + with pytest.warns(PytestDeprecationWarning, match=PATH_WARN_MATCH) as r: + hooks.pytest_ignore_collect( + config=request.config, path=path, collection_path=Path("/bla/bla") + ) + + def test_node_ctor_fspath_argument_is_deprecated(pytester: Pytester) -> None: mod = pytester.getmodulecol("") From 221097517b8499a11a2d9c1e1473989fbaf200d5 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 7 Mar 2024 19:38:05 -0300 Subject: [PATCH 29/47] Add changelog entry for #12069 --- changelog/12069.trivial.rst | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 changelog/12069.trivial.rst diff --git a/changelog/12069.trivial.rst b/changelog/12069.trivial.rst new file mode 100644 index 000000000..25c0db1c1 --- /dev/null +++ b/changelog/12069.trivial.rst @@ -0,0 +1,8 @@ +Delayed the deprecation of the following features to ``9.0.0``: + +* :ref:`node-ctor-fspath-deprecation`. +* :ref:`legacy-path-hooks-deprecated`. + +It was discovered after ``8.1.0`` was released that the warnings about the impeding removal were not being displayed, so the team decided to revert the removal. + +This was the reason for ``8.1.0`` being yanked. From 1a5e0eb71d2af0ad113ccd9ee596c7d724d7a4b6 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 7 Mar 2024 00:02:51 +0200 Subject: [PATCH 30/47] unittest: make `obj` work more like `Function`/`Class` Previously, the `obj` of a `TestCaseFunction` (the unittest plugin item type) was the unbound method. This is unlike regular `Class` where the `obj` is a bound method to a fresh instance. This difference necessitated several special cases in in places outside of the unittest plugin, such as `FixtureDef` and `FixtureRequest`, and made things a bit harder to understand. Instead, match how the python plugin does it, including collecting fixtures from a fresh instance. The downside is that now this instance for fixture-collection is kept around in memory, but it's the same as `Class` so nothing new. Users should only initialize stuff in `setUp`/`setUpClass` and similar methods, and not in `__init__` which is generally off-limits in `TestCase` subclasses. I am not sure why there was a difference in the first place, though I will say the previous unittest approach is probably the preferable one, but first let's get consistency. --- src/_pytest/compat.py | 8 +++--- src/_pytest/fixtures.py | 57 ++++++++++++---------------------------- src/_pytest/python.py | 1 - src/_pytest/unittest.py | 50 +++++++++++++++++++---------------- testing/test_unittest.py | 6 ++++- 5 files changed, 52 insertions(+), 70 deletions(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index fa387f6db..d39f96c4c 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -86,7 +86,6 @@ def getfuncargnames( function: Callable[..., object], *, name: str = "", - is_method: bool = False, cls: type | None = None, ) -> tuple[str, ...]: """Return the names of a function's mandatory arguments. @@ -97,9 +96,8 @@ def getfuncargnames( * Aren't bound with functools.partial. * Aren't replaced with mocks. - The is_method and cls arguments indicate that the function should - be treated as a bound method even though it's not unless, only in - the case of cls, the function is a static method. + The cls arguments indicate that the function should be treated as a bound + method even though it's not unless the function is a static method. The name parameter should be the original name in which the function was collected. """ @@ -137,7 +135,7 @@ def getfuncargnames( # If this function should be treated as a bound method even though # it's passed as an unbound method or function, remove the first # parameter name. - if is_method or ( + if ( # Not using `getattr` because we don't want to resolve the staticmethod. # Not using `cls.__dict__` because we want to check the entire MRO. cls diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 1ee7e84f7..45412159e 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -462,12 +462,8 @@ class FixtureRequest(abc.ABC): @property def instance(self): """Instance (can be None) on which test function was collected.""" - # unittest support hack, see _pytest.unittest.TestCaseFunction. - try: - return self._pyfuncitem._testcase # type: ignore[attr-defined] - except AttributeError: - function = getattr(self, "function", None) - return getattr(function, "__self__", None) + function = getattr(self, "function", None) + return getattr(function, "__self__", None) @property def module(self): @@ -965,7 +961,6 @@ class FixtureDef(Generic[FixtureValue]): func: "_FixtureFunc[FixtureValue]", scope: Union[Scope, _ScopeName, Callable[[str, Config], _ScopeName], None], params: Optional[Sequence[object]], - unittest: bool = False, ids: Optional[ Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]] ] = None, @@ -1011,9 +1006,7 @@ class FixtureDef(Generic[FixtureValue]): # a parameter value. self.ids: Final = ids # The names requested by the fixtures. - self.argnames: Final = getfuncargnames(func, name=argname, is_method=unittest) - # Whether the fixture was collected from a unittest TestCase class. - self.unittest: Final = unittest + self.argnames: Final = getfuncargnames(func, name=argname) # If the fixture was executed, the current value of the fixture. # Can change if the fixture is executed with different parameters. self.cached_result: Optional[_FixtureCachedResult[FixtureValue]] = None @@ -1092,25 +1085,20 @@ def resolve_fixture_function( """Get the actual callable that can be called to obtain the fixture value, dealing with unittest-specific instances and bound methods.""" fixturefunc = fixturedef.func - if fixturedef.unittest: - if request.instance is not None: - # Bind the unbound method to the TestCase instance. - fixturefunc = fixturedef.func.__get__(request.instance) # type: ignore[union-attr] - else: - # The fixture function needs to be bound to the actual - # request.instance so that code working with "fixturedef" behaves - # as expected. - if request.instance is not None: - # Handle the case where fixture is defined not in a test class, but some other class - # (for example a plugin class with a fixture), see #2270. - if hasattr(fixturefunc, "__self__") and not isinstance( - request.instance, - fixturefunc.__self__.__class__, # type: ignore[union-attr] - ): - return fixturefunc - fixturefunc = getimfunc(fixturedef.func) - if fixturefunc != fixturedef.func: - fixturefunc = fixturefunc.__get__(request.instance) # type: ignore[union-attr] + # The fixture function needs to be bound to the actual + # request.instance so that code working with "fixturedef" behaves + # as expected. + if request.instance is not None: + # Handle the case where fixture is defined not in a test class, but some other class + # (for example a plugin class with a fixture), see #2270. + if hasattr(fixturefunc, "__self__") and not isinstance( + request.instance, + fixturefunc.__self__.__class__, # type: ignore[union-attr] + ): + return fixturefunc + fixturefunc = getimfunc(fixturedef.func) + if fixturefunc != fixturedef.func: + fixturefunc = fixturefunc.__get__(request.instance) # type: ignore[union-attr] return fixturefunc @@ -1614,7 +1602,6 @@ class FixtureManager: Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]] ] = None, autouse: bool = False, - unittest: bool = False, ) -> None: """Register a fixture @@ -1635,8 +1622,6 @@ class FixtureManager: The fixture's IDs. :param autouse: Whether this is an autouse fixture. - :param unittest: - Set this if this is a unittest fixture. """ fixture_def = FixtureDef( config=self.config, @@ -1645,7 +1630,6 @@ class FixtureManager: func=func, scope=scope, params=params, - unittest=unittest, ids=ids, _ispytest=True, ) @@ -1667,8 +1651,6 @@ class FixtureManager: def parsefactories( self, node_or_obj: nodes.Node, - *, - unittest: bool = ..., ) -> None: raise NotImplementedError() @@ -1677,8 +1659,6 @@ class FixtureManager: self, node_or_obj: object, nodeid: Optional[str], - *, - unittest: bool = ..., ) -> None: raise NotImplementedError() @@ -1686,8 +1666,6 @@ class FixtureManager: self, node_or_obj: Union[nodes.Node, object], nodeid: Union[str, NotSetType, None] = NOTSET, - *, - unittest: bool = False, ) -> None: """Collect fixtures from a collection node or object. @@ -1739,7 +1717,6 @@ class FixtureManager: func=func, scope=marker.scope, params=marker.params, - unittest=unittest, ids=marker.ids, autouse=marker.autouse, ) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index e1730b1a7..a28c3befb 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1314,7 +1314,6 @@ class Metafunc: func=get_direct_param_fixture_func, scope=scope_, params=None, - unittest=False, ids=None, _ispytest=True, ) diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 2b7966531..b0ec02e7d 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -15,7 +15,6 @@ from typing import TYPE_CHECKING from typing import Union import _pytest._code -from _pytest.compat import getimfunc from _pytest.compat import is_async_function from _pytest.config import hookimpl from _pytest.fixtures import FixtureRequest @@ -63,6 +62,14 @@ class UnitTestCase(Class): # to declare that our children do not support funcargs. nofuncargs = True + def newinstance(self): + # TestCase __init__ takes the method (test) name. The TestCase + # constructor treats the name "runTest" as a special no-op, so it can be + # used when a dummy instance is needed. While unittest.TestCase has a + # default, some subclasses omit the default (#9610), so always supply + # it. + return self.obj("runTest") + def collect(self) -> Iterable[Union[Item, Collector]]: from unittest import TestLoader @@ -76,15 +83,15 @@ class UnitTestCase(Class): self._register_unittest_setup_class_fixture(cls) self._register_setup_class_fixture() - self.session._fixturemanager.parsefactories(self, unittest=True) + self.session._fixturemanager.parsefactories(self.newinstance(), self.nodeid) + loader = TestLoader() foundsomething = False for name in loader.getTestCaseNames(self.obj): x = getattr(self.obj, name) if not getattr(x, "__test__", True): continue - funcobj = getimfunc(x) - yield TestCaseFunction.from_parent(self, name=name, callobj=funcobj) + yield TestCaseFunction.from_parent(self, name=name) foundsomething = True if not foundsomething: @@ -169,23 +176,21 @@ class UnitTestCase(Class): class TestCaseFunction(Function): nofuncargs = True _excinfo: Optional[List[_pytest._code.ExceptionInfo[BaseException]]] = None - _testcase: Optional["unittest.TestCase"] = None def _getobj(self): - assert self.parent is not None - # Unlike a regular Function in a Class, where `item.obj` returns - # a *bound* method (attached to an instance), TestCaseFunction's - # `obj` returns an *unbound* method (not attached to an instance). - # This inconsistency is probably not desirable, but needs some - # consideration before changing. - return getattr(self.parent.obj, self.originalname) # type: ignore[attr-defined] + assert isinstance(self.parent, UnitTestCase) + testcase = self.parent.obj(self.name) + return getattr(testcase, self.name) + + # Backward compat for pytest-django; can be removed after pytest-django + # updates + some slack. + @property + def _testcase(self): + return self._obj.__self__ def setup(self) -> None: # A bound method to be called during teardown() if set (see 'runtest()'). self._explicit_tearDown: Optional[Callable[[], None]] = None - assert self.parent is not None - self._testcase = self.parent.obj(self.name) # type: ignore[attr-defined] - self._obj = getattr(self._testcase, self.name) super().setup() def teardown(self) -> None: @@ -193,7 +198,6 @@ class TestCaseFunction(Function): if self._explicit_tearDown is not None: self._explicit_tearDown() self._explicit_tearDown = None - self._testcase = None self._obj = None def startTest(self, testcase: "unittest.TestCase") -> None: @@ -292,14 +296,14 @@ class TestCaseFunction(Function): def runtest(self) -> None: from _pytest.debugging import maybe_wrap_pytest_function_for_tracing - assert self._testcase is not None + testcase = self.obj.__self__ maybe_wrap_pytest_function_for_tracing(self) # Let the unittest framework handle async functions. if is_async_function(self.obj): # Type ignored because self acts as the TestResult, but is not actually one. - self._testcase(result=self) # type: ignore[arg-type] + testcase(result=self) # type: ignore[arg-type] else: # When --pdb is given, we want to postpone calling tearDown() otherwise # when entering the pdb prompt, tearDown() would have probably cleaned up @@ -311,16 +315,16 @@ class TestCaseFunction(Function): assert isinstance(self.parent, UnitTestCase) skipped = _is_skipped(self.obj) or _is_skipped(self.parent.obj) if self.config.getoption("usepdb") and not skipped: - self._explicit_tearDown = self._testcase.tearDown - setattr(self._testcase, "tearDown", lambda *args: None) + self._explicit_tearDown = testcase.tearDown + setattr(testcase, "tearDown", lambda *args: None) # We need to update the actual bound method with self.obj, because # wrap_pytest_function_for_tracing replaces self.obj by a wrapper. - setattr(self._testcase, self.name, self.obj) + setattr(testcase, self.name, self.obj) try: - self._testcase(result=self) # type: ignore[arg-type] + testcase(result=self) # type: ignore[arg-type] finally: - delattr(self._testcase, self.name) + delattr(testcase, self.name) def _traceback_filter( self, excinfo: _pytest._code.ExceptionInfo[BaseException] diff --git a/testing/test_unittest.py b/testing/test_unittest.py index b5d182c14..9ecb548ee 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -208,10 +208,14 @@ def test_teardown_issue1649(pytester: Pytester) -> None: """ ) + pytester.inline_run("-s", testpath) gc.collect() + + # Either already destroyed, or didn't run setUp. for obj in gc.get_objects(): - assert type(obj).__name__ != "TestCaseObjectsShouldBeCleanedUp" + if type(obj).__name__ == "TestCaseObjectsShouldBeCleanedUp": + assert not hasattr(obj, "an_expensive_obj") def test_unittest_skip_issue148(pytester: Pytester) -> None: From a9d1f55a0f9b6f536967caf0f317cdbc8034a073 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 8 Mar 2024 22:39:44 +0200 Subject: [PATCH 31/47] fixtures: simplify scope checking There are two non-optimal things in the current way scope checking is done: - It runs on `SubRequest`, but doesn't use the `SubRequest's scope, which is confusing. Instead it takes `invoking_scope` and `requested_scope`. - Because `_check_scope` is only defined on `SubRequest` and not `TopRequest`, `_compute_fixture_value` first creates the `SubRequest` only then checks the scope (hence the need for the previous point). Instead, also define `_check_scope` on `TopRequest` (always valid), and remove `invoking_scope`, using `self._scope` instead. --- src/_pytest/fixtures.py | 64 ++++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 45412159e..e322c3f18 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -388,6 +388,14 @@ class FixtureRequest(abc.ABC): """Scope string, one of "function", "class", "module", "package", "session".""" return self._scope.value + @abc.abstractmethod + def _check_scope( + self, + requested_fixturedef: Union["FixtureDef[object]", PseudoFixtureDef[object]], + requested_scope: Scope, + ) -> None: + raise NotImplementedError() + @property def fixturenames(self) -> List[str]: """Names of all active fixtures in this request.""" @@ -632,12 +640,12 @@ class FixtureRequest(abc.ABC): ) fail(msg, pytrace=False) + # Check if a higher-level scoped fixture accesses a lower level one. + self._check_scope(fixturedef, scope) + subrequest = SubRequest( self, scope, param, param_index, fixturedef, _ispytest=True ) - - # Check if a higher-level scoped fixture accesses a lower level one. - subrequest._check_scope(argname, self._scope, scope) try: # Call the fixture function. fixturedef.execute(request=subrequest) @@ -669,6 +677,14 @@ class TopRequest(FixtureRequest): def _scope(self) -> Scope: return Scope.Function + def _check_scope( + self, + requested_fixturedef: Union["FixtureDef[object]", PseudoFixtureDef[object]], + requested_scope: Scope, + ) -> None: + # TopRequest always has function scope so always valid. + pass + @property def node(self): return self._pyfuncitem @@ -740,37 +756,33 @@ class SubRequest(FixtureRequest): def _check_scope( self, - argname: str, - invoking_scope: Scope, + requested_fixturedef: Union["FixtureDef[object]", PseudoFixtureDef[object]], requested_scope: Scope, ) -> None: - if argname == "request": + if isinstance(requested_fixturedef, PseudoFixtureDef): return - if invoking_scope > requested_scope: + if self._scope > requested_scope: # Try to report something helpful. - text = "\n".join(self._factorytraceback()) + argname = requested_fixturedef.argname + fixture_stack = "\n".join( + self._format_fixturedef_line(fixturedef) + for fixturedef in self._get_fixturestack() + ) + requested_fixture = self._format_fixturedef_line(requested_fixturedef) fail( f"ScopeMismatch: You tried to access the {requested_scope.value} scoped " - f"fixture {argname} with a {invoking_scope.value} scoped request object, " - f"involved factories:\n{text}", + f"fixture {argname} with a {self._scope.value} scoped request object, " + f"involved factories:\n{fixture_stack}\n{requested_fixture}", pytrace=False, ) - def _factorytraceback(self) -> List[str]: - lines = [] - for fixturedef in self._get_fixturestack(): - factory = fixturedef.func - fs, lineno = getfslineno(factory) - if isinstance(fs, Path): - session: Session = self._pyfuncitem.session - p = bestrelpath(session.path, fs) - else: - p = fs - lines.append( - "%s:%d: def %s%s" - % (p, lineno + 1, factory.__name__, inspect.signature(factory)) - ) - return lines + def _format_fixturedef_line(self, fixturedef: "FixtureDef[object]") -> str: + factory = fixturedef.func + path, lineno = getfslineno(factory) + if isinstance(path, Path): + path = bestrelpath(self._pyfuncitem.session.path, path) + signature = inspect.signature(factory) + return f"{path}:{lineno + 1}: def {factory.__name__}{signature}" def addfinalizer(self, finalizer: Callable[[], object]) -> None: self._fixturedef.addfinalizer(finalizer) @@ -1111,7 +1123,7 @@ def pytest_fixture_setup( fixdef = request._get_active_fixturedef(argname) assert fixdef.cached_result is not None result, arg_cache_key, exc = fixdef.cached_result - request._check_scope(argname, request._scope, fixdef._scope) + request._check_scope(fixdef, fixdef._scope) kwargs[argname] = result fixturefunc = resolve_fixture_function(fixturedef, request) From 71671f60b570e631d0b663ec4285319ade3c1ce5 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 8 Mar 2024 23:08:28 +0200 Subject: [PATCH 32/47] fixtures: improve fixture scope mismatch message - Separate the requesting from the requested. - Avoid the term "factory", I think most people don't distinguish between "fixture" and "fixture function" (i.e. "factory") and would find the term "factory" unfamiliar. --- src/_pytest/fixtures.py | 5 +++-- testing/python/fixtures.py | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index e322c3f18..40568a4a4 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -771,8 +771,9 @@ class SubRequest(FixtureRequest): requested_fixture = self._format_fixturedef_line(requested_fixturedef) fail( f"ScopeMismatch: You tried to access the {requested_scope.value} scoped " - f"fixture {argname} with a {self._scope.value} scoped request object, " - f"involved factories:\n{fixture_stack}\n{requested_fixture}", + f"fixture {argname} with a {self._scope.value} scoped request object. " + f"Requesting fixture stack:\n{fixture_stack}\n" + f"Requested fixture:\n{requested_fixture}", pytrace=False, ) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 6edff6ecd..8d59b36d3 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1247,8 +1247,9 @@ class TestFixtureUsages: result = pytester.runpytest() result.stdout.fnmatch_lines( [ - "*ScopeMismatch*involved factories*", + "*ScopeMismatch*Requesting fixture stack*", "test_receives_funcargs_scope_mismatch.py:6: def arg2(arg1)", + "Requested fixture:", "test_receives_funcargs_scope_mismatch.py:2: def arg1()", "*1 error*", ] @@ -1274,7 +1275,13 @@ class TestFixtureUsages: ) result = pytester.runpytest() result.stdout.fnmatch_lines( - ["*ScopeMismatch*involved factories*", "* def arg2*", "*1 error*"] + [ + "*ScopeMismatch*Requesting fixture stack*", + "* def arg2(arg1)", + "Requested fixture:", + "* def arg1()", + "*1 error*", + ], ) def test_invalid_scope(self, pytester: Pytester) -> None: @@ -2488,8 +2495,10 @@ class TestFixtureMarker: assert result.ret == ExitCode.TESTS_FAILED result.stdout.fnmatch_lines( [ - "*ScopeMismatch*involved factories*", + "*ScopeMismatch*Requesting fixture stack*", "test_it.py:6: def fixmod(fixfunc)", + "Requested fixture:", + "test_it.py:3: def fixfunc()", ] ) From f5de111357d26780760df78b503fdc8f4731f611 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 8 Mar 2024 23:18:43 +0200 Subject: [PATCH 33/47] fixtures: check scope mismatch in `getfixturevalue` already-cached case This makes sure the scope is always compatible, and also allows using `getfixturevalue` in `pytest_fixture_setup` so less internal magic. --- src/_pytest/fixtures.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 40568a4a4..2d3593df0 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -573,6 +573,8 @@ class FixtureRequest(abc.ABC): raise self._compute_fixture_value(fixturedef) self._fixture_defs[argname] = fixturedef + else: + self._check_scope(fixturedef, fixturedef._scope) return fixturedef def _get_fixturestack(self) -> List["FixtureDef[Any]"]: @@ -1121,11 +1123,7 @@ def pytest_fixture_setup( """Execution of fixture setup.""" kwargs = {} for argname in fixturedef.argnames: - fixdef = request._get_active_fixturedef(argname) - assert fixdef.cached_result is not None - result, arg_cache_key, exc = fixdef.cached_result - request._check_scope(fixdef, fixdef._scope) - kwargs[argname] = result + kwargs[argname] = request.getfixturevalue(argname) fixturefunc = resolve_fixture_function(fixturedef, request) my_cache_key = fixturedef.cache_key(request) From ff551b768520b1f510dbd8413d810191a79d1471 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 8 Mar 2024 23:24:38 +0200 Subject: [PATCH 34/47] fixtures: simplify a bit of code --- src/_pytest/fixtures.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 2d3593df0..4b7c10752 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1061,9 +1061,7 @@ class FixtureDef(Generic[FixtureValue]): # with their finalization. for argname in self.argnames: fixturedef = request._get_active_fixturedef(argname) - if argname != "request": - # PseudoFixtureDef is only for "request". - assert isinstance(fixturedef, FixtureDef) + if not isinstance(fixturedef, PseudoFixtureDef): fixturedef.addfinalizer(functools.partial(self.finish, request=request)) my_cache_key = self.cache_key(request) From 9033d4d3fff11e52c6812dfeba32f1cfecdea61a Mon Sep 17 00:00:00 2001 From: Levon Saldamli Date: Sat, 9 Mar 2024 07:51:52 +0100 Subject: [PATCH 35/47] Parse args from file (#12085) Co-authored-by: Ran Benita Co-authored-by: Bruno Oliveira --- .gitignore | 1 + AUTHORS | 1 + changelog/11871.feature.rst | 1 + doc/en/how-to/usage.rst | 25 ++++++++++++++++++++++++- src/_pytest/config/argparsing.py | 1 + testing/acceptance_test.py | 27 +++++++++++++++++++++++++++ testing/test_parseopt.py | 11 +++++++++++ 7 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 changelog/11871.feature.rst diff --git a/.gitignore b/.gitignore index 3cac2474a..9fccf93f7 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,7 @@ coverage.xml .settings .vscode __pycache__/ +.python-version # generated by pip pip-wheel-metadata/ diff --git a/AUTHORS b/AUTHORS index 4c4d68df1..53f7a8c2a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -235,6 +235,7 @@ Kyle Altendorf Lawrence Mitchell Lee Kamentsky Lev Maximov +Levon Saldamli Lewis Cowles Llandy Riveron Del Risco Loic Esteve diff --git a/changelog/11871.feature.rst b/changelog/11871.feature.rst new file mode 100644 index 000000000..530db8c3c --- /dev/null +++ b/changelog/11871.feature.rst @@ -0,0 +1 @@ +Added support for reading command line arguments from a file using the prefix character ``@``, like e.g.: ``pytest @tests.txt``. The file must have one argument per line. diff --git a/doc/en/how-to/usage.rst b/doc/en/how-to/usage.rst index 65f9debd8..fe46fad2d 100644 --- a/doc/en/how-to/usage.rst +++ b/doc/en/how-to/usage.rst @@ -17,7 +17,8 @@ in the current directory and its subdirectories. More generally, pytest follows Specifying which tests to run ------------------------------ -Pytest supports several ways to run and select tests from the command-line. +Pytest supports several ways to run and select tests from the command-line or from a file +(see below for :ref:`reading arguments from file `). **Run tests in a module** @@ -91,6 +92,28 @@ For more information see :ref:`marks `. This will import ``pkg.testing`` and use its filesystem location to find and run tests from. +.. _args-from-file: + +**Read arguments from file** + +.. versionadded:: 8.2 + +All of the above can be read from a file using the ``@`` prefix: + +.. code-block:: bash + + pytest @tests_to_run.txt + +where ``tests_to_run.txt`` contains an entry per line, e.g.: + +.. code-block:: text + + tests/test_file.py + tests/test_mod.py::test_func[x1,y2] + tests/test_mod.py::TestClass + -m slow + +This file can also be generated using ``pytest --collect-only -q`` and modified as needed. Getting help on version, option names, environment variables -------------------------------------------------------------- diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index d98f1ae9a..441d79e90 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -415,6 +415,7 @@ class MyOptionParser(argparse.ArgumentParser): add_help=False, formatter_class=DropShorterLongHelpFormatter, allow_abbrev=False, + fromfile_prefix_chars="@", ) # extra_info is a dict of (param -> value) to display if there's # an usage error to provide more contextual information to the user. diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index e41d7a81f..8f001bc24 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -2,6 +2,7 @@ import dataclasses import importlib.metadata import os +from pathlib import Path import subprocess import sys import types @@ -541,6 +542,32 @@ class TestGeneralUsage: res = pytester.runpytest(p) res.assert_outcomes(passed=3) + # Warning ignore because of: + # https://github.com/python/cpython/issues/85308 + # Can be removed once Python<3.12 support is dropped. + @pytest.mark.filterwarnings("ignore:'encoding' argument not specified") + def test_command_line_args_from_file( + self, pytester: Pytester, tmp_path: Path + ) -> None: + pytester.makepyfile( + test_file=""" + import pytest + + class TestClass: + @pytest.mark.parametrize("a", ["x","y"]) + def test_func(self, a): + pass + """ + ) + tests = [ + "test_file.py::TestClass::test_func[x]", + "test_file.py::TestClass::test_func[y]", + "-q", + ] + args_file = pytester.maketxtfile(tests="\n".join(tests)) + result = pytester.runpytest(f"@{args_file}") + result.assert_outcomes(failed=0, passed=2) + class TestInvocationVariants: def test_earlyinit(self, pytester: Pytester) -> None: diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py index 4678d8bdb..e959dfd63 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -125,6 +125,17 @@ class TestParser: args = parser.parse([Path(".")]) assert getattr(args, parseopt.FILE_OR_DIR)[0] == "." + # Warning ignore because of: + # https://github.com/python/cpython/issues/85308 + # Can be removed once Python<3.12 support is dropped. + @pytest.mark.filterwarnings("ignore:'encoding' argument not specified") + def test_parse_from_file(self, parser: parseopt.Parser, tmp_path: Path) -> None: + tests = [".", "some.py::Test::test_method[param0]", "other/test_file.py"] + args_file = tmp_path / "tests.txt" + args_file.write_text("\n".join(tests), encoding="utf-8") + args = parser.parse([f"@{args_file.absolute()}"]) + assert getattr(args, parseopt.FILE_OR_DIR) == tests + def test_parse_known_args(self, parser: parseopt.Parser) -> None: parser.parse_known_args([Path(".")]) parser.addoption("--hello", action="store_true") From 006058f1f90b00988cc61043f1ea55bdd2c55883 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 9 Mar 2024 10:13:15 +0200 Subject: [PATCH 36/47] fixtures: update outdated comment No longer does unittest stuff. Also the rest of the sentence is not really necessary for a docstring. --- src/_pytest/fixtures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 4b7c10752..58b515434 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1096,7 +1096,7 @@ def resolve_fixture_function( fixturedef: FixtureDef[FixtureValue], request: FixtureRequest ) -> "_FixtureFunc[FixtureValue]": """Get the actual callable that can be called to obtain the fixture - value, dealing with unittest-specific instances and bound methods.""" + value.""" fixturefunc = fixturedef.func # The fixture function needs to be bound to the actual # request.instance so that code working with "fixturedef" behaves From 774f0c44e63077efb2a7cf35b19b99a255b22332 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 9 Mar 2024 09:11:33 +0200 Subject: [PATCH 37/47] fixtures: only call `instance` property once in function No need to compute the property multiple times. --- src/_pytest/fixtures.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 58b515434..1eca68207 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1101,17 +1101,18 @@ def resolve_fixture_function( # The fixture function needs to be bound to the actual # request.instance so that code working with "fixturedef" behaves # as expected. - if request.instance is not None: + instance = request.instance + if instance is not None: # Handle the case where fixture is defined not in a test class, but some other class # (for example a plugin class with a fixture), see #2270. if hasattr(fixturefunc, "__self__") and not isinstance( - request.instance, + instance, fixturefunc.__self__.__class__, # type: ignore[union-attr] ): return fixturefunc fixturefunc = getimfunc(fixturedef.func) if fixturefunc != fixturedef.func: - fixturefunc = fixturefunc.__get__(request.instance) # type: ignore[union-attr] + fixturefunc = fixturefunc.__get__(instance) # type: ignore[union-attr] return fixturefunc From 140c7775901c0f1e17210fed20774945ee82bd0f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 9 Mar 2024 08:51:20 -0300 Subject: [PATCH 38/47] Merge pull request #12094 from pytest-dev/release-8.1.1 Prepare release 8.1.1 (cherry picked from commit abb0cf4922919e3554bd16e9fc540bc107289ee9) --- changelog/11475.bugfix.rst | 1 - changelog/12069.trivial.rst | 8 ------- doc/en/announce/index.rst | 1 + doc/en/announce/release-8.1.1.rst | 18 ++++++++++++++ doc/en/builtin.rst | 4 ++-- doc/en/changelog.rst | 37 ++++++++++++++++++++++------- doc/en/example/parametrize.rst | 6 ++--- doc/en/example/pythoncollection.rst | 4 ++-- doc/en/getting-started.rst | 2 +- doc/en/how-to/fixtures.rst | 2 +- 10 files changed, 57 insertions(+), 26 deletions(-) delete mode 100644 changelog/11475.bugfix.rst delete mode 100644 changelog/12069.trivial.rst create mode 100644 doc/en/announce/release-8.1.1.rst diff --git a/changelog/11475.bugfix.rst b/changelog/11475.bugfix.rst deleted file mode 100644 index bef8f4f76..000000000 --- a/changelog/11475.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed regression where ``--importmode=importlib`` would import non-test modules more than once. diff --git a/changelog/12069.trivial.rst b/changelog/12069.trivial.rst deleted file mode 100644 index 25c0db1c1..000000000 --- a/changelog/12069.trivial.rst +++ /dev/null @@ -1,8 +0,0 @@ -Delayed the deprecation of the following features to ``9.0.0``: - -* :ref:`node-ctor-fspath-deprecation`. -* :ref:`legacy-path-hooks-deprecated`. - -It was discovered after ``8.1.0`` was released that the warnings about the impeding removal were not being displayed, so the team decided to revert the removal. - -This was the reason for ``8.1.0`` being yanked. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 68cae83d7..40eccdd74 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-8.1.1 release-8.1.0 release-8.0.2 release-8.0.1 diff --git a/doc/en/announce/release-8.1.1.rst b/doc/en/announce/release-8.1.1.rst new file mode 100644 index 000000000..89b617b48 --- /dev/null +++ b/doc/en/announce/release-8.1.1.rst @@ -0,0 +1,18 @@ +pytest-8.1.1 +======================================= + +pytest 8.1.1 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Ran Benita + + +Happy testing, +The pytest Development Team diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst index 1e1210648..9d49389f1 100644 --- a/doc/en/builtin.rst +++ b/doc/en/builtin.rst @@ -170,10 +170,10 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a `pytest-xdist `__ plugin. See :issue:`7767` for details. - tmpdir_factory [session scope] -- .../_pytest/legacypath.py:317 + tmpdir_factory [session scope] -- .../_pytest/legacypath.py:303 Return a :class:`pytest.TempdirFactory` instance for the test session. - tmpdir -- .../_pytest/legacypath.py:324 + tmpdir -- .../_pytest/legacypath.py:310 Return a temporary directory path object which is unique to each test function invocation, created as a sub directory of the base temporary directory. diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 344230594..bea4257af 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -28,16 +28,13 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start -pytest 8.1.0 (YANKED) -===================== - +pytest 8.1.1 (2024-03-08) +========================= .. note:: - This release has been **yanked**: it broke some plugins without the proper warning period, due to - some warnings not showing up as expected. - - See `#12069 `__. + This release is not a usual bug fix release -- it contains features and improvements, being a follow up + to ``8.1.0``, which has been yanked from PyPI. Features -------- @@ -94,6 +91,9 @@ Improvements Bug Fixes --------- +- `#11475 `_: Fixed regression where ``--importmode=importlib`` would import non-test modules more than once. + + - `#11904 `_: Fixed a regression in pytest 8.0.0 that would cause test collection to fail due to permission errors when using ``--pyargs``. This change improves the collection tree for tests specified using ``--pyargs``, see :pull:`12043` for a comparison with pytest 8.0 and <8. @@ -108,7 +108,6 @@ Bug Fixes - `#12039 `_: Fixed a regression in ``8.0.2`` where tests created using :fixture:`tmp_path` have been collected multiple times in CI under Windows. - Improved Documentation ---------------------- @@ -128,6 +127,28 @@ Trivial/Internal Changes If you really need to, copy the function from the previous pytest release. +- `#12069 `_: Delayed the deprecation of the following features to ``9.0.0``: + + * :ref:`node-ctor-fspath-deprecation`. + * :ref:`legacy-path-hooks-deprecated`. + + It was discovered after ``8.1.0`` was released that the warnings about the impeding removal were not being displayed, so the team decided to revert the removal. + + This is the reason for ``8.1.0`` being yanked. + + +pytest 8.1.0 (YANKED) +===================== + + +.. note:: + + This release has been **yanked**: it broke some plugins without the proper warning period, due to + some warnings not showing up as expected. + + See `#12069 `__. + + pytest 8.0.2 (2024-02-24) ========================= diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 85c683679..ad17ce0b4 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -162,7 +162,7 @@ objects, they are still using the default pytest representation: rootdir: /home/sweet/project collected 8 items - + @@ -239,7 +239,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia rootdir: /home/sweet/project collected 4 items - + @@ -318,7 +318,7 @@ Let's first see how it looks like at collection time: rootdir: /home/sweet/project collected 2 items - + diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index 6822aa68e..68737267e 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -152,7 +152,7 @@ The test collection would look like this: configfile: pytest.ini collected 2 items - + @@ -215,7 +215,7 @@ You can always peek at the collection tree without running tests like this: configfile: pytest.ini collected 3 items - + diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 89381c8c7..40632645d 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -22,7 +22,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - pytest 8.1.0 + pytest 8.1.1 .. _`simpletest`: diff --git a/doc/en/how-to/fixtures.rst b/doc/en/how-to/fixtures.rst index c32de1610..795d2caf5 100644 --- a/doc/en/how-to/fixtures.rst +++ b/doc/en/how-to/fixtures.rst @@ -1418,7 +1418,7 @@ Running the above tests results in the following test IDs being used: rootdir: /home/sweet/project collected 12 items - + From 0dc036035107b213c9b73bf965cbd7356111b85a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 9 Mar 2024 09:08:44 +0200 Subject: [PATCH 39/47] python: fix instance handling in static and class method tests and also fixes a regression in pytest 8.0.0 where `setup_method` crashes if the class has static or class method tests. It is allowed to have a test class with static/class methods which request non-static/class method fixtures (including `setup_method` xunit-fixture). I take it as a given that we need to support this somewhat odd scenario (stdlib unittest also supports it). This raises a question -- when a staticmethod test requests a bound fixture, what is that fixture's `self`? stdlib unittest says - a fresh instance for the test. Previously, pytest said - some instance that is shared by all static/class methods. This is definitely broken since it breaks test isolation. Change pytest to behave like stdlib unittest here. In practice, this means stopping to rely on `self.obj.__self__` to get to the instance from the test function's binding. This doesn't work because staticmethods are not bound to anything. Instead, keep the instance explicitly and use that. BTW, I think this will allow us to change `Class`'s fixture collection (`parsefactories`) to happen on the class itself instead of a class instance, allowing us to avoid one class instantiation. But needs more work. Fixes #12065. --- changelog/12065.bugfix.rst | 4 ++++ src/_pytest/fixtures.py | 5 ++-- src/_pytest/python.py | 34 ++++++++++++++++++++------ src/_pytest/unittest.py | 10 ++++---- testing/python/fixtures.py | 45 +++++++++++++++++++++++++++++++++++ testing/python/integration.py | 17 ++++++++++++- 6 files changed, 100 insertions(+), 15 deletions(-) create mode 100644 changelog/12065.bugfix.rst diff --git a/changelog/12065.bugfix.rst b/changelog/12065.bugfix.rst new file mode 100644 index 000000000..ca55b327e --- /dev/null +++ b/changelog/12065.bugfix.rst @@ -0,0 +1,4 @@ +Fixed a regression in pytest 8.0.0 where test classes containing ``setup_method`` and tests using ``@staticmethod`` or ``@classmethod`` would crash with ``AttributeError: 'NoneType' object has no attribute 'setup_method'``. + +Now the :attr:`request.instance ` attribute of tests using ``@staticmethod`` and ``@classmethod`` is no longer ``None``, but a fresh instance of the class, like in non-static methods. +Previously it was ``None``, and all fixtures of such tests would share a single ``self``. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 1eca68207..daf3145aa 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -470,8 +470,9 @@ class FixtureRequest(abc.ABC): @property def instance(self): """Instance (can be None) on which test function was collected.""" - function = getattr(self, "function", None) - return getattr(function, "__self__", None) + if self.scope != "function": + return None + return getattr(self._pyfuncitem, "instance", None) @property def module(self): diff --git a/src/_pytest/python.py b/src/_pytest/python.py index fce2078cd..7b0683b6e 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -302,10 +302,10 @@ class PyobjMixin(nodes.Node): """Python instance object the function is bound to. Returns None if not a test method, e.g. for a standalone test function, - a staticmethod, a class or a module. + a class or a module. """ - node = self.getparent(Function) - return getattr(node.obj, "__self__", None) if node is not None else None + # Overridden by Function. + return None @property def obj(self): @@ -1702,7 +1702,8 @@ class Function(PyobjMixin, nodes.Item): super().__init__(name, parent, config=config, session=session) if callobj is not NOTSET: - self.obj = callobj + self._obj = callobj + self._instance = getattr(callobj, "__self__", None) #: Original function name, without any decorations (for example #: parametrization adds a ``"[...]"`` suffix to function names), used to access @@ -1752,12 +1753,31 @@ class Function(PyobjMixin, nodes.Item): """Underlying python 'function' object.""" return getimfunc(self.obj) - def _getobj(self): - assert self.parent is not None + @property + def instance(self): + try: + return self._instance + except AttributeError: + if isinstance(self.parent, Class): + # Each Function gets a fresh class instance. + self._instance = self._getinstance() + else: + self._instance = None + return self._instance + + def _getinstance(self): if isinstance(self.parent, Class): # Each Function gets a fresh class instance. - parent_obj = self.parent.newinstance() + return self.parent.newinstance() else: + return None + + def _getobj(self): + instance = self.instance + if instance is not None: + parent_obj = instance + else: + assert self.parent is not None parent_obj = self.parent.obj # type: ignore[attr-defined] return getattr(parent_obj, self.originalname) diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index b0ec02e7d..32eb361c6 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -177,16 +177,15 @@ class TestCaseFunction(Function): nofuncargs = True _excinfo: Optional[List[_pytest._code.ExceptionInfo[BaseException]]] = None - def _getobj(self): + def _getinstance(self): assert isinstance(self.parent, UnitTestCase) - testcase = self.parent.obj(self.name) - return getattr(testcase, self.name) + return self.parent.obj(self.name) # Backward compat for pytest-django; can be removed after pytest-django # updates + some slack. @property def _testcase(self): - return self._obj.__self__ + return self.instance def setup(self) -> None: # A bound method to be called during teardown() if set (see 'runtest()'). @@ -296,7 +295,8 @@ class TestCaseFunction(Function): def runtest(self) -> None: from _pytest.debugging import maybe_wrap_pytest_function_for_tracing - testcase = self.obj.__self__ + testcase = self.instance + assert testcase is not None maybe_wrap_pytest_function_for_tracing(self) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 8d59b36d3..2e277626c 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4577,3 +4577,48 @@ def test_deduplicate_names() -> None: assert items == ("a", "b", "c", "d") items = deduplicate_names((*items, "g", "f", "g", "e", "b")) assert items == ("a", "b", "c", "d", "g", "f", "e") + + +def test_staticmethod_classmethod_fixture_instance(pytester: Pytester) -> None: + """Ensure that static and class methods get and have access to a fresh + instance. + + This also ensures `setup_method` works well with static and class methods. + + Regression test for #12065. + """ + pytester.makepyfile( + """ + import pytest + + class Test: + ran_setup_method = False + ran_fixture = False + + def setup_method(self): + assert not self.ran_setup_method + self.ran_setup_method = True + + @pytest.fixture(autouse=True) + def fixture(self): + assert not self.ran_fixture + self.ran_fixture = True + + def test_method(self): + assert self.ran_setup_method + assert self.ran_fixture + + @staticmethod + def test_1(request): + assert request.instance.ran_setup_method + assert request.instance.ran_fixture + + @classmethod + def test_2(cls, request): + assert request.instance.ran_setup_method + assert request.instance.ran_fixture + """ + ) + result = pytester.runpytest() + assert result.ret == ExitCode.OK + result.assert_outcomes(passed=3) diff --git a/testing/python/integration.py b/testing/python/integration.py index a6c14ece4..219ebf9ce 100644 --- a/testing/python/integration.py +++ b/testing/python/integration.py @@ -410,22 +410,37 @@ def test_function_instance(pytester: Pytester) -> None: items = pytester.getitems( """ def test_func(): pass + class TestIt: def test_method(self): pass + @classmethod def test_class(cls): pass + @staticmethod def test_static(): pass """ ) assert len(items) == 4 + assert isinstance(items[0], Function) assert items[0].name == "test_func" assert items[0].instance is None + assert isinstance(items[1], Function) assert items[1].name == "test_method" assert items[1].instance is not None assert items[1].instance.__class__.__name__ == "TestIt" + + # Even class and static methods get an instance! + # This is the instance used for bound fixture methods, which + # class/staticmethod tests are perfectly able to request. + assert isinstance(items[2], Function) + assert items[2].name == "test_class" + assert items[2].instance is not None + assert isinstance(items[3], Function) assert items[3].name == "test_static" - assert items[3].instance is None + assert items[3].instance is not None + + assert items[1].instance is not items[2].instance is not items[3].instance From b777b05c0e845fd3d26d22348991b2d19212250b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 10 Mar 2024 09:57:13 -0300 Subject: [PATCH 40/47] [automated] Update plugin list (#12098) Co-authored-by: pytest bot --- doc/en/reference/plugin_list.rst | 184 +++++++++++++++++-------------- 1 file changed, 104 insertions(+), 80 deletions(-) diff --git a/doc/en/reference/plugin_list.rst b/doc/en/reference/plugin_list.rst index fb3ff912c..8ca3a748b 100644 --- a/doc/en/reference/plugin_list.rst +++ b/doc/en/reference/plugin_list.rst @@ -27,7 +27,7 @@ please refer to `the update script =6.1.0 :pypi:`pytest-airflow` pytest support for airflow. Apr 03, 2019 3 - Alpha pytest (>=4.4.0) :pypi:`pytest-airflow-utils` Nov 15, 2021 N/A N/A - :pypi:`pytest-alembic` A pytest plugin for verifying alembic migrations. Jul 06, 2023 N/A pytest (>=6.0) + :pypi:`pytest-alembic` A pytest plugin for verifying alembic migrations. Mar 04, 2024 N/A pytest (>=6.0) :pypi:`pytest-allclose` Pytest fixture extending Numpy's allclose function Jul 30, 2019 5 - Production/Stable pytest :pypi:`pytest-allure-adaptor` Plugin for py.test to generate allure xml reports Jan 10, 2018 N/A pytest (>=2.7.3) :pypi:`pytest-allure-adaptor2` Plugin for py.test to generate allure xml reports Oct 14, 2020 N/A pytest (>=2.7.3) @@ -107,7 +107,7 @@ This list contains 1408 plugins. :pypi:`pytest-ast-transformer` May 04, 2019 3 - Alpha pytest :pypi:`pytest_async` pytest-async - Run your coroutine in event loop without decorator Feb 26, 2020 N/A N/A :pypi:`pytest-async-generators` Pytest fixtures for async generators Jul 05, 2023 N/A N/A - :pypi:`pytest-asyncio` Pytest support for asyncio Feb 09, 2024 4 - Beta pytest <9,>=7.0.0 + :pypi:`pytest-asyncio` Pytest support for asyncio Mar 08, 2024 4 - Beta pytest <9,>=7.0.0 :pypi:`pytest-asyncio-cooperative` Run all your asynchronous tests cooperatively. Feb 25, 2024 N/A N/A :pypi:`pytest-asyncio-network-simulator` pytest-asyncio-network-simulator: Plugin for pytest for simulator the network in tests Jul 31, 2018 3 - Alpha pytest (<3.7.0,>=3.3.2) :pypi:`pytest-async-mongodb` pytest plugin for async MongoDB Oct 18, 2017 5 - Production/Stable pytest (>=2.5.2) @@ -135,7 +135,7 @@ This list contains 1408 plugins. :pypi:`pytest-bandit` A bandit plugin for pytest Feb 23, 2021 4 - Beta pytest (>=3.5.0) :pypi:`pytest-bandit-xayon` A bandit plugin for pytest Oct 17, 2022 4 - Beta pytest (>=3.5.0) :pypi:`pytest-base-url` pytest plugin for URL based testing Jan 31, 2024 5 - Production/Stable pytest>=7.0.0 - :pypi:`pytest-bdd` BDD for pytest Dec 02, 2023 6 - Mature pytest (>=6.2.0) + :pypi:`pytest-bdd` BDD for pytest Mar 04, 2024 6 - Mature pytest (>=6.2.0) :pypi:`pytest-bdd-html` pytest plugin to display BDD info in HTML test report Nov 22, 2022 3 - Alpha pytest (!=6.0.0,>=5.0) :pypi:`pytest-bdd-ng` BDD for pytest Dec 31, 2023 4 - Beta pytest >=5.0 :pypi:`pytest-bdd-report` A pytest-bdd plugin for generating useful and informative BDD test reports Feb 19, 2024 N/A pytest >=7.1.3 @@ -149,7 +149,7 @@ This list contains 1408 plugins. :pypi:`pytest-bench` Benchmark utility that plugs into pytest. Jul 21, 2014 3 - Alpha N/A :pypi:`pytest-benchmark` A \`\`pytest\`\` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer. Oct 25, 2022 5 - Production/Stable pytest (>=3.8) :pypi:`pytest-better-datadir` A small example package Mar 13, 2023 N/A N/A - :pypi:`pytest-better-parametrize` Better description of parametrized test cases Feb 26, 2024 4 - Beta pytest >=6.2.0 + :pypi:`pytest-better-parametrize` Better description of parametrized test cases Mar 05, 2024 4 - Beta pytest >=6.2.0 :pypi:`pytest-bg-process` Pytest plugin to initialize background process Jan 24, 2022 4 - Beta pytest (>=3.5.0) :pypi:`pytest-bigchaindb` A BigchainDB plugin for pytest. Jan 24, 2022 4 - Beta N/A :pypi:`pytest-bigquery-mock` Provides a mock fixture for python bigquery client Dec 28, 2022 N/A pytest (>=5.0) @@ -193,11 +193,11 @@ This list contains 1408 plugins. :pypi:`pytest-caprng` A plugin that replays pRNG state on failure. May 02, 2018 4 - Beta N/A :pypi:`pytest-capture-deprecatedwarnings` pytest plugin to capture all deprecatedwarnings and put them in one file Apr 30, 2019 N/A N/A :pypi:`pytest-capture-warnings` pytest plugin to capture all warnings and put them in one file of your choice May 03, 2022 N/A pytest - :pypi:`pytest-cases` Separate test code from test cases in pytest. Jan 12, 2024 5 - Production/Stable N/A + :pypi:`pytest-cases` Separate test code from test cases in pytest. Mar 08, 2024 5 - Production/Stable N/A :pypi:`pytest-cassandra` Cassandra CCM Test Fixtures for pytest Nov 04, 2017 1 - Planning N/A :pypi:`pytest-catchlog` py.test plugin to catch log messages. This is a fork of pytest-capturelog. Jan 24, 2016 4 - Beta pytest (>=2.6) :pypi:`pytest-catch-server` Pytest plugin with server for catching HTTP requests. Dec 12, 2019 5 - Production/Stable N/A - :pypi:`pytest-celery` pytest-celery a shim pytest plugin to enable celery.contrib.pytest Feb 12, 2024 N/A N/A + :pypi:`pytest-celery` pytest-celery a shim pytest plugin to enable celery.contrib.pytest Mar 09, 2024 N/A N/A :pypi:`pytest-cfg-fetcher` Pass config options to your unit tests. Feb 26, 2024 N/A N/A :pypi:`pytest-chainmaker` pytest plugin for chainmaker Oct 15, 2021 N/A N/A :pypi:`pytest-chalice` A set of py.test fixtures for AWS Chalice Jul 01, 2020 4 - Beta N/A @@ -211,7 +211,8 @@ This list contains 1408 plugins. :pypi:`pytest-checkipdb` plugin to check if there are ipdb debugs left Dec 04, 2023 5 - Production/Stable pytest >=2.9.2 :pypi:`pytest-check-library` check your missing library Jul 17, 2022 N/A N/A :pypi:`pytest-check-libs` check your missing library Jul 17, 2022 N/A N/A - :pypi:`pytest-check-links` Check links in files Jul 29, 2020 N/A pytest>=7.0 + :pypi:`pytest-check-links` Check links in files Jul 29, 2020 N/A pytest<8,>=7.0 + :pypi:`pytest-checklist` Pytest plugin to track and report unit/function coverage. Mar 06, 2024 N/A N/A :pypi:`pytest-check-mk` pytest plugin to test Check_MK checks Nov 19, 2015 4 - Beta pytest :pypi:`pytest-check-requirements` A package to prevent Dependency Confusion attacks against Yandex. Feb 20, 2024 N/A N/A :pypi:`pytest-chic-report` A pytest plugin to send a report and printing summary of tests. Jan 31, 2023 5 - Production/Stable N/A @@ -232,7 +233,7 @@ This list contains 1408 plugins. :pypi:`pytest-cloud` Distributed tests planner plugin for pytest testing framework. Oct 05, 2020 6 - Mature N/A :pypi:`pytest-cloudflare-worker` pytest plugin for testing cloudflare workers Mar 30, 2021 4 - Beta pytest (>=6.0.0) :pypi:`pytest-cloudist` Distribute tests to cloud machines without fuss Sep 02, 2022 4 - Beta pytest (>=7.1.2,<8.0.0) - :pypi:`pytest-cmake` Provide CMake module for Pytest Jul 19, 2023 N/A pytest<8,>=4 + :pypi:`pytest-cmake` Provide CMake module for Pytest Mar 04, 2024 N/A pytest<9,>=4 :pypi:`pytest-cmake-presets` Execute CMake Presets via pytest Dec 26, 2022 N/A pytest (>=7.2.0,<8.0.0) :pypi:`pytest-cobra` PyTest plugin for testing Smart Contracts for Ethereum blockchain. Jun 29, 2019 3 - Alpha pytest (<4.0.0,>=3.7.1) :pypi:`pytest_codeblocks` Test code blocks in your READMEs Sep 17, 2023 5 - Production/Stable pytest >= 7.0.0 @@ -274,7 +275,7 @@ This list contains 1408 plugins. :pypi:`pytest-cov-exclude` Pytest plugin for excluding tests based on coverage data Apr 29, 2016 4 - Beta pytest (>=2.8.0,<2.9.0); extra == 'dev' :pypi:`pytest_covid` Too many faillure, less tests. Jun 24, 2020 N/A N/A :pypi:`pytest-cpp` Use pytest's runner to discover and execute C++ tests Nov 01, 2023 5 - Production/Stable pytest >=7.0 - :pypi:`pytest-cppython` A pytest plugin that imports CPPython testing types Aug 26, 2023 N/A N/A + :pypi:`pytest-cppython` A pytest plugin that imports CPPython testing types Mar 09, 2024 N/A N/A :pypi:`pytest-cqase` Custom qase pytest plugin Aug 22, 2022 N/A pytest (>=7.1.2,<8.0.0) :pypi:`pytest-cram` Run cram tests with pytest. Aug 08, 2020 N/A N/A :pypi:`pytest-crate` Manages CrateDB instances during your integration tests May 28, 2019 3 - Alpha pytest (>=4.0) @@ -393,7 +394,7 @@ This list contains 1408 plugins. :pypi:`pytest-doctest-ellipsis-markers` Setup additional values for ELLIPSIS_MARKER for doctests Jan 12, 2018 4 - Beta N/A :pypi:`pytest-doctest-import` A simple pytest plugin to import names and add them to the doctest namespace. Nov 13, 2018 4 - Beta pytest (>=3.3.0) :pypi:`pytest-doctest-mkdocstrings` Run pytest --doctest-modules with markdown docstrings in code blocks (\`\`\`) Mar 02, 2024 N/A pytest - :pypi:`pytest-doctestplus` Pytest plugin with advanced doctest features. Dec 13, 2023 5 - Production/Stable pytest >=4.6 + :pypi:`pytest-doctestplus` Pytest plugin with advanced doctest features. Mar 04, 2024 5 - Production/Stable pytest >=4.6 :pypi:`pytest-dogu-report` pytest plugin for dogu report Jul 07, 2023 N/A N/A :pypi:`pytest-dogu-sdk` pytest plugin for the Dogu Dec 14, 2023 N/A N/A :pypi:`pytest-dolphin` Some extra stuff that we use ininternally Nov 30, 2016 4 - Beta pytest (==3.0.4) @@ -404,7 +405,7 @@ This list contains 1408 plugins. :pypi:`pytest-draw` Pytest plugin for randomly selecting a specific number of tests Mar 21, 2023 3 - Alpha pytest :pypi:`pytest-drf` A Django REST framework plugin for pytest. Jul 12, 2022 5 - Production/Stable pytest (>=3.7) :pypi:`pytest-drivings` Tool to allow webdriver automation to be ran locally or remotely Jan 13, 2021 N/A N/A - :pypi:`pytest-drop-dup-tests` A Pytest plugin to drop duplicated tests during collection May 23, 2020 4 - Beta pytest (>=2.7) + :pypi:`pytest-drop-dup-tests` A Pytest plugin to drop duplicated tests during collection Mar 04, 2024 5 - Production/Stable pytest >=7 :pypi:`pytest-dryrun` A Pytest plugin to ignore tests during collection without reporting them in the test summary. Jul 18, 2023 5 - Production/Stable pytest (>=7.4.0,<8.0.0) :pypi:`pytest-dummynet` A py.test plugin providing access to a dummynet. Dec 15, 2021 5 - Production/Stable pytest :pypi:`pytest-dump2json` A pytest plugin for dumping test results to json. Jun 29, 2015 N/A N/A @@ -483,7 +484,7 @@ This list contains 1408 plugins. :pypi:`pytest-fabric` Provides test utilities to run fabric task tests by using docker containers Sep 12, 2018 5 - Production/Stable N/A :pypi:`pytest-factor` A package to prevent Dependency Confusion attacks against Yandex. Feb 20, 2024 N/A N/A :pypi:`pytest-factory` Use factories for test setup with py.test Sep 06, 2020 3 - Alpha pytest (>4.3) - :pypi:`pytest-factoryboy` Factory Boy support for pytest. Oct 10, 2023 6 - Mature pytest (>=6.2) + :pypi:`pytest-factoryboy` Factory Boy support for pytest. Mar 05, 2024 6 - Mature pytest (>=6.2) :pypi:`pytest-factoryboy-fixtures` Generates pytest fixtures that allow the use of type hinting Jun 25, 2020 N/A N/A :pypi:`pytest-factoryboy-state` Simple factoryboy random state management Mar 22, 2022 5 - Production/Stable pytest (>=5.0) :pypi:`pytest-failed-screen-record` Create a video of the screen when pytest fails Jan 05, 2023 4 - Beta pytest (>=7.1.2d,<8.0.0) @@ -506,7 +507,7 @@ This list contains 1408 plugins. :pypi:`pytest-filemarker` A pytest plugin that runs marked tests when files change. Dec 01, 2020 N/A pytest :pypi:`pytest-file-watcher` Pytest-File-Watcher is a CLI tool that watches for changes in your code and runs pytest on the changed files. Mar 23, 2023 N/A pytest :pypi:`pytest-filter-case` run test cases filter by mark Nov 05, 2020 N/A N/A - :pypi:`pytest-filter-subpackage` Pytest plugin for filtering based on sub-packages Dec 12, 2022 3 - Alpha pytest (>=3.0) + :pypi:`pytest-filter-subpackage` Pytest plugin for filtering based on sub-packages Mar 04, 2024 5 - Production/Stable pytest >=4.6 :pypi:`pytest-find-dependencies` A pytest plugin to find dependencies between tests Apr 09, 2022 4 - Beta pytest (>=4.3.0) :pypi:`pytest-finer-verdicts` A pytest plugin to treat non-assertion failures as test errors. Jun 18, 2020 N/A pytest (>=5.4.3) :pypi:`pytest-firefox` pytest plugin to manipulate firefox Aug 08, 2017 3 - Alpha pytest (>=3.0.2) @@ -539,6 +540,7 @@ This list contains 1408 plugins. :pypi:`pytest-focus` A pytest plugin that alerts user of failed test cases with screen notifications May 04, 2019 4 - Beta pytest :pypi:`pytest-forbid` Mar 07, 2023 N/A pytest (>=7.2.2,<8.0.0) :pypi:`pytest-forcefail` py.test plugin to make the test failing regardless of pytest.mark.xfail May 15, 2018 4 - Beta N/A + :pypi:`pytest-forks` Fork helper for pytest Mar 05, 2024 N/A N/A :pypi:`pytest-forward-compatability` A name to avoid typosquating pytest-foward-compatibility Sep 06, 2020 N/A N/A :pypi:`pytest-forward-compatibility` A pytest plugin to shim pytest commandline options for fowards compatibility Sep 29, 2020 N/A N/A :pypi:`pytest-frappe` Pytest Frappe Plugin - A set of pytest fixtures to test Frappe applications Oct 29, 2023 4 - Beta pytest>=7.0.0 @@ -600,7 +602,7 @@ This list contains 1408 plugins. :pypi:`pytest-history` Pytest plugin to keep a history of your pytest runs Jan 14, 2024 N/A pytest (>=7.4.3,<8.0.0) :pypi:`pytest-home` Home directory fixtures Oct 09, 2023 5 - Production/Stable pytest :pypi:`pytest-homeassistant` A pytest plugin for use with homeassistant custom components. Aug 12, 2020 4 - Beta N/A - :pypi:`pytest-homeassistant-custom-component` Experimental package to automatically extract test plugins for Home Assistant custom components Mar 01, 2024 3 - Alpha pytest ==8.0.2 + :pypi:`pytest-homeassistant-custom-component` Experimental package to automatically extract test plugins for Home Assistant custom components Mar 07, 2024 3 - Alpha pytest ==8.0.2 :pypi:`pytest-honey` A simple plugin to use with pytest Jan 07, 2022 4 - Beta pytest (>=3.5.0) :pypi:`pytest-honors` Report on tests that honor constraints, and guard against regressions Mar 06, 2020 4 - Beta N/A :pypi:`pytest-hot-reloading` Jan 06, 2024 N/A N/A @@ -669,7 +671,7 @@ This list contains 1408 plugins. :pypi:`pytest-ipdb` A py.test plug-in to enable drop to ipdb debugger on test failure. Mar 20, 2013 2 - Pre-Alpha N/A :pypi:`pytest-ipynb` THIS PROJECT IS ABANDONED Jan 29, 2019 3 - Alpha N/A :pypi:`pytest-isolate` Feb 20, 2023 4 - Beta pytest - :pypi:`pytest-isort` py.test plugin to check import ordering using isort Oct 31, 2022 5 - Production/Stable pytest (>=5.0) + :pypi:`pytest-isort` py.test plugin to check import ordering using isort Mar 05, 2024 5 - Production/Stable pytest (>=5.0) :pypi:`pytest-it` Pytest plugin to display test reports as a plaintext spec, inspired by Rspec: https://github.com/mattduck/pytest-it. Jan 29, 2024 4 - Beta N/A :pypi:`pytest-iterassert` Nicer list and iterable assertion messages for pytest May 11, 2020 3 - Alpha N/A :pypi:`pytest-iters` A contextmanager pytest fixture for handling multiple mock iters May 24, 2022 N/A N/A @@ -718,7 +720,7 @@ This list contains 1408 plugins. :pypi:`pytest-ldap` python-ldap fixtures for pytest Aug 18, 2020 N/A pytest :pypi:`pytest-leak-finder` Find the test that's leaking before the one that fails Feb 15, 2023 4 - Beta pytest (>=3.5.0) :pypi:`pytest-leaks` A pytest plugin to trace resource leaks. Nov 27, 2019 1 - Planning N/A - :pypi:`pytest-leaping` Coming soon! Mar 02, 2024 N/A N/A + :pypi:`pytest-leaping` A simple plugin to use with pytest Mar 08, 2024 4 - Beta pytest>=6.2.0 :pypi:`pytest-level` Select tests of a given level or lower Oct 21, 2019 N/A pytest :pypi:`pytest-libfaketime` A python-libfaketime plugin for pytest. Dec 22, 2018 4 - Beta pytest (>=3.0.0) :pypi:`pytest-libiio` A pytest plugin to manage interfacing with libiio contexts Dec 22, 2023 4 - Beta N/A @@ -754,7 +756,7 @@ This list contains 1408 plugins. :pypi:`pytest-manual-marker` pytest marker for marking manual tests Aug 04, 2022 3 - Alpha pytest>=7 :pypi:`pytest-markdoctest` A pytest plugin to doctest your markdown files Jul 22, 2022 4 - Beta pytest (>=6) :pypi:`pytest-markdown` Test your markdown docs with pytest Jan 15, 2021 4 - Beta pytest (>=6.0.1,<7.0.0) - :pypi:`pytest-markdown-docs` Run markdown code fences through pytest Feb 07, 2024 N/A pytest (>=7.0.0) + :pypi:`pytest-markdown-docs` Run markdown code fences through pytest Mar 05, 2024 N/A pytest (>=7.0.0) :pypi:`pytest-marker-bugzilla` py.test bugzilla integration plugin, using markers Jan 09, 2020 N/A N/A :pypi:`pytest-markers-presence` A simple plugin to detect missed pytest tags and markers" Feb 04, 2021 4 - Beta pytest (>=6.0) :pypi:`pytest-markfiltration` UNKNOWN Nov 08, 2011 3 - Alpha N/A @@ -787,7 +789,7 @@ This list contains 1408 plugins. :pypi:`pytest-mini` A plugin to test mp Feb 06, 2023 N/A pytest (>=7.2.0,<8.0.0) :pypi:`pytest-minio-mock` A pytest plugin for mocking Minio S3 interactions Jan 04, 2024 N/A pytest >=5.0.0 :pypi:`pytest-missing-fixtures` Pytest plugin that creates missing fixtures Oct 14, 2020 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-mitmproxy` pytest plugin for mitmproxy tests Feb 28, 2024 N/A pytest >=7.0 + :pypi:`pytest-mitmproxy` pytest plugin for mitmproxy tests Mar 07, 2024 N/A pytest >=7.0 :pypi:`pytest-ml` Test your machine learning! May 04, 2019 4 - Beta N/A :pypi:`pytest-mocha` pytest plugin to display test execution output like a mochajs Apr 02, 2020 4 - Beta pytest (>=5.4.0) :pypi:`pytest-mock` Thin-wrapper around the mock package for easier use with pytest Oct 19, 2023 5 - Production/Stable pytest >=5.0 @@ -796,7 +798,7 @@ This list contains 1408 plugins. :pypi:`pytest-mock-helper` Help you mock HTTP call and generate mock code Jan 24, 2018 N/A pytest :pypi:`pytest-mockito` Base fixtures for mockito Jul 11, 2018 4 - Beta N/A :pypi:`pytest-mockredis` An in-memory mock of a Redis server that runs in a separate thread. This is to be used for unit-tests that require a Redis database. Jan 02, 2018 2 - Pre-Alpha N/A - :pypi:`pytest-mock-resources` A pytest plugin for easily instantiating reproducible mock resources. Feb 01, 2024 N/A pytest (>=1.0) + :pypi:`pytest-mock-resources` A pytest plugin for easily instantiating reproducible mock resources. Mar 06, 2024 N/A pytest (>=1.0) :pypi:`pytest-mock-server` Mock server plugin for pytest Jan 09, 2022 4 - Beta pytest (>=3.5.0) :pypi:`pytest-mockservers` A set of fixtures to test your requests to HTTP/UDP servers Mar 31, 2020 N/A pytest (>=4.3.0) :pypi:`pytest-mocktcp` A pytest plugin for testing TCP clients Oct 11, 2022 N/A pytest @@ -829,12 +831,12 @@ This list contains 1408 plugins. :pypi:`pytest-mypyd` Mypy static type checker plugin for Pytest Aug 20, 2019 4 - Beta pytest (<4.7,>=2.8) ; python_version < "3.5" :pypi:`pytest-mypy-plugins` pytest plugin for writing tests for mypy plugins Feb 29, 2024 4 - Beta pytest >=7.0.0 :pypi:`pytest-mypy-plugins-shim` Substitute for "pytest-mypy-plugins" for Python implementations which aren't supported by mypy. Apr 12, 2021 N/A pytest>=6.0.0 - :pypi:`pytest-mypy-testing` Pytest plugin to check mypy output. Feb 26, 2024 N/A pytest>=7,<9 + :pypi:`pytest-mypy-testing` Pytest plugin to check mypy output. Mar 04, 2024 N/A pytest>=7,<9 :pypi:`pytest-mysql` MySQL process and client fixtures for pytest Oct 30, 2023 5 - Production/Stable pytest >=6.2 :pypi:`pytest-ndb` pytest notebook debugger Oct 15, 2023 N/A pytest :pypi:`pytest-needle` pytest plugin for visual testing websites using selenium Dec 10, 2018 4 - Beta pytest (<5.0.0,>=3.0.0) :pypi:`pytest-neo` pytest-neo is a plugin for pytest that shows tests like screen of Matrix. Jan 08, 2022 3 - Alpha pytest (>=6.2.0) - :pypi:`pytest-netdut` "Automated software testing for switches using pytest" Oct 26, 2023 N/A pytest <7.3,>=3.5.0 + :pypi:`pytest-netdut` "Automated software testing for switches using pytest" Mar 07, 2024 N/A pytest <7.3,>=3.5.0 :pypi:`pytest-network` A simple plugin to disable network on socket level. May 07, 2020 N/A N/A :pypi:`pytest-network-endpoints` Network endpoints plugin for pytest Mar 06, 2022 N/A pytest :pypi:`pytest-never-sleep` pytest plugin helps to avoid adding tests without mock \`time.sleep\` May 05, 2021 3 - Alpha pytest (>=3.5.1) @@ -868,7 +870,7 @@ This list contains 1408 plugins. :pypi:`pytest-offline` Mar 09, 2023 1 - Planning pytest (>=7.0.0,<8.0.0) :pypi:`pytest-ogsm-plugin` 针对特定项目定制化插件,优化了pytest报告展示方式,并添加了项目所需特定参数 May 16, 2023 N/A N/A :pypi:`pytest-ok` The ultimate pytest output plugin Apr 01, 2019 4 - Beta N/A - :pypi:`pytest-only` Use @pytest.mark.only to run a single test Jun 14, 2022 5 - Production/Stable pytest (<7.1); python_version <= "3.6" + :pypi:`pytest-only` Use @pytest.mark.only to run a single test Mar 09, 2024 5 - Production/Stable pytest (<7.1) ; python_full_version <= "3.6.0" :pypi:`pytest-oof` A Pytest plugin providing structured, programmatic access to a test run's results Dec 11, 2023 4 - Beta N/A :pypi:`pytest-oot` Run object-oriented tests in a simple format Sep 18, 2016 4 - Beta N/A :pypi:`pytest-openfiles` Pytest plugin for detecting inadvertent open file handles Apr 16, 2020 3 - Alpha pytest (>=4.6) @@ -956,7 +958,7 @@ This list contains 1408 plugins. :pypi:`pytest-porringer` Jan 18, 2024 N/A pytest>=7.4.4 :pypi:`pytest-portion` Select a portion of the collected tests Jan 28, 2021 4 - Beta pytest (>=3.5.0) :pypi:`pytest-postgres` Run PostgreSQL in Docker container in Pytest. Mar 22, 2020 N/A pytest - :pypi:`pytest-postgresql` Postgresql fixtures and fixture factories for Pytest. Jan 29, 2024 5 - Production/Stable pytest >=6.2 + :pypi:`pytest-postgresql` Postgresql fixtures and fixture factories for Pytest. Mar 07, 2024 5 - Production/Stable pytest >=6.2 :pypi:`pytest-power` pytest plugin with powerful fixtures Dec 31, 2020 N/A pytest (>=5.4) :pypi:`pytest-prefer-nested-dup-tests` A Pytest plugin to drop duplicated tests during collection, but will prefer keeping nested packages. Apr 27, 2022 4 - Beta pytest (>=7.1.1,<8.0.0) :pypi:`pytest-pretty` pytest plugin for printing summary data as I want it Apr 05, 2023 5 - Production/Stable pytest>=7 @@ -1082,7 +1084,7 @@ This list contains 1408 plugins. :pypi:`pytest-responses` py.test integration for responses Oct 11, 2022 N/A pytest (>=2.5) :pypi:`pytest-rest-api` Aug 08, 2022 N/A pytest (>=7.1.2,<8.0.0) :pypi:`pytest-restrict` Pytest plugin to restrict the test types allowed Jul 10, 2023 5 - Production/Stable pytest - :pypi:`pytest-result-log` A pytest plugin that records the start, end, and result information of each use case in a log file Feb 27, 2024 N/A pytest>=7.2.0 + :pypi:`pytest-result-log` A pytest plugin that records the start, end, and result information of each use case in a log file Jan 10, 2024 N/A pytest>=7.2.0 :pypi:`pytest-result-sender` Apr 20, 2023 N/A pytest>=7.3.1 :pypi:`pytest-resume` A Pytest plugin to resuming from the last run test Apr 22, 2023 4 - Beta pytest (>=7.0) :pypi:`pytest-rethinkdb` A RethinkDB plugin for pytest. Jul 24, 2016 4 - Beta N/A @@ -1098,14 +1100,14 @@ This list contains 1408 plugins. :pypi:`pytest-rmsis` Sycronise pytest results to Jira RMsis Aug 10, 2022 N/A pytest (>=5.3.5) :pypi:`pytest-rng` Fixtures for seeding tests and making randomness reproducible Aug 08, 2019 5 - Production/Stable pytest :pypi:`pytest-roast` pytest plugin for ROAST configuration override and fixtures Nov 09, 2022 5 - Production/Stable pytest - :pypi:`pytest_robotframework` a pytest plugin that can run both python and robotframework tests while generating robot reports for them Feb 27, 2024 N/A pytest<9,>=7 + :pypi:`pytest_robotframework` a pytest plugin that can run both python and robotframework tests while generating robot reports for them Mar 08, 2024 N/A pytest<9,>=7 :pypi:`pytest-rocketchat` Pytest to Rocket.Chat reporting plugin Apr 18, 2021 5 - Production/Stable N/A :pypi:`pytest-rotest` Pytest integration with rotest Sep 08, 2019 N/A pytest (>=3.5.0) :pypi:`pytest-rpc` Extend py.test for RPC OpenStack testing. Feb 22, 2019 4 - Beta pytest (~=3.6) :pypi:`pytest-rst` Test code from RST documents with pytest Jan 26, 2023 N/A N/A :pypi:`pytest-rt` pytest data collector plugin for Testgr May 05, 2022 N/A N/A :pypi:`pytest-rts` Coverage-based regression test selection (RTS) plugin for pytest May 17, 2021 N/A pytest - :pypi:`pytest-ruff` pytest plugin to check ruff requirements. Oct 31, 2023 4 - Beta N/A + :pypi:`pytest-ruff` pytest plugin to check ruff requirements. Mar 03, 2024 4 - Beta pytest (>=5) :pypi:`pytest-run-changed` Pytest plugin that runs changed tests only Apr 02, 2021 3 - Alpha pytest :pypi:`pytest-runfailed` implement a --failed option for pytest Mar 24, 2016 N/A N/A :pypi:`pytest-run-subprocess` Pytest Plugin for running and testing subprocesses. Nov 12, 2022 5 - Production/Stable pytest @@ -1122,7 +1124,7 @@ This list contains 1408 plugins. :pypi:`pytest-sanity` Dec 07, 2020 N/A N/A :pypi:`pytest-sa-pg` May 14, 2019 N/A N/A :pypi:`pytest_sauce` pytest_sauce provides sane and helpful methods worked out in clearcode to run py.test tests with selenium/saucelabs Jul 14, 2014 3 - Alpha N/A - :pypi:`pytest-sbase` A complete web automation framework for end-to-end testing. Mar 01, 2024 5 - Production/Stable N/A + :pypi:`pytest-sbase` A complete web automation framework for end-to-end testing. Mar 09, 2024 5 - Production/Stable N/A :pypi:`pytest-scenario` pytest plugin for test scenarios Feb 06, 2017 3 - Alpha N/A :pypi:`pytest-schedule` The job of test scheduling for humans. Jan 07, 2023 5 - Production/Stable N/A :pypi:`pytest-schema` 👍 Validate return values against a schema-like object in testing Feb 16, 2024 5 - Production/Stable pytest >=3.5.0 @@ -1131,7 +1133,7 @@ This list contains 1408 plugins. :pypi:`pytest-select` A pytest plugin which allows to (de-)select tests from a file. Jan 18, 2019 3 - Alpha pytest (>=3.0) :pypi:`pytest-selenium` pytest plugin for Selenium Feb 01, 2024 5 - Production/Stable pytest>=6.0.0 :pypi:`pytest-selenium-auto` pytest plugin to automatically capture screenshots upon selenium webdriver events Nov 07, 2023 N/A pytest >= 7.0.0 - :pypi:`pytest-seleniumbase` A complete web automation framework for end-to-end testing. Mar 01, 2024 5 - Production/Stable N/A + :pypi:`pytest-seleniumbase` A complete web automation framework for end-to-end testing. Mar 09, 2024 5 - Production/Stable N/A :pypi:`pytest-selenium-enhancer` pytest plugin for Selenium Apr 29, 2022 5 - Production/Stable N/A :pypi:`pytest-selenium-pdiff` A pytest package implementing perceptualdiff for Selenium tests. Apr 06, 2017 2 - Pre-Alpha N/A :pypi:`pytest-send-email` Send pytest execution result email Dec 04, 2019 N/A N/A @@ -1236,7 +1238,7 @@ This list contains 1408 plugins. :pypi:`pytest-subinterpreter` Run pytest in a subinterpreter Nov 25, 2023 N/A pytest>=7.0.0 :pypi:`pytest-subprocess` A plugin to fake subprocess for pytest Jan 28, 2023 5 - Production/Stable pytest (>=4.0.0) :pypi:`pytest-subtesthack` A hack to explicitly set up and tear down fixtures. Jul 16, 2022 N/A N/A - :pypi:`pytest-subtests` unittest subTest() support and subtests fixture May 15, 2023 4 - Beta pytest (>=7.0) + :pypi:`pytest-subtests` unittest subTest() support and subtests fixture Mar 07, 2024 4 - Beta pytest >=7.0 :pypi:`pytest-subunit` pytest-subunit is a plugin for py.test which outputs testsresult in subunit format. Sep 17, 2023 N/A pytest (>=2.3) :pypi:`pytest-sugar` pytest-sugar is a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly). Feb 01, 2024 4 - Beta pytest >=6.2.0 :pypi:`pytest-suitemanager` A simple plugin to use with pytest Apr 28, 2023 4 - Beta N/A @@ -1244,7 +1246,7 @@ This list contains 1408 plugins. :pypi:`pytest-supercov` Pytest plugin for measuring explicit test-file to source-file coverage Jul 02, 2023 N/A N/A :pypi:`pytest-svn` SVN repository fixture for py.test May 28, 2019 5 - Production/Stable pytest :pypi:`pytest-symbols` pytest-symbols is a pytest plugin that adds support for passing test environment symbols into pytest tests. Nov 20, 2017 3 - Alpha N/A - :pypi:`pytest-synodic` Synodic Pytest utilities Jan 12, 2024 N/A pytest>=7.4.4 + :pypi:`pytest-synodic` Synodic Pytest utilities Mar 09, 2024 N/A pytest>=8.0.2 :pypi:`pytest-system-statistics` Pytest plugin to track and report system usage statistics Feb 16, 2022 5 - Production/Stable pytest (>=6.0.0) :pypi:`pytest-system-test-plugin` Pyst - Pytest System-Test Plugin Feb 03, 2022 N/A N/A :pypi:`pytest_tagging` a pytest plugin to tag tests Feb 15, 2024 N/A pytest (>=7.1.3,<8.0.0) @@ -1290,7 +1292,7 @@ This list contains 1408 plugins. :pypi:`pytest-testrail-ns` pytest plugin for creating TestRail runs and adding results Aug 12, 2022 N/A N/A :pypi:`pytest-testrail-plugin` PyTest plugin for TestRail Apr 21, 2020 3 - Alpha pytest :pypi:`pytest-testrail-reporter` Sep 10, 2018 N/A N/A - :pypi:`pytest-testrail-results` A pytest plugin to upload results to TestRail. Mar 01, 2024 N/A pytest >=7.2.0 + :pypi:`pytest-testrail-results` A pytest plugin to upload results to TestRail. Mar 04, 2024 N/A pytest >=7.2.0 :pypi:`pytest-testreport` Dec 01, 2022 4 - Beta pytest (>=3.5.0) :pypi:`pytest-testreport-new` Oct 07, 2023 4 - Beta pytest >=3.5.0 :pypi:`pytest-testslide` TestSlide fixture for pytest Jan 07, 2021 5 - Production/Stable pytest (~=6.2) @@ -1307,7 +1309,7 @@ This list contains 1408 plugins. :pypi:`pytest-time` Jun 24, 2023 3 - Alpha pytest :pypi:`pytest-timeassert-ethan` execution duration Dec 25, 2023 N/A pytest :pypi:`pytest-timeit` A pytest plugin to time test function runs Oct 13, 2016 4 - Beta N/A - :pypi:`pytest-timeout` pytest plugin to abort hanging tests Oct 08, 2023 5 - Production/Stable pytest >=5.0.0 + :pypi:`pytest-timeout` pytest plugin to abort hanging tests Mar 07, 2024 5 - Production/Stable pytest >=7.0.0 :pypi:`pytest-timeouts` Linux-only Pytest plugin to control durations of various test case execution phases Sep 21, 2019 5 - Production/Stable N/A :pypi:`pytest-timer` A timer plugin for pytest Dec 26, 2023 N/A pytest :pypi:`pytest-timestamper` Pytest plugin to add a timestamp prefix to the pytest output Jun 06, 2021 N/A N/A @@ -1416,7 +1418,7 @@ This list contains 1408 plugins. :pypi:`pytest-xfiles` Pytest fixtures providing data read from function, module or package related (x)files. Feb 27, 2018 N/A N/A :pypi:`pytest-xiuyu` This is a pytest plugin Jul 25, 2023 5 - Production/Stable N/A :pypi:`pytest-xlog` Extended logging for test and decorators May 31, 2020 4 - Beta N/A - :pypi:`pytest-xlsx` pytest plugin for generating test cases by xlsx(excel) Jan 28, 2024 N/A pytest<8.1,>=7.4.0 + :pypi:`pytest-xlsx` pytest plugin for generating test cases by xlsx(excel) Jan 28, 2024 N/A pytest<8,>=7.4.0 :pypi:`pytest-xpara` An extended parametrizing plugin of pytest. Oct 30, 2017 3 - Alpha pytest :pypi:`pytest-xprocess` A pytest plugin for managing processes across test runs. Sep 23, 2023 4 - Beta pytest (>=2.8) :pypi:`pytest-xray` May 30, 2019 3 - Alpha N/A @@ -1440,6 +1442,7 @@ This list contains 1408 plugins. :pypi:`pytest-zebrunner` Pytest connector for Zebrunner reporting Jan 08, 2024 5 - Production/Stable pytest (>=4.5.0) :pypi:`pytest-zeebe` Pytest fixtures for testing Camunda 8 processes using a Zeebe test engine. Feb 01, 2024 N/A pytest (>=7.4.2,<8.0.0) :pypi:`pytest-zest` Zesty additions to pytest. Nov 17, 2022 N/A N/A + :pypi:`pytest-zhongwen-wendang` PyTest 中文文档 Mar 04, 2024 4 - Beta N/A :pypi:`pytest-zigzag` Extend py.test for RPC OpenStack testing. Feb 27, 2019 4 - Beta pytest (~=3.6) :pypi:`pytest-zulip` Pytest report plugin for Zulip May 07, 2022 5 - Production/Stable pytest =============================================== ====================================================================================================================================================================================================================================================================================================================================================================================== ============== ===================== ================================================ @@ -1623,7 +1626,7 @@ This list contains 1408 plugins. :pypi:`pytest-alembic` - *last release*: Jul 06, 2023, + *last release*: Mar 04, 2024, *status*: N/A, *requires*: pytest (>=6.0) @@ -1959,7 +1962,7 @@ This list contains 1408 plugins. Pytest fixtures for async generators :pypi:`pytest-asyncio` - *last release*: Feb 09, 2024, + *last release*: Mar 08, 2024, *status*: 4 - Beta, *requires*: pytest <9,>=7.0.0 @@ -2155,7 +2158,7 @@ This list contains 1408 plugins. pytest plugin for URL based testing :pypi:`pytest-bdd` - *last release*: Dec 02, 2023, + *last release*: Mar 04, 2024, *status*: 6 - Mature, *requires*: pytest (>=6.2.0) @@ -2253,7 +2256,7 @@ This list contains 1408 plugins. A small example package :pypi:`pytest-better-parametrize` - *last release*: Feb 26, 2024, + *last release*: Mar 05, 2024, *status*: 4 - Beta, *requires*: pytest >=6.2.0 @@ -2561,7 +2564,7 @@ This list contains 1408 plugins. pytest plugin to capture all warnings and put them in one file of your choice :pypi:`pytest-cases` - *last release*: Jan 12, 2024, + *last release*: Mar 08, 2024, *status*: 5 - Production/Stable, *requires*: N/A @@ -2589,7 +2592,7 @@ This list contains 1408 plugins. Pytest plugin with server for catching HTTP requests. :pypi:`pytest-celery` - *last release*: Feb 12, 2024, + *last release*: Mar 09, 2024, *status*: N/A, *requires*: N/A @@ -2689,10 +2692,17 @@ This list contains 1408 plugins. :pypi:`pytest-check-links` *last release*: Jul 29, 2020, *status*: N/A, - *requires*: pytest>=7.0 + *requires*: pytest<8,>=7.0 Check links in files + :pypi:`pytest-checklist` + *last release*: Mar 06, 2024, + *status*: N/A, + *requires*: N/A + + Pytest plugin to track and report unit/function coverage. + :pypi:`pytest-check-mk` *last release*: Nov 19, 2015, *status*: 4 - Beta, @@ -2834,9 +2844,9 @@ This list contains 1408 plugins. Distribute tests to cloud machines without fuss :pypi:`pytest-cmake` - *last release*: Jul 19, 2023, + *last release*: Mar 04, 2024, *status*: N/A, - *requires*: pytest<8,>=4 + *requires*: pytest<9,>=4 Provide CMake module for Pytest @@ -3128,7 +3138,7 @@ This list contains 1408 plugins. Use pytest's runner to discover and execute C++ tests :pypi:`pytest-cppython` - *last release*: Aug 26, 2023, + *last release*: Mar 09, 2024, *status*: N/A, *requires*: N/A @@ -3961,7 +3971,7 @@ This list contains 1408 plugins. Run pytest --doctest-modules with markdown docstrings in code blocks (\`\`\`) :pypi:`pytest-doctestplus` - *last release*: Dec 13, 2023, + *last release*: Mar 04, 2024, *status*: 5 - Production/Stable, *requires*: pytest >=4.6 @@ -4038,9 +4048,9 @@ This list contains 1408 plugins. Tool to allow webdriver automation to be ran locally or remotely :pypi:`pytest-drop-dup-tests` - *last release*: May 23, 2020, - *status*: 4 - Beta, - *requires*: pytest (>=2.7) + *last release*: Mar 04, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest >=7 A Pytest plugin to drop duplicated tests during collection @@ -4591,7 +4601,7 @@ This list contains 1408 plugins. Use factories for test setup with py.test :pypi:`pytest-factoryboy` - *last release*: Oct 10, 2023, + *last release*: Mar 05, 2024, *status*: 6 - Mature, *requires*: pytest (>=6.2) @@ -4752,9 +4762,9 @@ This list contains 1408 plugins. run test cases filter by mark :pypi:`pytest-filter-subpackage` - *last release*: Dec 12, 2022, - *status*: 3 - Alpha, - *requires*: pytest (>=3.0) + *last release*: Mar 04, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest >=4.6 Pytest plugin for filtering based on sub-packages @@ -4982,6 +4992,13 @@ This list contains 1408 plugins. py.test plugin to make the test failing regardless of pytest.mark.xfail + :pypi:`pytest-forks` + *last release*: Mar 05, 2024, + *status*: N/A, + *requires*: N/A + + Fork helper for pytest + :pypi:`pytest-forward-compatability` *last release*: Sep 06, 2020, *status*: N/A, @@ -5410,7 +5427,7 @@ This list contains 1408 plugins. A pytest plugin for use with homeassistant custom components. :pypi:`pytest-homeassistant-custom-component` - *last release*: Mar 01, 2024, + *last release*: Mar 07, 2024, *status*: 3 - Alpha, *requires*: pytest ==8.0.2 @@ -5893,7 +5910,7 @@ This list contains 1408 plugins. :pypi:`pytest-isort` - *last release*: Oct 31, 2022, + *last release*: Mar 05, 2024, *status*: 5 - Production/Stable, *requires*: pytest (>=5.0) @@ -6236,11 +6253,11 @@ This list contains 1408 plugins. A pytest plugin to trace resource leaks. :pypi:`pytest-leaping` - *last release*: Mar 02, 2024, - *status*: N/A, - *requires*: N/A + *last release*: Mar 08, 2024, + *status*: 4 - Beta, + *requires*: pytest>=6.2.0 - Coming soon! + A simple plugin to use with pytest :pypi:`pytest-level` *last release*: Oct 21, 2019, @@ -6488,7 +6505,7 @@ This list contains 1408 plugins. Test your markdown docs with pytest :pypi:`pytest-markdown-docs` - *last release*: Feb 07, 2024, + *last release*: Mar 05, 2024, *status*: N/A, *requires*: pytest (>=7.0.0) @@ -6719,7 +6736,7 @@ This list contains 1408 plugins. Pytest plugin that creates missing fixtures :pypi:`pytest-mitmproxy` - *last release*: Feb 28, 2024, + *last release*: Mar 07, 2024, *status*: N/A, *requires*: pytest >=7.0 @@ -6782,7 +6799,7 @@ This list contains 1408 plugins. An in-memory mock of a Redis server that runs in a separate thread. This is to be used for unit-tests that require a Redis database. :pypi:`pytest-mock-resources` - *last release*: Feb 01, 2024, + *last release*: Mar 06, 2024, *status*: N/A, *requires*: pytest (>=1.0) @@ -7013,7 +7030,7 @@ This list contains 1408 plugins. Substitute for "pytest-mypy-plugins" for Python implementations which aren't supported by mypy. :pypi:`pytest-mypy-testing` - *last release*: Feb 26, 2024, + *last release*: Mar 04, 2024, *status*: N/A, *requires*: pytest>=7,<9 @@ -7048,7 +7065,7 @@ This list contains 1408 plugins. pytest-neo is a plugin for pytest that shows tests like screen of Matrix. :pypi:`pytest-netdut` - *last release*: Oct 26, 2023, + *last release*: Mar 07, 2024, *status*: N/A, *requires*: pytest <7.3,>=3.5.0 @@ -7286,9 +7303,9 @@ This list contains 1408 plugins. The ultimate pytest output plugin :pypi:`pytest-only` - *last release*: Jun 14, 2022, + *last release*: Mar 09, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (<7.1); python_version <= "3.6" + *requires*: pytest (<7.1) ; python_full_version <= "3.6.0" Use @pytest.mark.only to run a single test @@ -7902,7 +7919,7 @@ This list contains 1408 plugins. Run PostgreSQL in Docker container in Pytest. :pypi:`pytest-postgresql` - *last release*: Jan 29, 2024, + *last release*: Mar 07, 2024, *status*: 5 - Production/Stable, *requires*: pytest >=6.2 @@ -8784,7 +8801,7 @@ This list contains 1408 plugins. Pytest plugin to restrict the test types allowed :pypi:`pytest-result-log` - *last release*: Feb 27, 2024, + *last release*: Jan 10, 2024, *status*: N/A, *requires*: pytest>=7.2.0 @@ -8896,7 +8913,7 @@ This list contains 1408 plugins. pytest plugin for ROAST configuration override and fixtures :pypi:`pytest_robotframework` - *last release*: Feb 27, 2024, + *last release*: Mar 08, 2024, *status*: N/A, *requires*: pytest<9,>=7 @@ -8945,9 +8962,9 @@ This list contains 1408 plugins. Coverage-based regression test selection (RTS) plugin for pytest :pypi:`pytest-ruff` - *last release*: Oct 31, 2023, + *last release*: Mar 03, 2024, *status*: 4 - Beta, - *requires*: N/A + *requires*: pytest (>=5) pytest plugin to check ruff requirements. @@ -9064,7 +9081,7 @@ This list contains 1408 plugins. pytest_sauce provides sane and helpful methods worked out in clearcode to run py.test tests with selenium/saucelabs :pypi:`pytest-sbase` - *last release*: Mar 01, 2024, + *last release*: Mar 09, 2024, *status*: 5 - Production/Stable, *requires*: N/A @@ -9127,7 +9144,7 @@ This list contains 1408 plugins. pytest plugin to automatically capture screenshots upon selenium webdriver events :pypi:`pytest-seleniumbase` - *last release*: Mar 01, 2024, + *last release*: Mar 09, 2024, *status*: 5 - Production/Stable, *requires*: N/A @@ -9862,9 +9879,9 @@ This list contains 1408 plugins. A hack to explicitly set up and tear down fixtures. :pypi:`pytest-subtests` - *last release*: May 15, 2023, + *last release*: Mar 07, 2024, *status*: 4 - Beta, - *requires*: pytest (>=7.0) + *requires*: pytest >=7.0 unittest subTest() support and subtests fixture @@ -9918,9 +9935,9 @@ This list contains 1408 plugins. pytest-symbols is a pytest plugin that adds support for passing test environment symbols into pytest tests. :pypi:`pytest-synodic` - *last release*: Jan 12, 2024, + *last release*: Mar 09, 2024, *status*: N/A, - *requires*: pytest>=7.4.4 + *requires*: pytest>=8.0.2 Synodic Pytest utilities @@ -10240,7 +10257,7 @@ This list contains 1408 plugins. :pypi:`pytest-testrail-results` - *last release*: Mar 01, 2024, + *last release*: Mar 04, 2024, *status*: N/A, *requires*: pytest >=7.2.0 @@ -10359,9 +10376,9 @@ This list contains 1408 plugins. A pytest plugin to time test function runs :pypi:`pytest-timeout` - *last release*: Oct 08, 2023, + *last release*: Mar 07, 2024, *status*: 5 - Production/Stable, - *requires*: pytest >=5.0.0 + *requires*: pytest >=7.0.0 pytest plugin to abort hanging tests @@ -11124,7 +11141,7 @@ This list contains 1408 plugins. :pypi:`pytest-xlsx` *last release*: Jan 28, 2024, *status*: N/A, - *requires*: pytest<8.1,>=7.4.0 + *requires*: pytest<8,>=7.4.0 pytest plugin for generating test cases by xlsx(excel) @@ -11289,6 +11306,13 @@ This list contains 1408 plugins. Zesty additions to pytest. + :pypi:`pytest-zhongwen-wendang` + *last release*: Mar 04, 2024, + *status*: 4 - Beta, + *requires*: N/A + + PyTest 中文文档 + :pypi:`pytest-zigzag` *last release*: Feb 27, 2019, *status*: 4 - Beta, From 0a442a959920df1d782a19b48b8c63abf6b2aef4 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 10 Mar 2024 16:51:04 +0200 Subject: [PATCH 41/47] doc/flaky: remove `box/flaky` plugin suggestion (#12100) The plugin is abandoned and no longer working with new pytest versions. I also reordered a bit to put pytest-rerunfailures first since it seems most maintained and is under pytest-dev. --- doc/en/explanation/flaky.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/en/explanation/flaky.rst b/doc/en/explanation/flaky.rst index ccf3fbb2b..2beeb34e0 100644 --- a/doc/en/explanation/flaky.rst +++ b/doc/en/explanation/flaky.rst @@ -52,10 +52,9 @@ Plugins Rerunning any failed tests can mitigate the negative effects of flaky tests by giving them additional chances to pass, so that the overall build does not fail. Several pytest plugins support this: -* `flaky `_ -* `pytest-flakefinder `_ - `blog post `_ * `pytest-rerunfailures `_ * `pytest-replay `_: This plugin helps to reproduce locally crashes or flaky tests observed during CI runs. +* `pytest-flakefinder `_ - `blog post `_ Plugins to deliberately randomize tests can help expose tests with state problems: From a29ea1bc326dee3f6ddb66d3749fcfea917b8093 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 07:04:19 +0100 Subject: [PATCH 42/47] build(deps): Bump pypa/gh-action-pypi-publish from 1.8.12 to 1.8.14 (#12105) Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.12 to 1.8.14. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.12...v1.8.14) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d46f1f91d..9d23d22e8 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -47,7 +47,7 @@ jobs: path: dist - name: Publish package to PyPI - uses: pypa/gh-action-pypi-publish@v1.8.12 + uses: pypa/gh-action-pypi-publish@v1.8.14 - name: Push tag run: | From 122b43439c872ed39fc5099344191ab418b6a2a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 07:05:41 +0100 Subject: [PATCH 43/47] build(deps): Bump django in /testing/plugins_integration (#12103) Bumps [django](https://github.com/django/django) from 5.0.2 to 5.0.3. - [Commits](https://github.com/django/django/compare/5.0.2...5.0.3) --- updated-dependencies: - dependency-name: django dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- testing/plugins_integration/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/plugins_integration/requirements.txt b/testing/plugins_integration/requirements.txt index d2f3fad17..43e7ed4f2 100644 --- a/testing/plugins_integration/requirements.txt +++ b/testing/plugins_integration/requirements.txt @@ -1,5 +1,5 @@ anyio[curio,trio]==4.3.0 -django==5.0.2 +django==5.0.3 pytest-asyncio==0.23.5 # Temporarily not installed until pytest-bdd is fixed: # https://github.com/pytest-dev/pytest/pull/11785 From 6c9e1076813f9be6c87bdb31b81a8149eb047c6c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 11:48:06 -0300 Subject: [PATCH 44/47] build(deps): Bump softprops/action-gh-release from 1 to 2 (#12106) Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 1 to 2. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/v1...v2) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9d23d22e8..4ed68c286 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -94,7 +94,7 @@ jobs: tox -e generate-gh-release-notes -- ${{ github.event.inputs.version }} scripts/latest-release-notes.md - name: Publish GitHub Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: body_path: scripts/latest-release-notes.md files: dist/* From 7eaaf370bb40252701bf1dfad39669541d92c27e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 11 Mar 2024 18:22:16 +0200 Subject: [PATCH 45/47] doc: add versionadded to `Stash` and `StashKey` Fixes #12107 --- src/_pytest/stash.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/_pytest/stash.py b/src/_pytest/stash.py index e61d75b95..a4b829fc6 100644 --- a/src/_pytest/stash.py +++ b/src/_pytest/stash.py @@ -19,6 +19,8 @@ class StashKey(Generic[T]): A ``StashKey`` is associated with the type ``T`` of the value of the key. A ``StashKey`` is unique and cannot conflict with another key. + + .. versionadded:: 7.0 """ __slots__ = () @@ -61,6 +63,8 @@ class Stash: some_str = stash[some_str_key] # The static type of some_bool is bool. some_bool = stash[some_bool_key] + + .. versionadded:: 7.0 """ __slots__ = ("_storage",) From c0532dda18a5cfd0e6f67113bb164485fa80eb86 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 13 Mar 2024 15:30:18 +0200 Subject: [PATCH 46/47] [pre-commit.ci] pre-commit autoupdate (#12115) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Pierre Sassoulas Co-authored-by: Ran Benita --- .pre-commit-config.yaml | 4 +- pyproject.toml | 2 + scripts/generate-gh-release-notes.py | 1 + scripts/prepare-release-pr.py | 1 + scripts/release.py | 1 + src/_pytest/__init__.py | 2 +- src/_pytest/_code/code.py | 28 +++++++------- src/_pytest/_code/source.py | 6 +-- src/_pytest/_io/terminalwriter.py | 3 ++ src/_pytest/_py/path.py | 37 ++++++------------- src/_pytest/assertion/__init__.py | 1 + src/_pytest/assertion/rewrite.py | 14 +++---- src/_pytest/assertion/util.py | 6 +-- src/_pytest/cacheprovider.py | 3 +- src/_pytest/capture.py | 33 +++++++---------- src/_pytest/compat.py | 2 +- src/_pytest/config/__init__.py | 1 + src/_pytest/config/argparsing.py | 3 +- src/_pytest/debugging.py | 5 +-- src/_pytest/doctest.py | 1 + src/_pytest/fixtures.py | 34 ++++++++--------- src/_pytest/freeze_support.py | 2 +- src/_pytest/helpconfig.py | 1 + src/_pytest/hookspec.py | 1 + src/_pytest/junitxml.py | 5 ++- src/_pytest/logging.py | 5 ++- src/_pytest/main.py | 6 +-- src/_pytest/mark/structures.py | 24 +++++------- src/_pytest/monkeypatch.py | 7 ++-- src/_pytest/nodes.py | 6 +-- src/_pytest/pastebin.py | 1 + src/_pytest/pytester.py | 22 ++++------- src/_pytest/python.py | 1 + src/_pytest/python_api.py | 16 ++++---- src/_pytest/recwarn.py | 16 +++----- src/_pytest/reports.py | 9 ++--- src/_pytest/runner.py | 6 +-- src/_pytest/setuponly.py | 4 +- src/_pytest/skipping.py | 3 +- src/_pytest/terminal.py | 1 + src/_pytest/tmpdir.py | 1 + src/_pytest/unittest.py | 13 +++---- src/pytest/__init__.py | 1 + testing/_py/test_local.py | 10 ++--- testing/code/test_excinfo.py | 2 +- .../acceptance/fixture_mock_integration.py | 1 + .../unittest/test_setup_skip.py | 1 + .../unittest/test_setup_skip_class.py | 1 + .../unittest/test_setup_skip_module.py | 1 + .../unittest/test_unittest_asynctest.py | 1 + testing/io/test_wcwidth.py | 4 +- testing/python/metafunc.py | 4 +- testing/test_config.py | 2 +- testing/test_debugging.py | 2 +- testing/test_junitxml.py | 2 +- testing/test_mark.py | 4 +- testing/test_pathlib.py | 14 +++---- testing/test_reports.py | 6 +-- testing/test_runner_xunit.py | 1 + testing/test_terminal.py | 1 + testing/typing_checks.py | 1 + 61 files changed, 185 insertions(+), 212 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 78cf36bae..deb187fec 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.2.2" + rev: "v0.3.2" hooks: - id: ruff args: ["--fix"] @@ -26,7 +26,7 @@ repos: hooks: - id: python-use-type-annotations - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.8.0 + rev: v1.9.0 hooks: - id: mypy files: ^(src/|testing/|scripts/) diff --git a/pyproject.toml b/pyproject.toml index e14556f2f..7d1b8a22d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -281,6 +281,7 @@ template = "changelog/_template.rst" showcontent = true [tool.mypy] +files = ["src", "testing", "scripts"] mypy_path = ["src"] check_untyped_defs = true disallow_any_generics = true @@ -293,3 +294,4 @@ warn_return_any = true warn_unreachable = true warn_unused_configs = true no_implicit_reexport = true +warn_unused_ignores = true diff --git a/scripts/generate-gh-release-notes.py b/scripts/generate-gh-release-notes.py index c27f5774b..4222702d5 100644 --- a/scripts/generate-gh-release-notes.py +++ b/scripts/generate-gh-release-notes.py @@ -8,6 +8,7 @@ our CHANGELOG) into Markdown (which is required by GitHub Releases). Requires Python3.6+. """ + from pathlib import Path import re import sys diff --git a/scripts/prepare-release-pr.py b/scripts/prepare-release-pr.py index 8a9f0aa0f..7dabbd3b3 100644 --- a/scripts/prepare-release-pr.py +++ b/scripts/prepare-release-pr.py @@ -13,6 +13,7 @@ After that, it will create a release using the `release` tox environment, and pu **Token**: currently the token from the GitHub Actions is used, pushed with `pytest bot ` commit author. """ + import argparse from pathlib import Path import re diff --git a/scripts/release.py b/scripts/release.py index 73f5f52b1..bcbc4262d 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -1,5 +1,6 @@ # mypy: disallow-untyped-defs """Invoke development tasks.""" + import argparse import os from pathlib import Path diff --git a/src/_pytest/__init__.py b/src/_pytest/__init__.py index 9062768ea..b694a5f24 100644 --- a/src/_pytest/__init__.py +++ b/src/_pytest/__init__.py @@ -7,4 +7,4 @@ except ImportError: # pragma: no cover # broken installation, we don't even try # unknown only works because we do poor mans version compare __version__ = "unknown" - version_tuple = (0, 0, "unknown") # type:ignore[assignment] + version_tuple = (0, 0, "unknown") diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 12168be60..64a8f243a 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -279,9 +279,9 @@ class TracebackEntry: Mostly for internal use. """ - tbh: Union[ - bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool] - ] = False + tbh: Union[bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]] = ( + False + ) for maybe_ns_dct in (self.frame.f_locals, self.frame.f_globals): # in normal cases, f_locals and f_globals are dictionaries # however via `exec(...)` / `eval(...)` they can be other types @@ -378,12 +378,10 @@ class Traceback(List[TracebackEntry]): return self @overload - def __getitem__(self, key: "SupportsIndex") -> TracebackEntry: - ... + def __getitem__(self, key: "SupportsIndex") -> TracebackEntry: ... @overload - def __getitem__(self, key: slice) -> "Traceback": - ... + def __getitem__(self, key: slice) -> "Traceback": ... def __getitem__( self, key: Union["SupportsIndex", slice] @@ -1051,13 +1049,13 @@ class FormattedExcinfo: # full support for exception groups added to ExceptionInfo. # See https://github.com/pytest-dev/pytest/issues/9159 if isinstance(e, BaseExceptionGroup): - reprtraceback: Union[ - ReprTracebackNative, ReprTraceback - ] = ReprTracebackNative( - traceback.format_exception( - type(excinfo_.value), - excinfo_.value, - excinfo_.traceback[0]._rawentry, + reprtraceback: Union[ReprTracebackNative, ReprTraceback] = ( + ReprTracebackNative( + traceback.format_exception( + type(excinfo_.value), + excinfo_.value, + excinfo_.traceback[0]._rawentry, + ) ) ) else: @@ -1348,7 +1346,7 @@ def getfslineno(obj: object) -> Tuple[Union[str, Path], int]: # in 6ec13a2b9. It ("place_as") appears to be something very custom. obj = get_real_func(obj) if hasattr(obj, "place_as"): - obj = obj.place_as # type: ignore[attr-defined] + obj = obj.place_as try: code = Code.from_function(obj) diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index dac3c3867..7fa577e03 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -47,12 +47,10 @@ class Source: __hash__ = None # type: ignore @overload - def __getitem__(self, key: int) -> str: - ... + def __getitem__(self, key: int) -> str: ... @overload - def __getitem__(self, key: slice) -> "Source": - ... + def __getitem__(self, key: slice) -> "Source": ... def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]: if isinstance(key, int): diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index badbb7e4a..deb6ecc3c 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -9,6 +9,7 @@ from typing import Optional from typing import Sequence from typing import TextIO +from ..compat import assert_never from .wcwidth import wcswidth @@ -209,6 +210,8 @@ class TerminalWriter: from pygments.lexers.python import PythonLexer as Lexer elif lexer == "diff": from pygments.lexers.diff import DiffLexer as Lexer + else: + assert_never(lexer) from pygments import highlight import pygments.util except ImportError: diff --git a/src/_pytest/_py/path.py b/src/_pytest/_py/path.py index 7701561d9..7bb3693f9 100644 --- a/src/_pytest/_py/path.py +++ b/src/_pytest/_py/path.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs """local path implementation.""" + from __future__ import annotations import atexit @@ -205,12 +206,10 @@ class Stat: if TYPE_CHECKING: @property - def size(self) -> int: - ... + def size(self) -> int: ... @property - def mtime(self) -> float: - ... + def mtime(self) -> float: ... def __getattr__(self, name: str) -> Any: return getattr(self._osstatresult, "st_" + name) @@ -225,7 +224,7 @@ class Stat: raise NotImplementedError("XXX win32") import pwd - entry = error.checked_call(pwd.getpwuid, self.uid) # type:ignore[attr-defined] + entry = error.checked_call(pwd.getpwuid, self.uid) # type:ignore[attr-defined,unused-ignore] return entry[0] @property @@ -235,7 +234,7 @@ class Stat: raise NotImplementedError("XXX win32") import grp - entry = error.checked_call(grp.getgrgid, self.gid) # type:ignore[attr-defined] + entry = error.checked_call(grp.getgrgid, self.gid) # type:ignore[attr-defined,unused-ignore] return entry[0] def isdir(self): @@ -253,7 +252,7 @@ def getuserid(user): import pwd if not isinstance(user, int): - user = pwd.getpwnam(user)[2] # type:ignore[attr-defined] + user = pwd.getpwnam(user)[2] # type:ignore[attr-defined,unused-ignore] return user @@ -261,7 +260,7 @@ def getgroupid(group): import grp if not isinstance(group, int): - group = grp.getgrnam(group)[2] # type:ignore[attr-defined] + group = grp.getgrnam(group)[2] # type:ignore[attr-defined,unused-ignore] return group @@ -318,7 +317,7 @@ class LocalPath: def readlink(self) -> str: """Return value of a symbolic link.""" # https://github.com/python/mypy/issues/12278 - return error.checked_call(os.readlink, self.strpath) # type: ignore[arg-type,return-value] + return error.checked_call(os.readlink, self.strpath) # type: ignore[arg-type,return-value,unused-ignore] def mklinkto(self, oldname): """Posix style hard link to another name.""" @@ -757,15 +756,11 @@ class LocalPath: if ensure: self.dirpath().ensure(dir=1) if encoding: - # Using type ignore here because of this error: - # error: Argument 1 has incompatible type overloaded function; - # expected "Callable[[str, Any, Any], TextIOWrapper]" [arg-type] - # Which seems incorrect, given io.open supports the given argument types. return error.checked_call( io.open, self.strpath, mode, - encoding=encoding, # type:ignore[arg-type] + encoding=encoding, ) return error.checked_call(open, self.strpath, mode) @@ -966,12 +961,10 @@ class LocalPath: return p @overload - def stat(self, raising: Literal[True] = ...) -> Stat: - ... + def stat(self, raising: Literal[True] = ...) -> Stat: ... @overload - def stat(self, raising: Literal[False]) -> Stat | None: - ... + def stat(self, raising: Literal[False]) -> Stat | None: ... def stat(self, raising: bool = True) -> Stat | None: """Return an os.stat() tuple.""" @@ -1277,13 +1270,7 @@ class LocalPath: if rootdir is None: rootdir = cls.get_temproot() - # Using type ignore here because of this error: - # error: Argument 1 has incompatible type overloaded function; expected "Callable[[str], str]" [arg-type] - # Which seems incorrect, given tempfile.mkdtemp supports the given argument types. - path = error.checked_call( - tempfile.mkdtemp, - dir=str(rootdir), # type:ignore[arg-type] - ) + path = error.checked_call(tempfile.mkdtemp, dir=str(rootdir)) return cls(path) @classmethod diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index ea71230e1..21dd4a4a4 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs """Support for presenting detailed information in failing assertions.""" + import sys from typing import Any from typing import Generator diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index ddae34c73..678471ee9 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -289,15 +289,13 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader) else: from importlib.abc import TraversableResources - def get_resource_reader(self, name: str) -> TraversableResources: # type: ignore + def get_resource_reader(self, name: str) -> TraversableResources: if sys.version_info < (3, 11): from importlib.readers import FileReader else: from importlib.resources.readers import FileReader - return FileReader( # type:ignore[no-any-return] - types.SimpleNamespace(path=self._rewritten_names[name]) - ) + return FileReader(types.SimpleNamespace(path=self._rewritten_names[name])) def _write_pyc_fp( @@ -672,9 +670,9 @@ class AssertionRewriter(ast.NodeVisitor): self.enable_assertion_pass_hook = False self.source = source self.scope: tuple[ast.AST, ...] = () - self.variables_overwrite: defaultdict[ - tuple[ast.AST, ...], Dict[str, str] - ] = defaultdict(dict) + self.variables_overwrite: defaultdict[tuple[ast.AST, ...], Dict[str, str]] = ( + defaultdict(dict) + ) def run(self, mod: ast.Module) -> None: """Find all assert statements in *mod* and rewrite them.""" @@ -975,7 +973,7 @@ class AssertionRewriter(ast.NodeVisitor): # name if it's a local variable or _should_repr_global_name() # thinks it's acceptable. locs = ast.Call(self.builtin("locals"), [], []) - target_id = name.target.id # type: ignore[attr-defined] + target_id = name.target.id inlocs = ast.Compare(ast.Constant(target_id), [ast.In()], [locs]) dorepr = self.helper("_should_repr_global_name", name) test = ast.BoolOp(ast.Or(), [inlocs, dorepr]) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index ca3df7490..cb6716410 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs """Utilities for assertion debugging.""" + import collections.abc import os import pprint @@ -222,10 +223,9 @@ def assertrepr_compare( except outcomes.Exit: raise except Exception: + repr_crash = _pytest._code.ExceptionInfo.from_current()._getreprcrash() explanation = [ - "(pytest_assertion plugin: representation of details failed: {}.".format( - _pytest._code.ExceptionInfo.from_current()._getreprcrash() - ), + f"(pytest_assertion plugin: representation of details failed: {repr_crash}.", " Probably an object has a faulty __repr__.)", ] diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 5ccd2168d..81703ddac 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs """Implementation of the cache provider.""" + # This plugin was not named "cache" to avoid conflicts with the external # pytest-cache version. import dataclasses @@ -432,7 +433,7 @@ class NFPlugin: return res def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item]: - return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True) # type: ignore[no-any-return] + return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True) def pytest_sessionfinish(self) -> None: config = self.config diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index dce431c3d..3f6a25103 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs """Per-test stdout/stderr capturing mechanism.""" + import abc import collections import contextlib @@ -105,17 +106,16 @@ def _windowsconsoleio_workaround(stream: TextIO) -> None: return # Bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666). - if not hasattr(stream, "buffer"): # type: ignore[unreachable] + if not hasattr(stream, "buffer"): # type: ignore[unreachable,unused-ignore] return - buffered = hasattr(stream.buffer, "raw") - raw_stdout = stream.buffer.raw if buffered else stream.buffer # type: ignore[attr-defined] + raw_stdout = stream.buffer.raw if hasattr(stream.buffer, "raw") else stream.buffer - if not isinstance(raw_stdout, io._WindowsConsoleIO): # type: ignore[attr-defined] + if not isinstance(raw_stdout, io._WindowsConsoleIO): # type: ignore[attr-defined,unused-ignore] return def _reopen_stdio(f, mode): - if not buffered and mode[0] == "w": + if not hasattr(stream.buffer, "raw") and mode[0] == "w": buffering = 0 else: buffering = -1 @@ -482,12 +482,9 @@ class FDCaptureBase(CaptureBase[AnyStr]): self._state = "initialized" def __repr__(self) -> str: - return "<{} {} oldfd={} _state={!r} tmpfile={!r}>".format( - self.__class__.__name__, - self.targetfd, - self.targetfd_save, - self._state, - self.tmpfile, + return ( + f"<{self.__class__.__name__} {self.targetfd} oldfd={self.targetfd_save} " + f"_state={self._state!r} tmpfile={self.tmpfile!r}>" ) def _assert_state(self, op: str, states: Tuple[str, ...]) -> None: @@ -621,12 +618,9 @@ class MultiCapture(Generic[AnyStr]): self.err: Optional[CaptureBase[AnyStr]] = err def __repr__(self) -> str: - return "".format( - self.out, - self.err, - self.in_, - self._state, - self._in_suspended, + return ( + f"" ) def start_capturing(self) -> None: @@ -735,8 +729,9 @@ class CaptureManager: self._capture_fixture: Optional[CaptureFixture[Any]] = None def __repr__(self) -> str: - return "".format( - self._method, self._global_capturing, self._capture_fixture + return ( + f"" ) def is_capturing(self) -> Union[str, bool]: diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 8d49d6fb6..9d9411818 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -304,7 +304,7 @@ def get_user_id() -> int | None: # mypy follows the version and platform checking expectation of PEP 484: # https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks # Containment checks are too complex for mypy v1.5.0 and cause failure. - if sys.platform == "win32" or sys.platform == "emscripten": # noqa: PLR1714 + if sys.platform == "win32" or sys.platform == "emscripten": # win32 does not have a getuid() function. # Emscripten has a return 0 stub. return None diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index bf2cfc399..7ff27643f 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs """Command line options, ini-file and conftest.py processing.""" + import argparse import collections.abc import copy diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 441d79e90..95dc28d4a 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -426,8 +426,7 @@ class MyOptionParser(argparse.ArgumentParser): msg = f"{self.prog}: error: {message}" if hasattr(self._parser, "_config_source_hint"): - # Type ignored because the attribute is set dynamically. - msg = f"{msg} ({self._parser._config_source_hint})" # type: ignore + msg = f"{msg} ({self._parser._config_source_hint})" raise UsageError(self.format_usage() + msg) diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index cb157cd67..6ed0c5c7a 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs """Interactive debugging with PDB, the Python Debugger.""" + import argparse import functools import sys @@ -154,9 +155,7 @@ class pytestPDB: def _get_pdb_wrapper_class(cls, pdb_cls, capman: Optional["CaptureManager"]): import _pytest.config - # Type ignored because mypy doesn't support "dynamic" - # inheritance like this. - class PytestPdbWrapper(pdb_cls): # type: ignore[valid-type,misc] + class PytestPdbWrapper(pdb_cls): _pytest_capman = capman _continued = False diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index ced3b82f5..7fff99f37 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs """Discover and run doctests in modules and test files.""" + import bdb from contextlib import contextmanager import functools diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index daf3145aa..8b25d743c 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -447,7 +447,7 @@ class FixtureRequest(abc.ABC): @property def config(self) -> Config: """The pytest config object associated with this request.""" - return self._pyfuncitem.config # type: ignore[no-any-return] + return self._pyfuncitem.config @property def function(self): @@ -499,7 +499,7 @@ class FixtureRequest(abc.ABC): @property def session(self) -> "Session": """Pytest session object.""" - return self._pyfuncitem.session # type: ignore[no-any-return] + return self._pyfuncitem.session @abc.abstractmethod def addfinalizer(self, finalizer: Callable[[], object]) -> None: @@ -629,17 +629,14 @@ class FixtureRequest(abc.ABC): ) except ValueError: source_path_str = str(source_path) + location = getlocation(fixturedef.func, funcitem.config.rootpath) msg = ( "The requested fixture has no parameter defined for test:\n" - " {}\n\n" - "Requested fixture '{}' defined in:\n{}" - "\n\nRequested here:\n{}:{}".format( - funcitem.nodeid, - fixturedef.argname, - getlocation(fixturedef.func, funcitem.config.rootpath), - source_path_str, - source_lineno, - ) + f" {funcitem.nodeid}\n\n" + f"Requested fixture '{fixturedef.argname}' defined in:\n" + f"{location}\n\n" + f"Requested here:\n" + f"{source_path_str}:{source_lineno}" ) fail(msg, pytrace=False) @@ -1108,12 +1105,12 @@ def resolve_fixture_function( # (for example a plugin class with a fixture), see #2270. if hasattr(fixturefunc, "__self__") and not isinstance( instance, - fixturefunc.__self__.__class__, # type: ignore[union-attr] + fixturefunc.__self__.__class__, ): return fixturefunc fixturefunc = getimfunc(fixturedef.func) if fixturefunc != fixturedef.func: - fixturefunc = fixturefunc.__get__(instance) # type: ignore[union-attr] + fixturefunc = fixturefunc.__get__(instance) return fixturefunc @@ -1147,12 +1144,13 @@ def wrap_function_to_error_out_if_called_directly( ) -> FixtureFunction: """Wrap the given fixture function so we can raise an error about it being called directly, instead of used as an argument in a test function.""" + name = fixture_marker.name or function.__name__ message = ( - 'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n' + f'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n' "but are created automatically when test functions request them as parameters.\n" "See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and\n" "https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly about how to update your code." - ).format(name=fixture_marker.name or function.__name__) + ) @functools.wraps(function) def result(*args, **kwargs): @@ -1219,8 +1217,7 @@ def fixture( Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]] ] = ..., name: Optional[str] = ..., -) -> FixtureFunction: - ... +) -> FixtureFunction: ... @overload @@ -1234,8 +1231,7 @@ def fixture( Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]] ] = ..., name: Optional[str] = None, -) -> FixtureFunctionMarker: - ... +) -> FixtureFunctionMarker: ... def fixture( diff --git a/src/_pytest/freeze_support.py b/src/_pytest/freeze_support.py index d028058e3..e03a6d175 100644 --- a/src/_pytest/freeze_support.py +++ b/src/_pytest/freeze_support.py @@ -35,7 +35,7 @@ def _iter_all_modules( else: # Type ignored because typeshed doesn't define ModuleType.__path__ # (only defined on packages). - package_path = package.__path__ # type: ignore[attr-defined] + package_path = package.__path__ path, prefix = package_path[0], package.__name__ + "." for _, name, is_package in pkgutil.iter_modules([path]): if is_package: diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index aa8bf65c7..37fbdf04d 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs """Version info, help messages, tracing configuration.""" + from argparse import Action import os import sys diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 4bee76f1e..db55bd82d 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -1,6 +1,7 @@ # mypy: allow-untyped-defs """Hook specifications for pytest plugins which are invoked by pytest itself and by builtin plugins.""" + from pathlib import Path from typing import Any from typing import Dict diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 4ca356f31..e6ccebc20 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -7,6 +7,7 @@ Based on initial code from Ross Lawley. Output conforms to https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd """ + from datetime import datetime import functools import os @@ -60,7 +61,7 @@ def bin_xml_escape(arg: object) -> str: # Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] # For an unknown(?) reason, we disallow #x7F (DEL) as well. illegal_xml_re = ( - "[^\u0009\u000A\u000D\u0020-\u007E\u0080-\uD7FF\uE000-\uFFFD\u10000-\u10FFFF]" + "[^\u0009\u000a\u000d\u0020-\u007e\u0080-\ud7ff\ue000-\ufffd\u10000-\u10ffff]" ) return re.sub(illegal_xml_re, repl, str(arg)) @@ -261,7 +262,7 @@ class _NodeReporter: self.__dict__.clear() # Type ignored because mypy doesn't like overriding a method. # Also the return value doesn't match... - self.to_xml = lambda: data # type: ignore[assignment] + self.to_xml = lambda: data # type: ignore[method-assign] def _warn_incompatibility_with_xunit2( diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index e9a3234fd..af5e443ce 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs """Access and control log capturing.""" + from contextlib import contextmanager from contextlib import nullcontext from datetime import datetime @@ -209,7 +210,7 @@ class PercentStyleMultiline(logging.PercentStyle): if "\n" in record.message: if hasattr(record, "auto_indent"): # Passed in from the "extra={}" kwarg on the call to logging.log(). - auto_indent = self._get_auto_indent(record.auto_indent) # type: ignore[attr-defined] + auto_indent = self._get_auto_indent(record.auto_indent) else: auto_indent = self._auto_indent @@ -512,7 +513,7 @@ class LogCaptureFixture: :return: The original disabled logging level. """ - original_disable_level: int = logger_obj.manager.disable # type: ignore[attr-defined] + original_disable_level: int = logger_obj.manager.disable if isinstance(level, str): # Try to translate the level string to an int for `logging.disable()` diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 3b9ac93cf..716d5cf78 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -736,14 +736,12 @@ class Session(nodes.Collector): @overload def perform_collect( self, args: Optional[Sequence[str]] = ..., genitems: "Literal[True]" = ... - ) -> Sequence[nodes.Item]: - ... + ) -> Sequence[nodes.Item]: ... @overload def perform_collect( self, args: Optional[Sequence[str]] = ..., genitems: bool = ... - ) -> Sequence[Union[nodes.Item, nodes.Collector]]: - ... + ) -> Sequence[Union[nodes.Item, nodes.Collector]]: ... def perform_collect( self, args: Optional[Sequence[str]] = None, genitems: bool = True diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 1da300c82..a6503bf1d 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -342,7 +342,7 @@ class MarkDecorator: # return type. Not much we can do about that. Thankfully mypy picks # the first match so it works out even if we break the rules. @overload - def __call__(self, arg: Markable) -> Markable: # type: ignore[misc] + def __call__(self, arg: Markable) -> Markable: # type: ignore[overload-overlap] pass @overload @@ -433,13 +433,11 @@ if TYPE_CHECKING: from _pytest.scope import _ScopeName class _SkipMarkDecorator(MarkDecorator): - @overload # type: ignore[override,misc,no-overload-impl] - def __call__(self, arg: Markable) -> Markable: - ... + @overload # type: ignore[override,no-overload-impl] + def __call__(self, arg: Markable) -> Markable: ... @overload - def __call__(self, reason: str = ...) -> "MarkDecorator": - ... + def __call__(self, reason: str = ...) -> "MarkDecorator": ... class _SkipifMarkDecorator(MarkDecorator): def __call__( # type: ignore[override] @@ -447,13 +445,11 @@ if TYPE_CHECKING: condition: Union[str, bool] = ..., *conditions: Union[str, bool], reason: str = ..., - ) -> MarkDecorator: - ... + ) -> MarkDecorator: ... class _XfailMarkDecorator(MarkDecorator): - @overload # type: ignore[override,misc,no-overload-impl] - def __call__(self, arg: Markable) -> Markable: - ... + @overload # type: ignore[override,no-overload-impl] + def __call__(self, arg: Markable) -> Markable: ... @overload def __call__( @@ -466,8 +462,7 @@ if TYPE_CHECKING: None, Type[BaseException], Tuple[Type[BaseException], ...] ] = ..., strict: bool = ..., - ) -> MarkDecorator: - ... + ) -> MarkDecorator: ... class _ParametrizeMarkDecorator(MarkDecorator): def __call__( # type: ignore[override] @@ -483,8 +478,7 @@ if TYPE_CHECKING: ] ] = ..., scope: Optional[_ScopeName] = ..., - ) -> MarkDecorator: - ... + ) -> MarkDecorator: ... class _UsefixturesMarkDecorator(MarkDecorator): def __call__(self, *fixtures: str) -> MarkDecorator: # type: ignore[override] diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index e96a93868..3f398df76 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs """Monkeypatching and mocking functionality.""" + from contextlib import contextmanager import os import re @@ -167,8 +168,7 @@ class MonkeyPatch: name: object, value: Notset = ..., raising: bool = ..., - ) -> None: - ... + ) -> None: ... @overload def setattr( @@ -177,8 +177,7 @@ class MonkeyPatch: name: str, value: object, raising: bool = ..., - ) -> None: - ... + ) -> None: ... def setattr( self, diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index cff15001c..1b91bdb6e 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -363,12 +363,10 @@ class Node(abc.ABC, metaclass=NodeMeta): yield node, mark @overload - def get_closest_marker(self, name: str) -> Optional[Mark]: - ... + def get_closest_marker(self, name: str) -> Optional[Mark]: ... @overload - def get_closest_marker(self, name: str, default: Mark) -> Mark: - ... + def get_closest_marker(self, name: str, default: Mark) -> Mark: ... def get_closest_marker( self, name: str, default: Optional[Mark] = None diff --git a/src/_pytest/pastebin.py b/src/_pytest/pastebin.py index 98ba5c9c1..533d78c9a 100644 --- a/src/_pytest/pastebin.py +++ b/src/_pytest/pastebin.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs """Submit failure or test session information to a pastebin service.""" + from io import StringIO import tempfile from typing import IO diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 8002528b9..23f44da69 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -3,6 +3,7 @@ PYTEST_DONT_REWRITE """ + import collections.abc import contextlib from fnmatch import fnmatch @@ -245,8 +246,7 @@ class RecordedHookCall: if TYPE_CHECKING: # The class has undetermined attributes, this tells mypy about it. - def __getattr__(self, key: str): - ... + def __getattr__(self, key: str): ... @final @@ -327,15 +327,13 @@ class HookRecorder: def getreports( self, names: "Literal['pytest_collectreport']", - ) -> Sequence[CollectReport]: - ... + ) -> Sequence[CollectReport]: ... @overload def getreports( self, names: "Literal['pytest_runtest_logreport']", - ) -> Sequence[TestReport]: - ... + ) -> Sequence[TestReport]: ... @overload def getreports( @@ -344,8 +342,7 @@ class HookRecorder: "pytest_collectreport", "pytest_runtest_logreport", ), - ) -> Sequence[Union[CollectReport, TestReport]]: - ... + ) -> Sequence[Union[CollectReport, TestReport]]: ... def getreports( self, @@ -390,15 +387,13 @@ class HookRecorder: def getfailures( self, names: "Literal['pytest_collectreport']", - ) -> Sequence[CollectReport]: - ... + ) -> Sequence[CollectReport]: ... @overload def getfailures( self, names: "Literal['pytest_runtest_logreport']", - ) -> Sequence[TestReport]: - ... + ) -> Sequence[TestReport]: ... @overload def getfailures( @@ -407,8 +402,7 @@ class HookRecorder: "pytest_collectreport", "pytest_runtest_logreport", ), - ) -> Sequence[Union[CollectReport, TestReport]]: - ... + ) -> Sequence[Union[CollectReport, TestReport]]: ... def getfailures( self, diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 7b0683b6e..3242d517e 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs """Python test discovery, setup and run of test functions.""" + import abc from collections import Counter from collections import defaultdict diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 7e51da319..0ba86e816 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -393,7 +393,7 @@ class ApproxScalar(ApproxBase): # tolerances, i.e. non-numerics and infinities. Need to call abs to # handle complex numbers, e.g. (inf + 1j). if (not isinstance(self.expected, (Complex, Decimal))) or math.isinf( - abs(self.expected) # type: ignore[arg-type] + abs(self.expected) ): return str(self.expected) @@ -437,8 +437,8 @@ class ApproxScalar(ApproxBase): # Allow the user to control whether NaNs are considered equal to each # other or not. The abs() calls are for compatibility with complex # numbers. - if math.isnan(abs(self.expected)): # type: ignore[arg-type] - return self.nan_ok and math.isnan(abs(actual)) # type: ignore[arg-type] + if math.isnan(abs(self.expected)): + return self.nan_ok and math.isnan(abs(actual)) # Infinity shouldn't be approximately equal to anything but itself, but # if there's a relative tolerance, it will be infinite and infinity @@ -446,11 +446,11 @@ class ApproxScalar(ApproxBase): # case would have been short circuited above, so here we can just # return false if the expected value is infinite. The abs() call is # for compatibility with complex numbers. - if math.isinf(abs(self.expected)): # type: ignore[arg-type] + if math.isinf(abs(self.expected)): return False # Return true if the two numbers are within the tolerance. - result: bool = abs(self.expected - actual) <= self.tolerance + result: bool = abs(self.expected - actual) <= self.tolerance # type: ignore[arg-type] return result # Ignore type because of https://github.com/python/mypy/issues/4266. @@ -769,8 +769,7 @@ def raises( expected_exception: Union[Type[E], Tuple[Type[E], ...]], *, match: Optional[Union[str, Pattern[str]]] = ..., -) -> "RaisesContext[E]": - ... +) -> "RaisesContext[E]": ... @overload @@ -779,8 +778,7 @@ def raises( func: Callable[..., Any], *args: Any, **kwargs: Any, -) -> _pytest._code.ExceptionInfo[E]: - ... +) -> _pytest._code.ExceptionInfo[E]: ... def raises( diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index bcf9f1466..63e7a4bd6 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs """Record warnings during test function execution.""" + from pprint import pformat import re from types import TracebackType @@ -43,13 +44,11 @@ def recwarn() -> Generator["WarningsRecorder", None, None]: @overload def deprecated_call( *, match: Optional[Union[str, Pattern[str]]] = ... -) -> "WarningsRecorder": - ... +) -> "WarningsRecorder": ... @overload -def deprecated_call(func: Callable[..., T], *args: Any, **kwargs: Any) -> T: - ... +def deprecated_call(func: Callable[..., T], *args: Any, **kwargs: Any) -> T: ... def deprecated_call( @@ -91,8 +90,7 @@ def warns( expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = ..., *, match: Optional[Union[str, Pattern[str]]] = ..., -) -> "WarningsChecker": - ... +) -> "WarningsChecker": ... @overload @@ -101,8 +99,7 @@ def warns( func: Callable[..., T], *args: Any, **kwargs: Any, -) -> T: - ... +) -> T: ... def warns( @@ -184,8 +181,7 @@ class WarningsRecorder(warnings.catch_warnings): # type:ignore[type-arg] def __init__(self, *, _ispytest: bool = False) -> None: check_ispytest(_ispytest) - # Type ignored due to the way typeshed handles warnings.catch_warnings. - super().__init__(record=True) # type: ignore[call-arg] + super().__init__(record=True) self._entered = False self._list: List[warnings.WarningMessage] = [] diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 7cdb70e32..70f3212ce 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -72,8 +72,7 @@ class BaseReport: if TYPE_CHECKING: # Can have arbitrary fields given to __init__(). - def __getattr__(self, key: str) -> Any: - ... + def __getattr__(self, key: str) -> Any: ... def toterminal(self, out: TerminalWriter) -> None: if hasattr(self, "node"): @@ -606,9 +605,9 @@ def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]: description, ) ) - exception_info: Union[ - ExceptionChainRepr, ReprExceptionInfo - ] = ExceptionChainRepr(chain) + exception_info: Union[ExceptionChainRepr, ReprExceptionInfo] = ( + ExceptionChainRepr(chain) + ) else: exception_info = ReprExceptionInfo( reprtraceback=reprtraceback, diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 16abb895d..3f706b927 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs """Basic collect and runtest protocol implementations.""" + import bdb import dataclasses import os @@ -84,7 +85,7 @@ def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None: dlist.append(rep) if not dlist: return - dlist.sort(key=lambda x: x.duration, reverse=True) # type: ignore[no-any-return] + dlist.sort(key=lambda x: x.duration, reverse=True) if not durations: tr.write_sep("=", "slowest durations") else: @@ -395,8 +396,7 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport: skip_exceptions = [Skipped] unittest = sys.modules.get("unittest") if unittest is not None: - # Type ignored because unittest is loaded dynamically. - skip_exceptions.append(unittest.SkipTest) # type: ignore + skip_exceptions.append(unittest.SkipTest) if isinstance(call.excinfo.value, tuple(skip_exceptions)): outcome = "skipped" r_ = collector._repr_failure_py(call.excinfo, "line") diff --git a/src/_pytest/setuponly.py b/src/_pytest/setuponly.py index c87de1e32..39ab28b46 100644 --- a/src/_pytest/setuponly.py +++ b/src/_pytest/setuponly.py @@ -58,7 +58,7 @@ def pytest_fixture_post_finalizer( if config.option.setupshow: _show_fixture_action(fixturedef, request.config, "TEARDOWN") if hasattr(fixturedef, "cached_param"): - del fixturedef.cached_param # type: ignore[attr-defined] + del fixturedef.cached_param def _show_fixture_action( @@ -87,7 +87,7 @@ def _show_fixture_action( tw.write(" (fixtures used: {})".format(", ".join(deps))) if hasattr(fixturedef, "cached_param"): - tw.write(f"[{saferepr(fixturedef.cached_param, maxsize=42)}]") # type: ignore[attr-defined] + tw.write(f"[{saferepr(fixturedef.cached_param, maxsize=42)}]") tw.flush() diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 4799ae649..188dcae3f 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs """Support for skip/xfail functions and markers.""" + from collections.abc import Mapping import dataclasses import os @@ -109,7 +110,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool, ) globals_.update(dictionary) if hasattr(item, "obj"): - globals_.update(item.obj.__globals__) # type: ignore[attr-defined] + globals_.update(item.obj.__globals__) try: filename = f"<{mark.name} condition>" condition_code = compile(condition, filename, "eval") diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 75d57197a..2c9c0d3b1 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -3,6 +3,7 @@ This is a good source for looking at the various reporting hooks. """ + import argparse from collections import Counter import dataclasses diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 1cb9fbbe0..72efed3e8 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs """Support for providing temporary directories to test functions.""" + import dataclasses import os from pathlib import Path diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 32eb361c6..5099904fd 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs """Discover and run std-library "unittest" style tests.""" + import sys import traceback import types @@ -98,8 +99,7 @@ class UnitTestCase(Class): runtest = getattr(self.obj, "runTest", None) if runtest is not None: ut = sys.modules.get("twisted.trial.unittest", None) - # Type ignored because `ut` is an opaque module. - if ut is None or runtest != ut.TestCase.runTest: # type: ignore + if ut is None or runtest != ut.TestCase.runTest: yield TestCaseFunction.from_parent(self, name="runTest") def _register_unittest_setup_class_fixture(self, cls: type) -> None: @@ -302,8 +302,7 @@ class TestCaseFunction(Function): # Let the unittest framework handle async functions. if is_async_function(self.obj): - # Type ignored because self acts as the TestResult, but is not actually one. - testcase(result=self) # type: ignore[arg-type] + testcase(result=self) else: # When --pdb is given, we want to postpone calling tearDown() otherwise # when entering the pdb prompt, tearDown() would have probably cleaned up @@ -322,7 +321,7 @@ class TestCaseFunction(Function): # wrap_pytest_function_for_tracing replaces self.obj by a wrapper. setattr(testcase, self.name, self.obj) try: - testcase(result=self) # type: ignore[arg-type] + testcase(result=self) finally: delattr(testcase, self.name) @@ -353,9 +352,7 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None: # its own nose.SkipTest. For unittest TestCases, SkipTest is already # handled internally, and doesn't reach here. unittest = sys.modules.get("unittest") - if ( - unittest and call.excinfo and isinstance(call.excinfo.value, unittest.SkipTest) # type: ignore[attr-defined] - ): + if unittest and call.excinfo and isinstance(call.excinfo.value, unittest.SkipTest): excinfo = call.excinfo call2 = CallInfo[None].from_call( lambda: pytest.skip(str(excinfo.value)), call.when diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index 20829aa58..c6b6de827 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -1,5 +1,6 @@ # PYTHON_ARGCOMPLETE_OK """pytest: unit and functional testing with Python.""" + from _pytest import __version__ from _pytest import version_tuple from _pytest._code import ExceptionInfo diff --git a/testing/_py/test_local.py b/testing/_py/test_local.py index 0c8575c4e..ad2526571 100644 --- a/testing/_py/test_local.py +++ b/testing/_py/test_local.py @@ -16,8 +16,8 @@ import pytest @contextlib.contextmanager def ignore_encoding_warning(): with warnings.catch_warnings(): - with contextlib.suppress(NameError): # new in 3.10 - warnings.simplefilter("ignore", EncodingWarning) # type: ignore [name-defined] + if sys.version_info > (3, 10): + warnings.simplefilter("ignore", EncodingWarning) yield @@ -822,7 +822,7 @@ class TestLocalPath(CommonFSTests): # depending on how the paths are used), but > 4096 (which is the # Linux' limitation) - the behaviour of paths with names > 4096 chars # is undetermined - newfilename = "/test" * 60 # type:ignore[unreachable] + newfilename = "/test" * 60 # type:ignore[unreachable,unused-ignore] l1 = tmpdir.join(newfilename) l1.ensure(file=True) l1.write_text("foo", encoding="utf-8") @@ -1368,8 +1368,8 @@ class TestPOSIXLocalPath: assert realpath.basename == "file" def test_owner(self, path1, tmpdir): - from grp import getgrgid # type:ignore[attr-defined] - from pwd import getpwuid # type:ignore[attr-defined] + from grp import getgrgid # type:ignore[attr-defined,unused-ignore] + from pwd import getpwuid # type:ignore[attr-defined,unused-ignore] stat = path1.stat() assert stat.path == path1 diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 49c5dd371..419c11abc 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -180,7 +180,7 @@ class TestTraceback_f_g_h: def test_traceback_cut_excludepath(self, pytester: Pytester) -> None: p = pytester.makepyfile("def f(): raise ValueError") with pytest.raises(ValueError) as excinfo: - import_path(p, root=pytester.path, consider_namespace_packages=False).f() # type: ignore[attr-defined] + import_path(p, root=pytester.path, consider_namespace_packages=False).f() basedir = Path(pytest.__file__).parent newtraceback = excinfo.traceback.cut(excludepath=basedir) for x in newtraceback: diff --git a/testing/example_scripts/acceptance/fixture_mock_integration.py b/testing/example_scripts/acceptance/fixture_mock_integration.py index 36e711f40..d802a7f87 100644 --- a/testing/example_scripts/acceptance/fixture_mock_integration.py +++ b/testing/example_scripts/acceptance/fixture_mock_integration.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs """Reproduces issue #3774""" + from unittest import mock import pytest diff --git a/testing/example_scripts/unittest/test_setup_skip.py b/testing/example_scripts/unittest/test_setup_skip.py index 4681cda03..7550a0975 100644 --- a/testing/example_scripts/unittest/test_setup_skip.py +++ b/testing/example_scripts/unittest/test_setup_skip.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs """Skipping an entire subclass with unittest.skip() should *not* call setUp from a base class.""" + import unittest diff --git a/testing/example_scripts/unittest/test_setup_skip_class.py b/testing/example_scripts/unittest/test_setup_skip_class.py index eae98287f..48f7e476f 100644 --- a/testing/example_scripts/unittest/test_setup_skip_class.py +++ b/testing/example_scripts/unittest/test_setup_skip_class.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs """Skipping an entire subclass with unittest.skip() should *not* call setUpClass from a base class.""" + import unittest diff --git a/testing/example_scripts/unittest/test_setup_skip_module.py b/testing/example_scripts/unittest/test_setup_skip_module.py index 43c24136e..eee4263d2 100644 --- a/testing/example_scripts/unittest/test_setup_skip_module.py +++ b/testing/example_scripts/unittest/test_setup_skip_module.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs """setUpModule is always called, even if all tests in the module are skipped""" + import unittest diff --git a/testing/example_scripts/unittest/test_unittest_asynctest.py b/testing/example_scripts/unittest/test_unittest_asynctest.py index b3f03e325..e9b10171e 100644 --- a/testing/example_scripts/unittest/test_unittest_asynctest.py +++ b/testing/example_scripts/unittest/test_unittest_asynctest.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs """Issue #7110""" + import asyncio from typing import List diff --git a/testing/io/test_wcwidth.py b/testing/io/test_wcwidth.py index 0989af00d..82503b830 100644 --- a/testing/io/test_wcwidth.py +++ b/testing/io/test_wcwidth.py @@ -11,8 +11,8 @@ import pytest ("a", 1), ("1", 1), ("א", 1), - ("\u200B", 0), - ("\u1ABE", 0), + ("\u200b", 0), + ("\u1abe", 0), ("\u0591", 0), ("🉐", 2), ("$", 2), # noqa: RUF001 diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index ed22c2b5a..3d0058fa0 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -109,7 +109,7 @@ class TestMetafunc: metafunc = self.Metafunc(func) # When the input is an iterator, only len(args) are taken, # so the bad Exc isn't reached. - metafunc.parametrize("x", [1, 2], ids=gen()) # type: ignore[arg-type] + metafunc.parametrize("x", [1, 2], ids=gen()) assert [(x.params, x.id) for x in metafunc._calls] == [ ({"x": 1}, "0"), ({"x": 2}, "2"), @@ -121,7 +121,7 @@ class TestMetafunc: r"Supported types are: .*" ), ): - metafunc.parametrize("x", [1, 2, 3], ids=gen()) # type: ignore[arg-type] + metafunc.parametrize("x", [1, 2, 3], ids=gen()) def test_parametrize_bad_scope(self) -> None: def func(x): diff --git a/testing/test_config.py b/testing/test_config.py index 88470ff2d..147c2cb85 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1243,7 +1243,7 @@ def test_disable_plugin_autoload( monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") monkeypatch.setattr(importlib.metadata, "distributions", distributions) - monkeypatch.setitem(sys.modules, "mytestplugin", PseudoPlugin()) # type: ignore[misc] + monkeypatch.setitem(sys.modules, "mytestplugin", PseudoPlugin()) config = pytester.parseconfig(*parse_args) has_loaded = config.pluginmanager.get_plugin("mytestplugin") is not None assert has_loaded == should_load diff --git a/testing/test_debugging.py b/testing/test_debugging.py index 91a0be481..53ebadbdb 100644 --- a/testing/test_debugging.py +++ b/testing/test_debugging.py @@ -29,7 +29,7 @@ def runpdb_and_get_stdout(pytester: Pytester, source: str): def runpdb_and_get_report(pytester: Pytester, source: str): result = runpdb(pytester, source) - reports = result.reprec.getreports("pytest_runtest_logreport") # type: ignore[attr-defined] + reports = result.reprec.getreports("pytest_runtest_logreport") assert len(reports) == 3, reports # setup/call/teardown return reports[1] diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 42104255b..3b92d65bd 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1202,7 +1202,7 @@ def test_unicode_issue368(pytester: Pytester) -> None: node_reporter.append_skipped(test_report) test_report.longrepr = "filename", 1, "Skipped: 卡嘣嘣" node_reporter.append_skipped(test_report) - test_report.wasxfail = ustr # type: ignore[attr-defined] + test_report.wasxfail = ustr node_reporter.append_skipped(test_report) log.pytest_sessionfinish() diff --git a/testing/test_mark.py b/testing/test_mark.py index 6e183a178..2896afa45 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -18,7 +18,7 @@ class TestMark: @pytest.mark.parametrize("attr", ["mark", "param"]) def test_pytest_exists_in_namespace_all(self, attr: str) -> None: module = sys.modules["pytest"] - assert attr in module.__all__ # type: ignore + assert attr in module.__all__ def test_pytest_mark_notcallable(self) -> None: mark = MarkGenerator(_ispytest=True) @@ -34,7 +34,7 @@ class TestMark: assert pytest.mark.foo(some_function) is some_function marked_with_args = pytest.mark.foo.with_args(some_function) - assert marked_with_args is not some_function # type: ignore[comparison-overlap] + assert marked_with_args is not some_function assert pytest.mark.foo(SomeClass) is SomeClass assert pytest.mark.foo.with_args(SomeClass) is not SomeClass # type: ignore[comparison-overlap] diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index a4bccb1b2..7f740a060 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -183,7 +183,7 @@ class TestImportPath: obj = import_path( path1 / "execfile.py", root=path1, consider_namespace_packages=ns_param ) - assert obj.x == 42 # type: ignore[attr-defined] + assert obj.x == 42 assert obj.__name__ == "execfile" def test_import_path_missing_file(self, path1: Path, ns_param: bool) -> None: @@ -246,7 +246,7 @@ class TestImportPath: mod = import_path( otherdir / "a.py", root=path1, consider_namespace_packages=ns_param ) - assert mod.result == "got it" # type: ignore[attr-defined] + assert mod.result == "got it" assert mod.__name__ == "otherdir.a" def test_b(self, path1: Path, ns_param: bool) -> None: @@ -254,7 +254,7 @@ class TestImportPath: mod = import_path( otherdir / "b.py", root=path1, consider_namespace_packages=ns_param ) - assert mod.stuff == "got it" # type: ignore[attr-defined] + assert mod.stuff == "got it" assert mod.__name__ == "otherdir.b" def test_c(self, path1: Path, ns_param: bool) -> None: @@ -262,14 +262,14 @@ class TestImportPath: mod = import_path( otherdir / "c.py", root=path1, consider_namespace_packages=ns_param ) - assert mod.value == "got it" # type: ignore[attr-defined] + assert mod.value == "got it" def test_d(self, path1: Path, ns_param: bool) -> None: otherdir = path1 / "otherdir" mod = import_path( otherdir / "d.py", root=path1, consider_namespace_packages=ns_param ) - assert mod.value2 == "got it" # type: ignore[attr-defined] + assert mod.value2 == "got it" def test_import_after(self, tmp_path: Path, ns_param: bool) -> None: tmp_path.joinpath("xxxpackage").mkdir() @@ -360,7 +360,7 @@ class TestImportPath: root=tmp_path, consider_namespace_packages=ns_param, ) - assert module.foo(2) == 42 # type: ignore[attr-defined] + assert module.foo(2) == 42 assert str(simple_module.parent) not in sys.path assert module.__name__ in sys.modules assert module.__name__ == f"_src.tests.mymod_{request.node.name}" @@ -400,7 +400,7 @@ class TestImportPath: root=tmp_path, consider_namespace_packages=ns_param, ) - assert module.foo(2) == 42 # type: ignore[attr-defined] + assert module.foo(2) == 42 # mode='importlib' fails if no spec is found to load the module import importlib.util diff --git a/testing/test_reports.py b/testing/test_reports.py index 2de5ae600..c6baeebc9 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -294,9 +294,9 @@ class TestReportSerialization: reprec = pytester.inline_run() if report_class is TestReport: - reports: Union[ - Sequence[TestReport], Sequence[CollectReport] - ] = reprec.getreports("pytest_runtest_logreport") + reports: Union[Sequence[TestReport], Sequence[CollectReport]] = ( + reprec.getreports("pytest_runtest_logreport") + ) # we have 3 reports: setup/call/teardown assert len(reports) == 3 # get the call report diff --git a/testing/test_runner_xunit.py b/testing/test_runner_xunit.py index 8076e20bc..587c9eb9f 100644 --- a/testing/test_runner_xunit.py +++ b/testing/test_runner_xunit.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs """Test correct setup/teardowns at module, class, and instance level.""" + from typing import List from _pytest.pytester import Pytester diff --git a/testing/test_terminal.py b/testing/test_terminal.py index b311d6c9b..f49425109 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs """Terminal reporting of the full testing process.""" + from io import StringIO import os from pathlib import Path diff --git a/testing/typing_checks.py b/testing/typing_checks.py index a2ceabcbd..4b146a251 100644 --- a/testing/typing_checks.py +++ b/testing/typing_checks.py @@ -4,6 +4,7 @@ This file is not executed, it is only checked by mypy to ensure that none of the code triggers any mypy errors. """ + import contextlib from typing import Optional From 2e5da5d2fbb7fba2d31c2ca90f2850fedbea7b25 Mon Sep 17 00:00:00 2001 From: Tobias Stoeckmann Date: Thu, 14 Mar 2024 17:36:11 +0100 Subject: [PATCH 47/47] doc: fix typos (#12118) * doc: add missing word * doc: fix typos Typos found with codespell --- doc/en/conf.py | 2 +- doc/en/how-to/writing_hook_functions.rst | 2 +- doc/en/index.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/en/conf.py b/doc/en/conf.py index 8059c359f..32ecaa174 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -395,7 +395,7 @@ epub_copyright = "2013, holger krekel et alii" # The format is a list of tuples containing the path and title. # epub_pre_files = [] -# HTML files shat should be inserted after the pages created by sphinx. +# HTML files that should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. # epub_post_files = [] diff --git a/doc/en/how-to/writing_hook_functions.rst b/doc/en/how-to/writing_hook_functions.rst index 5d0a52f9d..f4c00d04f 100644 --- a/doc/en/how-to/writing_hook_functions.rst +++ b/doc/en/how-to/writing_hook_functions.rst @@ -100,7 +100,7 @@ object, the wrapper may modify that result, but it's probably better to avoid it If the hook implementation failed with an exception, the wrapper can handle that exception using a ``try-catch-finally`` around the ``yield``, by propagating it, -supressing it, or raising a different exception entirely. +suppressing it, or raising a different exception entirely. For more information, consult the :ref:`pluggy documentation about hook wrappers `. diff --git a/doc/en/index.rst b/doc/en/index.rst index 9d97dfaa6..08a146898 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -87,7 +87,7 @@ Features Documentation ------------- -* :ref:`Get started ` - install pytest and grasp its basics just twenty minutes +* :ref:`Get started ` - install pytest and grasp its basics in just twenty minutes * :ref:`How-to guides ` - step-by-step guides, covering a vast range of use-cases and needs * :ref:`Reference guides ` - includes the complete pytest API reference, lists of plugins and more * :ref:`Explanation ` - background, discussion of key topics, answers to higher-level questions