Skip to content

Commit 15fadd8

Browse files
authored
Merge pull request #11219 from bluetech/fixtures2
fixtures: make FixtureRequest abstract, add TopRequest subclass
2 parents 73d754b + 9e164fc commit 15fadd8

File tree

6 files changed

+148
-96
lines changed

6 files changed

+148
-96
lines changed

changelog/11218.trivial.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
(This entry is meant to assist plugins which access private pytest internals to instantiate ``FixtureRequest`` objects.)
2+
3+
:class:`~pytest.FixtureRequest` is now an abstract class which can't be instantiated directly.
4+
A new concrete ``TopRequest`` subclass of ``FixtureRequest`` has been added for the ``request`` fixture in test functions,
5+
as counterpart to the existing ``SubRequest`` subclass for the ``request`` fixture in fixture functions.

src/_pytest/doctest.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from _pytest.config import Config
3333
from _pytest.config.argparsing import Parser
3434
from _pytest.fixtures import fixture
35-
from _pytest.fixtures import FixtureRequest
35+
from _pytest.fixtures import TopRequest
3636
from _pytest.nodes import Collector
3737
from _pytest.nodes import Item
3838
from _pytest.outcomes import OutcomeException
@@ -261,7 +261,7 @@ def __init__(
261261
self.runner = runner
262262
self.dtest = dtest
263263
self.obj = None
264-
self.fixture_request: Optional[FixtureRequest] = None
264+
self.fixture_request: Optional[TopRequest] = None
265265

266266
@classmethod
267267
def from_parent( # type: ignore
@@ -571,7 +571,7 @@ def _find(
571571
)
572572

573573

574-
def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest:
574+
def _setup_fixtures(doctest_item: DoctestItem) -> TopRequest:
575575
"""Used by DoctestTextfile and DoctestItem to setup fixture information."""
576576

577577
def func() -> None:
@@ -582,7 +582,7 @@ def func() -> None:
582582
doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined]
583583
node=doctest_item, func=func, cls=None, funcargs=False
584584
)
585-
fixture_request = FixtureRequest(doctest_item, _ispytest=True) # type: ignore[arg-type]
585+
fixture_request = TopRequest(doctest_item, _ispytest=True) # type: ignore[arg-type]
586586
fixture_request._fillfixtures()
587587
return fixture_request
588588

src/_pytest/fixtures.py

Lines changed: 125 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import abc
12
import dataclasses
23
import functools
34
import inspect
@@ -340,26 +341,32 @@ def prune_dependency_tree(self) -> None:
340341
self.names_closure[:] = sorted(closure, key=self.names_closure.index)
341342

342343

