Skip to content

Commit 9d4f36d

Browse files
authored
Merge pull request #12810 from FreerGit/dont-auto-discover-feat
Add `discover_imports` in conf, don't collect imported classes named Test* closes #12749`
2 parents e135d76 + d2327d9 commit 9d4f36d

File tree

6 files changed

+172
-3
lines changed

6 files changed

+172
-3
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@ Stefanie Molin
407407
Stefano Taschini
408408
Steffen Allner
409409
Stephan Obermann
410+
Sven
410411
Sven-Hendrik Haase
411412
Sviatoslav Sydorenko
412413
Sylvain Marié

changelog/12749.feature.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
pytest traditionally collects classes/functions in the test module namespace even if they are imported from another file.
2+
3+
For example:
4+
5+
.. code-block:: python
6+
7+
# contents of src/domain.py
8+
class Testament: ...
9+
10+
11+
# contents of tests/test_testament.py
12+
from domain import Testament
13+
14+
15+
def test_testament(): ...
16+
17+
In this scenario with the default options, pytest will collect the class `Testament` from `tests/test_testament.py` because it starts with `Test`, even though in this case it is a production class being imported in the test module namespace.
18+
19+
This behavior can now be prevented by setting the new :confval:`collect_imported_tests` configuration option to ``false``, which will make pytest collect classes/functions from test files **only** if they are defined in that file.
20+
21+
-- by :user:`FreerGit`

doc/en/reference/reference.rst

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1301,6 +1301,40 @@ passed multiple times. The expected format is ``name=value``. For example::
13011301
variables, that will be expanded. For more information about cache plugin
13021302
please refer to :ref:`cache_provider`.
13031303

1304+
.. confval:: collect_imported_tests
1305+
1306+
.. versionadded:: 8.4
1307+
1308+
Setting this to ``false`` will make pytest collect classes/functions from test
1309+
files **only** if they are defined in that file (as opposed to imported there).
1310+
1311+
.. code-block:: ini
1312+
1313+
[pytest]
1314+
collect_imported_tests = false
1315+
1316+
Default: ``true``
1317+
1318+
pytest traditionally collects classes/functions in the test module namespace even if they are imported from another file.
1319+
1320+
For example:
1321+
1322+
.. code-block:: python
1323+
1324+
# contents of src/domain.py
1325+
class Testament: ...
1326+
1327+
1328+
# contents of tests/test_testament.py
1329+
from domain import Testament
1330+
1331+
1332+
def test_testament(): ...
1333+
1334+
In this scenario, with the default options, pytest will collect the class `Testament` from `tests/test_testament.py` because it starts with `Test`, even though in this case it is a production class being imported in the test module namespace.
1335+
1336+
Set ``collected_imported_tests`` to ``false`` in the configuration file prevents that.
1337+
13041338
.. confval:: consider_namespace_packages
13051339

13061340
Controls if pytest should attempt to identify `namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages>`__
@@ -1861,11 +1895,8 @@ passed multiple times. The expected format is ``name=value``. For example::
18611895
18621896
pytest testing doc
18631897
1864-
18651898
.. confval:: tmp_path_retention_count
18661899

1867-
1868-
18691900
How many sessions should we keep the `tmp_path` directories,
18701901
according to `tmp_path_retention_policy`.
18711902

