Skip to content

Commit 6ccb5e7

Browse files
committed
Fix collection and simplify tests
We should not collect classes at the module level when the config is set. Doing so is not only the correct thing, as we will not collect the class, but also lets us avoid having the same logic at the Class collector (as it will not be collected at all now).
1 parent 022d316 commit 6ccb5e7

File tree

3 files changed

+108
-333
lines changed

3 files changed

+108
-333
lines changed

src/_pytest/python.py

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,7 @@ def collect(self) -> Iterable[nodes.Item | nodes.Collector]:
405405
# __dict__ is definition ordered.
406406
seen: set[str] = set()
407407
dict_values: list[list[nodes.Item | nodes.Collector]] = []
408+
collect_imported_tests = self.session.config.getini("collect_imported_tests")
408409
ihook = self.ihook
409410
for dic in dicts:
410411
values: list[nodes.Item | nodes.Collector] = []
@@ -417,12 +418,10 @@ def collect(self) -> Iterable[nodes.Item | nodes.Collector]:
417418
continue
418419
seen.add(name)
419420

420-
if not self.session.config.getini("collect_imported_tests"):
421-
# Do not collect imported functions
422-
if inspect.isfunction(obj) and isinstance(self, Module):
423-
fn_defined_at = obj.__module__
424-
in_module = self._getobj().__name__
425-
if fn_defined_at != in_module:
421+
if not collect_imported_tests and isinstance(self, Module):
422+
# Do not collect functions and classes from other modules.
423+
if inspect.isfunction(obj) or inspect.isclass(obj):
424+
if obj.__module__ != self._getobj().__name__:
426425
continue
427426

428427
res = ihook.pytest_pycollect_makeitem(
@@ -750,16 +749,6 @@ def newinstance(self):
750749
return self.obj()
751750

752751
def collect(self) -> Iterable[nodes.Item | nodes.Collector]:
753-
if not self.config.getini("collect_imported_tests"):
754-
# This entire branch will discard (not collect) a class
755-
# if it is imported (defined in a different module)
756-
if isinstance(self, Class) and isinstance(self.parent, Module):
757-
if inspect.isclass(self._getobj()):
758-
class_defined_at = self._getobj().__module__
759-
in_module = self.parent._getobj().__name__
760-
if class_defined_at != in_module:
761-
return []
762-
763752
if not safe_getattr(self.obj, "__test__", True):
764753
return []
765754
if hasinit(self.obj):
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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_import_class_test(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+
testpaths = "tests"
53+
collect_imported_tests = false
54+
"""
55+
)
56+
57+
setup_import_class_test(pytester)
58+
result = pytester.runpytest("-v")
59+
result.stdout.fnmatch_lines(
60+
[
61+
"tests/foo_test.py::TestDomain::test PASSED*",
62+
]
63+
)
64+
65+
# Ensure that the hooks were only called for the collected item.
66+
reprec = result.reprec # type:ignore[attr-defined]
67+
reports = reprec.getreports("pytest_collectreport")
68+
[modified] = reprec.getcalls("pytest_collection_modifyitems")
69+
[item_collected] = reprec.getcalls("pytest_itemcollected")
70+
71+
assert [x.nodeid for x in reports] == [
72+
"",
73+
"tests/foo_test.py::TestDomain",
74+
"tests/foo_test.py",
75+
"tests",
76+
]
77+
assert [x.nodeid for x in modified.items] == ["tests/foo_test.py::TestDomain::test"]
78+
assert item_collected.item.nodeid == "tests/foo_test.py::TestDomain::test"
79+
80+
81+
@pytest.mark.parametrize("configure_ini", [False, True])
82+
def test_collect_imports_enabled(pytester: Pytester, configure_ini: bool) -> None:
83+
"""
84+
When collect_imported_tests is enabled (the default), all names in the
85+
test modules are collected as tests.
86+
"""
87+
if configure_ini:
88+
pytester.makeini(
89+
"""
90+
[pytest]
91+
collect_imported_tests = true
92+
"""
93+
)
94+
95+
setup_import_class_test(pytester)
96+
result = pytester.runpytest("-v")
97+
result.stdout.fnmatch_lines(
98+
[
99+
"tests/foo_test.py::Testament::test_collections PASSED*",
100+
"tests/foo_test.py::test_testament PASSED*",
101+
"tests/foo_test.py::TestDomain::test PASSED*",
102+
]
103+
)

0 commit comments

Comments
 (0)