343-
class FixtureRequest:
344-
"""A request for a fixture from a test or fixture function.
344+
class FixtureRequest(abc.ABC):
345+
"""The type of the ``request`` fixture.
345346
346-
A request object gives access to the requesting test context and has
347-
an optional ``param`` attribute in case the fixture is parametrized
348-
indirectly.
347+
A request object gives access to the requesting test context and has a
348+
``param`` attribute in case the fixture is parametrized.
349349
"""
350350

351-
def __init__(self, pyfuncitem: "Function", *, _ispytest: bool = False) -> None:
351+
def __init__(
352+
self,
353+
pyfuncitem: "Function",
354+
fixturename: Optional[str],
355+
arg2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]],
356+
arg2index: Dict[str, int],
357+
fixture_defs: Dict[str, "FixtureDef[Any]"],
358+
*,
359+
_ispytest: bool = False,
360+
) -> None:
352361
check_ispytest(_ispytest)
353362
#: Fixture for which this request is being performed.
354-
self.fixturename: Optional[str] = None
355-
self._pyfuncitem = pyfuncitem
356-
self._fixturemanager = pyfuncitem.session._fixturemanager
357-
self._scope = Scope.Function
363+
self.fixturename: Final = fixturename
364+
self._pyfuncitem: Final = pyfuncitem
358365
# The FixtureDefs for each fixture name requested by this item.
359366
# Starts from the statically-known fixturedefs resolved during
360367
# collection. Dynamically requested fixtures (using
361368
# `request.getfixturevalue("foo")`) are added dynamically.
362-
self._arg2fixturedefs = pyfuncitem._fixtureinfo.name2fixturedefs.copy()
369+
self._arg2fixturedefs: Final = arg2fixturedefs
363370
# A fixture may override another fixture with the same name, e.g. a fixture
364371
# in a module can override a fixture in a conftest, a fixture in a class can
365372
# override a fixture in the module, and so on.
@@ -369,10 +376,10 @@ def __init__(self, pyfuncitem: "Function", *, _ispytest: bool = False) -> None:
369376
# The fixturedefs list in _arg2fixturedefs for a given name is ordered from
370377
# furthest to closest, so we use negative indexing -1, -2, ... to go from
371378
# last to first.
372-
self._arg2index: Dict[str, int] = {}
379+
self._arg2index: Final = arg2index
373380
# The evaluated argnames so far, mapping to the FixtureDef they resolved
374381
# to.
375-
self._fixture_defs: Dict[str, FixtureDef[Any]] = {}
382+
self._fixture_defs: Final = fixture_defs
376383
# Notes on the type of `param`:
377384
# -`request.param` is only defined in parametrized fixtures, and will raise
378385
# AttributeError otherwise. Python typing has no notion of "undefined", so
@@ -383,6 +390,15 @@ def __init__(self, pyfuncitem: "Function", *, _ispytest: bool = False) -> None:
383390
# for now just using Any.
384391
self.param: Any
385392

393+
@property
394+
def _fixturemanager(self) -> "FixtureManager":
395+
return self._pyfuncitem.session._fixturemanager
396+
397+
@property
398+
@abc.abstractmethod
399+
def _scope(self) -> Scope:
400+
raise NotImplementedError()
401+
386402
@property
387403
def scope(self) -> _ScopeName:
388404
"""Scope string, one of "function", "class", "module", "package", "session"."""
@@ -396,25 +412,10 @@ def fixturenames(self) -> List[str]:
396412
return result
397413

398414
@property
415+
@abc.abstractmethod
399416
def node(self):
400417
"""Underlying collection node (depends on current request scope)."""
401-
scope = self._scope
402-
if scope is Scope.Function:
403-
# This might also be a non-function Item despite its attribute name.
404-
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
405-
elif scope is Scope.Package:
406-
# FIXME: _fixturedef is not defined on FixtureRequest (this class),
407-
# but on SubRequest (a subclass).
408-
node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
409-
else:
410-
node = get_scope_node(self._pyfuncitem, scope)
411-
if node is None and scope is Scope.Class:
412-
# Fallback to function item itself.
413-
node = self._pyfuncitem
414-
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
415-
scope, self._pyfuncitem
416-
)
417-
return node
418+
raise NotImplementedError()
418419

419420
def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
420421
fixturedefs = self._arg2fixturedefs.get(argname, None)
@@ -500,11 +501,11 @@ def session(self) -> "Session":
500501
"""Pytest session object."""
501502
return self._pyfuncitem.session # type: ignore[no-any-return]
502503

504+
@abc.abstractmethod
503505
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
504506
"""Add finalizer/teardown function to be called without arguments after
505507
the last test within the requesting test context finished execution."""
506-
# XXX usually this method is shadowed by fixturedef specific ones.
507-
self.node.addfinalizer(finalizer)
508+
raise NotImplementedError()
508509

509510
def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
510511
"""Apply a marker to a single test function invocation.
@@ -525,13 +526,6 @@ def raiseerror(self, msg: Optional[str]) -> NoReturn:
525526
"""
526527
raise self._fixturemanager.FixtureLookupError(None, self, msg)
527528

528-
def _fillfixtures(self) -> None:
529-
item = self._pyfuncitem
530-
fixturenames = getattr(item, "fixturenames", self.fixturenames)
531-
for argname in fixturenames:
532-
if argname not in item.funcargs:
533-
item.funcargs[argname] = self.getfixturevalue(argname)
534-
535529
def getfixturevalue(self, argname: str) -> Any:
536530
"""Dynamically run a named fixture function.
537531
@@ -665,6 +659,98 @@ def _schedule_finalizers(
665659
finalizer = functools.partial(fixturedef.finish, request=subrequest)
666660
subrequest.node.addfinalizer(finalizer)
667661

662+
663+
@final
664+
class TopRequest(FixtureRequest):
665+
"""The type of the ``request`` fixture in a test function."""
666+
667+
def __init__(self, pyfuncitem: "Function", *, _ispytest: bool = False) -> None:
668+
super().__init__(
669+
fixturename=None,
670+
pyfuncitem=pyfuncitem,
671+
arg2fixturedefs=pyfuncitem._fixtureinfo.name2fixturedefs.copy(),
672+
arg2index={},
673+
fixture_defs={},
674+
_ispytest=_ispytest,
675+
)
676+
677+
@property
678+
def _scope(self) -> Scope:
679+
return Scope.Function
680+
681+
@property
682+
def node(self):
683+
return self._pyfuncitem
684+
685+
def __repr__(self) -> str:
686+
return "<FixtureRequest for %r>" % (self.node)
687+
688+
def _fillfixtures(self) -> None:
689+
item = self._pyfuncitem
690+
fixturenames = getattr(item, "fixturenames", self.fixturenames)
691+
for argname in fixturenames:
692+
if argname not in item.funcargs:
693+
item.funcargs[argname] = self.getfixturevalue(argname)
694+
695+
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
696+
self.node.addfinalizer(finalizer)
697+
698+
699+
@final
700+
class SubRequest(FixtureRequest):
701+
"""The type of the ``request`` fixture in a fixture function requested
702+
(transitively) by a test function."""
703+
704+
def __init__(
705+
self,
706+
request: FixtureRequest,
707+
scope: Scope,
708+
param: Any,
709+
param_index: int,
710+
fixturedef: "FixtureDef[object]",
711+
*,
712+
_ispytest: bool = False,
713+
) -> None:
714+
super().__init__(
715+
pyfuncitem=request._pyfuncitem,
716+
fixturename=fixturedef.argname,
717+
fixture_defs=request._fixture_defs,
718+
arg2fixturedefs=request._arg2fixturedefs,
719+
arg2index=request._arg2index,
720+
_ispytest=_ispytest,
721+
)
722+
self._parent_request: Final[FixtureRequest] = request
723+
self._scope_field: Final = scope
724+
self._fixturedef: Final = fixturedef
725+
if param is not NOTSET:
726+
self.param = param
727+
self.param_index: Final = param_index
728+
729+
def __repr__(self) -> str:
730+
return f"<SubRequest {self.fixturename!r} for {self._pyfuncitem!r}>"
731+
732+
@property
733+
def _scope(self) -> Scope:
734+
return self._scope_field
735+
736+
@property
737+
def node(self):
738+
scope = self._scope
739+
if scope is Scope.Function:
740+
# This might also be a non-function Item despite its attribute name.
741+
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
742+
elif scope is Scope.Package:
743+
node = get_scope_package(self._pyfuncitem, self._fixturedef)
744+
else:
745+
node = get_scope_node(self._pyfuncitem, scope)
746+
if node is None and scope is Scope.Class:
747+
# Fallback to function item itself.
748+
node = self._pyfuncitem
749+
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
750+
scope, self._pyfuncitem
751+
)
752+
return node
753+
668754
def _check_scope(
669755
self,
670756
argname: str,
@@ -699,44 +785,7 @@ def _factorytraceback(self) -> List[str]:
699785
)
700786
return lines
701787