src/_pytest/main.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ def pytest_addoption(parser: Parser) -> None:
7777
type="args",
7878
default=[],
7979
)
80+
parser.addini(
81+
"collect_imported_tests",
82+
"Whether to collect tests in imported modules outside `testpaths`",
83+
type="bool",
84+
default=True,
85+
)
8086
group = parser.getgroup("general", "Running and selection options")
8187
group._addoption(
8288
"-x",

src/_pytest/python.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,7 @@ def collect(self) -> Iterable[nodes.Item | nodes.Collector]:
397397
# __dict__ is definition ordered.
398398
seen: set[str] = set()
399399
dict_values: list[list[nodes.Item | nodes.Collector]] = []
400+
collect_imported_tests = self.session.config.getini("collect_imported_tests")
400401
ihook = self.ihook
401402
for dic in dicts:
402403
values: list[nodes.Item | nodes.Collector] = []
@@ -408,6 +409,13 @@ def collect(self) -> Iterable[nodes.Item | nodes.Collector]:
408409
if name in seen:
409410
continue
410411
seen.add(name)
412+
413+
if not collect_imported_tests and isinstance(self, Module):
414+
# Do not collect functions and classes from other modules.
415+
if inspect.isfunction(obj) or inspect.isclass(obj):
416+
if obj.__module__ != self._getobj().__name__:
417+
continue
418+
411419
res = ihook.pytest_pycollect_makeitem(
412420
collector=self, name=name, obj=obj
413421
)
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""Tests for the `collect_imported_tests` configuration value."""
2+
3+
from __future__ import annotations
4+
5+
import textwrap
6+
7+
from _pytest.pytester import Pytester
8+
import pytest
9+
10+
11+
def setup_files(pytester: Pytester) -> None:
12+
src_dir = pytester.mkdir("src")
13+
tests_dir = pytester.mkdir("tests")
14+
src_file = src_dir / "foo.py"
15+
16+
src_file.write_text(
17+
textwrap.dedent("""\
18+
class Testament:
19+
def test_collections(self):
20+
pass
21+
22+
def test_testament(): pass
23+
"""),
24+
encoding="utf-8",
25+
)
26+
27+
test_file = tests_dir / "foo_test.py"
28+
test_file.write_text(
29+
textwrap.dedent("""\
30+
from foo import Testament, test_testament
31+
32+
class TestDomain:
33+
def test(self):
34+
testament = Testament()
35+
assert testament
36+
"""),
37+
encoding="utf-8",
38+
)
39+
40+
pytester.syspathinsert(src_dir)
41+
42+
43+
def test_collect_imports_disabled(pytester: Pytester) -> None:
44+
"""
45+
When collect_imported_tests is disabled, only objects in the
46+
test modules are collected as tests, so the imported names (`Testament` and `test_testament`)
47+
are not collected.
48+
"""
49+
pytester.makeini(
50+
"""
51+
[pytest]
52+
collect_imported_tests = false
53+
"""
54+
)
55+
56+
setup_files(pytester)
57+
result = pytester.runpytest("-v", "tests")
58+
result.stdout.fnmatch_lines(
59+
[
60+
"tests/foo_test.py::TestDomain::test PASSED*",
61+
]
62+
)
63+
64+
# Ensure that the hooks were only called for the collected item.
65+
reprec = result.reprec # type:ignore[attr-defined]
66+
reports = reprec.getreports("pytest_collectreport")
67+
[modified] = reprec.getcalls("pytest_collection_modifyitems")
68+
[item_collected] = reprec.getcalls("pytest_itemcollected")
69+
70+
assert [x.nodeid for x in reports] == [
71+
"",
72+
"tests/foo_test.py::TestDomain",
73+
"tests/foo_test.py",
74+
"tests",
75+
]
76+
assert [x.nodeid for x in modified.items] == ["tests/foo_test.py::TestDomain::test"]
77+
assert item_collected.item.nodeid == "tests/foo_test.py::TestDomain::test"
78+
79+
80+
@pytest.mark.parametrize("configure_ini", [False, True])
81+
def test_collect_imports_enabled(pytester: Pytester, configure_ini: bool) -> None:
82+
"""
83+
When collect_imported_tests is enabled (the default), all names in the
84+
test modules are collected as tests.
85+
"""
86+
if configure_ini:
87+
pytester.makeini(
88+
"""
89+
[pytest]
90+
collect_imported_tests = true
91+
"""
92+
)
93+
94+
setup_files(pytester)
95+
result = pytester.runpytest("-v", "tests")
96+
result.stdout.fnmatch_lines(
97+
[
98+
"tests/foo_test.py::Testament::test_collections PASSED*",
99+
"tests/foo_test.py::test_testament PASSED*",
100+
"tests/foo_test.py::TestDomain::test PASSED*",
101+
]
102+
)

0 commit comments

Comments
 (0)