From 631dd86ea08fe5d3330f530074331b6f81354c9c Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 9 Oct 2025 00:10:00 +0300 Subject: [PATCH 1/2] fixtures: make the FixtureValue TypeVar covariant This TypeVar represents the return value of the fixture function, so should be covariant. --- src/_pytest/fixtures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index b0ce620c13a..205b71c9200 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -82,7 +82,7 @@ # The value of the fixture -- return/yield of the fixture function (type variable). -FixtureValue = TypeVar("FixtureValue") +FixtureValue = TypeVar("FixtureValue", covariant=True) # The type of the fixture function (type variable). FixtureFunction = TypeVar("FixtureFunction", bound=Callable[..., object]) # The type of a fixture function (type alias generic in fixture value). From 6d17c8508a5b0218816868682a432ad5acfea192 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 9 Oct 2025 00:03:35 +0300 Subject: [PATCH 2/2] fixtures: replace PseudoFixtureDef with RequestFixtureDef which is a real FixtureDef Remove some special cases from the code, and make the types more regular. --- src/_pytest/fixtures.py | 47 +++++++++++++++++++++++--------------- testing/python/fixtures.py | 4 ++-- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 205b71c9200..2f56eb6b932 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -106,12 +106,6 @@ ) -@dataclasses.dataclass(frozen=True) -class PseudoFixtureDef(Generic[FixtureValue]): - cached_result: _FixtureCachedResult[FixtureValue] - _scope: Scope - - def pytest_sessionstart(session: Session) -> None: session._fixturemanager = FixtureManager(session) @@ -420,7 +414,7 @@ def scope(self) -> _ScopeName: @abc.abstractmethod def _check_scope( self, - requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object], + requested_fixturedef: FixtureDef[object], requested_scope: Scope, ) -> None: raise NotImplementedError() @@ -559,12 +553,9 @@ def _iter_chain(self) -> Iterator[SubRequest]: yield current current = current._parent_request - def _get_active_fixturedef( - self, argname: str - ) -> FixtureDef[object] | PseudoFixtureDef[object]: + def _get_active_fixturedef(self, argname: str) -> FixtureDef[object]: if argname == "request": - cached_result = (self, [0], None) - return PseudoFixtureDef(cached_result, Scope.Function) + return RequestFixtureDef(self) # If we already finished computing a fixture by this name in this item, # return it. @@ -696,7 +687,7 @@ def _scope(self) -> Scope: def _check_scope( self, - requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object], + requested_fixturedef: FixtureDef[object], requested_scope: Scope, ) -> None: # TopRequest always has function scope so always valid. @@ -775,11 +766,9 @@ def node(self): def _check_scope( self, - requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object], + requested_fixturedef: FixtureDef[object], requested_scope: Scope, ) -> None: - if isinstance(requested_fixturedef, PseudoFixtureDef): - return if self._scope > requested_scope: # Try to report something helpful. argname = requested_fixturedef.argname @@ -968,7 +957,6 @@ def _eval_scope_callable( return result -@final class FixtureDef(Generic[FixtureValue]): """A container for a fixture definition. @@ -1083,8 +1071,7 @@ def execute(self, request: SubRequest) -> FixtureValue: # down first. This is generally handled by SetupState, but still currently # needed when this fixture is not parametrized but depends on a parametrized # fixture. - if not isinstance(fixturedef, PseudoFixtureDef): - requested_fixtures_that_should_finalize_us.append(fixturedef) + requested_fixtures_that_should_finalize_us.append(fixturedef) # Check for (and return) cached value/exception. if self.cached_result is not None: @@ -1136,6 +1123,28 @@ def __repr__(self) -> str: return f"" +class RequestFixtureDef(FixtureDef[FixtureRequest]): + """A custom FixtureDef for the special "request" fixture. + + A new one is generated on-demand whenever "request" is requested. + """ + + def __init__(self, request: FixtureRequest) -> None: + super().__init__( + config=request.config, + baseid=None, + argname="request", + func=lambda: request, + scope=Scope.Function, + params=None, + _ispytest=True, + ) + self.cached_result = (request, [0], None) + + def addfinalizer(self, finalizer: Callable[[], object]) -> None: + pass + + def resolve_fixture_function( fixturedef: FixtureDef[FixtureValue], request: FixtureRequest ) -> _FixtureFunc[FixtureValue]: diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 10941c3f9e3..5644b9567b7 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -750,7 +750,7 @@ def test_request_garbage(self, pytester: Pytester) -> None: """ import sys import pytest - from _pytest.fixtures import PseudoFixtureDef + from _pytest.fixtures import RequestFixtureDef import gc @pytest.fixture(autouse=True) @@ -763,7 +763,7 @@ def something(request): try: gc.collect() - leaked = [x for _ in gc.garbage if isinstance(_, PseudoFixtureDef)] + leaked = [x for _ in gc.garbage if isinstance(_, RequestFixtureDef)] assert leaked == [] finally: gc.set_debug(original)