Skip to content

Warn when a shared page reads an object its fixture already tore down #90

Description

@tony

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions