fixtures: say where a fixture is defined, when it is missing and when it is doubled - #15073
Draft
RonnyPfannschmidt wants to merge 2 commits into
Draft
RonnyPfannschmidt wants to merge 2 commits into
RonnyPfannschmidt wants to merge 2 commits into
Conversation
RonnyPfannschmidt
force-pushed
the
feature/fixture-visibility-diagnostics
branch
3 times, most recently
from
September 21, 2026 18:46
f8dfb55 to
065014c
Compare
Moving a fixture into a conftest one directory over leaves the name registered but out of scope, and the error said only that it was "not found" plus a list of the fixtures that *were* available. Neither half mentions the definition sitting in the tree, so the reader goes looking for a name that is already in front of them. List those definitions. The fixturedefs registered under a name are now a dict keyed by node (pytest-dev#14984), so enumerating them regardless of visibility is a lookup rather than a scan -- which is what makes this cheap enough to do on the error path. Past five distinct locations the paths stop being a hint and start being a wall, so collapse the tail into a count. The hint only sees conftests that this run actually loaded. Collecting a single file does not load its siblings, so the case that prompted the issue reports nothing when run that narrowly; a whole-suite run, which is how the confusion arises in the first place, has them all. Fixes pytest-dev#10151. Co-Authored-By: Claude Opus 5 (1M context) via Claude Code <noreply@anthropic.com>
RonnyPfannschmidt
force-pushed
the
feature/fixture-visibility-diagnostics
branch
from
September 21, 2026 18:48
065014c to
4e3c9a7
Compare
Importing a fixture to reuse it binds it in the importing module too. If the module it came from is itself a plugin or a collected module, both are fixture holders, parsefactories runs over both, and the same function is registered twice under two different visibilities. A session-scoped fixture registered twice runs twice, and neither file shows why -- the import looks like any other import. This is pytest-dev#1511, open since 2016. Warn on the second registration, with PytestImportedFixtureWarning. The trigger is the duplicate, not the import. An import whose origin module is never itself a holder registers the fixture exactly once and behaves exactly as if it were defined locally, so it is not warned about. That distinction is what separates the bug from the shapes that merely look like it: - a plugin keeping its fixtures in a sibling module of its own and importing them into the module it registers, which is pytest-django's layout and registers each fixture once; - a class inheriting fixtures from a base in another module, which registers the same function once per subclass, by design -- so only module holders are tracked at all; - a fixture factory defined elsewhere, which returns a fresh function per call, so two calls are two fixtures rather than one registered twice. The warning is anchored at the second module's file, not at a line in it. Which line bound the name is not recorded anywhere -- it is gone once the module is imported -- and recovering it would mean parsing the source at warning time. The message names both modules and the defining one, which is what the reader acts on; the import is at the top of the file it names. Fixes pytest-dev#1511. Co-Authored-By: Claude Opus 5 (1M context) via Claude Code <noreply@anthropic.com>
RonnyPfannschmidt
force-pushed
the
feature/fixture-visibility-diagnostics
branch
from
September 21, 2026 19:20
4e3c9a7 to
26eec22
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Written by Claude Opus 5 (1M context) via Claude Code for the pytest maintainers; I prompted it, it did the work, I read it.
Two long-open fixture-diagnostics issues that turn out to be the same complaint from opposite ends: pytest knows where a fixture name is defined and does not say so.
Draft: both are implemented and green. Draft because #1511 turns on a new warning for a pattern that is widespread in the wild, and that is a call for the team, not for me — see the question at the end.
#10151 — hint at out-of-scope definitions
A fixture moved into a conftest one directory over stays registered — it is just not visible where it is being requested. The error said only that the name was not found, then listed the fixtures that were available. Neither half mentions the definition sitting in the tree, so the reader goes looking for a name that is already in front of them.
One hint line, between the failure and the available list:
Past five distinct locations the paths stop being a hint and start being a wall, so the tail collapses into a count:
Locations are rendered with the existing
_pretty_fixture_path, so pytest's own builtins show as.../_pytest/...rather than a site-packages path.Why now
This has been open since 2022 because answering "where else is this name defined?" used to mean scanning the whole definition list for that name, which is not something to do on an error path in a large suite. After #14984 the fixturedefs for a name are a dict keyed by node, so the enumeration is a lookup. The new
FixtureManager._get_all_fixture_defs_for_name()sits next to the existing_get_all_fixture_defs*helpers and is three lines.Known limitation
The hint only sees conftests that the run actually loaded. Collecting a single file does not load its siblings, so running just
mypkg/utils/tests/test_bar.pyreports nothing. A whole-suite run — which is how the confusion arises in the first place — has them all. This is inherent to lazy conftest loading and is called out in the commit message.#1511 — warn on imported fixtures
Importing a fixture to reuse it binds it in the importing module too. If the module it came from is itself a plugin or a collected module, both are fixture holders,
parsefactoriesruns over both, and the same function is registered twice under two different visibilities. A session-scoped fixture registered twice runs twice, and neither file shows why. Open since 2016.New
PytestImportedFixtureWarning, raised on the second registration and pointed at the line that brought the fixture in:The trigger is the duplicate, not the import
The first version of this warned on the import itself, and the
pluginsCI job rejected it:pytest_django/plugin.pydoesfrom pytest_django.fixtures import _django_db_helper, andpytest_django.fixturesis never itself registered. One holder, one registration, no bug — a false positive, and a whole class of them.So the check is now whether two module holders register the same function object. An import whose origin module is never a holder registers the fixture exactly once and behaves exactly as if it were defined locally, so it is silent. That distinction is what separates the bug from the shapes that merely resemble it:
from helpers import shared,helpersnever collected or a pluginfrom helpers import sharedEach row has a test.
Anchoring
The warning is anchored at the second module's file, not at a line in it. Which line bound the name is not recorded anywhere — it is gone once the module is imported — and recovering it would mean parsing the source at warning time, which is not worth it for a line the reader can find by looking at the top of the file. The message names both registering modules and the defining one, which is what there is to act on.
Verification against real plugins
tox -e pluginsis what caught the false positive, so it is also the evidence: the integration run now passes with all 14 plugins loaded, pytest-django among them. (bdd_wallet.pyandpytest_rerunfailures_integration.pyfail in that directory onmaintoo, for unrelated reasons — the former is the_arg2node2fixturedefsbreakage already commented out intox.ini— and neither is part of the CI command.)Bikeshedding, deliberately not done here
The #10151 sketch also reworded the following line to
available fixtures for 'mypkg/utils/tests/test_bar.py':. It is independent of the hint, it would touch every test matchingavailable fixtures:, and the exact wording is worth an argument on its own. Flagging it as a bikeshed rather than smuggling it in; happy to do it in a follow-up once someone picks a colour.Checks
Full suite
4637 passed, 46 skipped, 17 xfailed, 4 xpassedunder-n auto;tox -e pluginsgreen;pre-commit run -aclean.Fifteen tests added: five in
TestFillFixturesfor the hint, and ten in a newTestImportedFixtureWarning— one per row of the table above, plus a definition registered after the module that imported it, a plugin module built at runtime with no__file__, and what the message looks like once-W errorstrips the location off it.Two commits, each standalone and each green on its own.
testing/test_debugging.py::TestPDB::test_pdb_interaction_exceptionfails intermittently under-n auto, on this branch and on an unmodifiedmainalike — pre-existing, checked in a clean worktree, unrelated to this change.The question for reviewers
PytestImportedFixtureWarningfires by default. It now fires only on genuine duplicate registration, which is narrower than the first cut, but suites that do this will still be noisy on first upgrade — which is the point of the issue, though the noise lands on people who did not ask for it. Options, in the order I would rank them:PytestDeprecationWarningso the existing deprecation machinery and timeline apply.Happy to do any of the three; this is a policy call rather than a technical one, which is why the PR is a draft.