From 0303285d7c554435a701d056fb907092f50371c5 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 26 Nov 2024 11:02:41 -0300 Subject: [PATCH 1/4] typing: Improve FixtureDefinition and FixtureDef * Carry around parameters and return value in `FixtureFunctionDefinition`. * Add `FixtureParams` to `FixtureDef`. Follow up to #12473. --- pyproject.toml | 1 + src/_pytest/fixtures.py | 108 +++++++++++++++++++++---------------- src/_pytest/hookspec.py | 4 +- src/_pytest/python.py | 6 +-- src/_pytest/setuponly.py | 7 +-- src/_pytest/setupplan.py | 4 +- testing/python/metafunc.py | 4 +- 7 files changed, 78 insertions(+), 56 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index dce6a0870e1..0b0164862a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,7 @@ dependencies = [ "packaging", "pluggy>=1.5,<2", "tomli>=1; python_version<'3.11'", + "typing-extensions; python_version<'3.10'", ] optional-dependencies.dev = [ "argcomplete", diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 8b79dbcb932..3a4b6fe5e62 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -75,6 +75,13 @@ if sys.version_info < (3, 11): from exceptiongroup import BaseExceptionGroup +if sys.version_info < (3, 10): + from typing_extensions import ParamSpec + from typing_extensions import TypeAlias +else: + from typing import ParamSpec + from typing import TypeAlias + if TYPE_CHECKING: from _pytest.python import CallSpec2 @@ -84,14 +91,17 @@ # 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]] + +# The parameters that a fixture function receives. +FixtureParams = ParamSpec("FixtureParams") + +# The type of fixture function (type alias generic in fixture params and value). +_FixtureFunc: TypeAlias = Union[ + Callable[FixtureParams, FixtureValue], + Callable[FixtureParams, Generator[FixtureValue, None, None]], ] # The type of FixtureDef.cached_result (type alias generic in fixture value). -_FixtureCachedResult = Union[ +_FixtureCachedResult: TypeAlias = Union[ tuple[ # The result. FixtureValue, @@ -121,7 +131,7 @@ def pytest_sessionstart(session: Session) -> None: def get_scope_package( node: nodes.Item, - fixturedef: FixtureDef[object], + fixturedef: FixtureDef[Any, object], ) -> nodes.Node | None: from _pytest.python import Package @@ -318,7 +328,7 @@ class FuncFixtureInfo: # matching the name which are applicable to this function. # There may be multiple overriding fixtures with the same name. The # sequence is ordered from furthest to closes to the function. - name2fixturedefs: dict[str, Sequence[FixtureDef[Any]]] + name2fixturedefs: dict[str, Sequence[FixtureDef[Any, Any]]] def prune_dependency_tree(self) -> None: """Recompute names_closure from initialnames and name2fixturedefs. @@ -359,8 +369,8 @@ def __init__( self, pyfuncitem: Function, fixturename: str | None, - arg2fixturedefs: dict[str, Sequence[FixtureDef[Any]]], - fixture_defs: dict[str, FixtureDef[Any]], + arg2fixturedefs: dict[str, Sequence[FixtureDef[Any, Any]]], + fixture_defs: dict[str, FixtureDef[Any, Any]], *, _ispytest: bool = False, ) -> None: @@ -403,7 +413,7 @@ def scope(self) -> _ScopeName: @abc.abstractmethod def _check_scope( self, - requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object], + requested_fixturedef: FixtureDef[Any, object] | PseudoFixtureDef[object], requested_scope: Scope, ) -> None: raise NotImplementedError() @@ -544,7 +554,7 @@ def _iter_chain(self) -> Iterator[SubRequest]: def _get_active_fixturedef( self, argname: str - ) -> FixtureDef[object] | PseudoFixtureDef[object]: + ) -> FixtureDef[Any, object] | PseudoFixtureDef[object]: if argname == "request": cached_result = (self, [0], None) return PseudoFixtureDef(cached_result, Scope.Function) @@ -616,7 +626,9 @@ def _get_active_fixturedef( self._fixture_defs[argname] = fixturedef return fixturedef - def _check_fixturedef_without_param(self, fixturedef: FixtureDef[object]) -> None: + def _check_fixturedef_without_param( + self, fixturedef: FixtureDef[Any, object] + ) -> None: """Check that this request is allowed to execute this fixturedef without a param.""" funcitem = self._pyfuncitem @@ -649,7 +661,7 @@ def _check_fixturedef_without_param(self, fixturedef: FixtureDef[object]) -> Non ) fail(msg, pytrace=False) - def _get_fixturestack(self) -> list[FixtureDef[Any]]: + def _get_fixturestack(self) -> list[FixtureDef[Any, Any]]: values = [request._fixturedef for request in self._iter_chain()] values.reverse() return values @@ -674,7 +686,7 @@ def _scope(self) -> Scope: def _check_scope( self, - requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object], + requested_fixturedef: FixtureDef[Any, object] | PseudoFixtureDef[object], requested_scope: Scope, ) -> None: # TopRequest always has function scope so always valid. @@ -708,7 +720,7 @@ def __init__( scope: Scope, param: Any, param_index: int, - fixturedef: FixtureDef[object], + fixturedef: FixtureDef[Any, object], *, _ispytest: bool = False, ) -> None: @@ -721,7 +733,7 @@ def __init__( ) self._parent_request: Final[FixtureRequest] = request self._scope_field: Final = scope - self._fixturedef: Final[FixtureDef[object]] = fixturedef + self._fixturedef: Final[FixtureDef[Any, object]] = fixturedef if param is not NOTSET: self.param = param self.param_index: Final = param_index @@ -751,7 +763,7 @@ def node(self): def _check_scope( self, - requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object], + requested_fixturedef: FixtureDef[Any, object] | PseudoFixtureDef[object], requested_scope: Scope, ) -> None: if isinstance(requested_fixturedef, PseudoFixtureDef): @@ -772,7 +784,7 @@ def _check_scope( pytrace=False, ) - def _format_fixturedef_line(self, fixturedef: FixtureDef[object]) -> str: + def _format_fixturedef_line(self, fixturedef: FixtureDef[Any, object]) -> str: factory = fixturedef.func path, lineno = getfslineno(factory) if isinstance(path, Path): @@ -886,7 +898,9 @@ def toterminal(self, tw: TerminalWriter) -> None: def call_fixture_func( - fixturefunc: _FixtureFunc[FixtureValue], request: FixtureRequest, kwargs + fixturefunc: _FixtureFunc[FixtureParams, FixtureValue], + request: FixtureRequest, + kwargs: FixtureParams.kwargs, ) -> FixtureValue: if inspect.isgeneratorfunction(fixturefunc): fixturefunc = cast(Callable[..., Generator[FixtureValue]], fixturefunc) @@ -945,9 +959,11 @@ def _eval_scope_callable( @final -class FixtureDef(Generic[FixtureValue]): +class FixtureDef(Generic[FixtureParams, FixtureValue]): """A container for a fixture definition. + This is a generic class parametrized on the parameters that a fixture function receives and its return value. + Note: At this time, only explicitly documented fields and methods are considered public stable API. """ @@ -957,7 +973,7 @@ def __init__( config: Config, baseid: str | None, argname: str, - func: _FixtureFunc[FixtureValue], + func: _FixtureFunc[FixtureParams, FixtureValue], scope: Scope | _ScopeName | Callable[[str, Config], _ScopeName] | None, params: Sequence[object] | None, ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None, @@ -1112,8 +1128,8 @@ def __repr__(self) -> str: def resolve_fixture_function( - fixturedef: FixtureDef[FixtureValue], request: FixtureRequest -) -> _FixtureFunc[FixtureValue]: + fixturedef: FixtureDef[FixtureParams, FixtureValue], request: FixtureRequest +) -> _FixtureFunc[FixtureParams, FixtureValue]: """Get the actual callable that can be called to obtain the fixture value.""" fixturefunc = fixturedef.func @@ -1136,7 +1152,7 @@ def resolve_fixture_function( def pytest_fixture_setup( - fixturedef: FixtureDef[FixtureValue], request: SubRequest + fixturedef: FixtureDef[FixtureParams, FixtureValue], request: SubRequest ) -> FixtureValue: """Execution of fixture setup.""" kwargs = {} @@ -1192,7 +1208,9 @@ class FixtureFunctionMarker: def __post_init__(self, _ispytest: bool) -> None: check_ispytest(_ispytest) - def __call__(self, function: FixtureFunction) -> FixtureFunctionDefinition: + def __call__( + self, function: Callable[FixtureParams, FixtureValue] + ) -> FixtureFunctionDefinition[FixtureParams, FixtureValue]: if inspect.isclass(function): raise ValueError("class fixtures not supported (maybe in the future)") @@ -1219,12 +1237,10 @@ def __call__(self, function: FixtureFunction) -> FixtureFunctionDefinition: return fixture_definition -# TODO: paramspec/return type annotation tracking and storing -class FixtureFunctionDefinition: +class FixtureFunctionDefinition(Generic[FixtureParams, FixtureValue]): def __init__( self, - *, - function: Callable[..., Any], + function: Callable[FixtureParams, FixtureValue], fixture_function_marker: FixtureFunctionMarker, instance: object | None = None, _ispytest: bool = False, @@ -1237,7 +1253,7 @@ def __init__( self._fixture_function_marker = fixture_function_marker if instance is not None: self._fixture_function = cast( - Callable[..., Any], function.__get__(instance) + Callable[FixtureParams, FixtureValue], function.__get__(instance) ) else: self._fixture_function = function @@ -1246,12 +1262,14 @@ def __init__( def __repr__(self) -> str: return f"" - def __get__(self, instance, owner=None): + def __get__( + self, obj: object, objtype: type | None = None + ) -> FixtureFunctionDefinition[FixtureParams, FixtureValue]: """Behave like a method if the function it was applied to was a method.""" return FixtureFunctionDefinition( function=self._fixture_function, fixture_function_marker=self._fixture_function_marker, - instance=instance, + instance=obj, _ispytest=True, ) @@ -1270,14 +1288,14 @@ def _get_wrapped_function(self) -> Callable[..., Any]: @overload def fixture( - fixture_function: Callable[..., object], + fixture_function: Callable[FixtureParams, FixtureValue], *, scope: _ScopeName | Callable[[str, Config], _ScopeName] = ..., params: Iterable[object] | None = ..., autouse: bool = ..., ids: Sequence[object | None] | Callable[[Any], object | None] | None = ..., name: str | None = ..., -) -> FixtureFunctionDefinition: ... +) -> FixtureFunctionDefinition[FixtureParams, FixtureValue]: ... @overload @@ -1293,14 +1311,14 @@ def fixture( def fixture( - fixture_function: FixtureFunction | None = None, + fixture_function: Callable[FixtureParams, FixtureValue] | None = None, *, scope: _ScopeName | Callable[[str, Config], _ScopeName] = "function", params: Iterable[object] | None = None, autouse: bool = False, ids: Sequence[object | None] | Callable[[Any], object | None] | None = None, name: str | None = None, -) -> FixtureFunctionMarker | FixtureFunctionDefinition: +) -> FixtureFunctionMarker | FixtureFunctionDefinition[FixtureParams, FixtureValue]: """Decorator to mark a fixture factory function. This decorator can be used, with or without parameters, to define a @@ -1507,7 +1525,7 @@ def __init__(self, session: Session) -> None: # suite/plugins defined with this name. Populated by parsefactories(). # TODO: The order of the FixtureDefs list of each arg is significant, # explain. - self._arg2fixturedefs: Final[dict[str, list[FixtureDef[Any]]]] = {} + self._arg2fixturedefs: Final[dict[str, list[FixtureDef[Any, Any]]]] = {} self._holderobjseen: Final[set[object]] = set() # A mapping from a nodeid to a list of autouse fixtures it defines. self._nodeid_autousenames: Final[dict[str, list[str]]] = { @@ -1598,7 +1616,7 @@ def getfixtureclosure( parentnode: nodes.Node, initialnames: tuple[str, ...], ignore_args: AbstractSet[str], - ) -> tuple[list[str], dict[str, Sequence[FixtureDef[Any]]]]: + ) -> tuple[list[str], dict[str, Sequence[FixtureDef[Any, Any]]]]: # Collect the closure of all fixtures, starting with the given # fixturenames as the initial set. As we have to visit all # factory definitions anyway, we also return an arg2fixturedefs @@ -1608,7 +1626,7 @@ def getfixtureclosure( fixturenames_closure = list(initialnames) - arg2fixturedefs: dict[str, Sequence[FixtureDef[Any]]] = {} + arg2fixturedefs: dict[str, Sequence[FixtureDef[Any, Any]]] = {} lastlen = -1 while lastlen != len(fixturenames_closure): lastlen = len(fixturenames_closure) @@ -1688,7 +1706,7 @@ def _register_fixture( self, *, name: str, - func: _FixtureFunc[object], + func: _FixtureFunc[Any, object], nodeid: str | None, scope: Scope | _ScopeName | Callable[[str, Config], _ScopeName] = "function", params: Sequence[object] | None = None, @@ -1823,7 +1841,7 @@ def parsefactories( def getfixturedefs( self, argname: str, node: nodes.Node - ) -> Sequence[FixtureDef[Any]] | None: + ) -> Sequence[FixtureDef[Any, Any]] | None: """Get FixtureDefs for a fixture name which are applicable to a given node. @@ -1842,8 +1860,8 @@ def getfixturedefs( return tuple(self._matchfactories(fixturedefs, node)) def _matchfactories( - self, fixturedefs: Iterable[FixtureDef[Any]], node: nodes.Node - ) -> Iterator[FixtureDef[Any]]: + self, fixturedefs: Iterable[FixtureDef[Any, Any]], node: nodes.Node + ) -> Iterator[FixtureDef[Any, Any]]: parentnodeids = {n.nodeid for n in node.iter_parents()} for fixturedef in fixturedefs: if fixturedef.baseid in parentnodeids: @@ -1880,7 +1898,7 @@ def get_best_relpath(func) -> str: loc = getlocation(func, invocation_dir) return bestrelpath(invocation_dir, Path(loc)) - def write_fixture(fixture_def: FixtureDef[object]) -> None: + def write_fixture(fixture_def: FixtureDef[Any, object]) -> None: argname = fixture_def.argname if verbose <= 0 and argname.startswith("_"): return diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 8b20061d6f0..66700a3938b 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -866,7 +866,7 @@ def pytest_report_from_serializable( @hookspec(firstresult=True) def pytest_fixture_setup( - fixturedef: FixtureDef[Any], request: SubRequest + fixturedef: FixtureDef[Any, Any], request: SubRequest ) -> object | None: """Perform fixture setup execution. @@ -894,7 +894,7 @@ def pytest_fixture_setup( def pytest_fixture_post_finalizer( - fixturedef: FixtureDef[Any], request: SubRequest + fixturedef: FixtureDef[Any, Any], request: SubRequest ) -> None: """Called after fixture teardown, but before the cache is cleared, so the fixture result ``fixturedef.cached_result`` is still available (not diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 85e3cb0ae71..c8db81e73c9 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1085,7 +1085,7 @@ def get_direct_param_fixture_func(request: FixtureRequest) -> Any: # Used for storing pseudo fixturedefs for direct parametrization. -name2pseudofixturedef_key = StashKey[dict[str, FixtureDef[Any]]]() +name2pseudofixturedef_key = StashKey[dict[str, FixtureDef[Any, Any]]]() @final @@ -1271,7 +1271,7 @@ def parametrize( if node is None: name2pseudofixturedef = None else: - default: dict[str, FixtureDef[Any]] = {} + default: dict[str, FixtureDef[Any, Any]] = {} name2pseudofixturedef = node.stash.setdefault( name2pseudofixturedef_key, default ) @@ -1458,7 +1458,7 @@ def _recompute_direct_params_indices(self) -> None: def _find_parametrized_scope( argnames: Sequence[str], - arg2fixturedefs: Mapping[str, Sequence[fixtures.FixtureDef[object]]], + arg2fixturedefs: Mapping[str, Sequence[fixtures.FixtureDef[Any, object]]], indirect: bool | Sequence[str], ) -> Scope: """Find the most appropriate scope for a parametrized call based on its arguments. diff --git a/src/_pytest/setuponly.py b/src/_pytest/setuponly.py index 1e887a896f5..ed09c83732d 100644 --- a/src/_pytest/setuponly.py +++ b/src/_pytest/setuponly.py @@ -1,6 +1,7 @@ from __future__ import annotations from collections.abc import Generator +from typing import Any from _pytest._io.saferepr import saferepr from _pytest.config import Config @@ -30,7 +31,7 @@ def pytest_addoption(parser: Parser) -> None: @pytest.hookimpl(wrapper=True) def pytest_fixture_setup( - fixturedef: FixtureDef[object], request: SubRequest + fixturedef: FixtureDef[Any, object], request: SubRequest ) -> Generator[None, object, object]: try: return (yield) @@ -51,7 +52,7 @@ def pytest_fixture_setup( def pytest_fixture_post_finalizer( - fixturedef: FixtureDef[object], request: SubRequest + fixturedef: FixtureDef[Any, object], request: SubRequest ) -> None: if fixturedef.cached_result is not None: config = request.config @@ -62,7 +63,7 @@ def pytest_fixture_post_finalizer( def _show_fixture_action( - fixturedef: FixtureDef[object], config: Config, msg: str + fixturedef: FixtureDef[Any, object], config: Config, msg: str ) -> None: capman = config.pluginmanager.getplugin("capturemanager") if capman: diff --git a/src/_pytest/setupplan.py b/src/_pytest/setupplan.py index 4e124cce243..0a6f2f668e5 100644 --- a/src/_pytest/setupplan.py +++ b/src/_pytest/setupplan.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Any + from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config.argparsing import Parser @@ -21,7 +23,7 @@ def pytest_addoption(parser: Parser) -> None: @pytest.hookimpl(tryfirst=True) def pytest_fixture_setup( - fixturedef: FixtureDef[object], request: SubRequest + fixturedef: FixtureDef[Any, object], request: SubRequest ) -> object | None: # Will return a dummy fixture if the setuponly option is provided. if request.config.option.setupplan: diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 4e7e441768c..09ea1e451f9 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -32,7 +32,7 @@ def Metafunc(self, func, config=None) -> python.Metafunc: # on the funcarg level, so we don't need a full blown # initialization. class FuncFixtureInfoMock: - name2fixturedefs: dict[str, list[fixtures.FixtureDef[object]]] = {} + name2fixturedefs: dict[str, list[fixtures.FixtureDef[Any, object]]] = {} def __init__(self, names): self.names_closure = names @@ -153,7 +153,7 @@ class DummyFixtureDef: _scope: Scope fixtures_defs = cast( - dict[str, Sequence[fixtures.FixtureDef[object]]], + dict[str, Sequence[fixtures.FixtureDef[Any, object]]], dict( session_fix=[DummyFixtureDef(Scope.Session)], package_fix=[DummyFixtureDef(Scope.Package)], From af0d67a5aa94dfa386f46454a8a349032086ca5b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 7 Dec 2024 19:37:14 -0300 Subject: [PATCH 2/4] Use instance/owner for __get__ --- src/_pytest/fixtures.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 3a4b6fe5e62..451fbd2a5da 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1263,13 +1263,13 @@ def __repr__(self) -> str: return f"" def __get__( - self, obj: object, objtype: type | None = None + self, instance: object, owner: type | None = None ) -> FixtureFunctionDefinition[FixtureParams, FixtureValue]: """Behave like a method if the function it was applied to was a method.""" return FixtureFunctionDefinition( function=self._fixture_function, fixture_function_marker=self._fixture_function_marker, - instance=obj, + instance=instance, _ispytest=True, ) From 9f78c6c45bafe01dc8b05e1d8ab5905c39f063a8 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 7 Dec 2024 19:42:16 -0300 Subject: [PATCH 3/4] Revert FixtureDef to a single argument --- src/_pytest/fixtures.py | 54 ++++++++++++++++++-------------------- src/_pytest/hookspec.py | 4 +-- src/_pytest/python.py | 6 ++--- src/_pytest/setuponly.py | 7 +++-- src/_pytest/setupplan.py | 4 +-- testing/python/metafunc.py | 4 +-- 6 files changed, 36 insertions(+), 43 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 451fbd2a5da..1b301b125b4 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -131,7 +131,7 @@ def pytest_sessionstart(session: Session) -> None: def get_scope_package( node: nodes.Item, - fixturedef: FixtureDef[Any, object], + fixturedef: FixtureDef[object], ) -> nodes.Node | None: from _pytest.python import Package @@ -328,7 +328,7 @@ class FuncFixtureInfo: # matching the name which are applicable to this function. # There may be multiple overriding fixtures with the same name. The # sequence is ordered from furthest to closes to the function. - name2fixturedefs: dict[str, Sequence[FixtureDef[Any, Any]]] + name2fixturedefs: dict[str, Sequence[FixtureDef[Any]]] def prune_dependency_tree(self) -> None: """Recompute names_closure from initialnames and name2fixturedefs. @@ -369,8 +369,8 @@ def __init__( self, pyfuncitem: Function, fixturename: str | None, - arg2fixturedefs: dict[str, Sequence[FixtureDef[Any, Any]]], - fixture_defs: dict[str, FixtureDef[Any, Any]], + arg2fixturedefs: dict[str, Sequence[FixtureDef[Any]]], + fixture_defs: dict[str, FixtureDef[Any]], *, _ispytest: bool = False, ) -> None: @@ -413,7 +413,7 @@ def scope(self) -> _ScopeName: @abc.abstractmethod def _check_scope( self, - requested_fixturedef: FixtureDef[Any, object] | PseudoFixtureDef[object], + requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object], requested_scope: Scope, ) -> None: raise NotImplementedError() @@ -554,7 +554,7 @@ def _iter_chain(self) -> Iterator[SubRequest]: def _get_active_fixturedef( self, argname: str - ) -> FixtureDef[Any, object] | PseudoFixtureDef[object]: + ) -> FixtureDef[object] | PseudoFixtureDef[object]: if argname == "request": cached_result = (self, [0], None) return PseudoFixtureDef(cached_result, Scope.Function) @@ -626,9 +626,7 @@ def _get_active_fixturedef( self._fixture_defs[argname] = fixturedef return fixturedef - def _check_fixturedef_without_param( - self, fixturedef: FixtureDef[Any, object] - ) -> None: + def _check_fixturedef_without_param(self, fixturedef: FixtureDef[object]) -> None: """Check that this request is allowed to execute this fixturedef without a param.""" funcitem = self._pyfuncitem @@ -661,7 +659,7 @@ def _check_fixturedef_without_param( ) fail(msg, pytrace=False) - def _get_fixturestack(self) -> list[FixtureDef[Any, Any]]: + def _get_fixturestack(self) -> list[FixtureDef[Any]]: values = [request._fixturedef for request in self._iter_chain()] values.reverse() return values @@ -686,7 +684,7 @@ def _scope(self) -> Scope: def _check_scope( self, - requested_fixturedef: FixtureDef[Any, object] | PseudoFixtureDef[object], + requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object], requested_scope: Scope, ) -> None: # TopRequest always has function scope so always valid. @@ -720,7 +718,7 @@ def __init__( scope: Scope, param: Any, param_index: int, - fixturedef: FixtureDef[Any, object], + fixturedef: FixtureDef[object], *, _ispytest: bool = False, ) -> None: @@ -733,7 +731,7 @@ def __init__( ) self._parent_request: Final[FixtureRequest] = request self._scope_field: Final = scope - self._fixturedef: Final[FixtureDef[Any, object]] = fixturedef + self._fixturedef: Final[FixtureDef[object]] = fixturedef if param is not NOTSET: self.param = param self.param_index: Final = param_index @@ -763,7 +761,7 @@ def node(self): def _check_scope( self, - requested_fixturedef: FixtureDef[Any, object] | PseudoFixtureDef[object], + requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object], requested_scope: Scope, ) -> None: if isinstance(requested_fixturedef, PseudoFixtureDef): @@ -784,7 +782,7 @@ def _check_scope( pytrace=False, ) - def _format_fixturedef_line(self, fixturedef: FixtureDef[Any, object]) -> str: + def _format_fixturedef_line(self, fixturedef: FixtureDef[object]) -> str: factory = fixturedef.func path, lineno = getfslineno(factory) if isinstance(path, Path): @@ -959,11 +957,9 @@ def _eval_scope_callable( @final -class FixtureDef(Generic[FixtureParams, FixtureValue]): +class FixtureDef(Generic[FixtureValue]): """A container for a fixture definition. - This is a generic class parametrized on the parameters that a fixture function receives and its return value. - Note: At this time, only explicitly documented fields and methods are considered public stable API. """ @@ -973,7 +969,7 @@ def __init__( config: Config, baseid: str | None, argname: str, - func: _FixtureFunc[FixtureParams, FixtureValue], + func: _FixtureFunc[Any, FixtureValue], scope: Scope | _ScopeName | Callable[[str, Config], _ScopeName] | None, params: Sequence[object] | None, ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None, @@ -1128,8 +1124,8 @@ def __repr__(self) -> str: def resolve_fixture_function( - fixturedef: FixtureDef[FixtureParams, FixtureValue], request: FixtureRequest -) -> _FixtureFunc[FixtureParams, FixtureValue]: + fixturedef: FixtureDef[FixtureValue], request: FixtureRequest +) -> _FixtureFunc[Any, FixtureValue]: """Get the actual callable that can be called to obtain the fixture value.""" fixturefunc = fixturedef.func @@ -1152,7 +1148,7 @@ def resolve_fixture_function( def pytest_fixture_setup( - fixturedef: FixtureDef[FixtureParams, FixtureValue], request: SubRequest + fixturedef: FixtureDef[FixtureValue], request: SubRequest ) -> FixtureValue: """Execution of fixture setup.""" kwargs = {} @@ -1525,7 +1521,7 @@ def __init__(self, session: Session) -> None: # suite/plugins defined with this name. Populated by parsefactories(). # TODO: The order of the FixtureDefs list of each arg is significant, # explain. - self._arg2fixturedefs: Final[dict[str, list[FixtureDef[Any, Any]]]] = {} + self._arg2fixturedefs: Final[dict[str, list[FixtureDef[Any]]]] = {} self._holderobjseen: Final[set[object]] = set() # A mapping from a nodeid to a list of autouse fixtures it defines. self._nodeid_autousenames: Final[dict[str, list[str]]] = { @@ -1616,7 +1612,7 @@ def getfixtureclosure( parentnode: nodes.Node, initialnames: tuple[str, ...], ignore_args: AbstractSet[str], - ) -> tuple[list[str], dict[str, Sequence[FixtureDef[Any, Any]]]]: + ) -> tuple[list[str], dict[str, Sequence[FixtureDef[Any]]]]: # Collect the closure of all fixtures, starting with the given # fixturenames as the initial set. As we have to visit all # factory definitions anyway, we also return an arg2fixturedefs @@ -1626,7 +1622,7 @@ def getfixtureclosure( fixturenames_closure = list(initialnames) - arg2fixturedefs: dict[str, Sequence[FixtureDef[Any, Any]]] = {} + arg2fixturedefs: dict[str, Sequence[FixtureDef[Any]]] = {} lastlen = -1 while lastlen != len(fixturenames_closure): lastlen = len(fixturenames_closure) @@ -1841,7 +1837,7 @@ def parsefactories( def getfixturedefs( self, argname: str, node: nodes.Node - ) -> Sequence[FixtureDef[Any, Any]] | None: + ) -> Sequence[FixtureDef[Any]] | None: """Get FixtureDefs for a fixture name which are applicable to a given node. @@ -1860,8 +1856,8 @@ def getfixturedefs( return tuple(self._matchfactories(fixturedefs, node)) def _matchfactories( - self, fixturedefs: Iterable[FixtureDef[Any, Any]], node: nodes.Node - ) -> Iterator[FixtureDef[Any, Any]]: + self, fixturedefs: Iterable[FixtureDef[Any]], node: nodes.Node + ) -> Iterator[FixtureDef[Any]]: parentnodeids = {n.nodeid for n in node.iter_parents()} for fixturedef in fixturedefs: if fixturedef.baseid in parentnodeids: @@ -1898,7 +1894,7 @@ def get_best_relpath(func) -> str: loc = getlocation(func, invocation_dir) return bestrelpath(invocation_dir, Path(loc)) - def write_fixture(fixture_def: FixtureDef[Any, object]) -> None: + def write_fixture(fixture_def: FixtureDef[object]) -> None: argname = fixture_def.argname if verbose <= 0 and argname.startswith("_"): return diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 66700a3938b..8b20061d6f0 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -866,7 +866,7 @@ def pytest_report_from_serializable( @hookspec(firstresult=True) def pytest_fixture_setup( - fixturedef: FixtureDef[Any, Any], request: SubRequest + fixturedef: FixtureDef[Any], request: SubRequest ) -> object | None: """Perform fixture setup execution. @@ -894,7 +894,7 @@ def pytest_fixture_setup( def pytest_fixture_post_finalizer( - fixturedef: FixtureDef[Any, Any], request: SubRequest + fixturedef: FixtureDef[Any], request: SubRequest ) -> None: """Called after fixture teardown, but before the cache is cleared, so the fixture result ``fixturedef.cached_result`` is still available (not diff --git a/src/_pytest/python.py b/src/_pytest/python.py index c8db81e73c9..85e3cb0ae71 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1085,7 +1085,7 @@ def get_direct_param_fixture_func(request: FixtureRequest) -> Any: # Used for storing pseudo fixturedefs for direct parametrization. -name2pseudofixturedef_key = StashKey[dict[str, FixtureDef[Any, Any]]]() +name2pseudofixturedef_key = StashKey[dict[str, FixtureDef[Any]]]() @final @@ -1271,7 +1271,7 @@ def parametrize( if node is None: name2pseudofixturedef = None else: - default: dict[str, FixtureDef[Any, Any]] = {} + default: dict[str, FixtureDef[Any]] = {} name2pseudofixturedef = node.stash.setdefault( name2pseudofixturedef_key, default ) @@ -1458,7 +1458,7 @@ def _recompute_direct_params_indices(self) -> None: def _find_parametrized_scope( argnames: Sequence[str], - arg2fixturedefs: Mapping[str, Sequence[fixtures.FixtureDef[Any, object]]], + arg2fixturedefs: Mapping[str, Sequence[fixtures.FixtureDef[object]]], indirect: bool | Sequence[str], ) -> Scope: """Find the most appropriate scope for a parametrized call based on its arguments. diff --git a/src/_pytest/setuponly.py b/src/_pytest/setuponly.py index ed09c83732d..1e887a896f5 100644 --- a/src/_pytest/setuponly.py +++ b/src/_pytest/setuponly.py @@ -1,7 +1,6 @@ from __future__ import annotations from collections.abc import Generator -from typing import Any from _pytest._io.saferepr import saferepr from _pytest.config import Config @@ -31,7 +30,7 @@ def pytest_addoption(parser: Parser) -> None: @pytest.hookimpl(wrapper=True) def pytest_fixture_setup( - fixturedef: FixtureDef[Any, object], request: SubRequest + fixturedef: FixtureDef[object], request: SubRequest ) -> Generator[None, object, object]: try: return (yield) @@ -52,7 +51,7 @@ def pytest_fixture_setup( def pytest_fixture_post_finalizer( - fixturedef: FixtureDef[Any, object], request: SubRequest + fixturedef: FixtureDef[object], request: SubRequest ) -> None: if fixturedef.cached_result is not None: config = request.config @@ -63,7 +62,7 @@ def pytest_fixture_post_finalizer( def _show_fixture_action( - fixturedef: FixtureDef[Any, object], config: Config, msg: str + fixturedef: FixtureDef[object], config: Config, msg: str ) -> None: capman = config.pluginmanager.getplugin("capturemanager") if capman: diff --git a/src/_pytest/setupplan.py b/src/_pytest/setupplan.py index 0a6f2f668e5..4e124cce243 100644 --- a/src/_pytest/setupplan.py +++ b/src/_pytest/setupplan.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import Any - from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config.argparsing import Parser @@ -23,7 +21,7 @@ def pytest_addoption(parser: Parser) -> None: @pytest.hookimpl(tryfirst=True) def pytest_fixture_setup( - fixturedef: FixtureDef[Any, object], request: SubRequest + fixturedef: FixtureDef[object], request: SubRequest ) -> object | None: # Will return a dummy fixture if the setuponly option is provided. if request.config.option.setupplan: diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 09ea1e451f9..4e7e441768c 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -32,7 +32,7 @@ def Metafunc(self, func, config=None) -> python.Metafunc: # on the funcarg level, so we don't need a full blown # initialization. class FuncFixtureInfoMock: - name2fixturedefs: dict[str, list[fixtures.FixtureDef[Any, object]]] = {} + name2fixturedefs: dict[str, list[fixtures.FixtureDef[object]]] = {} def __init__(self, names): self.names_closure = names @@ -153,7 +153,7 @@ class DummyFixtureDef: _scope: Scope fixtures_defs = cast( - dict[str, Sequence[fixtures.FixtureDef[Any, object]]], + dict[str, Sequence[fixtures.FixtureDef[object]]], dict( session_fix=[DummyFixtureDef(Scope.Session)], package_fix=[DummyFixtureDef(Scope.Package)], From 348068ccad91074ddcc8f26f3c4412afcbf66c6f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 7 Dec 2024 19:45:34 -0300 Subject: [PATCH 4/4] Add FixtureDefDict type alias --- src/_pytest/fixtures.py | 5 ++++- src/_pytest/python.py | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 1b301b125b4..454a1e52a6c 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -95,6 +95,9 @@ # The parameters that a fixture function receives. FixtureParams = ParamSpec("FixtureParams") +# A dict of fixture name -> its FixtureDef. +FixtureDefDict: TypeAlias = dict[str, "FixtureDef[Any]"] + # The type of fixture function (type alias generic in fixture params and value). _FixtureFunc: TypeAlias = Union[ Callable[FixtureParams, FixtureValue], @@ -370,7 +373,7 @@ def __init__( pyfuncitem: Function, fixturename: str | None, arg2fixturedefs: dict[str, Sequence[FixtureDef[Any]]], - fixture_defs: dict[str, FixtureDef[Any]], + fixture_defs: FixtureDefDict, *, _ispytest: bool = False, ) -> None: diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 85e3cb0ae71..f17a696799a 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -51,6 +51,7 @@ from _pytest.config.argparsing import Parser from _pytest.deprecated import check_ispytest from _pytest.fixtures import FixtureDef +from _pytest.fixtures import FixtureDefDict from _pytest.fixtures import FixtureRequest from _pytest.fixtures import FuncFixtureInfo from _pytest.fixtures import get_scope_node @@ -1085,7 +1086,7 @@ def get_direct_param_fixture_func(request: FixtureRequest) -> Any: # Used for storing pseudo fixturedefs for direct parametrization. -name2pseudofixturedef_key = StashKey[dict[str, FixtureDef[Any]]]() +name2pseudofixturedef_key = StashKey[FixtureDefDict]() @final @@ -1271,7 +1272,7 @@ def parametrize( if node is None: name2pseudofixturedef = None else: - default: dict[str, FixtureDef[Any]] = {} + default: FixtureDefDict = {} name2pseudofixturedef = node.stash.setdefault( name2pseudofixturedef_key, default )