diff --git a/changelog/14533.bugfix.rst b/changelog/14533.bugfix.rst new file mode 100644 index 00000000000..9640642faba --- /dev/null +++ b/changelog/14533.bugfix.rst @@ -0,0 +1 @@ +Fixed a regression where enabling :option:`--doctest-modules` could make fixtures defined in a Python module unavailable to normal tests collected from the same module. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 9e123be230a..ffd53188885 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1676,7 +1676,7 @@ def __init__(self, session: Session) -> None: # TODO: The order of the FixtureDefs list of each arg is significant, # explain. self._arg2fixturedefs: Final[dict[str, list[FixtureDef[Any]]]] = {} - self._holderobjseen: Final[set[object]] = set() + self._holderobjseen: Final[set[tuple[object, nodes.Node | str | None]]] = set() # A mapping from a node to a list of autouse fixture names it defines. # The Session entry holds global usefixtures from config. self._node_autousenames: Final[dict[nodes.Node, list[str]]] = { @@ -2031,7 +2031,10 @@ def parsefactories( assert isinstance(node_or_obj, nodes.Node) holderobj = cast(object, node_or_obj.obj) # type: ignore[attr-defined] effective_node = node_or_obj - if holderobj in self._holderobjseen: + # The same holder may be parsed with different visibility scopes, e.g. + # a module collected as both DoctestModule and Module. + holderobj_key = (holderobj, effective_node or effective_nodeid) + if holderobj_key in self._holderobjseen: return # Avoid accessing `@property` (and other descriptors) when iterating fixtures. @@ -2040,7 +2043,7 @@ def parsefactories( else: holderobj_tp = holderobj - self._holderobjseen.add(holderobj) + self._holderobjseen.add(holderobj_key) for name in dir(holderobj): # The attribute can be an arbitrary descriptor, so the attribute # access below can raise. safe_getattr() ignores such exceptions. diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 8b71dabbc77..fb69e4c9860 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -600,6 +600,24 @@ def test_doctestmodule_with_fixtures(self, pytester: Pytester): reprec = pytester.inline_run(p, "--doctest-modules") reprec.assertoutcome(passed=1) + def test_doctestmodule_does_not_hide_module_fixtures( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( + """ + import pytest + + @pytest.fixture + def foo_fixture(): + return 1 + + def test_foo(foo_fixture): + assert foo_fixture + """ + ) + result = pytester.runpytest("--doctest-modules") + result.assert_outcomes(passed=1) + def test_doctestmodule_three_tests(self, pytester: Pytester): p = pytester.makepyfile( """