Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ Mike Fiedler (miketheman)
Mike Hoyle (hoylemd)
Mike Lundy
Milan Lesnek
minbang930
Miro Hrončok
Mulat Mekonen
mrbean-bremen
Expand Down
1 change: 1 addition & 0 deletions changelog/14533.bugfix.rst
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 0 additions & 4 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]]] = {
Expand Down Expand Up @@ -2031,16 +2030,13 @@ 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):
holderobj_tp: object = type(holderobj)
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.
Expand Down
64 changes: 64 additions & 0 deletions testing/test_doctest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add a mark that links the issue and add a note on the regression on the docstring

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(
"""
Expand Down Expand Up @@ -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):
Expand Down
Loading