Skip to content

Commit 00043f7

Browse files
authored
Merge pull request #12038 from bluetech/fixtures-rm-arg2index
fixtures: avoid mutable arg2index state in favor of looking up the request chain
2 parents f4e1025 + bd45ccd commit 00043f7

File tree

1 file changed

+28
-22
lines changed

1 file changed

+28
-22
lines changed

src/_pytest/fixtures.py

+28-22
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,6 @@ def __init__(
348348
pyfuncitem: "Function",
349349
fixturename: Optional[str],
350350
arg2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]],
351-
arg2index: Dict[str, int],
352351
fixture_defs: Dict[str, "FixtureDef[Any]"],
353352
*,
354353
_ispytest: bool = False,
@@ -362,16 +361,6 @@ def __init__(
362361
# collection. Dynamically requested fixtures (using
363362
# `request.getfixturevalue("foo")`) are added dynamically.
364363
self._arg2fixturedefs: Final = arg2fixturedefs
365-
# A fixture may override another fixture with the same name, e.g. a fixture
366-
# in a module can override a fixture in a conftest, a fixture in a class can
367-
# override a fixture in the module, and so on.
368-
# An overriding fixture can request its own name; in this case it gets
369-
# the value of the fixture it overrides, one level up.
370-
# The _arg2index state keeps the current depth in the overriding chain.
371-
# The fixturedefs list in _arg2fixturedefs for a given name is ordered from
372-
# furthest to closest, so we use negative indexing -1, -2, ... to go from
373-
# last to first.
374-
self._arg2index: Final = arg2index
375364
# The evaluated argnames so far, mapping to the FixtureDef they resolved
376365
# to.
377366
self._fixture_defs: Final = fixture_defs
@@ -427,11 +416,24 @@ def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
427416
# The are no fixtures with this name applicable for the function.
428417
if not fixturedefs:
429418
raise FixtureLookupError(argname, self)
430-
index = self._arg2index.get(argname, 0) - 1
431-
# The fixture requested its own name, but no remaining to override.
419+
420+
# A fixture may override another fixture with the same name, e.g. a
421+
# fixture in a module can override a fixture in a conftest, a fixture in
422+
# a class can override a fixture in the module, and so on.
423+
# An overriding fixture can request its own name (possibly indirectly);
424+
# in this case it gets the value of the fixture it overrides, one level
425+
# up.
426+
# Check how many `argname`s deep we are, and take the next one.
427+
# `fixturedefs` is sorted from furthest to closest, so use negative
428+
# indexing to go in reverse.
429+
index = -1
430+
for request in self._iter_chain():
431+
if request.fixturename == argname:
432+
index -= 1
433+
# If already consumed all of the available levels, fail.
432434
if -index > len(fixturedefs):
433435
raise FixtureLookupError(argname, self)
434-
self._arg2index[argname] = index
436+
435437
return fixturedefs[index]
436438

437439
@property
@@ -543,6 +545,16 @@ def getfixturevalue(self, argname: str) -> Any:
543545
)
544546
return fixturedef.cached_result[0]
545547

548+
def _iter_chain(self) -> Iterator["SubRequest"]:
549+
"""Yield all SubRequests in the chain, from self up.
550+
551+
Note: does *not* yield the TopRequest.
552+
"""
553+
current = self
554+
while isinstance(current, SubRequest):
555+
yield current
556+
current = current._parent_request
557+
546558
def _get_active_fixturedef(
547559
self, argname: str
548560
) -> Union["FixtureDef[object]", PseudoFixtureDef[object]]:
@@ -560,11 +572,7 @@ def _get_active_fixturedef(
560572
return fixturedef
561573

562574
def _get_fixturestack(self) -> List["FixtureDef[Any]"]:
563-
current = self
564-
values: List[FixtureDef[Any]] = []
565-
while isinstance(current, SubRequest):
566-
values.append(current._fixturedef) # type: ignore[has-type]
567-
current = current._parent_request
575+
values = [request._fixturedef for request in self._iter_chain()]
568576
values.reverse()
569577
return values
570578

@@ -657,7 +665,6 @@ def __init__(self, pyfuncitem: "Function", *, _ispytest: bool = False) -> None:
657665
fixturename=None,
658666
pyfuncitem=pyfuncitem,
659667
arg2fixturedefs=pyfuncitem._fixtureinfo.name2fixturedefs.copy(),
660-
arg2index={},
661668
fixture_defs={},
662669
_ispytest=_ispytest,
663670
)
@@ -703,12 +710,11 @@ def __init__(
703710
fixturename=fixturedef.argname,
704711
fixture_defs=request._fixture_defs,
705712
arg2fixturedefs=request._arg2fixturedefs,
706-
arg2index=request._arg2index,
707713
_ispytest=_ispytest,
708714
)
709715
self._parent_request: Final[FixtureRequest] = request
710716
self._scope_field: Final = scope
711-
self._fixturedef: Final = fixturedef
717+
self._fixturedef: Final[FixtureDef[object]] = fixturedef
712718
if param is not NOTSET:
713719
self.param = param
714720
self.param_index: Final = param_index

0 commit comments

Comments
 (0)