|
82 | 82 |
|
83 | 83 |
|
84 | 84 | # The value of the fixture -- return/yield of the fixture function (type variable). |
85 | | -FixtureValue = TypeVar("FixtureValue") |
| 85 | +FixtureValue = TypeVar("FixtureValue", covariant=True) |
86 | 86 | # The type of the fixture function (type variable). |
87 | 87 | FixtureFunction = TypeVar("FixtureFunction", bound=Callable[..., object]) |
88 | 88 | # The type of a fixture function (type alias generic in fixture value). |
|
106 | 106 | ) |
107 | 107 |
|
108 | 108 |
|
109 | | -@dataclasses.dataclass(frozen=True) |
110 | | -class PseudoFixtureDef(Generic[FixtureValue]): |
111 | | - cached_result: _FixtureCachedResult[FixtureValue] |
112 | | - _scope: Scope |
113 | | - |
114 | | - |
115 | 109 | def pytest_sessionstart(session: Session) -> None: |
116 | 110 | session._fixturemanager = FixtureManager(session) |
117 | 111 |
|
@@ -420,7 +414,7 @@ def scope(self) -> _ScopeName: |
420 | 414 | @abc.abstractmethod |
421 | 415 | def _check_scope( |
422 | 416 | self, |
423 | | - requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object], |
| 417 | + requested_fixturedef: FixtureDef[object], |
424 | 418 | requested_scope: Scope, |
425 | 419 | ) -> None: |
426 | 420 | raise NotImplementedError() |
@@ -559,12 +553,9 @@ def _iter_chain(self) -> Iterator[SubRequest]: |
559 | 553 | yield current |
560 | 554 | current = current._parent_request |
561 | 555 |
|
562 | | - def _get_active_fixturedef( |
563 | | - self, argname: str |
564 | | - ) -> FixtureDef[object] | PseudoFixtureDef[object]: |
| 556 | + def _get_active_fixturedef(self, argname: str) -> FixtureDef[object]: |
565 | 557 | if argname == "request": |
566 | | - cached_result = (self, [0], None) |
567 | | - return PseudoFixtureDef(cached_result, Scope.Function) |
| 558 | + return RequestFixtureDef(self) |
568 | 559 |
|
569 | 560 | # If we already finished computing a fixture by this name in this item, |
570 | 561 | # return it. |
@@ -696,7 +687,7 @@ def _scope(self) -> Scope: |
696 | 687 |
|
697 | 688 | def _check_scope( |
698 | 689 | self, |
699 | | - requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object], |
| 690 | + requested_fixturedef: FixtureDef[object], |
700 | 691 | requested_scope: Scope, |
701 | 692 | ) -> None: |
702 | 693 | # TopRequest always has function scope so always valid. |
@@ -775,11 +766,9 @@ def node(self): |
775 | 766 |
|
776 | 767 | def _check_scope( |
777 | 768 | self, |
778 | | - requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object], |
| 769 | + requested_fixturedef: FixtureDef[object], |
779 | 770 | requested_scope: Scope, |
780 | 771 | ) -> None: |
781 | | - if isinstance(requested_fixturedef, PseudoFixtureDef): |
782 | | - return |
783 | 772 | if self._scope > requested_scope: |
784 | 773 | # Try to report something helpful. |
785 | 774 | argname = requested_fixturedef.argname |
@@ -968,7 +957,6 @@ def _eval_scope_callable( |
968 | 957 | return result |
969 | 958 |
|
970 | 959 |
|
971 | | -@final |
972 | 960 | class FixtureDef(Generic[FixtureValue]): |
973 | 961 | """A container for a fixture definition. |
974 | 962 |
|
@@ -1083,8 +1071,7 @@ def execute(self, request: SubRequest) -> FixtureValue: |
1083 | 1071 | # down first. This is generally handled by SetupState, but still currently |
1084 | 1072 | # needed when this fixture is not parametrized but depends on a parametrized |
1085 | 1073 | # fixture. |
1086 | | - if not isinstance(fixturedef, PseudoFixtureDef): |
1087 | | - requested_fixtures_that_should_finalize_us.append(fixturedef) |
| 1074 | + requested_fixtures_that_should_finalize_us.append(fixturedef) |
1088 | 1075 |
|
1089 | 1076 | # Check for (and return) cached value/exception. |
1090 | 1077 | if self.cached_result is not None: |
@@ -1136,6 +1123,28 @@ def __repr__(self) -> str: |
1136 | 1123 | return f"<FixtureDef argname={self.argname!r} scope={self.scope!r} baseid={self.baseid!r}>" |
1137 | 1124 |
|
1138 | 1125 |
|
| 1126 | +class RequestFixtureDef(FixtureDef[FixtureRequest]): |
| 1127 | + """A custom FixtureDef for the special "request" fixture. |
| 1128 | +
|
| 1129 | + A new one is generated on-demand whenever "request" is requested. |
| 1130 | + """ |
| 1131 | + |
| 1132 | + def __init__(self, request: FixtureRequest) -> None: |
| 1133 | + super().__init__( |
| 1134 | + config=request.config, |
| 1135 | + baseid=None, |
| 1136 | + argname="request", |
| 1137 | + func=lambda: request, |
| 1138 | + scope=Scope.Function, |
| 1139 | + params=None, |
| 1140 | + _ispytest=True, |
| 1141 | + ) |
| 1142 | + self.cached_result = (request, [0], None) |
| 1143 | + |
| 1144 | + def addfinalizer(self, finalizer: Callable[[], object]) -> None: |
| 1145 | + pass |
| 1146 | + |
| 1147 | + |
1139 | 1148 | def resolve_fixture_function( |
1140 | 1149 | fixturedef: FixtureDef[FixtureValue], request: FixtureRequest |
1141 | 1150 | ) -> _FixtureFunc[FixtureValue]: |
@@ -1634,29 +1643,44 @@ def getfixtureclosure( |
1634 | 1643 | fixturenames_closure = list(initialnames) |
1635 | 1644 |
|
1636 | 1645 | arg2fixturedefs: dict[str, Sequence[FixtureDef[Any]]] = {} |
1637 | | - lastlen = -1 |
1638 | | - while lastlen != len(fixturenames_closure): |
1639 | | - lastlen = len(fixturenames_closure) |
1640 | | - for argname in fixturenames_closure: |
1641 | | - if argname in ignore_args: |
1642 | | - continue |
1643 | | - if argname in arg2fixturedefs: |
1644 | | - continue |
| 1646 | + |
| 1647 | + # Track the index for each fixture name in the simulated stack. |
| 1648 | + # Needed for handling override chains correctly, similar to _get_active_fixturedef. |
| 1649 | + # Using negative indices: -1 is the most specific (last), -2 is second to last, etc. |
| 1650 | + current_indices: dict[str, int] = {} |
| 1651 | + |
| 1652 | + def process_argname(argname: str) -> None: |
| 1653 | + # Optimization: already processed this argname. |
| 1654 | + if current_indices.get(argname) == -1: |
| 1655 | + return |
| 1656 | + |
| 1657 | + if argname not in fixturenames_closure: |
| 1658 | + fixturenames_closure.append(argname) |
| 1659 | + |
| 1660 | + if argname in ignore_args: |
| 1661 | + return |
| 1662 | + |
| 1663 | + fixturedefs = arg2fixturedefs.get(argname) |
| 1664 | + if not fixturedefs: |
1645 | 1665 | fixturedefs = self.getfixturedefs(argname, parentnode) |
1646 | | - if fixturedefs: |
1647 | | - arg2fixturedefs[argname] = fixturedefs |
1648 | | - |
1649 | | - # Add dependencies from this fixture. |
1650 | | - # If it overrides a fixture with the same name and requests |
1651 | | - # it, also add dependencies from the overridden fixtures in |
1652 | | - # the chain. See also similar dealing in _get_active_fixturedef(). |
1653 | | - for fixturedef in reversed(fixturedefs): # pragma: no cover |
1654 | | - for arg in fixturedef.argnames: |
1655 | | - if arg not in fixturenames_closure: |
1656 | | - fixturenames_closure.append(arg) |
1657 | | - if argname not in fixturedef.argnames: |
1658 | | - # Overrides, but doesn't request super. |
1659 | | - break |
| 1666 | + if not fixturedefs: |
| 1667 | + # Fixture not defined or not visible (will error during runtest). |
| 1668 | + return |
| 1669 | + arg2fixturedefs[argname] = fixturedefs |
| 1670 | + |
| 1671 | + index = current_indices.get(argname, -1) |
| 1672 | + if -index > len(fixturedefs): |
| 1673 | + # Exhausted the override chain (will error during runtest). |
| 1674 | + return |
| 1675 | + fixturedef = fixturedefs[index] |
| 1676 | + |
| 1677 | + current_indices[argname] = index - 1 |
| 1678 | + for dep in fixturedef.argnames: |
| 1679 | + process_argname(dep) |
| 1680 | + current_indices[argname] = index |
| 1681 | + |
| 1682 | + for name in initialnames: |
| 1683 | + process_argname(name) |
1660 | 1684 |
|
1661 | 1685 | def sort_by_scope(arg_name: str) -> Scope: |
1662 | 1686 | try: |
|
0 commit comments