From 6e5008f19f3c8fffa1ea2b980d4214d9c338b481 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 6 Feb 2024 23:40:13 +0200 Subject: [PATCH 1/2] doctest: don't open code the module import Currently, `DoctestModule` does `import_path` on its own. This changes it to use `importtestmodule` as `Module` does. The behavioral changes are: - Much better error messages on import errors. - Handles a few more error cases (see `importtestmodule`). This technically expands the cover of `--doctest-ignore-import-errors` but I think it makes sense. - Considers `pytest_plugins` in the module. - Populates `self.obj` as properly (without double-imports) as is expected from a `PyCollector`. This is also needed for the next commit. --- src/_pytest/doctest.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index fdf84d300..e0fb7c161 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -40,7 +40,6 @@ from _pytest.nodes import Item from _pytest.outcomes import OutcomeException from _pytest.outcomes import skip from _pytest.pathlib import fnmatch_ex -from _pytest.pathlib import import_path from _pytest.python import Module from _pytest.python_api import approx from _pytest.warning_types import PytestWarning @@ -107,7 +106,7 @@ def pytest_addoption(parser: Parser) -> None: "--doctest-ignore-import-errors", action="store_true", default=False, - help="Ignore doctest ImportErrors", + help="Ignore doctest collection errors", dest="doctest_ignore_import_errors", ) group.addoption( @@ -561,12 +560,8 @@ class DoctestModule(Module): pass try: - module = import_path( - self.path, - root=self.config.rootpath, - mode=self.config.getoption("importmode"), - ) - except ImportError: + module = self.obj + except Collector.CollectError: if self.config.getvalue("doctest_ignore_import_errors"): skip("unable to import module %r" % self.path) else: From 9cd14b4ffb1d96342a2e9d1f5cb92eab53bb5079 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 6 Feb 2024 23:09:00 +0200 Subject: [PATCH 2/2] doctest: fix autouse fixtures possibly not getting picked up Fix #11929. Figured out what's going on. We have the following collection tree: ``` ``` And the `test_main.py` contains an autouse fixture (`fake_game_ui`) that `doctest_main` needs in order to run properly. The fixture doesn't run! It doesn't run because nothing collects the fixtures from (calls `parsefactories()` on) the `test_main.py` `DoctestModule`. How come it only started happening with commit ab63ebb3dc07b89670b96ae97044f48406c44fa0? Turns out it mostly only worked accidentally. Each `DoctestModule` is also collected as a normal `Module`, with the `Module` collected after the `DoctestModule`. For example, if we add a non-doctest test to `test_main.py`, the collection tree looks like this: ``` ``` Now, `Module` *does* collect fixtures. When autouse fixtures are collected, they are added to the `_nodeid_autousenames` dict. Before ab63ebb3dc07b89670b96ae97044f48406c44fa0, `DoctestItem` consults `_nodeid_autousenames` at *setup* time. At this point, the `Module` has collected and so it ended up picking the autouse fixture (this relies on another "accident", that the `DoctestModule` and `Module` have the same node ID). After ab63ebb3dc07b89670b96ae97044f48406c44fa0, `DoctestItem` consults `_nodeid_autousenames` at *collection* time (= when it's created). At this point, the `Module` hasn't collected yet, so the autouse fixture is not picked out. The fix is simple -- have `DoctestModule.collect()` call `parsefactories`. From some testing I've done it shouldn't have negative consequences (I hope). --- changelog/11929.bugfix.rst | 1 + src/_pytest/doctest.py | 4 ++++ testing/test_doctest.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 changelog/11929.bugfix.rst diff --git a/changelog/11929.bugfix.rst b/changelog/11929.bugfix.rst new file mode 100644 index 000000000..8ab50e6f4 --- /dev/null +++ b/changelog/11929.bugfix.rst @@ -0,0 +1 @@ +Fix a regression in pytest 8.0.0 whereby autouse fixtures defined in a module get ignored by the doctests in the module. diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index e0fb7c161..8fe992b6a 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -567,6 +567,10 @@ class DoctestModule(Module): else: raise + # While doctests currently don't support fixtures directly, we still + # need to pick up autouse fixtures. + self.session._fixturemanager.parsefactories(self) + # Uses internal doctest module parsing mechanism. finder = MockAwareDocTestFinder() optionflags = get_optionflags(self.config) diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 372528490..c91ec31cd 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -1376,6 +1376,38 @@ class TestDoctestAutoUseFixtures: str(result.stdout.no_fnmatch_line("*FAILURES*")) result.stdout.fnmatch_lines(["*=== 1 passed in *"]) + @pytest.mark.parametrize("scope", [*SCOPES, "package"]) + def test_auto_use_defined_in_same_module( + self, pytester: Pytester, scope: str + ) -> None: + """Autouse fixtures defined in the same module as the doctest get picked + up properly. + + Regression test for #11929. + """ + pytester.makepyfile( + f""" + import pytest + + AUTO = "the fixture did not run" + + @pytest.fixture(autouse=True, scope="{scope}") + def auto(request): + global AUTO + AUTO = "the fixture ran" + + def my_doctest(): + '''My doctest. + + >>> my_doctest() + 'the fixture ran' + ''' + return AUTO + """ + ) + result = pytester.runpytest("--doctest-modules") + result.assert_outcomes(passed=1) + class TestDoctestNamespaceFixture: SCOPES = ["module", "session", "class", "function"]