Skip to content

Commit 01f38ac

Browse files
committed
fixtures: expand comments and annotations on fixture internals
1 parent ecfab4d commit 01f38ac

File tree

5 files changed

+85
-35
lines changed

5 files changed

+85
-35
lines changed

src/_pytest/compat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def is_async_function(func: object) -> bool:
6868
return iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
6969

7070

71-
def getlocation(function, curdir: str | None = None) -> str:
71+
def getlocation(function, curdir: str | os.PathLike[str] | None = None) -> str:
7272
function = get_real_func(function)
7373
fn = Path(inspect.getfile(function))
7474
lineno = function.__code__.co_firstlineno

src/_pytest/doctest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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)
585+
fixture_request = FixtureRequest(doctest_item, _ispytest=True) # type: ignore[arg-type]
586586
fixture_request._fillfixtures()
587587
return fixture_request
588588

src/_pytest/fixtures.py

Lines changed: 73 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from typing import Callable
1414
from typing import cast
1515
from typing import Dict
16+
from typing import Final
1617
from typing import final
1718
from typing import Generator
1819
from typing import Generic
@@ -73,6 +74,7 @@
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)
356358
class 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]

testing/python/fixtures.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,7 @@ def test_method(self, something):
699699
"""
700700
)
701701
(item1,) = pytester.genitems([modcol])
702+
assert isinstance(item1, Function)
702703
assert item1.name == "test_method"
703704
arg2fixturedefs = fixtures.FixtureRequest(
704705
item1, _ispytest=True
@@ -967,6 +968,7 @@ def test_second():
967968
def test_request_getmodulepath(self, pytester: Pytester) -> None:
968969
modcol = pytester.getmodulecol("def test_somefunc(): pass")
969970
(item,) = pytester.genitems([modcol])
971+
assert isinstance(item, Function)
970972
req = fixtures.FixtureRequest(item, _ispytest=True)
971973
assert req.path == modcol.path
972974

@@ -1125,6 +1127,7 @@ def test_func2(self, something):
11251127
pass
11261128
"""
11271129
)
1130+
assert isinstance(item1, Function)
11281131
req1 = fixtures.FixtureRequest(item1, _ispytest=True)
11291132
assert "xfail" not in item1.keywords
11301133
req1.applymarker(pytest.mark.xfail)
@@ -4009,6 +4012,7 @@ def test_func(m1):
40094012
"""
40104013
)
40114014
items, _ = pytester.inline_genitems()
4015+
assert isinstance(items[0], Function)
40124016
request = FixtureRequest(items[0], _ispytest=True)
40134017
assert request.fixturenames == "m1 f1".split()
40144018

@@ -4057,6 +4061,7 @@ def test_foo(f1, p1, m1, f2, s1): pass
40574061
"""
40584062
)
40594063
items, _ = pytester.inline_genitems()
4064+
assert isinstance(items[0], Function)
40604065
request = FixtureRequest(items[0], _ispytest=True)
40614066
# order of fixtures based on their scope and position in the parameter list
40624067
assert (
@@ -4084,6 +4089,7 @@ def test_func(f1, m1):
40844089
"""
40854090
)
40864091
items, _ = pytester.inline_genitems()
4092+
assert isinstance(items[0], Function)
40874093
request = FixtureRequest(items[0], _ispytest=True)
40884094
assert request.fixturenames == "m1 f1".split()
40894095

@@ -4117,6 +4123,7 @@ def test_func(self, f2, f1, c1, m1, s1):
41174123
"""
41184124
)
41194125
items, _ = pytester.inline_genitems()
4126+
assert isinstance(items[0], Function)
41204127
request = FixtureRequest(items[0], _ispytest=True)
41214128
assert request.fixturenames == "s1 m1 c1 f2 f1".split()
41224129

@@ -4159,6 +4166,7 @@ def test_func(m_test, f1):
41594166
}
41604167
)
41614168
items, _ = pytester.inline_genitems()
4169+
assert isinstance(items[0], Function)
41624170
request = FixtureRequest(items[0], _ispytest=True)
41634171
assert request.fixturenames == "p_sub m_conf m_sub m_test f1".split()
41644172

@@ -4203,6 +4211,7 @@ def test_func(self, f2, f1, m2):
42034211
"""
42044212
)
42054213
items, _ = pytester.inline_genitems()
4214+
assert isinstance(items[0], Function)
42064215
request = FixtureRequest(items[0], _ispytest=True)
42074216
assert request.fixturenames == "s1 p1 m1 m2 c1 f2 f1".split()
42084217

testing/test_legacypath.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ def test_cache_makedir(cache: pytest.Cache) -> None:
9090
def test_fixturerequest_getmodulepath(pytester: pytest.Pytester) -> None:
9191
modcol = pytester.getmodulecol("def test_somefunc(): pass")
9292
(item,) = pytester.genitems([modcol])
93+
assert isinstance(item, pytest.Function)
9394
req = pytest.FixtureRequest(item, _ispytest=True)
9495
assert req.path == modcol.path
9596
assert req.fspath == modcol.fspath # type: ignore[attr-defined]

0 commit comments

Comments
 (0)