Skip to content

Commit 2833884

Browse files
committed
Type annotate pytest.fixture and more improvements to _pytest.fixtures
1 parent 8bcf1d6 commit 2833884

File tree

2 files changed

+105
-36
lines changed

2 files changed

+105
-36
lines changed

src/_pytest/fixtures.py

Lines changed: 98 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@
1010
from typing import Callable
1111
from typing import cast
1212
from typing import Dict
13+
from typing import Generator
14+
from typing import Generic
1315
from typing import Iterable
1416
from typing import Iterator
1517
from typing import List
1618
from typing import Optional
1719
from typing import Sequence
1820
from typing import Set
1921
from typing import Tuple
22+
from typing import TypeVar
2023
from typing import Union
2124

2225
import attr
@@ -37,6 +40,7 @@
3740
from _pytest.compat import is_generator
3841
from _pytest.compat import NOTSET
3942
from _pytest.compat import order_preserving_dict
43+
from _pytest.compat import overload
4044
from _pytest.compat import safe_getattr
4145
from _pytest.compat import TYPE_CHECKING
4246
from _pytest.config import _PluggyPlugin
@@ -64,13 +68,30 @@
6468
_Scope = Literal["session", "package", "module", "class", "function"]
6569

6670

67-
_FixtureCachedResult = Tuple[
68-
# The result.
69-
Optional[object],
70-
# Cache key.
71-
object,
72-
# Exc info if raised.
73-
Optional[Tuple["Type[BaseException]", BaseException, TracebackType]],
71+
# The value of the fixture -- return/yield of the fixture function (type variable).
72+
_FixtureValue = TypeVar("_FixtureValue")
73+
# The type of the fixture function (type variable).
74+
_FixtureFunction = TypeVar("_FixtureFunction", bound=Callable[..., object])
75+
# The type of a fixture function (type alias generic in fixture value).
76+
_FixtureFunc = Union[
77+
Callable[..., _FixtureValue], Callable[..., Generator[_FixtureValue, None, None]]
78+
]
79+
# The type of FixtureDef.cached_result (type alias generic in fixture value).
80+
_FixtureCachedResult = Union[
81+
Tuple[
82+
# The result.
83+
_FixtureValue,
84+
# Cache key.
85+
object,
86+
None,
87+
],
88+
Tuple[
89+
None,
90+
# Cache key.
91+
object,
92+
# Exc info if raised.
93+
Tuple["Type[BaseException]", BaseException, TracebackType],
94+
],
7495
]
7596

7697

@@ -871,9 +892,13 @@ def fail_fixturefunc(fixturefunc, msg: str) -> "NoReturn":
871892
fail(msg + ":\n\n" + str(source.indent()) + "\n" + location, pytrace=False)
872893

873894

874-
def call_fixture_func(fixturefunc, request: FixtureRequest, kwargs):
875-
yieldctx = is_generator(fixturefunc)
876-
if yieldctx:
895+
def call_fixture_func(
896+
fixturefunc: "_FixtureFunc[_FixtureValue]", request: FixtureRequest, kwargs
897+
) -> _FixtureValue:
898+
if is_generator(fixturefunc):
899+
fixturefunc = cast(
900+
Callable[..., Generator[_FixtureValue, None, None]], fixturefunc
901+
)
877902
generator = fixturefunc(**kwargs)
878903
try:
879904
fixture_result = next(generator)
@@ -884,6 +909,7 @@ def call_fixture_func(fixturefunc, request: FixtureRequest, kwargs):
884909
finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, generator)
885910
request.addfinalizer(finalizer)
886911
else:
912+
fixturefunc = cast(Callable[..., _FixtureValue], fixturefunc)
887913
fixture_result = fixturefunc(**kwargs)
888914
return fixture_result
889915

@@ -926,15 +952,15 @@ def _eval_scope_callable(
926952
return result
927953

928954

929-
class FixtureDef:
955+
class FixtureDef(Generic[_FixtureValue]):
930956
""" A container for a factory definition. """
931957

932958
def __init__(
933959
self,
934960
fixturemanager: "FixtureManager",
935961
baseid,
936962
argname: str,
937-
func,
963+
func: "_FixtureFunc[_FixtureValue]",
938964
scope: "Union[_Scope, Callable[[str, Config], _Scope]]",
939965
params: Optional[Sequence[object]],
940966
unittest: bool = False,
@@ -966,7 +992,7 @@ def __init__(
966992
) # type: Tuple[str, ...]
967993
self.unittest = unittest
968994
self.ids = ids
969-
self.cached_result = None # type: Optional[_FixtureCachedResult]
995+
self.cached_result = None # type: Optional[_FixtureCachedResult[_FixtureValue]]
970996
self._finalizers = [] # type: List[Callable[[], object]]
971997

972998
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
@@ -996,7 +1022,7 @@ def finish(self, request: SubRequest) -> None:
9961022
self.cached_result = None
9971023
self._finalizers = []
9981024

999-
def execute(self, request: SubRequest):
1025+
def execute(self, request: SubRequest) -> _FixtureValue:
10001026
# get required arguments and register our own finish()
10011027
# with their finalization
10021028
for argname in self.argnames:
@@ -1008,22 +1034,24 @@ def execute(self, request: SubRequest):
10081034

10091035
my_cache_key = self.cache_key(request)
10101036
if self.cached_result is not None:
1011-
result, cache_key, err = self.cached_result
10121037
# note: comparison with `==` can fail (or be expensive) for e.g.
10131038
# numpy arrays (#6497)
1039+
cache_key = self.cached_result[1]
10141040
if my_cache_key is cache_key:
1015-
if err is not None:
1016-
_, val, tb = err
1041+
if self.cached_result[2] is not None:
1042+
_, val, tb = self.cached_result[2]
10171043
raise val.with_traceback(tb)
10181044
else:
1045+
result = self.cached_result[0]
10191046
return result
10201047
# we have a previous but differently parametrized fixture instance
10211048
# so we need to tear it down before creating a new one
10221049
self.finish(request)
10231050
assert self.cached_result is None
10241051

10251052
hook = self._fixturemanager.session.gethookproxy(request.node.fspath)
1026-
return hook.pytest_fixture_setup(fixturedef=self, request=request)
1053+
result = hook.pytest_fixture_setup(fixturedef=self, request=request)
1054+
return result
10271055

10281056
def cache_key(self, request: SubRequest) -> object:
10291057
return request.param_index if not hasattr(request, "param") else request.param
@@ -1034,15 +1062,17 @@ def __repr__(self) -> str:
10341062
)
10351063

10361064

1037-
def resolve_fixture_function(fixturedef: FixtureDef, request: FixtureRequest):
1065+
def resolve_fixture_function(
1066+
fixturedef: FixtureDef[_FixtureValue], request: FixtureRequest
1067+
) -> "_FixtureFunc[_FixtureValue]":
10381068
"""Gets the actual callable that can be called to obtain the fixture value, dealing with unittest-specific
10391069
instances and bound methods.
10401070
"""
10411071
fixturefunc = fixturedef.func
10421072
if fixturedef.unittest:
10431073
if request.instance is not None:
10441074
# bind the unbound method to the TestCase instance
1045-
fixturefunc = fixturedef.func.__get__(request.instance)
1075+
fixturefunc = fixturedef.func.__get__(request.instance) # type: ignore[union-attr] # noqa: F821
10461076
else:
10471077
# the fixture function needs to be bound to the actual
10481078
# request.instance so that code working with "fixturedef" behaves
@@ -1051,16 +1081,18 @@ def resolve_fixture_function(fixturedef: FixtureDef, request: FixtureRequest):
10511081
# handle the case where fixture is defined not in a test class, but some other class
10521082
# (for example a plugin class with a fixture), see #2270
10531083
if hasattr(fixturefunc, "__self__") and not isinstance(
1054-
request.instance, fixturefunc.__self__.__class__
1084+
request.instance, fixturefunc.__self__.__class__ # type: ignore[union-attr] # noqa: F821
10551085
):
10561086
return fixturefunc
10571087
fixturefunc = getimfunc(fixturedef.func)
10581088
if fixturefunc != fixturedef.func:
1059-
fixturefunc = fixturefunc.__get__(request.instance)
1089+
fixturefunc = fixturefunc.__get__(request.instance) # type: ignore[union-attr] # noqa: F821
10601090
return fixturefunc
10611091

10621092

1063-
def pytest_fixture_setup(fixturedef: FixtureDef, request: SubRequest) -> object:
1093+
def pytest_fixture_setup(
1094+
fixturedef: FixtureDef[_FixtureValue], request: SubRequest
1095+
) -> _FixtureValue:
10641096
""" Execution of fixture setup. """
10651097
kwargs = {}
10661098
for argname in fixturedef.argnames:
@@ -1146,7 +1178,7 @@ class FixtureFunctionMarker:
11461178
)
11471179
name = attr.ib(type=Optional[str], default=None)
11481180

1149-
def __call__(self, function):
1181+
def __call__(self, function: _FixtureFunction) -> _FixtureFunction:
11501182
if inspect.isclass(function):
11511183
raise ValueError("class fixtures not supported (maybe in the future)")
11521184

@@ -1166,12 +1198,50 @@ def __call__(self, function):
11661198
),
11671199
pytrace=False,
11681200
)
1169-
function._pytestfixturefunction = self
1201+
1202+
# Type ignored because https://github.com/python/mypy/issues/2087.
1203+
function._pytestfixturefunction = self # type: ignore[attr-defined] # noqa: F821
11701204
return function
11711205

11721206

