diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index b0049f8cc..8aa5d73a8 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -10,6 +10,8 @@ from typing import Any from typing import Callable from typing import cast from typing import Dict +from typing import Generator +from typing import Generic from typing import Iterable from typing import Iterator from typing import List @@ -17,6 +19,7 @@ from typing import Optional from typing import Sequence from typing import Set from typing import Tuple +from typing import TypeVar from typing import Union import attr @@ -37,6 +40,7 @@ from _pytest.compat import getlocation from _pytest.compat import is_generator from _pytest.compat import NOTSET from _pytest.compat import order_preserving_dict +from _pytest.compat import overload from _pytest.compat import safe_getattr from _pytest.compat import TYPE_CHECKING from _pytest.config import _PluggyPlugin @@ -64,13 +68,30 @@ if TYPE_CHECKING: _Scope = Literal["session", "package", "module", "class", "function"] -_FixtureCachedResult = Tuple[ - # The result. - Optional[object], - # Cache key. - object, - # Exc info if raised. - Optional[Tuple["Type[BaseException]", BaseException, TracebackType]], +# The value of the fixture -- return/yield of the fixture function (type variable). +_FixtureValue = TypeVar("_FixtureValue") +# The type of the fixture function (type variable). +_FixtureFunction = TypeVar("_FixtureFunction", bound=Callable[..., object]) +# The type of a fixture function (type alias generic in fixture value). +_FixtureFunc = Union[ + Callable[..., _FixtureValue], Callable[..., Generator[_FixtureValue, None, None]] +] +# The type of FixtureDef.cached_result (type alias generic in fixture value). +_FixtureCachedResult = Union[ + Tuple[ + # The result. + _FixtureValue, + # Cache key. + object, + None, + ], + Tuple[ + None, + # Cache key. + object, + # Exc info if raised. + Tuple["Type[BaseException]", BaseException, TracebackType], + ], ] @@ -871,9 +892,13 @@ def fail_fixturefunc(fixturefunc, msg: str) -> "NoReturn": fail(msg + ":\n\n" + str(source.indent()) + "\n" + location, pytrace=False) -def call_fixture_func(fixturefunc, request: FixtureRequest, kwargs): - yieldctx = is_generator(fixturefunc) - if yieldctx: +def call_fixture_func( + fixturefunc: "_FixtureFunc[_FixtureValue]", request: FixtureRequest, kwargs +) -> _FixtureValue: + if is_generator(fixturefunc): + fixturefunc = cast( + Callable[..., Generator[_FixtureValue, None, None]], fixturefunc + ) generator = fixturefunc(**kwargs) try: fixture_result = next(generator) @@ -884,6 +909,7 @@ def call_fixture_func(fixturefunc, request: FixtureRequest, kwargs): finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, generator) request.addfinalizer(finalizer) else: + fixturefunc = cast(Callable[..., _FixtureValue], fixturefunc) fixture_result = fixturefunc(**kwargs) return fixture_result @@ -926,7 +952,7 @@ def _eval_scope_callable( return result -class FixtureDef: +class FixtureDef(Generic[_FixtureValue]): """ A container for a factory definition. """ def __init__( @@ -934,7 +960,7 @@ class FixtureDef: fixturemanager: "FixtureManager", baseid, argname: str, - func, + func: "_FixtureFunc[_FixtureValue]", scope: "Union[_Scope, Callable[[str, Config], _Scope]]", params: Optional[Sequence[object]], unittest: bool = False, @@ -966,7 +992,7 @@ class FixtureDef: ) # type: Tuple[str, ...] self.unittest = unittest self.ids = ids - self.cached_result = None # type: Optional[_FixtureCachedResult] + self.cached_result = None # type: Optional[_FixtureCachedResult[_FixtureValue]] self._finalizers = [] # type: List[Callable[[], object]] def addfinalizer(self, finalizer: Callable[[], object]) -> None: @@ -996,7 +1022,7 @@ class FixtureDef: self.cached_result = None self._finalizers = [] - def execute(self, request: SubRequest): + def execute(self, request: SubRequest) -> _FixtureValue: # get required arguments and register our own finish() # with their finalization for argname in self.argnames: @@ -1008,14 +1034,15 @@ class FixtureDef: my_cache_key = self.cache_key(request) if self.cached_result is not None: - result, cache_key, err = self.cached_result # note: comparison with `==` can fail (or be expensive) for e.g. # numpy arrays (#6497) + cache_key = self.cached_result[1] if my_cache_key is cache_key: - if err is not None: - _, val, tb = err + if self.cached_result[2] is not None: + _, val, tb = self.cached_result[2] raise val.with_traceback(tb) else: + result = self.cached_result[0] return result # we have a previous but differently parametrized fixture instance # so we need to tear it down before creating a new one @@ -1023,7 +1050,8 @@ class FixtureDef: assert self.cached_result is None hook = self._fixturemanager.session.gethookproxy(request.node.fspath) - return hook.pytest_fixture_setup(fixturedef=self, request=request) + result = hook.pytest_fixture_setup(fixturedef=self, request=request) + return result def cache_key(self, request: SubRequest) -> object: return request.param_index if not hasattr(request, "param") else request.param @@ -1034,7 +1062,9 @@ class FixtureDef: ) -def resolve_fixture_function(fixturedef: FixtureDef, request: FixtureRequest): +def resolve_fixture_function( + fixturedef: FixtureDef[_FixtureValue], request: FixtureRequest +) -> "_FixtureFunc[_FixtureValue]": """Gets the actual callable that can be called to obtain the fixture value, dealing with unittest-specific instances and bound methods. """ @@ -1042,7 +1072,7 @@ def resolve_fixture_function(fixturedef: FixtureDef, request: FixtureRequest): if fixturedef.unittest: if request.instance is not None: # bind the unbound method to the TestCase instance - fixturefunc = fixturedef.func.__get__(request.instance) + fixturefunc = fixturedef.func.__get__(request.instance) # type: ignore[union-attr] # noqa: F821 else: # the fixture function needs to be bound to the actual # request.instance so that code working with "fixturedef" behaves @@ -1051,16 +1081,18 @@ def resolve_fixture_function(fixturedef: FixtureDef, request: FixtureRequest): # handle the case where fixture is defined not in a test class, but some other class # (for example a plugin class with a fixture), see #2270 if hasattr(fixturefunc, "__self__") and not isinstance( - request.instance, fixturefunc.__self__.__class__ + request.instance, fixturefunc.__self__.__class__ # type: ignore[union-attr] # noqa: F821 ): return fixturefunc fixturefunc = getimfunc(fixturedef.func) if fixturefunc != fixturedef.func: - fixturefunc = fixturefunc.__get__(request.instance) + fixturefunc = fixturefunc.__get__(request.instance) # type: ignore[union-attr] # noqa: F821 return fixturefunc -def pytest_fixture_setup(fixturedef: FixtureDef, request: SubRequest) -> object: +def pytest_fixture_setup( + fixturedef: FixtureDef[_FixtureValue], request: SubRequest +) -> _FixtureValue: """ Execution of fixture setup. """ kwargs = {} for argname in fixturedef.argnames: @@ -1146,7 +1178,7 @@ class FixtureFunctionMarker: ) name = attr.ib(type=Optional[str], default=None) - def __call__(self, function): + def __call__(self, function: _FixtureFunction) -> _FixtureFunction: if inspect.isclass(function): raise ValueError("class fixtures not supported (maybe in the future)") @@ -1166,12 +1198,50 @@ class FixtureFunctionMarker: ), pytrace=False, ) - function._pytestfixturefunction = self + + # Type ignored because https://github.com/python/mypy/issues/2087. + function._pytestfixturefunction = self # type: ignore[attr-defined] # noqa: F821 return function +@overload def fixture( - fixture_function=None, + fixture_function: _FixtureFunction, + *, + scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = ..., + params: Optional[Iterable[object]] = ..., + autouse: bool = ..., + ids: Optional[ + Union[ + Iterable[Union[None, str, float, int, bool]], + Callable[[object], Optional[object]], + ] + ] = ..., + name: Optional[str] = ... +) -> _FixtureFunction: + raise NotImplementedError() + + +@overload # noqa: F811 +def fixture( # noqa: F811 + fixture_function: None = ..., + *, + scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = ..., + params: Optional[Iterable[object]] = ..., + autouse: bool = ..., + ids: Optional[ + Union[ + Iterable[Union[None, str, float, int, bool]], + Callable[[object], Optional[object]], + ] + ] = ..., + name: Optional[str] = None +) -> FixtureFunctionMarker: + raise NotImplementedError() + + +def fixture( # noqa: F811 + fixture_function: Optional[_FixtureFunction] = None, *args: Any, scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = "function", params: Optional[Iterable[object]] = None, @@ -1183,7 +1253,7 @@ def fixture( ] ] = None, name: Optional[str] = None -): +) -> Union[FixtureFunctionMarker, _FixtureFunction]: """Decorator to mark a fixture factory function. This decorator can be used, with or without parameters, to define a @@ -1317,7 +1387,7 @@ def yield_fixture( @fixture(scope="session") -def pytestconfig(request: FixtureRequest): +def pytestconfig(request: FixtureRequest) -> Config: """Session-scoped fixture that returns the :class:`_pytest.config.Config` object. Example:: diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 7fc87e387..353ce46cd 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -3799,7 +3799,7 @@ class TestScopeOrdering: request = FixtureRequest(items[0]) assert request.fixturenames == "m1 f1".split() - def test_func_closure_with_native_fixtures(self, testdir, monkeypatch): + def test_func_closure_with_native_fixtures(self, testdir, monkeypatch) -> None: """Sanity check that verifies the order returned by the closures and the actual fixture execution order: The execution order may differ because of fixture inter-dependencies. """ @@ -3849,9 +3849,8 @@ class TestScopeOrdering: ) testdir.runpytest() # actual fixture execution differs: dependent fixtures must be created first ("my_tmpdir") - assert ( - pytest.FIXTURE_ORDER == "s1 my_tmpdir_factory p1 m1 my_tmpdir f1 f2".split() - ) + FIXTURE_ORDER = pytest.FIXTURE_ORDER # type: ignore[attr-defined] # noqa: F821 + assert FIXTURE_ORDER == "s1 my_tmpdir_factory p1 m1 my_tmpdir f1 f2".split() def test_func_closure_module(self, testdir): testdir.makepyfile( @@ -4159,7 +4158,7 @@ def test_fixture_duplicated_arguments() -> None: """Raise error if there are positional and keyword arguments for the same parameter (#1682).""" with pytest.raises(TypeError) as excinfo: - @pytest.fixture("session", scope="session") + @pytest.fixture("session", scope="session") # type: ignore[call-overload] # noqa: F821 def arg(arg): pass @@ -4171,7 +4170,7 @@ def test_fixture_duplicated_arguments() -> None: with pytest.raises(TypeError) as excinfo: - @pytest.fixture( + @pytest.fixture( # type: ignore[call-overload] # noqa: F821 "function", ["p1"], True, @@ -4199,7 +4198,7 @@ def test_fixture_with_positionals() -> None: with pytest.warns(pytest.PytestDeprecationWarning) as warnings: - @pytest.fixture("function", [0], True) + @pytest.fixture("function", [0], True) # type: ignore[call-overload] # noqa: F821 def fixture_with_positionals(): pass @@ -4213,7 +4212,7 @@ def test_fixture_with_positionals() -> None: def test_fixture_with_too_many_positionals() -> None: with pytest.raises(TypeError) as excinfo: - @pytest.fixture("function", [0], True, ["id"], "name", "extra") + @pytest.fixture("function", [0], True, ["id"], "name", "extra") # type: ignore[call-overload] # noqa: F821 def fixture_with_positionals(): pass