Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 29 additions & 20 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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)

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed, request always has Function scope so will never fail this check.

return
if self._scope > requested_scope:
# Try to report something helpful.
argname = requested_fixturedef.argname
Expand Down Expand Up @@ -968,7 +957,6 @@ def _eval_scope_callable(
return result


@final
class FixtureDef(Generic[FixtureValue]):
"""A container for a fixture definition.

Expand Down Expand Up @@ -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):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To remove this special case, I made RequestFixtureDef just ignore addfinalizer calls.

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:
Expand Down Expand Up @@ -1136,6 +1123,28 @@ def __repr__(self) -> str:
return f"<FixtureDef argname={self.argname!r} scope={self.scope!r} baseid={self.baseid!r}>"


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]:
Expand Down
4 changes: 2 additions & 2 deletions testing/python/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down