702-
def __repr__(self) -> str:
703-
return "<FixtureRequest for %r>" % (self.node)
704-
705-
706-
@final
707-
class SubRequest(FixtureRequest):
708-
"""A sub request for handling getting a fixture from a test function/fixture."""
709-
710-
def __init__(
711-
self,
712-
request: "FixtureRequest",
713-
scope: Scope,
714-
param: Any,
715-
param_index: int,
716-
fixturedef: "FixtureDef[object]",
717-
*,
718-
_ispytest: bool = False,
719-
) -> None:
720-
check_ispytest(_ispytest)
721-
self._parent_request = request
722-
self.fixturename = fixturedef.argname
723-
if param is not NOTSET:
724-
self.param = param
725-
self.param_index = param_index
726-
self._scope = scope
727-
self._fixturedef = fixturedef
728-
self._pyfuncitem = request._pyfuncitem
729-
self._fixture_defs = request._fixture_defs
730-
self._arg2fixturedefs = request._arg2fixturedefs
731-
self._arg2index = request._arg2index
732-
self._fixturemanager = request._fixturemanager
733-
734-
def __repr__(self) -> str:
735-
return f"<SubRequest {self.fixturename!r} for {self._pyfuncitem!r}>"
736-
737788
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
738-
"""Add finalizer/teardown function to be called without arguments after
739-
the last test within the requesting test context finished execution."""
740789
self._fixturedef.addfinalizer(finalizer)
741790

742791
def _schedule_finalizers(

src/_pytest/python.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1812,7 +1812,7 @@ def from_parent(cls, parent, **kw): # todo: determine sound type limitations
18121812

18131813
def _initrequest(self) -> None:
18141814
self.funcargs: Dict[str, object] = {}
1815-
self._request = fixtures.FixtureRequest(self, _ispytest=True)
1815+
self._request = fixtures.TopRequest(self, _ispytest=True)
18161816

18171817
@property
18181818
def function(self):

0 commit comments

Comments
 (0)