Repository navigation
API to get a test's fixture definitions during parametrization #14991
Description
What's the problem this feature will solve?
During pytest_generate_tests, I want to be able to get the functions that define a test's fixture dependencies to determine whether and how to parametrize the test (including fixtures requested via other fixtures).
Specifically, pytest-asyncio can run tests with different event loop configurations. It needs to apply this parametrization to all tests that use its async fixtures, including sync tests.
Describe the solution you'd like
A public API on Metafunc to get the fixture definitions upon which a test depends, without running the fixtures.
pytest.FixtureDef is already public. What I am looking for is a way to get the definitions used by a particular test during pytest_generate_tests.
Something like this could work:
def pytest_generate_tests(metafunc):
funcs = [fd.func for fd in metafunc.fixturedefs] # proposed new metafunc.fixturedefs API
# use the above to decide whether to call metafunc.parametrize(...), etc.Metafunc.fixturedefs could be a read-only property that returns a tuple of pytest.FixtureDef objects. My specific use case needs supported access to each definition's .func, though. The returned fixture definitions for a test should follow fixture resolution rules (i.e., handle the different ways to request a fixture, overrides, and fixture configuration via parametrization).
I don't need a fixed iteration order or for the API to pre-empt future changes to the dependencies (I'm not sure it'd even be possible to deal with future parametrization). It would be fine (and, IMO, intuitive) for each access to reflect the fixture definitions so far. I'd like the plugin to be able to read the dependencies after other hooks apply parametrization, and then apply further parametrization itself.
Alternative Solutions
There doesn't seem to be a solution at the moment other than attempting to re-implement fixture resolution via private APIs.
This mapping gives access to fixture definitions:
fixturedefs = metafunc._arg2fixturedefs["resource"]
functions = [fixturedef.func for fixturedef in fixturedefs]However, it includes definitions that the test does not use.
# conftest.py
import pytest
@pytest.fixture
def resource():
return "base"# test_example.py
import pytest
@pytest.fixture
def resource(resource):
return resource
def test_example(resource):
passHere, the mapping contains both resource definitions, and the proposed API should return both functions. If the local fixture takes no arguments and returns "replacement", the mapping still contains both definitions. The proposed API should then return only the local fixture function.
The private traverse_fixture_closure function resolves dependency names, but does not return the definitions it visits.
Additional context
This would help close pytest-dev/pytest-asyncio#1501.
Activity
bluetech commentedon Sep 9, 2026
Thanks for opening an issue. It is more common for plugins to just use the internals :)
Your request to have a way to inspect fixtures in metafunc sounds reasonable to me.
Indeed the "The proposed API should then return only the local fixture function" is the problematic part. We need all of the overrides to be present in
arg2fixturedefsjust in case the local fixture does something like this:I will try to think if there's a way we can do it.
RonnyPfannschmidt commentedon Sep 9, 2026
Id like to expose the statically discoverable fixture graph in some way
RonnyPfannschmidt commentedon Sep 13, 2026
I asked Claude (Opus 5, via Claude Code) to work through what this API actually has to
do, against the pytest-asyncio case this issue links. The exploration, the repros and the
writeup below are its work; I read it and I am posting it.
Some detail on what a solution here needs, because #15007 showed that the obvious shape
does not hold up.
It needs to be a graph of resolved
FixtureDefobjects, not a flat sequence of them.A flat tuple cannot express the thing that makes this hard. With an override chain you get
back
(resource, resource, resource)— the same argname three times, no edges, no parent,no way to tell which level is which or which one a given requester resolves to. Resolving
that is precisely what the closure walk has to compute (it carries a per-name index into
the override chain, the same state machine as
_get_active_fixturedef), and a tuple throwsall of it away at the API boundary. Consumers would have to re-derive it from private
state, which is where we came in.
So: nodes are the resolved
FixtureDefobjects, edges are "this def requests that def",roots are the item's initial names. Same information the walk already has.
Things such an API has to make an explicit decision about:
Closure mutation during
pytest_generate_tests. pytest-asyncio extends a test'sclosure by appending to
metafunc.fixturenamesand then parametrizing indirectly(plugin.py#L776):
Anything seeded from
fixtureinfo.initialnamescannot see that name —indirect=Truenever registers a def in
name2fixturedefseither. On the MRE fromLoop factory parametrization tears down async fixtures used by sync tests pytest-asyncio#1501, with a
trylasthook so pytest-asyncio has already run:(The appended name is then stripped straight back out by
prune_dependency_tree—at collection
item.fixturenames == ['event_loop_policy', 'parent']— andpytest-asyncio only survives because
PytestAsyncioFunction.setupre-appends the runnerid and calls
getfixturevalue.) Whether appending tofixturenamesis a supported wayto extend the closure is a question this issue has to answer, not route around.
Overrides that reach their base via
getfixturevalue. @bluetech raised this above.It is not statically discoverable, and the honest answer may be "over-approximate:
yield the whole chain". What it must not do is silently under-report:
The conftest def runs. An argnames-only walk does not report it. For the
pytest-asyncio use case that direction of error is the bad one — a false negative
silently fails to parametrize a test that really does use a managed fixture.
Desugared direct parametrization. Direct params are represented as
DirectParamFixtureDef, whose.funcis an internal stub;_get_fixtures_per_testalready filters it. A public graph must not leak it. Note thetwo spellings currently differ:
@pytest.mark.parametrize("x", ...)dropsxfromname2fixturedefsentirely viaignore_args, whilemetafunc.parametrize("x", ...)replaces the entry with a
DirectParamFixtureDef.Where it is read from. @tjkuson asked for it on
Metafunc; a graph is equallywanted on
Item, and the two differ (pre- vs post-pruning, pre- vspost-parametrization). Reading it during
pytest_generate_testsmeans the answerchanges across a
parametrize()call — which was asked for deliberately, and should bedocumented as such rather than discovered.
What already works, for the record. The transitive part is fine. On the
pytest-asyncio#1501 MRE, a purely synchronous
test_sync(child)wherechildandparentare session-scoped async fixtures does statically reach both defs, with
fd.funcandfd.scopeavailable — enough for "does this sync test's closure contain a managed fixture,at what widest loop scope". So the underlying walk is sound; the open questions are the
shape and the four decisions above.
I would rather land this once, as the graph, than land a tuple and need a second public API
later.
AdemVessell commentedon Sep 14, 2026
Following the graph discussion, our AI-assisted investigation produced two
small consumer examples that may help pin down the contract. These compare
pytest-dev/pytest-asyncio#1553 at
d86d02cwith a private adapter that reads definitionsvisited by pytest's existing traversal. This is not a proposed fix for pytest-dev/pytest-asyncio#1501;
pytest-dev/pytest-asyncio#1553 already passes that reproduction.
missed by the
fixturenamesplusfixturedefs[-1]lookup. With one loopfactory, two sync tests, and another function-scoped async fixture, the
session resource is initialized twice. Removing the wrapper gives one setup.
The adapter also gives one. The reduced test below reproduces the failure
with pytest 9.1.1 and Fix loop factory parametrization for sync fixtures pytest-asyncio#1553, without our pytest changes.
dependency in
fixturenames. The same lookup then adds loop-factory variantsto a sync test whose async dependency never runs: four tests instead of two.
Removing that dependency from the shadowed fixture makes both routes collect
two. The adapter also avoids the extra variants.
These show why the shallow lookup loses information, not that a graph is the
only solution for this consumer. Our prototype is not PR-ready: appended
registered fixture names remain unresolved, dynamic same-name base requests
are missed, and direct-param internals still escape through edges.
Would separating exact declared dependency edges from an explicitly
conservative set of possible override definitions fit the intended API?
The latter can include unused bases and cause extra parametrization; it would
not infer arbitrary computed
getfixturevaluerequests. The lifecycle ofplugin-added/removed roots and parameter nodes still needs agreement too.
Reduced session-fixture example
Run these two files with pytest 9.1.1 and pytest-dev/pytest-asyncio#1553 at
d86d02c:Observed on macOS ARM64/Python 3.12.13:
test_secondfails atassert 2 == 1.Delete only the synchronous
resource(resource)wrapper and both tests pass.This is a local reproduction, not cross-platform validation.
tjkuson commentedon Sep 14, 2026
Thanks for the analysis @RonnyPfannschmidt.
An item-level graph would be helpful beyond the original request. In pytest-asyncio, we already read from
item._fixtureinfo.name2fixturedefsfor the strict-mode fixture checks. A public API would replace that private access. For pytest-dev/pytest-asyncio#1514, we also need to know which test or fixture requests each dependency, so the graph would be useful there as well.On the API questions you raised, these are pytest-asyncio's requirements for the item-level checks:
getfixturevalue()calls. It should include overridden definitions reached through declared dependencies. Returning whole override chains would leave us having to resolve them ourselves.Those checks would use collected items, after parametrization and pruning. The parametrization use in the original request would still need access through
Metafuncduringpytest_generate_tests.I linked pytest-dev/pytest-asyncio#1501 because fixture inspection would help pytest-asyncio identify which tests need loop-factory parametrization. That addresses part of the problem, but it does not itself change how pytest-asyncio declares or manages its runner dependencies. Those need separate work, and I don't think a complete solution to the linked issue should be a requirement for this API.
On appending to
metafunc.fixturenames, I would like pytest-asyncio to stop doing it; pytest need not support that for us.Looking again at pytest-dev/pytest-asyncio#1501, the underlying problem is an undeclared dependency. Each async fixture needs an internal runner fixture, which in turn depends on the loop-factory fixture we parametrize. pytest cannot see that first dependency during collection.
We fetch the runner during setup, then compensate with early factory requests and manually registered finalizers. I would prefer to declare the dependency and let pytest handle setup, cache invalidation, and teardown.
We need a supported way to do that before pytest computes fixture dependencies. It must also work for plain
@pytest.fixtureasync functions in auto mode where pytest-asyncio does not control the decorator. Choosing the runner can depend on the pytest configuration as well.With that dependency declared, we could parametrize every test whose dependencies include the loop factory, including synchronous tests.
Would you recommend opening a separate feature request for declaring fixture dependencies? #378 discusses the related user-facing need to declare a dependency without adding a function parameter.
AdemVessell commentedon Sep 15, 2026
Thanks @tjkuson, that helps separate fixture inspection from the runner-dependency work.
We've narrowed the AI-assisted local prototype to the declared dependencies you described. It now provides item-level access after parametrization and pruning, including each fixture's function and scope and which test or fixture requests it. It follows declared override paths without returning unused bases. Direct parameter names can still appear, but their internal fixture objects are excluded. The
Metafuncquery remains available duringpytest_generate_tests.As a separate consumer check, we used those edges for a small loop-scope diagnostic related to pytest-dev/pytest-asyncio#1514. One useful test case was two fixtures requesting the same dependency: both requester relationships must remain visible. The diagnostic was tested locally, enabled and disabled; it does not infer through synchronous wrappers or dynamic requests.
The API names and shape are still provisional. We've kept runner-dependency declaration out of this change. I can share the small item-query example and tests here if useful, alongside the generation-time case.
AdemVessell commentedon Oct 5, 2026
Thanks for laying out the design questions. I used an AI-assisted local prototype to test a concrete approach before suggesting another PR.
Code and runnable checks: prototype patch, case table and 40 core tests.
The implementation records resolution events from
traverse_fixture_closure, producing declared root names, edges(requester, requested_name, target), and the reachedFixtureDefobjects for.func/.scopeinspection. A test-level requester isNone. Same-name wrappers retain their base dependency; replacements omit unused bases. Direct parameters are terminal entries with no fixture target or internal stub.For
Metafunc, the fix reads existing decorator directness before the builtin generation hook and incorporates subsequentparametrize()calls. CollectedItemqueries use completed fixture mappings, so a marker added too late to be expanded cannot hide a real fixture. Each query takes a fresh structural snapshot; the referenced definitions remain live.I propose roots from function arguments, autouse and
usefixtures, captured when fixture information is built. Rawfixturenamesedits would not rewrite this declared graph, though they can still affect existing setup behavior. Dynamicgetfixturevalue()dependencies and effective invocation scopes remain outside this contract. This therefore cannot establish that a managed fixture will never execute.The patch passes 56 focused checks and 587 relevant upstream tests on main at
980445e. Seven cases fail on the previous prototype and pass after the roots/directness changes. A generation-hook consumer also exercises the wrapper and parameter-shadow cases discussed earlier. Tests ran locally on macOS/Python 3.12.Is this declared-root boundary suitable for the read API, or must direct
fixturenamesedits be reflected?RonnyPfannschmidt commentedon Oct 6, 2026
🤖 Written by Claude Opus 5.5 via Claude Code for the pytest maintainers; I prompted it, it did the work, I read it.
Thanks, this is the shape I was asking for. I applied the gist at
980445e, and both the 40 checks and the 587 upstream tests pass.On your question: I don't want the graph to silently disagree with what actually runs. It still does in two places, and both can be fixed inside the same walk.
1. Dynamic same-name bases. If
resource(request)callsrequest.getfixturevalue("resource"), the shadowed definition runs, but the graph drops it. This lookup isn't arbitrary:_get_active_fixturedefresolves it to exactly one level up the override chain. So when an override takesrequestand there's a shadowed level, the walk can follow that level as an edge withorigin="dynamic", and walk that level's dependencies the same way. Consumers that need certainty can filter onorigin. Consumers that must not under-report, like the loop-factory case, can keep those edges. Lookups of other names throughgetfixturevaluestay out of contract.2. Names appended to
fixturenames. We don't want to support appending, but those names are still set up at runtime: in a quick check, an appended conftest fixture ran for every test. Rather than ignoring them, the graph should report names that are infixturenamesbut not reachable from the declared roots as extra roots withorigin="closure". They should be resolved the way runtime resolves them, viagetfixturedefs(name, node). OnMetafunc, compare against the closure as it was before the generation hooks ran, so that names a directparametrize()is about to prune don't come back. This makes the mutation visible, which is also what we'd need in order to deprecate it later.I prototyped both on top of your patch, about 100 lines. Two of your tests assert the old ignore-appended contract and fail with it, as expected. Everything else passes.
3. Shape for the public API.
(requester, name, target, kind, origin), withrequester=Nonefor the test itself.target=None. That waydependents()works for them too and the graph has no holes.roots,dependencies(node),dependents(node), all reached definitions, and the definitions reachable through declared edges only.Metafunc.fixture_graph()andItem.fixture_graph(), returning a snapshotpytest.FixtureGraph.getfixturevaluenames, and the effective scope set byparametrize(scope=...). Please document both.With those changes I'd welcome a PR. Please coordinate with #15060, which rewrites the same walk.
Generated by Claude Code