Under doctest_docutils_namespace_items = per-block, each block is its own item, so a function-scoped fixture is torn down between blocks. The name the fixture fills is rebound fresh for the next block, but a name a block derived from it is not — it holds the finalized object, and a finalized object usually answers rather than raising. The page goes green on a wrong value.
Self-contained repro, no downstream project needed.
conftest.py:
import pytest
class Resource:
def __init__(self, n):
self.n = n
self.alive = True
COUNT = [0]
@pytest.fixture
def resource():
COUNT[0] += 1
r = Resource(COUNT[0])
yield r
r.alive = False
@pytest.fixture(autouse=True)
def _seed(doctest_namespace, resource):
doctest_namespace["resource"] = resource
page.md:
```
>>> saved = resource
>>> saved.n, saved.alive
(1, True)
```
```
>>> saved.n, saved.alive
(1, False)
>>> resource.n, resource.alive
(2, True)
>>> saved is resource
False
```
$ pytest page.md --doctest-docutils-namespace-scope=document --doctest-docutils-namespace-items=per-block
2 passed
The page passes while asserting that saved is a corpse.
What already covers this
scope="module" fixes it, and that is now documented. A page collects as a pytest.Module — the same shape pytest core uses for its own text-doctest collector — so module scope resolves to the page and the fixture spans every block of it. tests/regressions/test_autouse_fixtures.py::test_a_module_scoped_fixture_spans_a_shared_page pins that. So this issue is about the residual case: a project that has not widened the scope gets no signal.
Why this is not shipped yet
A detector keyed on the identity of the fixture's own returned value catches saved = resource and misses session = server.new_session(). That second shape is the one real documentation is written in — libtmux's docs/topics/traversal.md opens with literally >>> session = server.new_session(). A detector that stays quiet there teaches a reader the page is clean when it is not, which is worse than no detector.
Measured against both shapes: the first warns, the second is 2 passed with no warning on a page asserting torn-down state.
So this wants a design that reasons about the fixture's teardown having run rather than about value identity, or a weakref-tracked ownership graph that follows derived objects. Neither is a small change.
Implementation notes worth keeping
These were measured and are easy to re-derive wrongly.
Register the detector as a private plugin object from pytest_configure, and only when the resolved layout is per-block. That keeps the default path free of hook dispatch entirely — a pytest11 entry-point plugin that registers pytest_fixture_setup at module level makes every downstream project pay per fixture setup and teardown forever, default layout or not.
Report from pytest_runtest_call, not from a pytest_runtest_setup wrapper. Under -W error the setup-phase placement yields ERROR plus a PluggyTeardownRaisedWarning about raising in an old-style wrapper; the call-phase placement yields a clean FAILED.
pytest_fixture_post_finalizer is the right hook to record a finalized value: the hookspec guarantees cached_result is still populated there.
Scan item.dtest.globs for values of already-finalized fixtures, not the doctest_namespace dict. Item setup rebinds every seeded name to the freshly built value on each item, so the namespace never holds a dead object — only aliases the page itself created do.
Count namespace span inside DocTestDocutilsFile.collect, where the namespace is already in hand, and stash the count on the item. Computing it from item.session.items reads low on an xdist worker, which holds only its own slice.
Use filterwarnings as the escape hatch rather than a bespoke ini option. That forces the warning category to be module-level importable so getattr(module, klass) resolves it, and it avoids adding an ini option whose only meaningful value is the off switch.
Related
The nastier silence none of this reaches: doctest_namespace is session-scoped, so an object seeded from one directory's conftest.py stays bound in unrelated pages elsewhere in the run. Sybil's namespace is per-document. If a detector is ever built, that is the case to scope it against.
Refs #83, #89. Not a blocker for #87.
Under
doctest_docutils_namespace_items = per-block, each block is its own item, so a function-scoped fixture is torn down between blocks. The name the fixture fills is rebound fresh for the next block, but a name a block derived from it is not — it holds the finalized object, and a finalized object usually answers rather than raising. The page goes green on a wrong value.Self-contained repro, no downstream project needed.
conftest.py:page.md:The page passes while asserting that
savedis a corpse.What already covers this
scope="module"fixes it, and that is now documented. A page collects as apytest.Module— the same shape pytest core uses for its own text-doctest collector — so module scope resolves to the page and the fixture spans every block of it.tests/regressions/test_autouse_fixtures.py::test_a_module_scoped_fixture_spans_a_shared_pagepins that. So this issue is about the residual case: a project that has not widened the scope gets no signal.Why this is not shipped yet
A detector keyed on the identity of the fixture's own returned value catches
saved = resourceand missessession = server.new_session(). That second shape is the one real documentation is written in — libtmux'sdocs/topics/traversal.mdopens with literally>>> session = server.new_session(). A detector that stays quiet there teaches a reader the page is clean when it is not, which is worse than no detector.Measured against both shapes: the first warns, the second is
2 passedwith no warning on a page asserting torn-down state.So this wants a design that reasons about the fixture's teardown having run rather than about value identity, or a weakref-tracked ownership graph that follows derived objects. Neither is a small change.
Implementation notes worth keeping
These were measured and are easy to re-derive wrongly.
Register the detector as a private plugin object from
pytest_configure, and only when the resolved layout isper-block. That keeps the default path free of hook dispatch entirely — apytest11entry-point plugin that registerspytest_fixture_setupat module level makes every downstream project pay per fixture setup and teardown forever, default layout or not.Report from
pytest_runtest_call, not from apytest_runtest_setupwrapper. Under-W errorthe setup-phase placement yields ERROR plus aPluggyTeardownRaisedWarningabout raising in an old-style wrapper; the call-phase placement yields a clean FAILED.pytest_fixture_post_finalizeris the right hook to record a finalized value: the hookspec guaranteescached_resultis still populated there.Scan
item.dtest.globsfor values of already-finalized fixtures, not thedoctest_namespacedict. Item setup rebinds every seeded name to the freshly built value on each item, so the namespace never holds a dead object — only aliases the page itself created do.Count namespace span inside
DocTestDocutilsFile.collect, where the namespace is already in hand, and stash the count on the item. Computing it fromitem.session.itemsreads low on an xdist worker, which holds only its own slice.Use
filterwarningsas the escape hatch rather than a bespoke ini option. That forces the warning category to be module-level importable sogetattr(module, klass)resolves it, and it avoids adding an ini option whose only meaningful value is the off switch.Related
The nastier silence none of this reaches:
doctest_namespaceis session-scoped, so an object seeded from one directory'sconftest.pystays bound in unrelated pages elsewhere in the run. Sybil's namespace is per-document. If a detector is ever built, that is the case to scope it against.Refs #83, #89. Not a blocker for #87.