1313from typing import Callable
1414from typing import cast
1515from typing import Dict
16+ from typing import Final
1617from typing import final
1718from typing import Generator
1819from typing import Generic
7374 from _pytest .scope import _ScopeName
7475 from _pytest .main import Session
7576 from _pytest .python import CallSpec2
77+ from _pytest .python import Function
7678 from _pytest .python import Metafunc
7779
7880
@@ -352,17 +354,24 @@ def get_direct_param_fixture_func(request: "FixtureRequest") -> Any:
352354 return request .param
353355
354356
355- @dataclasses .dataclass
357+ @dataclasses .dataclass ( frozen = True )
356358class FuncFixtureInfo :
357359 __slots__ = ("argnames" , "initialnames" , "names_closure" , "name2fixturedefs" )
358360
359- # Original function argument names.
361+ # Original function argument names, i.e. fixture names that the function
362+ # requests directly.
360363 argnames : Tuple [str , ...]
361- # Argnames that function immediately requires. These include argnames +
362- # fixture names specified via usefixtures and via autouse=True in fixture
363- # definitions.
364+ # Fixture names that the function immediately requires. These include
365+ # argnames + fixture names specified via usefixtures and via autouse=True in
366+ # fixture definitions.
364367 initialnames : Tuple [str , ...]
368+ # The transitive closure of the fixture names that the function requires.
369+ # Note: can't include dynamic dependencies (`request.getfixturevalue` calls).
365370 names_closure : List [str ]
371+ # A map from a fixture name in the transitive closure to the FixtureDefs
372+ # matching the name which are applicable to this function.
373+ # There may be multiple overriding fixtures with the same name. The
374+ # sequence is ordered from furthest to closes to the function.
366375 name2fixturedefs : Dict [str , Sequence ["FixtureDef[Any]" ]]
367376
368377 def prune_dependency_tree (self ) -> None :
@@ -401,17 +410,31 @@ class FixtureRequest:
401410 indirectly.
402411 """
403412
404- def __init__ (self , pyfuncitem , * , _ispytest : bool = False ) -> None :
413+ def __init__ (self , pyfuncitem : "Function" , * , _ispytest : bool = False ) -> None :
405414 check_ispytest (_ispytest )
406- self ._pyfuncitem = pyfuncitem
407415 #: Fixture for which this request is being performed.
408416 self .fixturename : Optional [str ] = None
417+ self ._pyfuncitem = pyfuncitem
418+ self ._fixturemanager = pyfuncitem .session ._fixturemanager
409419 self ._scope = Scope .Function
410- self ._fixture_defs : Dict [str , FixtureDef [Any ]] = {}
411- fixtureinfo : FuncFixtureInfo = pyfuncitem ._fixtureinfo
412- self ._arg2fixturedefs = fixtureinfo .name2fixturedefs .copy ()
420+ # The FixtureDefs for each fixture name requested by this item.
421+ # Starts from the statically-known fixturedefs resolved during
422+ # collection. Dynamically requested fixtures (using
423+ # `request.getfixturevalue("foo")`) are added dynamically.
424+ self ._arg2fixturedefs = pyfuncitem ._fixtureinfo .name2fixturedefs .copy ()
425+ # A fixture may override another fixture with the same name, e.g. a fixture
426+ # in a module can override a fixture in a conftest, a fixture in a class can
427+ # override a fixture in the module, and so on.
428+ # An overriding fixture can request its own name; in this case it gets
429+ # the value of the fixture it overrides, one level up.
430+ # The _arg2index state keeps the current depth in the overriding chain.
431+ # The fixturedefs list in _arg2fixturedefs for a given name is ordered from
432+ # furthest to closest, so we use negative indexing -1, -2, ... to go from
433+ # last to first.
413434 self ._arg2index : Dict [str , int ] = {}
414- self ._fixturemanager : FixtureManager = pyfuncitem .session ._fixturemanager
435+ # The evaluated argnames so far, mapping to the FixtureDef they resolved
436+ # to.
437+ self ._fixture_defs : Dict [str , FixtureDef [Any ]] = {}
415438 # Notes on the type of `param`:
416439 # -`request.param` is only defined in parametrized fixtures, and will raise
417440 # AttributeError otherwise. Python typing has no notion of "undefined", so
@@ -466,10 +489,14 @@ def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
466489 fixturedefs = self ._fixturemanager .getfixturedefs (argname , parentid )
467490 if fixturedefs is not None :
468491 self ._arg2fixturedefs [argname ] = fixturedefs
492+ # No fixtures defined with this name.
469493 if fixturedefs is None :
470494 raise FixtureLookupError (argname , self )
471- # fixturedefs list is immutable so we maintain a decreasing index.
495+ # The are no fixtures with this name applicable for the function.
496+ if not fixturedefs :
497+ raise FixtureLookupError (argname , self )
472498 index = self ._arg2index .get (argname , 0 ) - 1
499+ # The fixture requested its own name, but no remaining to override.
473500 if - index > len (fixturedefs ):
474501 raise FixtureLookupError (argname , self )
475502 self ._arg2index [argname ] = index
@@ -503,7 +530,7 @@ def instance(self):
503530 """Instance (can be None) on which test function was collected."""
504531 # unittest support hack, see _pytest.unittest.TestCaseFunction.
505532 try :
506- return self ._pyfuncitem ._testcase
533+ return self ._pyfuncitem ._testcase # type: ignore[attr-defined]
507534 except AttributeError :
508535 function = getattr (self , "function" , None )
509536 return getattr (function , "__self__" , None )
@@ -513,7 +540,9 @@ def module(self):
513540 """Python module object where the test function was collected."""
514541 if self .scope not in ("function" , "class" , "module" ):
515542 raise AttributeError (f"module not available in { self .scope } -scoped context" )
516- return self ._pyfuncitem .getparent (_pytest .python .Module ).obj
543+ mod = self ._pyfuncitem .getparent (_pytest .python .Module )
544+ assert mod is not None
545+ return mod .obj
517546
518547 @property
519548 def path (self ) -> Path :
@@ -829,7 +858,9 @@ def formatrepr(self) -> "FixtureLookupErrorRepr":
829858 if msg is None :
830859 fm = self .request ._fixturemanager
831860 available = set ()
832- parentid = self .request ._pyfuncitem .parent .nodeid
861+ parent = self .request ._pyfuncitem .parent
862+ assert parent is not None
863+ parentid = parent .nodeid
833864 for name , fixturedefs in fm ._arg2fixturedefs .items ():
834865 faclist = list (fm ._matchfactories (fixturedefs , parentid ))
835866 if faclist :
@@ -976,15 +1007,15 @@ def __init__(
9761007 # directory path relative to the rootdir.
9771008 #
9781009 # For other plugins, the baseid is the empty string (always matches).
979- self .baseid = baseid or ""
1010+ self .baseid : Final = baseid or ""
9801011 # Whether the fixture was found from a node or a conftest in the
9811012 # collection tree. Will be false for fixtures defined in non-conftest
9821013 # plugins.
983- self .has_location = baseid is not None
1014+ self .has_location : Final = baseid is not None
9841015 # The fixture factory function.
985- self .func = func
1016+ self .func : Final = func
9861017 # The name by which the fixture may be requested.
987- self .argname = argname
1018+ self .argname : Final = argname
9881019 if scope is None :
9891020 scope = Scope .Function
9901021 elif callable (scope ):
@@ -993,23 +1024,23 @@ def __init__(
9931024 scope = Scope .from_user (
9941025 scope , descr = f"Fixture '{ func .__name__ } '" , where = baseid
9951026 )
996- self ._scope = scope
1027+ self ._scope : Final = scope
9971028 # If the fixture is directly parametrized, the parameter values.
998- self .params : Optional [ Sequence [ object ]] = params
1029+ self .params : Final = params
9991030 # If the fixture is directly parametrized, a tuple of explicit IDs to
10001031 # assign to the parameter values, or a callable to generate an ID given
10011032 # a parameter value.
1002- self .ids = ids
1033+ self .ids : Final = ids
10031034 # The names requested by the fixtures.
1004- self .argnames = getfuncargnames (func , name = argname , is_method = unittest )
1035+ self .argnames : Final = getfuncargnames (func , name = argname , is_method = unittest )
10051036 # Whether the fixture was collected from a unittest TestCase class.
10061037 # Note that it really only makes sense to define autouse fixtures in
10071038 # unittest TestCases.
1008- self .unittest = unittest
1039+ self .unittest : Final = unittest
10091040 # If the fixture was executed, the current value of the fixture.
10101041 # Can change if the fixture is executed with different parameters.
10111042 self .cached_result : Optional [_FixtureCachedResult [FixtureValue ]] = None
1012- self ._finalizers : List [Callable [[], object ]] = []
1043+ self ._finalizers : Final [ List [Callable [[], object ] ]] = []
10131044
10141045 @property
10151046 def scope (self ) -> "_ScopeName" :
@@ -1040,7 +1071,7 @@ def finish(self, request: SubRequest) -> None:
10401071 # value and remove all finalizers because they may be bound methods
10411072 # which will keep instances alive.
10421073 self .cached_result = None
1043- self ._finalizers = []
1074+ self ._finalizers . clear ()
10441075
10451076 def execute (self , request : SubRequest ) -> FixtureValue :
10461077 # Get required arguments and register our own finish()
@@ -1417,10 +1448,14 @@ class FixtureManager:
14171448 def __init__ (self , session : "Session" ) -> None :
14181449 self .session = session
14191450 self .config : Config = session .config
1420- self ._arg2fixturedefs : Dict [str , List [FixtureDef [Any ]]] = {}
1421- self ._holderobjseen : Set [object ] = set ()
1451+ # Maps a fixture name (argname) to all of the FixtureDefs in the test
1452+ # suite/plugins defined with this name. Populated by parsefactories().
1453+ # TODO: The order of the FixtureDefs list of each arg is significant,
1454+ # explain.
1455+ self ._arg2fixturedefs : Final [Dict [str , List [FixtureDef [Any ]]]] = {}
1456+ self ._holderobjseen : Final [Set [object ]] = set ()
14221457 # A mapping from a nodeid to a list of autouse fixtures it defines.
1423- self ._nodeid_autousenames : Dict [str , List [str ]] = {
1458+ self ._nodeid_autousenames : Final [ Dict [str , List [str ] ]] = {
14241459 "" : self .config .getini ("usefixtures" ),
14251460 }
14261461 session .config .pluginmanager .register (self , "funcmanage" )
@@ -1699,11 +1734,16 @@ def parsefactories( # noqa: F811
16991734 def getfixturedefs (
17001735 self , argname : str , nodeid : str
17011736 ) -> Optional [Sequence [FixtureDef [Any ]]]:
1702- """Get a list of fixtures which are applicable to the given node id.
1737+ """Get FixtureDefs for a fixture name which are applicable
1738+ to a given node.
1739+
1740+ Returns None if there are no fixtures at all defined with the given
1741+ name. (This is different from the case in which there are fixtures
1742+ with the given name, but none applicable to the node. In this case,
1743+ an empty result is returned).
17031744
1704- :param str argname: Name of the fixture to search for.
1705- :param str nodeid: Full node id of the requesting test.
1706- :rtype: Sequence[FixtureDef]
1745+ :param argname: Name of the fixture to search for.
1746+ :param nodeid: Full node id of the requesting test.
17071747 """
17081748 try :
17091749 fixturedefs = self ._arg2fixturedefs [argname ]
0 commit comments