fixtures: use a better data structure for storing FixtureDefs - #14984
Open
bluetech wants to merge 5 commits into
Open
fixtures: use a better data structure for storing FixtureDefs#14984bluetech wants to merge 5 commits into
bluetech wants to merge 5 commits into
Conversation
Let's use consistent terminology.
This is an internal function, so it's not a official guarantee, but let's explicitly document the behavior internally at least.
…ide the class It's easier to handle this way.
Previously, FixtureManager stored the registered FixtureDefs in
`_arg2fixturedefs` which is
<fixutre name> -> [FixtureDef]
where the FixtureDefs are ordered by visibility.
There are two inefficiencies with this:
1. When registering a fixture, we need to find the appropriate index to
insert in the list. This is done with slow quadratic
`is_visibility_more_specific` checks.
Before 7186cd4, FixtureDefs were
always appended, relying on the collection order, so there was no
quadratic issue. But then we added `pytest.register_fixture` which is
not guaranteed to be called in collection order.
This is pytest-dev#14942, introduced in v9.1.0.
2. When looking up FixtureDefs for a node, the entire list needed to be
filtered for visibility to the node (`_matchfactories`). With many
fixtures registered with the same name (even if completely
unrelated), this can be slow.
This is an old issue.
Change the way we store the FixtureDefs to `_arg2node2fixturedefs`,
which is
<fixture name> -> (Node -> [FixtureDef])
i.e. instead of storing the FixtureDefs for a name in a single big list,
store them by the Node under which they are registered.
This fixes (1) since now just need to append to
`arg2fixture2nodes[name][node]`.
Fixes (2) since no longer need to filter a big list (scaling with number
of fixtures registered with same name). Instead need to look up the
FixtureDefs registered for the node and its ancestors (scales with
height of the collection tree, which should be OK).
Fix pytest-dev#14942
bluetech
force-pushed
the
arg2node2fixturedef
branch
from
September 8, 2026 10:13
690d7d4 to
d938a56
Compare
nicoddemus
approved these changes
Sep 8, 2026
| @@ -0,0 +1,2 @@ | |||
| Fixed a regression in 9.1.0 that made collection very slow (quadratic) when many fixtures are defined with the same name. | |||
| Then can particularly happen when many tests are defined in separate classes which inherit from a base class which defines fixtures (the fixtures are repeated per class). | |||
Member
There was a problem hiding this comment.
Suggested change
| Then can particularly happen when many tests are defined in separate classes which inherit from a base class which defines fixtures (the fixtures are repeated per class). | |
| This can happen particularly when many tests are defined in separate classes which inherit from a base class which defines fixtures (the fixtures are repeated per class). |
Comment on lines
+2393
to
+2396
| for fixturedef in [ | ||
| *node2fixturedefs.get(parent, ()), | ||
| *nodeid2fixturedefs.get(parent.nodeid, ()), | ||
| ] |
Member
There was a problem hiding this comment.
We could use itertools.chain instead of building a list, might be a bit more efficient (or maybe not that much, given the list of fixturedefs is much smaller now).
| # Legacy fallback, for plugins still using the deprecated nodeid-based | ||
| # API without a node reference. | ||
| # Part of FIXTURE_NODEID_DEPRECATED deprecation. | ||
| self._arg2nodeid2fixturedefs: Final[ |
Member
There was a problem hiding this comment.
Instead of a separate datastructure, is it possible for us to get the nodeid from the session collection tree? Or perhaps the node might not exist at this point yet?
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.
This PR fixes #14942.
The commits
The 1st and 2nd commits are related internal documentation fixes.
The 3rd commit is a little refactor to encapsulate access to
FixtureManager._arg2fixturedefto make the next commit clearer.The 4th commit is main change, I reproduce the commit message below.
The 5th commit adds backward compat for string nodeids in
register_fixture. I split it from the previous commit to make it easier to review the main change without the ugly compat. I will squash it before merging.Description
Previously, FixtureManager stored the registered FixtureDefs in
_arg2fixturedefswhich iswhere the FixtureDefs are ordered by visibility.
There are two inefficiencies with this:
When registering a fixture, we need to find the appropriate index to insert in the list. This is done with slow quadratic
is_visibility_more_specificchecks.Before 7186cd4, FixtureDefs were always appended, relying on the collection order, so there was no quadratic issue. But then we added
pytest.register_fixturewhich is not guaranteed to be called in collection order.This is Test collection is 10x slower on 9.1.1 compared to 9.0.3 #14942, introduced in v9.1.0.
When looking up FixtureDefs for a node, the entire list needed to be filtered for visibility to the node (
_matchfactories). With many fixtures registered with the same name (even if completely unrelated), this can be slow.This is an old issue.
Change the way we store the FixtureDefs to
_arg2node2fixturedefs, which isi.e. instead of storing the FixtureDefs for a name in a single big list, store them by the Node under which they are registered.
This fixes (1) since now just need to append to
arg2fixture2nodes[name][node].Fixes (2) since no longer need to filter a big list (scaling with number of fixtures registered with same name). Instead need to look up the FixtureDefs registered for the node and its ancestors (scales with height of the collection tree, which should be OK).
Performance
Performance numbers with reproducer from #14942 (comment) (
NCLASSES=10000 time pytest reproducer.py):It's still kinda slow, but a profile shows that's for other preexisting reasons. The improvement over main is due to fixing problem (1), and over before the regression due to fixing problem (2).
Backward compat
This breaks plugins which directly access
FixtureManager._arg2fixturedefs(double private 😀). From my local corpus (678 plugins), I see it done in these plugins:I think this is acceptable.