diff --git a/AUTHORS b/AUTHORS index 27c0b3ac408..972f39aa45e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -334,6 +334,7 @@ Mike Fiedler (miketheman) Mike Hoyle (hoylemd) Mike Lundy Milan Lesnek +minbang930 Miro HronĨok Mulat Mekonen mrbean-bremen diff --git a/changelog/14533.bugfix.rst b/changelog/14533.bugfix.rst new file mode 100644 index 00000000000..74a44152f4a --- /dev/null +++ b/changelog/14533.bugfix.rst @@ -0,0 +1 @@ +Fixed module-level fixtures becoming unavailable to normal tests when ``--doctest-modules`` also collected the same Python module. Higher-scoped autouse fixtures defined in such modules may execute twice because ``--doctest-modules`` processes the same Python file twice; moving those fixtures to ``conftest.py`` avoids the duplicate execution. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 9e123be230a..2e971ddcea9 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1676,7 +1676,6 @@ 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() # 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,8 +2030,6 @@ 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: - return # Avoid accessing `@property` (and other descriptors) when iterating fixtures. if not safe_isclass(holderobj) and not isinstance(holderobj, types.ModuleType): @@ -2040,7 +2037,6 @@ def parsefactories( else: holderobj_tp = holderobj - self._holderobjseen.add(holderobj) 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..aa47248d303 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -600,6 +600,31 @@ def test_doctestmodule_with_fixtures(self, pytester: Pytester): reprec = pytester.inline_run(p, "--doctest-modules") reprec.assertoutcome(passed=1) + # @pytest.mark.issue("https://github.com/pytest-dev/pytest/issues/14533") + def test_module_fixture_available_to_normal_test_with_doctestmodules( + self, pytester: Pytester + ) -> None: + """Regression test for #14533. + + Module-level fixtures collected with ``--doctest-modules`` must remain + available to normal tests in the same file. + """ + pytester.makepyfile( + """ + import pytest + + @pytest.fixture + def foo_fixture(): + return "foo" + + def test_foo(foo_fixture): + assert foo_fixture == "foo" + """ + ) + + result = pytester.runpytest("--doctest-modules") + result.assert_outcomes(passed=1) + def test_doctestmodule_three_tests(self, pytester: Pytester): p = pytester.makepyfile( """ @@ -1302,6 +1327,45 @@ def bar(): result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines(["*2 passed*"]) + def test_same_module_session_autouse_fixture_runs_for_module_and_doctest( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( + """ + from pathlib import Path + + import pytest + + RUNS = Path("fixture-runs.txt") + + @pytest.fixture(autouse=True, scope="session") + def auto(): + RUNS.write_text( + RUNS.read_text(encoding="utf-8") + "run\\n" + if RUNS.exists() + else "run\\n", + encoding="utf-8", + ) + + def test_normal(): + assert True + + def doctest_target(): + ''' + >>> 1 + 1 + 2 + ''' + """ + ) + result = pytester.runpytest("--doctest-modules") + result.assert_outcomes(passed=2) + assert (pytester.path / "fixture-runs.txt").read_text( + encoding="utf-8" + ).splitlines() == [ + "run", + "run", + ] + @pytest.mark.parametrize("scope", SCOPES) @pytest.mark.parametrize("enable_doctest", [True, False]) def test_fixture_scopes(self, pytester, scope, enable_doctest):