From af80fa56c2aef4455cfb8e6b38562171271f1b6a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 14 Mar 2026 21:53:10 +0200 Subject: [PATCH] fixtures: remove dirty optimization for `request.getfixturevalue()` Currently for each `Function` we copy the `FunctionDefinition`'s `_arg2fixturedefs`. This is done due to an ineffective optimization for a dynamic `request.getfixturevalue()` when the fixture name wasn't statically requested. In this case, we would save the dynamically-found `FixtureDef` in `_arg2fixturedef`, such that if it is requested again in the same item, it is returned immediately instead of doing a `_matchfactories` check again. But this case is already covered by the `_fixture_defs` optimization. I've always disliked this copy and mutation. The `_arg2fixturedefs` shenanigans performed during collection are hard enough to follow, and this only adds to the complexity, due to the mutability and having multiple different `_arg2fixturedefs` with different contents. So summing up: Pros: faster repeated `request.getfixturevalue()` in same test (ineffective) Cons: complexity (reasoning about mutability), extra copy Even without the `_fixture_defs` optimization, since `request.getfixturevalue()` is mostly a last-resort thing, so shouldn't be too common, and *repeated* calls to it in the same test should be even less common, and if so `_matchfactories` shouldn't be *that* slow, it should be fine to remove it. --- src/_pytest/fixtures.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index acb9fc4c5ee..883ab462d83 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -363,7 +363,7 @@ def __init__( self, pyfuncitem: Function, fixturename: str | None, - arg2fixturedefs: dict[str, Sequence[FixtureDef[Any]]], + arg2fixturedefs: Mapping[str, Sequence[FixtureDef[Any]]], fixture_defs: dict[str, FixtureDef[Any]], *, _ispytest: bool = False, @@ -372,10 +372,9 @@ def __init__( #: Fixture for which this request is being performed. self.fixturename: Final = fixturename self._pyfuncitem: Final = pyfuncitem - # The FixtureDefs for each fixture name requested by this item. - # Starts from the statically-known fixturedefs resolved during - # collection. Dynamically requested fixtures (using - # `request.getfixturevalue("foo")`) are added dynamically. + # The FixtureDefs for each fixture name statically requested by this + # item (computed during collection). Dynamically requested fixtures + # (using `request.getfixturevalue("foo")`) are not included here. self._arg2fixturedefs: Final = arg2fixturedefs # The evaluated argnames so far, mapping to the FixtureDef they resolved # to. @@ -564,8 +563,6 @@ def _get_active_fixturedef(self, argname: str) -> FixtureDef[object]: # getfixturevalue(argname) which was naturally # not known at parsing/collection time. fixturedefs = self._fixturemanager.getfixturedefs(argname, self._pyfuncitem) - if fixturedefs is not None: - self._arg2fixturedefs[argname] = fixturedefs # No fixtures defined with this name. if fixturedefs is None: raise FixtureLookupError(argname, self) @@ -669,7 +666,7 @@ def __init__(self, pyfuncitem: Function, *, _ispytest: bool = False) -> None: super().__init__( fixturename=None, pyfuncitem=pyfuncitem, - arg2fixturedefs=pyfuncitem._fixtureinfo.name2fixturedefs.copy(), + arg2fixturedefs=pyfuncitem._fixtureinfo.name2fixturedefs, fixture_defs={}, _ispytest=_ispytest, )