Skip to content

Commit 0303285

Browse files
committed
typing: Improve FixtureDefinition and FixtureDef
* Carry around parameters and return value in `FixtureFunctionDefinition`. * Add `FixtureParams` to `FixtureDef`. Follow up to #12473.
1 parent ecde993 commit 0303285

File tree

7 files changed

+78
-56
lines changed

7 files changed

+78
-56
lines changed

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ dependencies = [
5252
"packaging",
5353
"pluggy>=1.5,<2",
5454
"tomli>=1; python_version<'3.11'",
55+
"typing-extensions; python_version<'3.10'",
5556
]
5657
optional-dependencies.dev = [
5758
"argcomplete",

src/_pytest/fixtures.py

+63-45
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@
7575
if sys.version_info < (3, 11):
7676
from exceptiongroup import BaseExceptionGroup
7777

78+
if sys.version_info < (3, 10):
79+
from typing_extensions import ParamSpec
80+
from typing_extensions import TypeAlias
81+
else:
82+
from typing import ParamSpec
83+
from typing import TypeAlias
84+
7885

7986
if TYPE_CHECKING:
8087
from _pytest.python import CallSpec2
@@ -84,14 +91,17 @@
8491

8592
# The value of the fixture -- return/yield of the fixture function (type variable).
8693
FixtureValue = TypeVar("FixtureValue")
87-
# The type of the fixture function (type variable).
88-
FixtureFunction = TypeVar("FixtureFunction", bound=Callable[..., object])
89-
# The type of a fixture function (type alias generic in fixture value).
90-
_FixtureFunc = Union[
91-
Callable[..., FixtureValue], Callable[..., Generator[FixtureValue]]
94+
95+
# The parameters that a fixture function receives.
96+
FixtureParams = ParamSpec("FixtureParams")
97+
98+
# The type of fixture function (type alias generic in fixture params and value).
99+
_FixtureFunc: TypeAlias = Union[
100+
Callable[FixtureParams, FixtureValue],
101+
Callable[FixtureParams, Generator[FixtureValue, None, None]],
92102
]
93103
# The type of FixtureDef.cached_result (type alias generic in fixture value).
94-
_FixtureCachedResult = Union[
104+
_FixtureCachedResult: TypeAlias = Union[
95105
tuple[
96106
# The result.
97107
FixtureValue,
@@ -121,7 +131,7 @@ def pytest_sessionstart(session: Session) -> None:
121131

122132
def get_scope_package(
123133
node: nodes.Item,
124-
fixturedef: FixtureDef[object],
134+
fixturedef: FixtureDef[Any, object],
125135
) -> nodes.Node | None:
126136
from _pytest.python import Package
127137

@@ -318,7 +328,7 @@ class FuncFixtureInfo:
318328
# matching the name which are applicable to this function.
319329
# There may be multiple overriding fixtures with the same name. The
320330
# sequence is ordered from furthest to closes to the function.
321-
name2fixturedefs: dict[str, Sequence[FixtureDef[Any]]]
331+
name2fixturedefs: dict[str, Sequence[FixtureDef[Any, Any]]]
322332

323333
def prune_dependency_tree(self) -> None:
324334
"""Recompute names_closure from initialnames and name2fixturedefs.
@@ -359,8 +369,8 @@ def __init__(
359369
self,
360370
pyfuncitem: Function,
361371
fixturename: str | None,
362-
arg2fixturedefs: dict[str, Sequence[FixtureDef[Any]]],
363-
fixture_defs: dict[str, FixtureDef[Any]],
372+
arg2fixturedefs: dict[str, Sequence[FixtureDef[Any, Any]]],
373+
fixture_defs: dict[str, FixtureDef[Any, Any]],
364374
*,
365375
_ispytest: bool = False,
366376
) -> None:
@@ -403,7 +413,7 @@ def scope(self) -> _ScopeName:
403413
@abc.abstractmethod
404414
def _check_scope(
405415
self,
406-
requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object],
416+
requested_fixturedef: FixtureDef[Any, object] | PseudoFixtureDef[object],
407417
requested_scope: Scope,
408418
) -> None:
409419
raise NotImplementedError()
@@ -544,7 +554,7 @@ def _iter_chain(self) -> Iterator[SubRequest]:
544554

545555
def _get_active_fixturedef(
546556
self, argname: str
547-
) -> FixtureDef[object] | PseudoFixtureDef[object]:
557+
) -> FixtureDef[Any, object] | PseudoFixtureDef[object]:
548558
if argname == "request":
549559
cached_result = (self, [0], None)
550560
return PseudoFixtureDef(cached_result, Scope.Function)
@@ -616,7 +626,9 @@ def _get_active_fixturedef(
616626
self._fixture_defs[argname] = fixturedef
617627
return fixturedef
618628

619-
def _check_fixturedef_without_param(self, fixturedef: FixtureDef[object]) -> None:
629+
def _check_fixturedef_without_param(
630+
self, fixturedef: FixtureDef[Any, object]
631+
) -> None:
620632
"""Check that this request is allowed to execute this fixturedef without
621633
a param."""
622634
funcitem = self._pyfuncitem
@@ -649,7 +661,7 @@ def _check_fixturedef_without_param(self, fixturedef: FixtureDef[object]) -> Non
649661
)
650662
fail(msg, pytrace=False)
651663

652-
def _get_fixturestack(self) -> list[FixtureDef[Any]]:
664+
def _get_fixturestack(self) -> list[FixtureDef[Any, Any]]:
653665
values = [request._fixturedef for request in self._iter_chain()]
654666
values.reverse()
655667
return values
@@ -674,7 +686,7 @@ def _scope(self) -> Scope:
674686

675687
def _check_scope(
676688
self,
677-
requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object],
689+
requested_fixturedef: FixtureDef[Any, object] | PseudoFixtureDef[object],
678690
requested_scope: Scope,
679691
) -> None:
680692
# TopRequest always has function scope so always valid.
@@ -708,7 +720,7 @@ def __init__(
708720
scope: Scope,
709721
param: Any,
710722
param_index: int,
711-
fixturedef: FixtureDef[object],
723+
fixturedef: FixtureDef[Any, object],
712724
*,
713725
_ispytest: bool = False,
714726
) -> None:
@@ -721,7 +733,7 @@ def __init__(
721733
)
722734
self._parent_request: Final[FixtureRequest] = request
723735
self._scope_field: Final = scope
724-
self._fixturedef: Final[FixtureDef[object]] = fixturedef
736+
self._fixturedef: Final[FixtureDef[Any, object]] = fixturedef
725737
if param is not NOTSET:
726738
self.param = param
727739
self.param_index: Final = param_index
@@ -751,7 +763,7 @@ def node(self):
751763

752764
def _check_scope(
753765
self,
754-
requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object],
766+
requested_fixturedef: FixtureDef[Any, object] | PseudoFixtureDef[object],
755767
requested_scope: Scope,
756768
) -> None:
757769
if isinstance(requested_fixturedef, PseudoFixtureDef):
@@ -772,7 +784,7 @@ def _check_scope(
772784
pytrace=False,
773785
)
774786

775-
def _format_fixturedef_line(self, fixturedef: FixtureDef[object]) -> str:
787+
def _format_fixturedef_line(self, fixturedef: FixtureDef[Any, object]) -> str:
776788
factory = fixturedef.func
777789
path, lineno = getfslineno(factory)
778790
if isinstance(path, Path):
@@ -886,7 +898,9 @@ def toterminal(self, tw: TerminalWriter) -> None:
886898

887899

888900
def call_fixture_func(
889-
fixturefunc: _FixtureFunc[FixtureValue], request: FixtureRequest, kwargs
901+
fixturefunc: _FixtureFunc[FixtureParams, FixtureValue],
902+
request: FixtureRequest,
903+
kwargs: FixtureParams.kwargs,
890904
) -> FixtureValue:
891905
if inspect.isgeneratorfunction(fixturefunc):
892906
fixturefunc = cast(Callable[..., Generator[FixtureValue]], fixturefunc)
@@ -945,9 +959,11 @@ def _eval_scope_callable(
945959

946960

947961
@final
948-
class FixtureDef(Generic[FixtureValue]):
962+
class FixtureDef(Generic[FixtureParams, FixtureValue]):
949963
"""A container for a fixture definition.
950964
965+
This is a generic class parametrized on the parameters that a fixture function receives and its return value.
966+
951967
Note: At this time, only explicitly documented fields and methods are
952968
considered public stable API.
953969
"""
@@ -957,7 +973,7 @@ def __init__(
957973
config: Config,
958974
baseid: str | None,
959975
argname: str,
960-
func: _FixtureFunc[FixtureValue],
976+
func: _FixtureFunc[FixtureParams, FixtureValue],
961977
scope: Scope | _ScopeName | Callable[[str, Config], _ScopeName] | None,
962978
params: Sequence[object] | None,
963979
ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None,
@@ -1112,8 +1128,8 @@ def __repr__(self) -> str:
11121128

11131129

11141130
def resolve_fixture_function(
1115-
fixturedef: FixtureDef[FixtureValue], request: FixtureRequest
1116-
) -> _FixtureFunc[FixtureValue]:
1131+
fixturedef: FixtureDef[FixtureParams, FixtureValue], request: FixtureRequest
1132+
) -> _FixtureFunc[FixtureParams, FixtureValue]:
11171133
"""Get the actual callable that can be called to obtain the fixture
11181134
value."""
11191135
fixturefunc = fixturedef.func
@@ -1136,7 +1152,7 @@ def resolve_fixture_function(
11361152

11371153

11381154
def pytest_fixture_setup(
1139-
fixturedef: FixtureDef[FixtureValue], request: SubRequest
1155+
fixturedef: FixtureDef[FixtureParams, FixtureValue], request: SubRequest
11401156
) -> FixtureValue:
11411157
"""Execution of fixture setup."""
11421158
kwargs = {}
@@ -1192,7 +1208,9 @@ class FixtureFunctionMarker:
11921208
def __post_init__(self, _ispytest: bool) -> None:
11931209
check_ispytest(_ispytest)
11941210

1195-
def __call__(self, function: FixtureFunction) -> FixtureFunctionDefinition:
1211+
def __call__(
1212+
self, function: Callable[FixtureParams, FixtureValue]
1213+
) -> FixtureFunctionDefinition[FixtureParams, FixtureValue]:
11961214
if inspect.isclass(function):
11971215
raise ValueError("class fixtures not supported (maybe in the future)")
11981216

@@ -1219,12 +1237,10 @@ def __call__(self, function: FixtureFunction) -> FixtureFunctionDefinition:
12191237
return fixture_definition
12201238

12211239

1222-
# TODO: paramspec/return type annotation tracking and storing
1223-
class FixtureFunctionDefinition:
1240+
class FixtureFunctionDefinition(Generic[FixtureParams, FixtureValue]):
12241241
def __init__(
12251242
self,
1226-
*,
1227-
function: Callable[..., Any],
1243+
function: Callable[FixtureParams, FixtureValue],
12281244
fixture_function_marker: FixtureFunctionMarker,
12291245
instance: object | None = None,
12301246
_ispytest: bool = False,
@@ -1237,7 +1253,7 @@ def __init__(
12371253
self._fixture_function_marker = fixture_function_marker
12381254
if instance is not None:
12391255
self._fixture_function = cast(
1240-
Callable[..., Any], function.__get__(instance)
1256+
Callable[FixtureParams, FixtureValue], function.__get__(instance)
12411257
)
12421258
else:
12431259
self._fixture_function = function
@@ -1246,12 +1262,14 @@ def __init__(
12461262
def __repr__(self) -> str:
12471263
return f"<pytest_fixture({self._fixture_function})>"
12481264

1249-
def __get__(self, instance, owner=None):
1265+
def __get__(
1266+
self, obj: object, objtype: type | None = None
1267+
) -> FixtureFunctionDefinition[FixtureParams, FixtureValue]:
12501268
"""Behave like a method if the function it was applied to was a method."""
12511269
return FixtureFunctionDefinition(
12521270
function=self._fixture_function,
12531271
fixture_function_marker=self._fixture_function_marker,
1254-
instance=instance,
1272+
instance=obj,
12551273
_ispytest=True,
12561274
)
12571275

@@ -1270,14 +1288,14 @@ def _get_wrapped_function(self) -> Callable[..., Any]:
12701288

12711289
@overload
12721290
def fixture(
1273-
fixture_function: Callable[..., object],
1291+
fixture_function: Callable[FixtureParams, FixtureValue],
12741292
*,
12751293
scope: _ScopeName | Callable[[str, Config], _ScopeName] = ...,
12761294
params: Iterable[object] | None = ...,
12771295
autouse: bool = ...,
12781296
ids: Sequence[object | None] | Callable[[Any], object | None] | None = ...,
12791297
name: str | None = ...,
1280-
) -> FixtureFunctionDefinition: ...
1298+
) -> FixtureFunctionDefinition[FixtureParams, FixtureValue]: ...
12811299

12821300

12831301
@overload
@@ -1293,14 +1311,14 @@ def fixture(
12931311

12941312

12951313
def fixture(
1296-
fixture_function: FixtureFunction | None = None,
1314+
fixture_function: Callable[FixtureParams, FixtureValue] | None = None,
12971315
*,
12981316
scope: _ScopeName | Callable[[str, Config], _ScopeName] = "function",
12991317
params: Iterable[object] | None = None,
13001318
autouse: bool = False,
13011319
ids: Sequence[object | None] | Callable[[Any], object | None] | None = None,
13021320
name: str | None = None,
1303-
) -> FixtureFunctionMarker | FixtureFunctionDefinition:
1321+
) -> FixtureFunctionMarker | FixtureFunctionDefinition[FixtureParams, FixtureValue]:
13041322
"""Decorator to mark a fixture factory function.
13051323
13061324
This decorator can be used, with or without parameters, to define a
@@ -1507,7 +1525,7 @@ def __init__(self, session: Session) -> None:
15071525
# suite/plugins defined with this name. Populated by parsefactories().
15081526
# TODO: The order of the FixtureDefs list of each arg is significant,
15091527
# explain.
1510-
self._arg2fixturedefs: Final[dict[str, list[FixtureDef[Any]]]] = {}
1528+
self._arg2fixturedefs: Final[dict[str, list[FixtureDef[Any, Any]]]] = {}
15111529
self._holderobjseen: Final[set[object]] = set()
15121530
# A mapping from a nodeid to a list of autouse fixtures it defines.
15131531
self._nodeid_autousenames: Final[dict[str, list[str]]] = {
@@ -1598,7 +1616,7 @@ def getfixtureclosure(
15981616
parentnode: nodes.Node,
15991617
initialnames: tuple[str, ...],
16001618
ignore_args: AbstractSet[str],
1601-
) -> tuple[list[str], dict[str, Sequence[FixtureDef[Any]]]]:
1619+
) -> tuple[list[str], dict[str, Sequence[FixtureDef[Any, Any]]]]:
16021620
# Collect the closure of all fixtures, starting with the given
16031621
# fixturenames as the initial set. As we have to visit all
16041622
# factory definitions anyway, we also return an arg2fixturedefs
@@ -1608,7 +1626,7 @@ def getfixtureclosure(
16081626

16091627
fixturenames_closure = list(initialnames)
16101628

1611-
arg2fixturedefs: dict[str, Sequence[FixtureDef[Any]]] = {}
1629+
arg2fixturedefs: dict[str, Sequence[FixtureDef[Any, Any]]] = {}
16121630
lastlen = -1
16131631
while lastlen != len(fixturenames_closure):
16141632
lastlen = len(fixturenames_closure)
@@ -1688,7 +1706,7 @@ def _register_fixture(
16881706
self,
16891707
*,
16901708
name: str,
1691-
func: _FixtureFunc[object],
1709+
func: _FixtureFunc[Any, object],
16921710
nodeid: str | None,
16931711
scope: Scope | _ScopeName | Callable[[str, Config], _ScopeName] = "function",
16941712
params: Sequence[object] | None = None,
@@ -1823,7 +1841,7 @@ def parsefactories(
18231841

18241842
def getfixturedefs(
18251843
self, argname: str, node: nodes.Node
1826-
) -> Sequence[FixtureDef[Any]] | None:
1844+
) -> Sequence[FixtureDef[Any, Any]] | None:
18271845
"""Get FixtureDefs for a fixture name which are applicable
18281846
to a given node.
18291847
@@ -1842,8 +1860,8 @@ def getfixturedefs(
18421860
return tuple(self._matchfactories(fixturedefs, node))
18431861

18441862
def _matchfactories(
1845-
self, fixturedefs: Iterable[FixtureDef[Any]], node: nodes.Node
1846-
) -> Iterator[FixtureDef[Any]]:
1863+
self, fixturedefs: Iterable[FixtureDef[Any, Any]], node: nodes.Node
1864+
) -> Iterator[FixtureDef[Any, Any]]:
18471865
parentnodeids = {n.nodeid for n in node.iter_parents()}
18481866
for fixturedef in fixturedefs:
18491867
if fixturedef.baseid in parentnodeids:
@@ -1880,7 +1898,7 @@ def get_best_relpath(func) -> str:
18801898
loc = getlocation(func, invocation_dir)
18811899
return bestrelpath(invocation_dir, Path(loc))
18821900

1883-
def write_fixture(fixture_def: FixtureDef[object]) -> None:
1901+
def write_fixture(fixture_def: FixtureDef[Any, object]) -> None:
18841902
argname = fixture_def.argname
18851903
if verbose <= 0 and argname.startswith("_"):
18861904
return

src/_pytest/hookspec.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -866,7 +866,7 @@ def pytest_report_from_serializable(
866866

867867
@hookspec(firstresult=True)
868868
def pytest_fixture_setup(
869-
fixturedef: FixtureDef[Any], request: SubRequest
869+
fixturedef: FixtureDef[Any, Any], request: SubRequest
870870
) -> object | None:
871871
"""Perform fixture setup execution.
872872
@@ -894,7 +894,7 @@ def pytest_fixture_setup(
894894

895895

896896
def pytest_fixture_post_finalizer(
897-
fixturedef: FixtureDef[Any], request: SubRequest
897+
fixturedef: FixtureDef[Any, Any], request: SubRequest
898898
) -> None:
899899
"""Called after fixture teardown, but before the cache is cleared, so
900900
the fixture result ``fixturedef.cached_result`` is still available (not

0 commit comments

Comments
 (0)