python: change `pytest pkg/__init__.py` to only collect the `__init__.py` Module

Previously it would collect the entire package, but this is not what
users expect.

Refs #3749
Fixes #8976
Fixes #9263
Fixes #9313
This commit is contained in:
Ran Benita 2023-05-27 14:42:19 +03:00
parent 2870157234
commit c8b1790ee7
9 changed files with 56 additions and 17 deletions

View File

@ -0,0 +1,5 @@
Running `pytest pkg/__init__.py` now collects the `pkg/__init__.py` file (module) only.
Previously, it collected the entire `pkg` package, including other test files in the directory, but excluding tests in the `__init__.py` file itself
(unless :confval:`python_files` was changed to allow `__init__.py` file).
To collect the entire package, specify just the directory: `pytest pkg`.

View File

@ -467,12 +467,26 @@ The ``yield_fixture`` function/decorator
It has been so for a very long time, so can be search/replaced safely. It has been so for a very long time, so can be search/replaced safely.
Removed Features Removed Features and Breaking Changes
---------------- -------------------------------------
As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after
an appropriate period of deprecation has passed. an appropriate period of deprecation has passed.
Some breaking changes which could not be deprecated are also listed.
Collecting ``__init__.py`` files no longer collects package
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. versionremoved:: 8.0
Running `pytest pkg/__init__.py` now collects the `pkg/__init__.py` file (module) only.
Previously, it collected the entire `pkg` package, including other test files in the directory, but excluding tests in the `__init__.py` file itself
(unless :confval:`python_files` was changed to allow `__init__.py` file).
To collect the entire package, specify just the directory: `pytest pkg`.
The ``pytest.collect`` module The ``pytest.collect`` module
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -736,7 +736,9 @@ class Package(Module):
this_path = self.path.parent this_path = self.path.parent
# Always collect the __init__ first. # Always collect the __init__ first.
if path_matches_patterns(self.path, self.config.getini("python_files")): if self.session.isinitpath(self.path) or path_matches_patterns(
self.path, self.config.getini("python_files")
):
yield Module.from_parent(self, path=self.path) yield Module.from_parent(self, path=self.path)
pkg_prefixes: Set[Path] = set() pkg_prefixes: Set[Path] = set()

View File

@ -0,0 +1,2 @@
def test_init():
pass

View File

@ -1,2 +1,2 @@
def test(): def test_foo():
pass pass

View File

@ -1420,10 +1420,15 @@ def test_package_collection_infinite_recursion(pytester: Pytester) -> None:
def test_package_collection_init_given_as_argument(pytester: Pytester) -> None: def test_package_collection_init_given_as_argument(pytester: Pytester) -> None:
"""Regression test for #3749""" """Regression test for #3749, #8976, #9263, #9313.
Specifying an __init__.py file directly should collect only the __init__.py
Module, not the entire package.
"""
p = pytester.copy_example("collect/package_init_given_as_arg") p = pytester.copy_example("collect/package_init_given_as_arg")
result = pytester.runpytest(p / "pkg" / "__init__.py") items, hookrecorder = pytester.inline_genitems(p / "pkg" / "__init__.py")
result.stdout.fnmatch_lines(["*1 passed*"]) assert len(items) == 1
assert items[0].name == "test_init"
def test_package_with_modules(pytester: Pytester) -> None: def test_package_with_modules(pytester: Pytester) -> None:

View File

@ -1392,19 +1392,27 @@ def test_collect_pkg_init_and_file_in_args(pytester: Pytester) -> None:
p = subdir.joinpath("test_file.py") p = subdir.joinpath("test_file.py")
p.write_text("def test_file(): pass", encoding="utf-8") p.write_text("def test_file(): pass", encoding="utf-8")
# NOTE: without "-o python_files=*.py" this collects test_file.py twice. # Just the package directory, the __init__.py module is filtered out.
# This changed/broke with "Add package scoped fixtures #2283" (2b1410895) result = pytester.runpytest("-v", subdir)
# initially (causing a RecursionError).
result = pytester.runpytest("-v", str(init), str(p))
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
"sub/test_file.py::test_file PASSED*", "sub/test_file.py::test_file PASSED*",
"*1 passed in*",
]
)
# But it's included if specified directly.
result = pytester.runpytest("-v", init, p)
result.stdout.fnmatch_lines(
[
"sub/__init__.py::test_init PASSED*",
"sub/test_file.py::test_file PASSED*", "sub/test_file.py::test_file PASSED*",
"*2 passed in*", "*2 passed in*",
] ]
) )
result = pytester.runpytest("-v", "-o", "python_files=*.py", str(init), str(p)) # Or if the pattern allows it.
result = pytester.runpytest("-v", "-o", "python_files=*.py", subdir)
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
"sub/__init__.py::test_init PASSED*", "sub/__init__.py::test_init PASSED*",
@ -1419,10 +1427,13 @@ def test_collect_pkg_init_only(pytester: Pytester) -> None:
init = subdir.joinpath("__init__.py") init = subdir.joinpath("__init__.py")
init.write_text("def test_init(): pass", encoding="utf-8") init.write_text("def test_init(): pass", encoding="utf-8")
result = pytester.runpytest(str(init)) result = pytester.runpytest(subdir)
result.stdout.fnmatch_lines(["*no tests ran in*"]) result.stdout.fnmatch_lines(["*no tests ran in*"])
result = pytester.runpytest("-v", "-o", "python_files=*.py", str(init)) result = pytester.runpytest("-v", init)
result.stdout.fnmatch_lines(["sub/__init__.py::test_init PASSED*", "*1 passed in*"])
result = pytester.runpytest("-v", "-o", "python_files=*.py", subdir)
result.stdout.fnmatch_lines(["sub/__init__.py::test_init PASSED*", "*1 passed in*"]) result.stdout.fnmatch_lines(["sub/__init__.py::test_init PASSED*", "*1 passed in*"])

View File

@ -114,7 +114,7 @@ class TestDoctests:
reprec.assertoutcome(failed=1) reprec.assertoutcome(failed=1)
def test_importmode(self, pytester: Pytester): def test_importmode(self, pytester: Pytester):
p = pytester.makepyfile( pytester.makepyfile(
**{ **{
"namespacepkg/innerpkg/__init__.py": "", "namespacepkg/innerpkg/__init__.py": "",
"namespacepkg/innerpkg/a.py": """ "namespacepkg/innerpkg/a.py": """
@ -132,7 +132,7 @@ class TestDoctests:
""", """,
} }
) )
reprec = pytester.inline_run(p, "--doctest-modules", "--import-mode=importlib") reprec = pytester.inline_run("--doctest-modules", "--import-mode=importlib")
reprec.assertoutcome(passed=1) reprec.assertoutcome(passed=1)
def test_new_pattern(self, pytester: Pytester): def test_new_pattern(self, pytester: Pytester):

View File

@ -504,7 +504,7 @@ def test_nose_setup_skipped_if_non_callable(pytester: Pytester) -> None:
pass pass
""", """,
) )
result = pytester.runpytest(p, "-p", "nose") result = pytester.runpytest(p.parent, "-p", "nose")
assert result.ret == 0 assert result.ret == 0