1207+
@overload
11731208
def fixture(
1174-
fixture_function=None,
1209+
fixture_function: _FixtureFunction,
1210+
*,
1211+
scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = ...,
1212+
params: Optional[Iterable[object]] = ...,
1213+
autouse: bool = ...,
1214+
ids: Optional[
1215+
Union[
1216+
Iterable[Union[None, str, float, int, bool]],
1217+
Callable[[object], Optional[object]],
1218+
]
1219+
] = ...,
1220+
name: Optional[str] = ...
1221+
) -> _FixtureFunction:
1222+
raise NotImplementedError()
1223+
1224+
1225+
@overload # noqa: F811
1226+
def fixture( # noqa: F811
1227+
fixture_function: None = ...,
1228+
*,
1229+
scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = ...,
1230+
params: Optional[Iterable[object]] = ...,
1231+
autouse: bool = ...,
1232+
ids: Optional[
1233+
Union[
1234+
Iterable[Union[None, str, float, int, bool]],
1235+
Callable[[object], Optional[object]],
1236+
]
1237+
] = ...,
1238+
name: Optional[str] = None
1239+
) -> FixtureFunctionMarker:
1240+
raise NotImplementedError()
1241+
1242+
1243+
def fixture( # noqa: F811
1244+
fixture_function: Optional[_FixtureFunction] = None,
11751245
*args: Any,
11761246
scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = "function",
11771247
params: Optional[Iterable[object]] = None,
@@ -1183,7 +1253,7 @@ def fixture(
11831253
]
11841254
] = None,
11851255
name: Optional[str] = None
1186-
):
1256+
) -> Union[FixtureFunctionMarker, _FixtureFunction]:
11871257
"""Decorator to mark a fixture factory function.
11881258
11891259
This decorator can be used, with or without parameters, to define a
@@ -1317,7 +1387,7 @@ def yield_fixture(
13171387

13181388

13191389
@fixture(scope="session")
1320-
def pytestconfig(request: FixtureRequest):
1390+
def pytestconfig(request: FixtureRequest) -> Config:
13211391
"""Session-scoped fixture that returns the :class:`_pytest.config.Config` object.
13221392
13231393
Example::

testing/python/fixtures.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3799,7 +3799,7 @@ def test_func(m1):
37993799
request = FixtureRequest(items[0])
38003800
assert request.fixturenames == "m1 f1".split()
38013801

3802-
def test_func_closure_with_native_fixtures(self, testdir, monkeypatch):
3802+
def test_func_closure_with_native_fixtures(self, testdir, monkeypatch) -> None:
38033803
"""Sanity check that verifies the order returned by the closures and the actual fixture execution order:
38043804
The execution order may differ because of fixture inter-dependencies.
38053805
"""
@@ -3849,9 +3849,8 @@ def test_foo(f1, p1, m1, f2, s1): pass
38493849
)
38503850
testdir.runpytest()
38513851
# actual fixture execution differs: dependent fixtures must be created first ("my_tmpdir")
3852-
assert (
3853-
pytest.FIXTURE_ORDER == "s1 my_tmpdir_factory p1 m1 my_tmpdir f1 f2".split()
3854-
)
3852+
FIXTURE_ORDER = pytest.FIXTURE_ORDER # type: ignore[attr-defined] # noqa: F821
3853+
assert FIXTURE_ORDER == "s1 my_tmpdir_factory p1 m1 my_tmpdir f1 f2".split()
38553854

38563855
def test_func_closure_module(self, testdir):
38573856
testdir.makepyfile(
@@ -4159,7 +4158,7 @@ def test_fixture_duplicated_arguments() -> None:
41594158
"""Raise error if there are positional and keyword arguments for the same parameter (#1682)."""
41604159
with pytest.raises(TypeError) as excinfo:
41614160

4162-
@pytest.fixture("session", scope="session")
4161+
@pytest.fixture("session", scope="session") # type: ignore[call-overload] # noqa: F821
41634162
def arg(arg):
41644163
pass
41654164

@@ -4171,7 +4170,7 @@ def arg(arg):
41714170

41724171
with pytest.raises(TypeError) as excinfo:
41734172

4174-
@pytest.fixture(
4173+
@pytest.fixture( # type: ignore[call-overload] # noqa: F821
41754174
"function",
41764175
["p1"],
41774176
True,
@@ -4199,7 +4198,7 @@ def test_fixture_with_positionals() -> None:
41994198

42004199
with pytest.warns(pytest.PytestDeprecationWarning) as warnings:
42014200

4202-
@pytest.fixture("function", [0], True)
4201+
@pytest.fixture("function", [0], True) # type: ignore[call-overload] # noqa: F821
42034202
def fixture_with_positionals():
42044203
pass
42054204

@@ -4213,7 +4212,7 @@ def fixture_with_positionals():
42134212
def test_fixture_with_too_many_positionals() -> None:
42144213
with pytest.raises(TypeError) as excinfo:
42154214

4216-
@pytest.fixture("function", [0], True, ["id"], "name", "extra")
4215+
@pytest.fixture("function", [0], True, ["id"], "name", "extra") # type: ignore[call-overload] # noqa: F821
42174216
def fixture_with_positionals():
42184217
pass
42194218

0 commit comments

Comments
 (0)