Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fixtures: avoid mutable arg2index state in favor of looking up the request chain #12038

Merged
merged 2 commits into from Mar 3, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
50 changes: 28 additions & 22 deletions src/_pytest/fixtures.py
Expand Up @@ -343,7 +343,6 @@
pyfuncitem: "Function",
fixturename: Optional[str],
arg2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]],
arg2index: Dict[str, int],
fixture_defs: Dict[str, "FixtureDef[Any]"],
*,
_ispytest: bool = False,
Expand All @@ -357,16 +356,6 @@
# collection. Dynamically requested fixtures (using
# `request.getfixturevalue("foo")`) are added dynamically.
self._arg2fixturedefs: Final = arg2fixturedefs
# A fixture may override another fixture with the same name, e.g. a fixture
# in a module can override a fixture in a conftest, a fixture in a class can
# override a fixture in the module, and so on.
# An overriding fixture can request its own name; in this case it gets
# the value of the fixture it overrides, one level up.
# The _arg2index state keeps the current depth in the overriding chain.
# The fixturedefs list in _arg2fixturedefs for a given name is ordered from
# furthest to closest, so we use negative indexing -1, -2, ... to go from
# last to first.
self._arg2index: Final = arg2index
# The evaluated argnames so far, mapping to the FixtureDef they resolved
# to.
self._fixture_defs: Final = fixture_defs
Expand Down Expand Up @@ -424,11 +413,24 @@
# The are no fixtures with this name applicable for the function.
if not fixturedefs:
raise FixtureLookupError(argname, self)
index = self._arg2index.get(argname, 0) - 1
# The fixture requested its own name, but no remaining to override.

# A fixture may override another fixture with the same name, e.g. a
# fixture in a module can override a fixture in a conftest, a fixture in
# a class can override a fixture in the module, and so on.
# An overriding fixture can request its own name (possibly indirectly);
# in this case it gets the value of the fixture it overrides, one level
# up.
# Check how many `argname`s deep we are, and take the next one.
# `fixturedefs` is sorted from furthest to closest, so use negative
# indexing to go in reverse.
index = -1
for request in self._iter_chain():
if request.fixturename == argname:
index -= 1

Check warning on line 429 in src/_pytest/fixtures.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/fixtures.py#L429

Added line #L429 was not covered by tests
# If already consumed all of the available levels, fail.
if -index > len(fixturedefs):
raise FixtureLookupError(argname, self)
self._arg2index[argname] = index

return fixturedefs[index]

@property
Expand Down Expand Up @@ -540,6 +542,16 @@
)
return fixturedef.cached_result[0]

def _iter_chain(self) -> Iterator["SubRequest"]:
"""Yield all SubRequests in the chain, from self up.

Note: does *not* yield the TopRequest.
"""
current = self
while isinstance(current, SubRequest):
yield current
current = current._parent_request

def _get_active_fixturedef(
self, argname: str
) -> Union["FixtureDef[object]", PseudoFixtureDef[object]]:
Expand All @@ -557,11 +569,7 @@
return fixturedef

def _get_fixturestack(self) -> List["FixtureDef[Any]"]:
current = self
values: List[FixtureDef[Any]] = []
while isinstance(current, SubRequest):
values.append(current._fixturedef) # type: ignore[has-type]
current = current._parent_request
values = [request._fixturedef for request in self._iter_chain()]
values.reverse()
return values

Expand Down Expand Up @@ -654,7 +662,6 @@
fixturename=None,
pyfuncitem=pyfuncitem,
arg2fixturedefs=pyfuncitem._fixtureinfo.name2fixturedefs.copy(),
arg2index={},
fixture_defs={},
_ispytest=_ispytest,
)
Expand Down Expand Up @@ -700,12 +707,11 @@
fixturename=fixturedef.argname,
fixture_defs=request._fixture_defs,
arg2fixturedefs=request._arg2fixturedefs,
arg2index=request._arg2index,
_ispytest=_ispytest,
)
self._parent_request: Final[FixtureRequest] = request
self._scope_field: Final = scope
self._fixturedef: Final = fixturedef
self._fixturedef: Final[FixtureDef[object]] = fixturedef
if param is not NOTSET:
self.param = param
self.param_index: Final = param_index
Expand Down