Type annotate pytest.fixture and more improvements to _pytest.fixtures

This commit is contained in:
Ran Benita 2020-05-01 14:40:16 +03:00
parent 8bcf1d6de1
commit 2833884688
2 changed files with 105 additions and 36 deletions

View File

@ -10,6 +10,8 @@ from typing import Any
from typing import Callable from typing import Callable
from typing import cast from typing import cast
from typing import Dict from typing import Dict
from typing import Generator
from typing import Generic
from typing import Iterable from typing import Iterable
from typing import Iterator from typing import Iterator
from typing import List from typing import List
@ -17,6 +19,7 @@ from typing import Optional
from typing import Sequence from typing import Sequence
from typing import Set from typing import Set
from typing import Tuple from typing import Tuple
from typing import TypeVar
from typing import Union from typing import Union
import attr import attr
@ -37,6 +40,7 @@ from _pytest.compat import getlocation
from _pytest.compat import is_generator from _pytest.compat import is_generator
from _pytest.compat import NOTSET from _pytest.compat import NOTSET
from _pytest.compat import order_preserving_dict from _pytest.compat import order_preserving_dict
from _pytest.compat import overload
from _pytest.compat import safe_getattr from _pytest.compat import safe_getattr
from _pytest.compat import TYPE_CHECKING from _pytest.compat import TYPE_CHECKING
from _pytest.config import _PluggyPlugin from _pytest.config import _PluggyPlugin
@ -64,13 +68,30 @@ if TYPE_CHECKING:
_Scope = Literal["session", "package", "module", "class", "function"] _Scope = Literal["session", "package", "module", "class", "function"]
_FixtureCachedResult = Tuple[ # 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. # The result.
Optional[object], _FixtureValue,
# Cache key.
object,
None,
],
Tuple[
None,
# Cache key. # Cache key.
object, object,
# Exc info if raised. # Exc info if raised.
Optional[Tuple["Type[BaseException]", BaseException, TracebackType]], 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) fail(msg + ":\n\n" + str(source.indent()) + "\n" + location, pytrace=False)
def call_fixture_func(fixturefunc, request: FixtureRequest, kwargs): def call_fixture_func(
yieldctx = is_generator(fixturefunc) fixturefunc: "_FixtureFunc[_FixtureValue]", request: FixtureRequest, kwargs
if yieldctx: ) -> _FixtureValue:
if is_generator(fixturefunc):
fixturefunc = cast(
Callable[..., Generator[_FixtureValue, None, None]], fixturefunc
)
generator = fixturefunc(**kwargs) generator = fixturefunc(**kwargs)
try: try:
fixture_result = next(generator) fixture_result = next(generator)
@ -884,6 +909,7 @@ def call_fixture_func(fixturefunc, request: FixtureRequest, kwargs):
finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, generator) finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, generator)
request.addfinalizer(finalizer) request.addfinalizer(finalizer)
else: else:
fixturefunc = cast(Callable[..., _FixtureValue], fixturefunc)
fixture_result = fixturefunc(**kwargs) fixture_result = fixturefunc(**kwargs)
return fixture_result return fixture_result
@ -926,7 +952,7 @@ def _eval_scope_callable(
return result return result
class FixtureDef: class FixtureDef(Generic[_FixtureValue]):
""" A container for a factory definition. """ """ A container for a factory definition. """
def __init__( def __init__(
@ -934,7 +960,7 @@ class FixtureDef:
fixturemanager: "FixtureManager", fixturemanager: "FixtureManager",
baseid, baseid,
argname: str, argname: str,
func, func: "_FixtureFunc[_FixtureValue]",
scope: "Union[_Scope, Callable[[str, Config], _Scope]]", scope: "Union[_Scope, Callable[[str, Config], _Scope]]",
params: Optional[Sequence[object]], params: Optional[Sequence[object]],
unittest: bool = False, unittest: bool = False,
@ -966,7 +992,7 @@ class FixtureDef:
) # type: Tuple[str, ...] ) # type: Tuple[str, ...]
self.unittest = unittest self.unittest = unittest
self.ids = ids self.ids = ids
self.cached_result = None # type: Optional[_FixtureCachedResult] self.cached_result = None # type: Optional[_FixtureCachedResult[_FixtureValue]]
self._finalizers = [] # type: List[Callable[[], object]] self._finalizers = [] # type: List[Callable[[], object]]
def addfinalizer(self, finalizer: Callable[[], object]) -> None: def addfinalizer(self, finalizer: Callable[[], object]) -> None:
@ -996,7 +1022,7 @@ class FixtureDef:
self.cached_result = None self.cached_result = None
self._finalizers = [] self._finalizers = []
def execute(self, request: SubRequest): def execute(self, request: SubRequest) -> _FixtureValue:
# get required arguments and register our own finish() # get required arguments and register our own finish()
# with their finalization # with their finalization
for argname in self.argnames: for argname in self.argnames:
@ -1008,14 +1034,15 @@ class FixtureDef:
my_cache_key = self.cache_key(request) my_cache_key = self.cache_key(request)
if self.cached_result is not None: 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. # note: comparison with `==` can fail (or be expensive) for e.g.
# numpy arrays (#6497) # numpy arrays (#6497)
cache_key = self.cached_result[1]
if my_cache_key is cache_key: if my_cache_key is cache_key:
if err is not None: if self.cached_result[2] is not None:
_, val, tb = err _, val, tb = self.cached_result[2]
raise val.with_traceback(tb) raise val.with_traceback(tb)
else: else:
result = self.cached_result[0]
return result return result
# we have a previous but differently parametrized fixture instance # we have a previous but differently parametrized fixture instance
# so we need to tear it down before creating a new one # so we need to tear it down before creating a new one
@ -1023,7 +1050,8 @@ class FixtureDef:
assert self.cached_result is None assert self.cached_result is None
hook = self._fixturemanager.session.gethookproxy(request.node.fspath) 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: def cache_key(self, request: SubRequest) -> object:
return request.param_index if not hasattr(request, "param") else request.param 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 """Gets the actual callable that can be called to obtain the fixture value, dealing with unittest-specific
instances and bound methods. instances and bound methods.
""" """
@ -1042,7 +1072,7 @@ def resolve_fixture_function(fixturedef: FixtureDef, request: FixtureRequest):
if fixturedef.unittest: if fixturedef.unittest:
if request.instance is not None: if request.instance is not None:
# bind the unbound method to the TestCase instance # 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: else:
# the fixture function needs to be bound to the actual # the fixture function needs to be bound to the actual
# request.instance so that code working with "fixturedef" behaves # 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 # 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 # (for example a plugin class with a fixture), see #2270
if hasattr(fixturefunc, "__self__") and not isinstance( 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 return fixturefunc
fixturefunc = getimfunc(fixturedef.func) fixturefunc = getimfunc(fixturedef.func)
if fixturefunc != fixturedef.func: if fixturefunc != fixturedef.func:
fixturefunc = fixturefunc.__get__(request.instance) fixturefunc = fixturefunc.__get__(request.instance) # type: ignore[union-attr] # noqa: F821
return fixturefunc 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. """ """ Execution of fixture setup. """
kwargs = {} kwargs = {}
for argname in fixturedef.argnames: for argname in fixturedef.argnames:
@ -1146,7 +1178,7 @@ class FixtureFunctionMarker:
) )
name = attr.ib(type=Optional[str], default=None) name = attr.ib(type=Optional[str], default=None)
def __call__(self, function): def __call__(self, function: _FixtureFunction) -> _FixtureFunction:
if inspect.isclass(function): if inspect.isclass(function):
raise ValueError("class fixtures not supported (maybe in the future)") raise ValueError("class fixtures not supported (maybe in the future)")
@ -1166,12 +1198,50 @@ class FixtureFunctionMarker:
), ),
pytrace=False, 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 return function
@overload
def fixture( 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, *args: Any,
scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = "function", scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = "function",
params: Optional[Iterable[object]] = None, params: Optional[Iterable[object]] = None,
@ -1183,7 +1253,7 @@ def fixture(
] ]
] = None, ] = None,
name: Optional[str] = None name: Optional[str] = None
): ) -> Union[FixtureFunctionMarker, _FixtureFunction]:
"""Decorator to mark a fixture factory function. """Decorator to mark a fixture factory function.
This decorator can be used, with or without parameters, to define a This decorator can be used, with or without parameters, to define a
@ -1317,7 +1387,7 @@ def yield_fixture(
@fixture(scope="session") @fixture(scope="session")
def pytestconfig(request: FixtureRequest): def pytestconfig(request: FixtureRequest) -> Config:
"""Session-scoped fixture that returns the :class:`_pytest.config.Config` object. """Session-scoped fixture that returns the :class:`_pytest.config.Config` object.
Example:: Example::

View File

@ -3799,7 +3799,7 @@ class TestScopeOrdering:
request = FixtureRequest(items[0]) request = FixtureRequest(items[0])
assert request.fixturenames == "m1 f1".split() 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: """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. The execution order may differ because of fixture inter-dependencies.
""" """
@ -3849,9 +3849,8 @@ class TestScopeOrdering:
) )
testdir.runpytest() testdir.runpytest()
# actual fixture execution differs: dependent fixtures must be created first ("my_tmpdir") # actual fixture execution differs: dependent fixtures must be created first ("my_tmpdir")
assert ( FIXTURE_ORDER = pytest.FIXTURE_ORDER # type: ignore[attr-defined] # noqa: F821
pytest.FIXTURE_ORDER == "s1 my_tmpdir_factory p1 m1 my_tmpdir f1 f2".split() assert FIXTURE_ORDER == "s1 my_tmpdir_factory p1 m1 my_tmpdir f1 f2".split()
)
def test_func_closure_module(self, testdir): def test_func_closure_module(self, testdir):
testdir.makepyfile( 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).""" """Raise error if there are positional and keyword arguments for the same parameter (#1682)."""
with pytest.raises(TypeError) as excinfo: 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): def arg(arg):
pass pass
@ -4171,7 +4170,7 @@ def test_fixture_duplicated_arguments() -> None:
with pytest.raises(TypeError) as excinfo: with pytest.raises(TypeError) as excinfo:
@pytest.fixture( @pytest.fixture( # type: ignore[call-overload] # noqa: F821
"function", "function",
["p1"], ["p1"],
True, True,
@ -4199,7 +4198,7 @@ def test_fixture_with_positionals() -> None:
with pytest.warns(pytest.PytestDeprecationWarning) as warnings: 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(): def fixture_with_positionals():
pass pass
@ -4213,7 +4212,7 @@ def test_fixture_with_positionals() -> None:
def test_fixture_with_too_many_positionals() -> None: def test_fixture_with_too_many_positionals() -> None:
with pytest.raises(TypeError) as excinfo: 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(): def fixture_with_positionals():
pass pass