diff --git a/AUTHORS b/AUTHORS index 303d04133cb..1826c734dea 100644 --- a/AUTHORS +++ b/AUTHORS @@ -390,6 +390,7 @@ Serhii Mozghovyi Seth Junot Shantanu Jain Sharad Nair +Shaygan Hooshyari Shubham Adep Simon Blanchard Simon Gomizelj diff --git a/changelog/11525.improvement.rst b/changelog/11525.improvement.rst new file mode 100644 index 00000000000..10d6ca8c353 --- /dev/null +++ b/changelog/11525.improvement.rst @@ -0,0 +1,3 @@ +The fixtures are now represented as fixture in test output. + +-- by :user:`the-compiler` and :user:`glyphack` diff --git a/doc/en/conf.py b/doc/en/conf.py index 9558a75f927..47fc70dce85 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -75,6 +75,7 @@ ("py:class", "_pytest._code.code.TerminalRepr"), ("py:class", "TerminalRepr"), ("py:class", "_pytest.fixtures.FixtureFunctionMarker"), + ("py:class", "_pytest.fixtures.FixtureFunctionDefinition"), ("py:class", "_pytest.logging.LogCaptureHandler"), ("py:class", "_pytest.mark.structures.ParameterSet"), # Intentionally undocumented/private diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 37c09b03467..33785dfb9a6 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -31,6 +31,7 @@ from _pytest._version import version from _pytest.assertion import util from _pytest.config import Config +from _pytest.fixtures import FixtureFunctionDefinition from _pytest.main import Session from _pytest.pathlib import absolutepath from _pytest.pathlib import fnmatch_ex @@ -472,7 +473,7 @@ def _format_assertmsg(obj: object) -> str: def _should_repr_global_name(obj: object) -> bool: if callable(obj): - return False + return isinstance(obj, FixtureFunctionDefinition) try: return not hasattr(obj, "__name__") diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 82aea5e635e..b3771d97c36 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -3,7 +3,6 @@ from __future__ import annotations -import dataclasses import enum import functools import inspect @@ -205,59 +204,16 @@ def ascii_escaped(val: bytes | str) -> str: return ret.translate(_non_printable_ascii_translate_table) -@dataclasses.dataclass -class _PytestWrapper: - """Dummy wrapper around a function object for internal use only. - - Used to correctly unwrap the underlying function object when we are - creating fixtures, because we wrap the function object ourselves with a - decorator to issue warnings when the fixture function is called directly. - """ - - obj: Any - - def get_real_func(obj): """Get the real function object of the (possibly) wrapped object by - functools.wraps or functools.partial.""" - start_obj = obj - for i in range(100): - # __pytest_wrapped__ is set by @pytest.fixture when wrapping the fixture function - # to trigger a warning if it gets called directly instead of by pytest: we don't - # want to unwrap further than this otherwise we lose useful wrappings like @mock.patch (#3774) - new_obj = getattr(obj, "__pytest_wrapped__", None) - if isinstance(new_obj, _PytestWrapper): - obj = new_obj.obj - break - new_obj = getattr(obj, "__wrapped__", None) - if new_obj is None: - break - obj = new_obj - else: - from _pytest._io.saferepr import saferepr + :func:`functools.wraps`, or :func:`functools.partial`.""" + obj = inspect.unwrap(obj) - raise ValueError( - f"could not find real function of {saferepr(start_obj)}\nstopped at {saferepr(obj)}" - ) if isinstance(obj, functools.partial): obj = obj.func return obj -def get_real_method(obj, holder): - """Attempt to obtain the real function object that might be wrapping - ``obj``, while at the same time returning a bound method to ``holder`` if - the original object was a bound method.""" - try: - is_method = hasattr(obj, "__func__") - obj = get_real_func(obj) - except Exception: # pragma: no cover - return obj - if is_method and hasattr(obj, "__get__") and callable(obj.__get__): - obj = obj.__get__(holder) - return obj - - def getimfunc(func): try: return func.__func__ diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 5817e88f47d..9096ac3f7c3 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -42,10 +42,8 @@ from _pytest._code.code import FormattedExcinfo from _pytest._code.code import TerminalRepr from _pytest._io import TerminalWriter -from _pytest.compat import _PytestWrapper from _pytest.compat import assert_never from _pytest.compat import get_real_func -from _pytest.compat import get_real_method from _pytest.compat import getfuncargnames from _pytest.compat import getimfunc from _pytest.compat import getlocation @@ -78,6 +76,11 @@ if sys.version_info < (3, 11): from exceptiongroup import BaseExceptionGroup +if sys.version_info < (3, 10): + from typing_extensions import ParamSpec +else: + from typing import ParamSpec + if TYPE_CHECKING: from _pytest.python import CallSpec2 @@ -87,11 +90,14 @@ # 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). + +# The parameters that a fixture function receives. +FixtureParams = ParamSpec("FixtureParams") + +# The type of a fixture function (type alias generic in fixture params and value). _FixtureFunc = Union[ - Callable[..., FixtureValue], Callable[..., Generator[FixtureValue, None, None]] + Callable[FixtureParams, FixtureValue], + Callable[FixtureParams, Generator[FixtureValue, None, None]], ] # The type of FixtureDef.cached_result (type alias generic in fixture value). _FixtureCachedResult = Union[ @@ -124,7 +130,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 @@ -153,13 +159,12 @@ def get_scope_node(node: nodes.Node, scope: Scope) -> nodes.Node | None: assert_never(scope) +# TODO: Try to use FixtureFunctionDefinition instead of the marker def getfixturemarker(obj: object) -> FixtureFunctionMarker | None: - """Return fixturemarker or None if it doesn't exist or raised - exceptions.""" - return cast( - Optional[FixtureFunctionMarker], - safe_getattr(obj, "_pytestfixturefunction", None), - ) + """Return fixturemarker or None if it doesn't exist""" + if isinstance(obj, FixtureFunctionDefinition): + return obj._fixture_function_marker + return None # Algorithm for sorting on a per-parametrized resource setup basis. @@ -322,7 +327,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. @@ -363,8 +368,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: @@ -407,7 +412,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() @@ -548,7 +553,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) @@ -620,7 +625,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 @@ -653,7 +660,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 @@ -678,7 +685,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. @@ -712,7 +719,7 @@ def __init__( scope: Scope, param: Any, param_index: int, - fixturedef: FixtureDef[object], + fixturedef: FixtureDef[Any, object], *, _ispytest: bool = False, ) -> None: @@ -725,7 +732,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 @@ -755,7 +762,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): @@ -776,7 +783,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): @@ -890,7 +897,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( @@ -951,7 +960,7 @@ def _eval_scope_callable( @final -class FixtureDef(Generic[FixtureValue]): +class FixtureDef(Generic[FixtureParams, FixtureValue]): """A container for a fixture definition. Note: At this time, only explicitly documented fields and methods are @@ -963,7 +972,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, @@ -1118,8 +1127,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 @@ -1142,7 +1151,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 = {} @@ -1184,31 +1193,6 @@ def pytest_fixture_setup( return result -def wrap_function_to_error_out_if_called_directly( - function: FixtureFunction, - fixture_marker: FixtureFunctionMarker, -) -> FixtureFunction: - """Wrap the given fixture function so we can raise an error about it being called directly, - instead of used as an argument in a test function.""" - name = fixture_marker.name or function.__name__ - message = ( - f'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n' - "but are created automatically when test functions request them as parameters.\n" - "See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and\n" - "https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly about how to update your code." - ) - - @functools.wraps(function) - def result(*args, **kwargs): - fail(message, pytrace=False) - - # Keep reference to the original function in our own custom attribute so we don't unwrap - # further than this point and lose useful wrappings like @mock.patch (#3774). - result.__pytest_wrapped__ = _PytestWrapper(function) # type: ignore[attr-defined] - - return cast(FixtureFunction, result) - - @final @dataclasses.dataclass(frozen=True) class FixtureFunctionMarker: @@ -1223,11 +1207,13 @@ class FixtureFunctionMarker: def __post_init__(self, _ispytest: bool) -> None: check_ispytest(_ispytest) - def __call__(self, function: FixtureFunction) -> FixtureFunction: + def __call__( + self, function: Callable[FixtureParams, FixtureValue] + ) -> FixtureFunctionDefinition[FixtureParams, FixtureValue]: if inspect.isclass(function): raise ValueError("class fixtures not supported (maybe in the future)") - if getattr(function, "_pytestfixturefunction", False): + if isinstance(function, FixtureFunctionDefinition): raise ValueError( f"@pytest.fixture is being applied more than once to the same function {function.__name__!r}" ) @@ -1235,7 +1221,7 @@ def __call__(self, function: FixtureFunction) -> FixtureFunction: if hasattr(function, "pytestmark"): warnings.warn(MARKED_FIXTURE, stacklevel=2) - function = wrap_function_to_error_out_if_called_directly(function, self) + fixture_definition = FixtureFunctionDefinition(function, self) name = self.name or function.__name__ if name == "request": @@ -1245,21 +1231,61 @@ def __call__(self, function: FixtureFunction) -> FixtureFunction: pytrace=False, ) - # Type ignored because https://github.com/python/mypy/issues/2087. - function._pytestfixturefunction = self # type: ignore[attr-defined] - return function + return fixture_definition + + +class FixtureFunctionDefinition(Generic[FixtureParams, FixtureValue]): + def __init__( + self, + function: Callable[FixtureParams, FixtureValue], + fixture_function_marker: FixtureFunctionMarker, + instance: object | None = None, + ) -> None: + self.name = fixture_function_marker.name or function.__name__ + self.__name__ = self.name + self._fixture_function_marker = fixture_function_marker + self._fixture_function = function + self._instance = instance + + def __repr__(self) -> str: + return f"" + + def __get__( + self, obj: object, objtype: type | None = None + ) -> FixtureFunctionDefinition[FixtureParams, FixtureValue]: + return FixtureFunctionDefinition( + self._fixture_function, self._fixture_function_marker, obj + ) + + def __call__(self, *args: Any, **kwds: Any) -> Any: + message = ( + f'Fixture "{self.name}" called directly. Fixtures are not meant to be called directly,\n' + "but are created automatically when test functions request them as parameters.\n" + "See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and\n" + "https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly" + ) + fail(message, pytrace=False) + + def _get_wrapped_function(self) -> Callable[FixtureParams, FixtureValue]: + if self._instance is None: + return self._fixture_function + + return cast( + Callable[FixtureParams, FixtureValue], + self._fixture_function.__get__(self._instance), + ) @overload def fixture( - fixture_function: FixtureFunction, + 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 = ..., -) -> FixtureFunction: ... +) -> FixtureFunctionDefinition[FixtureParams, FixtureValue]: ... @overload @@ -1275,14 +1301,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 | FixtureFunction: +) -> FixtureFunctionMarker | FixtureFunctionDefinition[FixtureParams, FixtureValue]: """Decorator to mark a fixture factory function. This decorator can be used, with or without parameters, to define a @@ -1352,7 +1378,7 @@ def fixture( def yield_fixture( fixture_function=None, *args, - scope="function", + scope: _ScopeName = "function", params=None, autouse=False, ids=None, @@ -1489,7 +1515,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]]] = { @@ -1574,7 +1600,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 @@ -1584,7 +1610,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) @@ -1664,7 +1690,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, @@ -1771,37 +1797,35 @@ def parsefactories( # The attribute can be an arbitrary descriptor, so the attribute # access below can raise. safe_getattr() ignores such exceptions. obj_ub = safe_getattr(holderobj_tp, name, None) - marker = getfixturemarker(obj_ub) - if not isinstance(marker, FixtureFunctionMarker): - # Magic globals with __getattr__ might have got us a wrong - # fixture attribute. - continue - - # OK we know it is a fixture -- now safe to look up on the _instance_. - obj = getattr(holderobj, name) - - if marker.name: - name = marker.name - - # During fixture definition we wrap the original fixture function - # to issue a warning if called directly, so here we unwrap it in - # order to not emit the warning when pytest itself calls the - # fixture function. - func = get_real_method(obj, holderobj) - - self._register_fixture( - name=name, - nodeid=nodeid, - func=func, - scope=marker.scope, - params=marker.params, - ids=marker.ids, - autouse=marker.autouse, - ) + if type(obj_ub) is FixtureFunctionDefinition: + marker = obj_ub._fixture_function_marker + if marker.name: + fixture_name = marker.name + else: + fixture_name = name + + # OK we know it is a fixture -- now safe to look up on the _instance_. + try: + obj = getattr(holderobj, name) + # if the fixture is named in the decorator we cannot find it in the module + except AttributeError: + obj = obj_ub + + func = obj._get_wrapped_function() + + self._register_fixture( + name=fixture_name, + nodeid=nodeid, + func=func, + scope=marker.scope, + params=marker.params, + ids=marker.ids, + autouse=marker.autouse, + ) 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. @@ -1820,8 +1844,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: @@ -1858,7 +1882,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 0a41b0aca47..48f964d2a7a 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 1456b5212d4..37c3b51774b 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1078,7 +1078,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 @@ -1264,7 +1264,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 ) @@ -1451,7 +1451,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 de297f408d3..bc934b14f33 100644 --- a/src/_pytest/setuponly.py +++ b/src/_pytest/setuponly.py @@ -1,5 +1,6 @@ from __future__ import annotations +from typing import Any from typing import Generator from _pytest._io.saferepr import saferepr @@ -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/code/test_source.py b/testing/code/test_source.py index a00259976c4..b4a485fca0c 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -478,14 +478,14 @@ def deco_mark(): def deco_fixture(): assert False - src = inspect.getsource(deco_fixture) + src = inspect.getsource(deco_fixture._get_wrapped_function()) assert src == " @pytest.fixture\n def deco_fixture():\n assert False\n" - # currently Source does not unwrap decorators, testing the - # existing behavior here for explicitness, but perhaps we should revisit/change this - # in the future - assert str(Source(deco_fixture)).startswith("@functools.wraps(function)") + # Make sure the decorator is not a wrapped function + assert not str(Source(deco_fixture)).startswith("@functools.wraps(function)") assert ( - textwrap.indent(str(Source(get_real_func(deco_fixture))), " ") + "\n" == src + textwrap.indent(str(Source(deco_fixture._get_wrapped_function())), " ") + + "\n" + == src ) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index c939b221f22..cfc3ccf1b57 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1,6 +1,7 @@ # mypy: allow-untyped-defs from __future__ import annotations +import inspect import os from pathlib import Path import sys @@ -3339,6 +3340,33 @@ def test_foo(B): assert output1 == output2 +class FixtureFunctionDefTestClass: + def __init__(self) -> None: + self.i = 10 + + @pytest.fixture + def fixture_function_def_test_method(self): + return self.i + + +@pytest.fixture +def fixture_function_def_test_func(): + return 9 + + +def test_get_wrapped_func_returns_method(): + obj = FixtureFunctionDefTestClass() + wrapped_function_result = ( + obj.fixture_function_def_test_method._get_wrapped_function() + ) + assert inspect.ismethod(wrapped_function_result) + assert wrapped_function_result() == 10 + + +def test_get_wrapped_func_returns_function(): + assert fixture_function_def_test_func._get_wrapped_function()() == 9 + + class TestRequestScopeAccess: pytestmark = pytest.mark.parametrize( ("scope", "ok", "error"), @@ -4509,6 +4537,21 @@ def fixt(): ) +def test_fixture_class(pytester: Pytester) -> None: + """Check if an error is raised when using @pytest.fixture on a class.""" + pytester.makepyfile( + """ + import pytest + + @pytest.fixture + class A: + pass + """ + ) + result = pytester.runpytest() + result.assert_outcomes(errors=1) + + def test_fixture_param_shadowing(pytester: Pytester) -> None: """Parametrized arguments would be shadowed if a fixture with the same name also exists (#5036)""" pytester.makepyfile( diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 0a4ebf2c9af..cb51b4ca7c0 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -33,7 +33,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 @@ -154,7 +154,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)], diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 7be473d897a..5b8a2ba1333 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -975,6 +975,23 @@ def __repr__(self): assert "UnicodeDecodeError" not in msg assert "UnicodeEncodeError" not in msg + def test_assert_fixture(self, pytester: Pytester) -> None: + pytester.makepyfile( + """\ + import pytest + @pytest.fixture + def fixt(): + return 42 + + def test_something(): # missing "fixt" argument + assert fixt == 42 + """ + ) + result = pytester.runpytest() + result.stdout.fnmatch_lines( + ["*assert )> == 42*"] + ) + class TestRewriteOnImport: def test_pycache_is_a_file(self, pytester: Pytester) -> None: diff --git a/testing/test_collection.py b/testing/test_collection.py index 7d28610e015..ccd57eeef43 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1284,7 +1284,7 @@ def test_1(): """ ) result = pytester.runpytest() - result.stdout.fnmatch_lines(["*1 passed in*"]) + result.assert_outcomes(passed=1) assert result.ret == 0 @@ -1348,7 +1348,7 @@ def test_collect_pyargs_with_testpaths( with monkeypatch.context() as mp: mp.chdir(root) result = pytester.runpytest_subprocess() - result.stdout.fnmatch_lines(["*1 passed in*"]) + result.assert_outcomes(passed=1) def test_initial_conftests_with_testpaths(pytester: Pytester) -> None: diff --git a/testing/test_compat.py b/testing/test_compat.py index 86868858956..94fa2a7885f 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -7,7 +7,6 @@ from functools import wraps from typing import TYPE_CHECKING -from _pytest.compat import _PytestWrapper from _pytest.compat import assert_never from _pytest.compat import get_real_func from _pytest.compat import safe_getattr @@ -38,10 +37,7 @@ def __getattr__(self, attr): with pytest.raises( ValueError, - match=( - "could not find real function of \n" - "stopped at " - ), + match=("wrapper loop when unwrapping "), ): get_real_func(evil) @@ -65,10 +61,17 @@ def func(): wrapped_func2 = decorator(decorator(wrapped_func)) assert get_real_func(wrapped_func2) is func - # special case for __pytest_wrapped__ attribute: used to obtain the function up until the point - # a function was wrapped by pytest itself - wrapped_func2.__pytest_wrapped__ = _PytestWrapper(wrapped_func) - assert get_real_func(wrapped_func2) is wrapped_func + # obtain the function up until the point a function was wrapped by pytest itself + @pytest.fixture + def wrapped_func3(): + pass # pragma: no cover + + wrapped_func4 = decorator(wrapped_func3) + assert ( + # get_real_func does not unwrap function that is wrapped by fixture hence we need to call _get_wrapped_function + get_real_func(wrapped_func4)._get_wrapped_function() + is wrapped_func3._get_wrapped_function() + ) def test_get_real_func_partial() -> None: