| | |
| | from __future__ import annotations |
| |
|
| | import abc |
| | from collections import defaultdict |
| | from collections import deque |
| | import dataclasses |
| | import functools |
| | import inspect |
| | import os |
| | from pathlib import Path |
| | import sys |
| | import types |
| | from typing import AbstractSet |
| | from typing import Any |
| | from typing import Callable |
| | from typing import cast |
| | from typing import Dict |
| | from typing import Final |
| | from typing import final |
| | from typing import Generator |
| | from typing import Generic |
| | from typing import Iterable |
| | from typing import Iterator |
| | from typing import Mapping |
| | from typing import MutableMapping |
| | from typing import NoReturn |
| | from typing import Optional |
| | from typing import OrderedDict |
| | from typing import overload |
| | from typing import Sequence |
| | from typing import Tuple |
| | from typing import TYPE_CHECKING |
| | from typing import TypeVar |
| | from typing import Union |
| | import warnings |
| |
|
| | import _pytest |
| | from _pytest import nodes |
| | from _pytest._code import getfslineno |
| | from _pytest._code import Source |
| | 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 |
| | from _pytest.compat import is_generator |
| | from _pytest.compat import NOTSET |
| | from _pytest.compat import NotSetType |
| | from _pytest.compat import safe_getattr |
| | from _pytest.compat import safe_isclass |
| | from _pytest.config import _PluggyPlugin |
| | from _pytest.config import Config |
| | from _pytest.config import ExitCode |
| | from _pytest.config.argparsing import Parser |
| | from _pytest.deprecated import check_ispytest |
| | from _pytest.deprecated import MARKED_FIXTURE |
| | from _pytest.deprecated import YIELD_FIXTURE |
| | from _pytest.main import Session |
| | from _pytest.mark import Mark |
| | from _pytest.mark import ParameterSet |
| | from _pytest.mark.structures import MarkDecorator |
| | from _pytest.outcomes import fail |
| | from _pytest.outcomes import skip |
| | from _pytest.outcomes import TEST_OUTCOME |
| | from _pytest.pathlib import absolutepath |
| | from _pytest.pathlib import bestrelpath |
| | from _pytest.scope import _ScopeName |
| | from _pytest.scope import HIGH_SCOPES |
| | from _pytest.scope import Scope |
| |
|
| |
|
| | if sys.version_info < (3, 11): |
| | from exceptiongroup import BaseExceptionGroup |
| |
|
| |
|
| | if TYPE_CHECKING: |
| | from _pytest.python import CallSpec2 |
| | from _pytest.python import Function |
| | from _pytest.python import Metafunc |
| |
|
| |
|
| | |
| | FixtureValue = TypeVar("FixtureValue") |
| | |
| | FixtureFunction = TypeVar("FixtureFunction", bound=Callable[..., object]) |
| | |
| | _FixtureFunc = Union[ |
| | Callable[..., FixtureValue], Callable[..., Generator[FixtureValue, None, None]] |
| | ] |
| | |
| | _FixtureCachedResult = Union[ |
| | Tuple[ |
| | |
| | FixtureValue, |
| | |
| | object, |
| | None, |
| | ], |
| | Tuple[ |
| | None, |
| | |
| | object, |
| | |
| | Tuple[BaseException, Optional[types.TracebackType]], |
| | ], |
| | ] |
| |
|
| |
|
| | @dataclasses.dataclass(frozen=True) |
| | class PseudoFixtureDef(Generic[FixtureValue]): |
| | cached_result: _FixtureCachedResult[FixtureValue] |
| | _scope: Scope |
| |
|
| |
|
| | def pytest_sessionstart(session: Session) -> None: |
| | session._fixturemanager = FixtureManager(session) |
| |
|
| |
|
| | def get_scope_package( |
| | node: nodes.Item, |
| | fixturedef: FixtureDef[object], |
| | ) -> nodes.Node | None: |
| | from _pytest.python import Package |
| |
|
| | for parent in node.iter_parents(): |
| | if isinstance(parent, Package) and parent.nodeid == fixturedef.baseid: |
| | return parent |
| | return node.session |
| |
|
| |
|
| | def get_scope_node(node: nodes.Node, scope: Scope) -> nodes.Node | None: |
| | import _pytest.python |
| |
|
| | if scope is Scope.Function: |
| | |
| | |
| | return node.getparent(nodes.Item) |
| | elif scope is Scope.Class: |
| | return node.getparent(_pytest.python.Class) |
| | elif scope is Scope.Module: |
| | return node.getparent(_pytest.python.Module) |
| | elif scope is Scope.Package: |
| | return node.getparent(_pytest.python.Package) |
| | elif scope is Scope.Session: |
| | return node.getparent(_pytest.main.Session) |
| | else: |
| | assert_never(scope) |
| |
|
| |
|
| | 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), |
| | ) |
| |
|
| |
|
| | |
| | |
| | |
| | |
| |
|
| |
|
| | @dataclasses.dataclass(frozen=True) |
| | class FixtureArgKey: |
| | argname: str |
| | param_index: int |
| | scoped_item_path: Path | None |
| | item_cls: type | None |
| |
|
| |
|
| | _V = TypeVar("_V") |
| | OrderedSet = Dict[_V, None] |
| |
|
| |
|
| | def get_parametrized_fixture_argkeys( |
| | item: nodes.Item, scope: Scope |
| | ) -> Iterator[FixtureArgKey]: |
| | """Return list of keys for all parametrized arguments which match |
| | the specified scope.""" |
| | assert scope is not Scope.Function |
| |
|
| | try: |
| | callspec: CallSpec2 = item.callspec |
| | except AttributeError: |
| | return |
| |
|
| | item_cls = None |
| | if scope is Scope.Session: |
| | scoped_item_path = None |
| | elif scope is Scope.Package: |
| | |
| | scoped_item_path = item.path.parent |
| | elif scope is Scope.Module: |
| | scoped_item_path = item.path |
| | elif scope is Scope.Class: |
| | scoped_item_path = item.path |
| | item_cls = item.cls |
| | else: |
| | assert_never(scope) |
| |
|
| | for argname in callspec.indices: |
| | if callspec._arg2scope[argname] != scope: |
| | continue |
| | param_index = callspec.indices[argname] |
| | yield FixtureArgKey(argname, param_index, scoped_item_path, item_cls) |
| |
|
| |
|
| | def reorder_items(items: Sequence[nodes.Item]) -> list[nodes.Item]: |
| | argkeys_by_item: dict[Scope, dict[nodes.Item, OrderedSet[FixtureArgKey]]] = {} |
| | items_by_argkey: dict[ |
| | Scope, dict[FixtureArgKey, OrderedDict[nodes.Item, None]] |
| | ] = {} |
| | for scope in HIGH_SCOPES: |
| | scoped_argkeys_by_item = argkeys_by_item[scope] = {} |
| | scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(OrderedDict) |
| | for item in items: |
| | argkeys = dict.fromkeys(get_parametrized_fixture_argkeys(item, scope)) |
| | if argkeys: |
| | scoped_argkeys_by_item[item] = argkeys |
| | for argkey in argkeys: |
| | scoped_items_by_argkey[argkey][item] = None |
| |
|
| | items_set = dict.fromkeys(items) |
| | return list( |
| | reorder_items_atscope( |
| | items_set, argkeys_by_item, items_by_argkey, Scope.Session |
| | ) |
| | ) |
| |
|
| |
|
| | def reorder_items_atscope( |
| | items: OrderedSet[nodes.Item], |
| | argkeys_by_item: Mapping[Scope, Mapping[nodes.Item, OrderedSet[FixtureArgKey]]], |
| | items_by_argkey: Mapping[ |
| | Scope, Mapping[FixtureArgKey, OrderedDict[nodes.Item, None]] |
| | ], |
| | scope: Scope, |
| | ) -> OrderedSet[nodes.Item]: |
| | if scope is Scope.Function or len(items) < 3: |
| | return items |
| |
|
| | scoped_items_by_argkey = items_by_argkey[scope] |
| | scoped_argkeys_by_item = argkeys_by_item[scope] |
| |
|
| | ignore: set[FixtureArgKey] = set() |
| | items_deque = deque(items) |
| | items_done: OrderedSet[nodes.Item] = {} |
| | while items_deque: |
| | no_argkey_items: OrderedSet[nodes.Item] = {} |
| | slicing_argkey = None |
| | while items_deque: |
| | item = items_deque.popleft() |
| | if item in items_done or item in no_argkey_items: |
| | continue |
| | argkeys = dict.fromkeys( |
| | k for k in scoped_argkeys_by_item.get(item, ()) if k not in ignore |
| | ) |
| | if not argkeys: |
| | no_argkey_items[item] = None |
| | else: |
| | slicing_argkey, _ = argkeys.popitem() |
| | |
| | |
| | matching_items = [ |
| | i for i in scoped_items_by_argkey[slicing_argkey] if i in items |
| | ] |
| | for i in reversed(matching_items): |
| | items_deque.appendleft(i) |
| | |
| | for other_scope in HIGH_SCOPES: |
| | other_scoped_items_by_argkey = items_by_argkey[other_scope] |
| | for argkey in argkeys_by_item[other_scope].get(i, ()): |
| | other_scoped_items_by_argkey[argkey][i] = None |
| | other_scoped_items_by_argkey[argkey].move_to_end( |
| | i, last=False |
| | ) |
| | break |
| | if no_argkey_items: |
| | reordered_no_argkey_items = reorder_items_atscope( |
| | no_argkey_items, argkeys_by_item, items_by_argkey, scope.next_lower() |
| | ) |
| | items_done.update(reordered_no_argkey_items) |
| | if slicing_argkey is not None: |
| | ignore.add(slicing_argkey) |
| | return items_done |
| |
|
| |
|
| | @dataclasses.dataclass(frozen=True) |
| | class FuncFixtureInfo: |
| | """Fixture-related information for a fixture-requesting item (e.g. test |
| | function). |
| | |
| | This is used to examine the fixtures which an item requests statically |
| | (known during collection). This includes autouse fixtures, fixtures |
| | requested by the `usefixtures` marker, fixtures requested in the function |
| | parameters, and the transitive closure of these. |
| | |
| | An item may also request fixtures dynamically (using `request.getfixturevalue`); |
| | these are not reflected here. |
| | """ |
| |
|
| | __slots__ = ("argnames", "initialnames", "names_closure", "name2fixturedefs") |
| |
|
| | |
| | argnames: tuple[str, ...] |
| | |
| | |
| | |
| | initialnames: tuple[str, ...] |
| | |
| | |
| | names_closure: list[str] |
| | |
| | |
| | |
| | |
| | name2fixturedefs: dict[str, Sequence[FixtureDef[Any]]] |
| |
|
| | def prune_dependency_tree(self) -> None: |
| | """Recompute names_closure from initialnames and name2fixturedefs. |
| | |
| | Can only reduce names_closure, which means that the new closure will |
| | always be a subset of the old one. The order is preserved. |
| | |
| | This method is needed because direct parametrization may shadow some |
| | of the fixtures that were included in the originally built dependency |
| | tree. In this way the dependency tree can get pruned, and the closure |
| | of argnames may get reduced. |
| | """ |
| | closure: set[str] = set() |
| | working_set = set(self.initialnames) |
| | while working_set: |
| | argname = working_set.pop() |
| | |
| | |
| | |
| | |
| | |
| | if argname not in closure and argname in self.names_closure: |
| | closure.add(argname) |
| | if argname in self.name2fixturedefs: |
| | working_set.update(self.name2fixturedefs[argname][-1].argnames) |
| |
|
| | self.names_closure[:] = sorted(closure, key=self.names_closure.index) |
| |
|
| |
|
| | class FixtureRequest(abc.ABC): |
| | """The type of the ``request`` fixture. |
| | |
| | A request object gives access to the requesting test context and has a |
| | ``param`` attribute in case the fixture is parametrized. |
| | """ |
| |
|
| | def __init__( |
| | self, |
| | pyfuncitem: Function, |
| | fixturename: str | None, |
| | arg2fixturedefs: dict[str, Sequence[FixtureDef[Any]]], |
| | fixture_defs: dict[str, FixtureDef[Any]], |
| | *, |
| | _ispytest: bool = False, |
| | ) -> None: |
| | check_ispytest(_ispytest) |
| | |
| | self.fixturename: Final = fixturename |
| | self._pyfuncitem: Final = pyfuncitem |
| | |
| | |
| | |
| | |
| | self._arg2fixturedefs: Final = arg2fixturedefs |
| | |
| | |
| | self._fixture_defs: Final = fixture_defs |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | self.param: Any |
| |
|
| | @property |
| | def _fixturemanager(self) -> FixtureManager: |
| | return self._pyfuncitem.session._fixturemanager |
| |
|
| | @property |
| | @abc.abstractmethod |
| | def _scope(self) -> Scope: |
| | raise NotImplementedError() |
| |
|
| | @property |
| | def scope(self) -> _ScopeName: |
| | """Scope string, one of "function", "class", "module", "package", "session".""" |
| | return self._scope.value |
| |
|
| | @abc.abstractmethod |
| | def _check_scope( |
| | self, |
| | requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object], |
| | requested_scope: Scope, |
| | ) -> None: |
| | raise NotImplementedError() |
| |
|
| | @property |
| | def fixturenames(self) -> list[str]: |
| | """Names of all active fixtures in this request.""" |
| | result = list(self._pyfuncitem.fixturenames) |
| | result.extend(set(self._fixture_defs).difference(result)) |
| | return result |
| |
|
| | @property |
| | @abc.abstractmethod |
| | def node(self): |
| | """Underlying collection node (depends on current request scope).""" |
| | raise NotImplementedError() |
| |
|
| | @property |
| | def config(self) -> Config: |
| | """The pytest config object associated with this request.""" |
| | return self._pyfuncitem.config |
| |
|
| | @property |
| | def function(self): |
| | """Test function object if the request has a per-function scope.""" |
| | if self.scope != "function": |
| | raise AttributeError( |
| | f"function not available in {self.scope}-scoped context" |
| | ) |
| | return self._pyfuncitem.obj |
| |
|
| | @property |
| | def cls(self): |
| | """Class (can be None) where the test function was collected.""" |
| | if self.scope not in ("class", "function"): |
| | raise AttributeError(f"cls not available in {self.scope}-scoped context") |
| | clscol = self._pyfuncitem.getparent(_pytest.python.Class) |
| | if clscol: |
| | return clscol.obj |
| |
|
| | @property |
| | def instance(self): |
| | """Instance (can be None) on which test function was collected.""" |
| | if self.scope != "function": |
| | return None |
| | return getattr(self._pyfuncitem, "instance", None) |
| |
|
| | @property |
| | def module(self): |
| | """Python module object where the test function was collected.""" |
| | if self.scope not in ("function", "class", "module"): |
| | raise AttributeError(f"module not available in {self.scope}-scoped context") |
| | mod = self._pyfuncitem.getparent(_pytest.python.Module) |
| | assert mod is not None |
| | return mod.obj |
| |
|
| | @property |
| | def path(self) -> Path: |
| | """Path where the test function was collected.""" |
| | if self.scope not in ("function", "class", "module", "package"): |
| | raise AttributeError(f"path not available in {self.scope}-scoped context") |
| | return self._pyfuncitem.path |
| |
|
| | @property |
| | def keywords(self) -> MutableMapping[str, Any]: |
| | """Keywords/markers dictionary for the underlying node.""" |
| | node: nodes.Node = self.node |
| | return node.keywords |
| |
|
| | @property |
| | def session(self) -> Session: |
| | """Pytest session object.""" |
| | return self._pyfuncitem.session |
| |
|
| | @abc.abstractmethod |
| | def addfinalizer(self, finalizer: Callable[[], object]) -> None: |
| | """Add finalizer/teardown function to be called without arguments after |
| | the last test within the requesting test context finished execution.""" |
| | raise NotImplementedError() |
| |
|
| | def applymarker(self, marker: str | MarkDecorator) -> None: |
| | """Apply a marker to a single test function invocation. |
| | |
| | This method is useful if you don't want to have a keyword/marker |
| | on all function invocations. |
| | |
| | :param marker: |
| | An object created by a call to ``pytest.mark.NAME(...)``. |
| | """ |
| | self.node.add_marker(marker) |
| |
|
| | def raiseerror(self, msg: str | None) -> NoReturn: |
| | """Raise a FixtureLookupError exception. |
| | |
| | :param msg: |
| | An optional custom error message. |
| | """ |
| | raise FixtureLookupError(None, self, msg) |
| |
|
| | def getfixturevalue(self, argname: str) -> Any: |
| | """Dynamically run a named fixture function. |
| | |
| | Declaring fixtures via function argument is recommended where possible. |
| | But if you can only decide whether to use another fixture at test |
| | setup time, you may use this function to retrieve it inside a fixture |
| | or test function body. |
| | |
| | This method can be used during the test setup phase or the test run |
| | phase, but during the test teardown phase a fixture's value may not |
| | be available. |
| | |
| | :param argname: |
| | The fixture name. |
| | :raises pytest.FixtureLookupError: |
| | If the given fixture could not be found. |
| | """ |
| | |
| | |
| | |
| | |
| |
|
| | fixturedef = self._get_active_fixturedef(argname) |
| | assert fixturedef.cached_result is not None, ( |
| | f'The fixture value for "{argname}" is not available. ' |
| | "This can happen when the fixture has already been torn down." |
| | ) |
| | return fixturedef.cached_result[0] |
| |
|
| | def _iter_chain(self) -> Iterator[SubRequest]: |
| | """Yield all SubRequests in the chain, from self up. |
| | |
| | Note: does *not* yield the TopRequest. |
| | """ |
| | current = self |
| | while isinstance(current, SubRequest): |
| | yield current |
| | current = current._parent_request |
| |
|
| | def _get_active_fixturedef( |
| | self, argname: str |
| | ) -> FixtureDef[object] | PseudoFixtureDef[object]: |
| | if argname == "request": |
| | cached_result = (self, [0], None) |
| | return PseudoFixtureDef(cached_result, Scope.Function) |
| |
|
| | |
| | |
| | fixturedef = self._fixture_defs.get(argname) |
| | if fixturedef is not None: |
| | self._check_scope(fixturedef, fixturedef._scope) |
| | return fixturedef |
| |
|
| | |
| | fixturedefs = self._arg2fixturedefs.get(argname, None) |
| | if fixturedefs is None: |
| | |
| | |
| | |
| | fixturedefs = self._fixturemanager.getfixturedefs(argname, self._pyfuncitem) |
| | if fixturedefs is not None: |
| | self._arg2fixturedefs[argname] = fixturedefs |
| | |
| | if fixturedefs is None: |
| | raise FixtureLookupError(argname, self) |
| | |
| | if not fixturedefs: |
| | raise FixtureLookupError(argname, self) |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | index = -1 |
| | for request in self._iter_chain(): |
| | if request.fixturename == argname: |
| | index -= 1 |
| | |
| | if -index > len(fixturedefs): |
| | raise FixtureLookupError(argname, self) |
| | fixturedef = fixturedefs[index] |
| |
|
| | |
| | try: |
| | callspec = self._pyfuncitem.callspec |
| | except AttributeError: |
| | callspec = None |
| | if callspec is not None and argname in callspec.params: |
| | param = callspec.params[argname] |
| | param_index = callspec.indices[argname] |
| | |
| | scope = callspec._arg2scope[argname] |
| | else: |
| | param = NOTSET |
| | param_index = 0 |
| | scope = fixturedef._scope |
| | self._check_fixturedef_without_param(fixturedef) |
| | self._check_scope(fixturedef, scope) |
| | subrequest = SubRequest( |
| | self, scope, param, param_index, fixturedef, _ispytest=True |
| | ) |
| |
|
| | |
| | fixturedef.execute(request=subrequest) |
| |
|
| | self._fixture_defs[argname] = fixturedef |
| | return fixturedef |
| |
|
| | 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 |
| | has_params = fixturedef.params is not None |
| | fixtures_not_supported = getattr(funcitem, "nofuncargs", False) |
| | if has_params and fixtures_not_supported: |
| | msg = ( |
| | f"{funcitem.name} does not support fixtures, maybe unittest.TestCase subclass?\n" |
| | f"Node id: {funcitem.nodeid}\n" |
| | f"Function type: {type(funcitem).__name__}" |
| | ) |
| | fail(msg, pytrace=False) |
| | if has_params: |
| | frame = inspect.stack()[3] |
| | frameinfo = inspect.getframeinfo(frame[0]) |
| | source_path = absolutepath(frameinfo.filename) |
| | source_lineno = frameinfo.lineno |
| | try: |
| | source_path_str = str(source_path.relative_to(funcitem.config.rootpath)) |
| | except ValueError: |
| | source_path_str = str(source_path) |
| | location = getlocation(fixturedef.func, funcitem.config.rootpath) |
| | msg = ( |
| | "The requested fixture has no parameter defined for test:\n" |
| | f" {funcitem.nodeid}\n\n" |
| | f"Requested fixture '{fixturedef.argname}' defined in:\n" |
| | f"{location}\n\n" |
| | f"Requested here:\n" |
| | f"{source_path_str}:{source_lineno}" |
| | ) |
| | fail(msg, pytrace=False) |
| |
|
| | def _get_fixturestack(self) -> list[FixtureDef[Any]]: |
| | values = [request._fixturedef for request in self._iter_chain()] |
| | values.reverse() |
| | return values |
| |
|
| |
|
| | @final |
| | class TopRequest(FixtureRequest): |
| | """The type of the ``request`` fixture in a test function.""" |
| |
|
| | def __init__(self, pyfuncitem: Function, *, _ispytest: bool = False) -> None: |
| | super().__init__( |
| | fixturename=None, |
| | pyfuncitem=pyfuncitem, |
| | arg2fixturedefs=pyfuncitem._fixtureinfo.name2fixturedefs.copy(), |
| | fixture_defs={}, |
| | _ispytest=_ispytest, |
| | ) |
| |
|
| | @property |
| | def _scope(self) -> Scope: |
| | return Scope.Function |
| |
|
| | def _check_scope( |
| | self, |
| | requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object], |
| | requested_scope: Scope, |
| | ) -> None: |
| | |
| | pass |
| |
|
| | @property |
| | def node(self): |
| | return self._pyfuncitem |
| |
|
| | def __repr__(self) -> str: |
| | return f"<FixtureRequest for {self.node!r}>" |
| |
|
| | def _fillfixtures(self) -> None: |
| | item = self._pyfuncitem |
| | for argname in item.fixturenames: |
| | if argname not in item.funcargs: |
| | item.funcargs[argname] = self.getfixturevalue(argname) |
| |
|
| | def addfinalizer(self, finalizer: Callable[[], object]) -> None: |
| | self.node.addfinalizer(finalizer) |
| |
|
| |
|
| | @final |
| | class SubRequest(FixtureRequest): |
| | """The type of the ``request`` fixture in a fixture function requested |
| | (transitively) by a test function.""" |
| |
|
| | def __init__( |
| | self, |
| | request: FixtureRequest, |
| | scope: Scope, |
| | param: Any, |
| | param_index: int, |
| | fixturedef: FixtureDef[object], |
| | *, |
| | _ispytest: bool = False, |
| | ) -> None: |
| | super().__init__( |
| | pyfuncitem=request._pyfuncitem, |
| | fixturename=fixturedef.argname, |
| | fixture_defs=request._fixture_defs, |
| | arg2fixturedefs=request._arg2fixturedefs, |
| | _ispytest=_ispytest, |
| | ) |
| | self._parent_request: Final[FixtureRequest] = request |
| | self._scope_field: Final = scope |
| | self._fixturedef: Final[FixtureDef[object]] = fixturedef |
| | if param is not NOTSET: |
| | self.param = param |
| | self.param_index: Final = param_index |
| |
|
| | def __repr__(self) -> str: |
| | return f"<SubRequest {self.fixturename!r} for {self._pyfuncitem!r}>" |
| |
|
| | @property |
| | def _scope(self) -> Scope: |
| | return self._scope_field |
| |
|
| | @property |
| | def node(self): |
| | scope = self._scope |
| | if scope is Scope.Function: |
| | |
| | node: nodes.Node | None = self._pyfuncitem |
| | elif scope is Scope.Package: |
| | node = get_scope_package(self._pyfuncitem, self._fixturedef) |
| | else: |
| | node = get_scope_node(self._pyfuncitem, scope) |
| | if node is None and scope is Scope.Class: |
| | |
| | node = self._pyfuncitem |
| | assert node, f'Could not obtain a node for scope "{scope}" for function {self._pyfuncitem!r}' |
| | return node |
| |
|
| | def _check_scope( |
| | self, |
| | requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object], |
| | requested_scope: Scope, |
| | ) -> None: |
| | if isinstance(requested_fixturedef, PseudoFixtureDef): |
| | return |
| | if self._scope > requested_scope: |
| | |
| | argname = requested_fixturedef.argname |
| | fixture_stack = "\n".join( |
| | self._format_fixturedef_line(fixturedef) |
| | for fixturedef in self._get_fixturestack() |
| | ) |
| | requested_fixture = self._format_fixturedef_line(requested_fixturedef) |
| | fail( |
| | f"ScopeMismatch: You tried to access the {requested_scope.value} scoped " |
| | f"fixture {argname} with a {self._scope.value} scoped request object. " |
| | f"Requesting fixture stack:\n{fixture_stack}\n" |
| | f"Requested fixture:\n{requested_fixture}", |
| | pytrace=False, |
| | ) |
| |
|
| | def _format_fixturedef_line(self, fixturedef: FixtureDef[object]) -> str: |
| | factory = fixturedef.func |
| | path, lineno = getfslineno(factory) |
| | if isinstance(path, Path): |
| | path = bestrelpath(self._pyfuncitem.session.path, path) |
| | signature = inspect.signature(factory) |
| | return f"{path}:{lineno + 1}: def {factory.__name__}{signature}" |
| |
|
| | def addfinalizer(self, finalizer: Callable[[], object]) -> None: |
| | self._fixturedef.addfinalizer(finalizer) |
| |
|
| |
|
| | @final |
| | class FixtureLookupError(LookupError): |
| | """Could not return a requested fixture (missing or invalid).""" |
| |
|
| | def __init__( |
| | self, argname: str | None, request: FixtureRequest, msg: str | None = None |
| | ) -> None: |
| | self.argname = argname |
| | self.request = request |
| | self.fixturestack = request._get_fixturestack() |
| | self.msg = msg |
| |
|
| | def formatrepr(self) -> FixtureLookupErrorRepr: |
| | tblines: list[str] = [] |
| | addline = tblines.append |
| | stack = [self.request._pyfuncitem.obj] |
| | stack.extend(map(lambda x: x.func, self.fixturestack)) |
| | msg = self.msg |
| | if msg is not None: |
| | |
| | |
| | stack = stack[:-1] |
| | for function in stack: |
| | fspath, lineno = getfslineno(function) |
| | try: |
| | lines, _ = inspect.getsourcelines(get_real_func(function)) |
| | except (OSError, IndexError, TypeError): |
| | error_msg = "file %s, line %s: source code not available" |
| | addline(error_msg % (fspath, lineno + 1)) |
| | else: |
| | addline(f"file {fspath}, line {lineno + 1}") |
| | for i, line in enumerate(lines): |
| | line = line.rstrip() |
| | addline(" " + line) |
| | if line.lstrip().startswith("def"): |
| | break |
| |
|
| | if msg is None: |
| | fm = self.request._fixturemanager |
| | available = set() |
| | parent = self.request._pyfuncitem.parent |
| | assert parent is not None |
| | for name, fixturedefs in fm._arg2fixturedefs.items(): |
| | faclist = list(fm._matchfactories(fixturedefs, parent)) |
| | if faclist: |
| | available.add(name) |
| | if self.argname in available: |
| | msg = ( |
| | f" recursive dependency involving fixture '{self.argname}' detected" |
| | ) |
| | else: |
| | msg = f"fixture '{self.argname}' not found" |
| | msg += "\n available fixtures: {}".format(", ".join(sorted(available))) |
| | msg += "\n use 'pytest --fixtures [testpath]' for help on them." |
| |
|
| | return FixtureLookupErrorRepr(fspath, lineno, tblines, msg, self.argname) |
| |
|
| |
|
| | class FixtureLookupErrorRepr(TerminalRepr): |
| | def __init__( |
| | self, |
| | filename: str | os.PathLike[str], |
| | firstlineno: int, |
| | tblines: Sequence[str], |
| | errorstring: str, |
| | argname: str | None, |
| | ) -> None: |
| | self.tblines = tblines |
| | self.errorstring = errorstring |
| | self.filename = filename |
| | self.firstlineno = firstlineno |
| | self.argname = argname |
| |
|
| | def toterminal(self, tw: TerminalWriter) -> None: |
| | |
| | for tbline in self.tblines: |
| | tw.line(tbline.rstrip()) |
| | lines = self.errorstring.split("\n") |
| | if lines: |
| | tw.line( |
| | f"{FormattedExcinfo.fail_marker} {lines[0].strip()}", |
| | red=True, |
| | ) |
| | for line in lines[1:]: |
| | tw.line( |
| | f"{FormattedExcinfo.flow_marker} {line.strip()}", |
| | red=True, |
| | ) |
| | tw.line() |
| | tw.line("%s:%d" % (os.fspath(self.filename), self.firstlineno + 1)) |
| |
|
| |
|
| | 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) |
| | except StopIteration: |
| | raise ValueError(f"{request.fixturename} did not yield a value") from None |
| | finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, generator) |
| | request.addfinalizer(finalizer) |
| | else: |
| | fixturefunc = cast(Callable[..., FixtureValue], fixturefunc) |
| | fixture_result = fixturefunc(**kwargs) |
| | return fixture_result |
| |
|
| |
|
| | def _teardown_yield_fixture(fixturefunc, it) -> None: |
| | """Execute the teardown of a fixture function by advancing the iterator |
| | after the yield and ensure the iteration ends (if not it means there is |
| | more than one yield in the function).""" |
| | try: |
| | next(it) |
| | except StopIteration: |
| | pass |
| | else: |
| | fs, lineno = getfslineno(fixturefunc) |
| | fail( |
| | f"fixture function has more than one 'yield':\n\n" |
| | f"{Source(fixturefunc).indent()}\n" |
| | f"{fs}:{lineno + 1}", |
| | pytrace=False, |
| | ) |
| |
|
| |
|
| | def _eval_scope_callable( |
| | scope_callable: Callable[[str, Config], _ScopeName], |
| | fixture_name: str, |
| | config: Config, |
| | ) -> _ScopeName: |
| | try: |
| | |
| | |
| | result = scope_callable(fixture_name=fixture_name, config=config) |
| | except Exception as e: |
| | raise TypeError( |
| | f"Error evaluating {scope_callable} while defining fixture '{fixture_name}'.\n" |
| | "Expected a function with the signature (*, fixture_name, config)" |
| | ) from e |
| | if not isinstance(result, str): |
| | fail( |
| | f"Expected {scope_callable} to return a 'str' while defining fixture '{fixture_name}', but it returned:\n" |
| | f"{result!r}", |
| | pytrace=False, |
| | ) |
| | return result |
| |
|
| |
|
| | @final |
| | class FixtureDef(Generic[FixtureValue]): |
| | """A container for a fixture definition. |
| | |
| | Note: At this time, only explicitly documented fields and methods are |
| | considered public stable API. |
| | """ |
| |
|
| | def __init__( |
| | self, |
| | config: Config, |
| | baseid: str | None, |
| | argname: str, |
| | func: _FixtureFunc[FixtureValue], |
| | scope: Scope | _ScopeName | Callable[[str, Config], _ScopeName] | None, |
| | params: Sequence[object] | None, |
| | ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None, |
| | *, |
| | _ispytest: bool = False, |
| | ) -> None: |
| | check_ispytest(_ispytest) |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | self.baseid: Final = baseid or "" |
| | |
| | |
| | |
| | self.has_location: Final = baseid is not None |
| | |
| | self.func: Final = func |
| | |
| | self.argname: Final = argname |
| | if scope is None: |
| | scope = Scope.Function |
| | elif callable(scope): |
| | scope = _eval_scope_callable(scope, argname, config) |
| | if isinstance(scope, str): |
| | scope = Scope.from_user( |
| | scope, descr=f"Fixture '{func.__name__}'", where=baseid |
| | ) |
| | self._scope: Final = scope |
| | |
| | self.params: Final = params |
| | |
| | |
| | |
| | self.ids: Final = ids |
| | |
| | self.argnames: Final = getfuncargnames(func, name=argname) |
| | |
| | |
| | self.cached_result: _FixtureCachedResult[FixtureValue] | None = None |
| | self._finalizers: Final[list[Callable[[], object]]] = [] |
| |
|
| | @property |
| | def scope(self) -> _ScopeName: |
| | """Scope string, one of "function", "class", "module", "package", "session".""" |
| | return self._scope.value |
| |
|
| | def addfinalizer(self, finalizer: Callable[[], object]) -> None: |
| | self._finalizers.append(finalizer) |
| |
|
| | def finish(self, request: SubRequest) -> None: |
| | exceptions: list[BaseException] = [] |
| | while self._finalizers: |
| | fin = self._finalizers.pop() |
| | try: |
| | fin() |
| | except BaseException as e: |
| | exceptions.append(e) |
| | node = request.node |
| | node.ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request) |
| | |
| | |
| | |
| | self.cached_result = None |
| | self._finalizers.clear() |
| | if len(exceptions) == 1: |
| | raise exceptions[0] |
| | elif len(exceptions) > 1: |
| | msg = f'errors while tearing down fixture "{self.argname}" of {node}' |
| | raise BaseExceptionGroup(msg, exceptions[::-1]) |
| |
|
| | def execute(self, request: SubRequest) -> FixtureValue: |
| | """Return the value of this fixture, executing it if not cached.""" |
| | |
| | |
| | |
| | |
| | |
| | |
| | requested_fixtures_that_should_finalize_us = [] |
| | for argname in self.argnames: |
| | fixturedef = request._get_active_fixturedef(argname) |
| | |
| | |
| | |
| | |
| | |
| | if not isinstance(fixturedef, PseudoFixtureDef): |
| | requested_fixtures_that_should_finalize_us.append(fixturedef) |
| |
|
| | |
| | if self.cached_result is not None: |
| | request_cache_key = self.cache_key(request) |
| | cache_key = self.cached_result[1] |
| | try: |
| | |
| | |
| | cache_hit = bool(request_cache_key == cache_key) |
| | except (ValueError, RuntimeError): |
| | |
| | cache_hit = request_cache_key is cache_key |
| |
|
| | if cache_hit: |
| | if self.cached_result[2] is not None: |
| | exc, exc_tb = self.cached_result[2] |
| | raise exc.with_traceback(exc_tb) |
| | else: |
| | result = self.cached_result[0] |
| | return result |
| | |
| | |
| | self.finish(request) |
| | assert self.cached_result is None |
| |
|
| | |
| | |
| | |
| | finalizer = functools.partial(self.finish, request=request) |
| | for parent_fixture in requested_fixtures_that_should_finalize_us: |
| | parent_fixture.addfinalizer(finalizer) |
| |
|
| | ihook = request.node.ihook |
| | try: |
| | |
| | |
| | result = ihook.pytest_fixture_setup(fixturedef=self, request=request) |
| | finally: |
| | |
| | request.node.addfinalizer(finalizer) |
| |
|
| | return result |
| |
|
| | def cache_key(self, request: SubRequest) -> object: |
| | return getattr(request, "param", None) |
| |
|
| | def __repr__(self) -> str: |
| | return f"<FixtureDef argname={self.argname!r} scope={self.scope!r} baseid={self.baseid!r}>" |
| |
|
| |
|
| | def resolve_fixture_function( |
| | fixturedef: FixtureDef[FixtureValue], request: FixtureRequest |
| | ) -> _FixtureFunc[FixtureValue]: |
| | """Get the actual callable that can be called to obtain the fixture |
| | value.""" |
| | fixturefunc = fixturedef.func |
| | |
| | |
| | |
| | instance = request.instance |
| | if instance is not None: |
| | |
| | |
| | if hasattr(fixturefunc, "__self__") and not isinstance( |
| | instance, |
| | fixturefunc.__self__.__class__, |
| | ): |
| | return fixturefunc |
| | fixturefunc = getimfunc(fixturedef.func) |
| | if fixturefunc != fixturedef.func: |
| | fixturefunc = fixturefunc.__get__(instance) |
| | return fixturefunc |
| |
|
| |
|
| | def pytest_fixture_setup( |
| | fixturedef: FixtureDef[FixtureValue], request: SubRequest |
| | ) -> FixtureValue: |
| | """Execution of fixture setup.""" |
| | kwargs = {} |
| | for argname in fixturedef.argnames: |
| | kwargs[argname] = request.getfixturevalue(argname) |
| |
|
| | fixturefunc = resolve_fixture_function(fixturedef, request) |
| | my_cache_key = fixturedef.cache_key(request) |
| | try: |
| | result = call_fixture_func(fixturefunc, request, kwargs) |
| | except TEST_OUTCOME as e: |
| | if isinstance(e, skip.Exception): |
| | |
| | |
| | |
| | e._use_item_location = True |
| | fixturedef.cached_result = (None, my_cache_key, (e, e.__traceback__)) |
| | raise |
| | fixturedef.cached_result = (result, my_cache_key, None) |
| | 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) |
| |
|
| | |
| | |
| | result.__pytest_wrapped__ = _PytestWrapper(function) |
| |
|
| | return cast(FixtureFunction, result) |
| |
|
| |
|
| | @final |
| | @dataclasses.dataclass(frozen=True) |
| | class FixtureFunctionMarker: |
| | scope: _ScopeName | Callable[[str, Config], _ScopeName] |
| | params: tuple[object, ...] | None |
| | autouse: bool = False |
| | ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None |
| | name: str | None = None |
| |
|
| | _ispytest: dataclasses.InitVar[bool] = False |
| |
|
| | def __post_init__(self, _ispytest: bool) -> None: |
| | check_ispytest(_ispytest) |
| |
|
| | def __call__(self, function: FixtureFunction) -> FixtureFunction: |
| | if inspect.isclass(function): |
| | raise ValueError("class fixtures not supported (maybe in the future)") |
| |
|
| | if getattr(function, "_pytestfixturefunction", False): |
| | raise ValueError( |
| | f"@pytest.fixture is being applied more than once to the same function {function.__name__!r}" |
| | ) |
| |
|
| | if hasattr(function, "pytestmark"): |
| | warnings.warn(MARKED_FIXTURE, stacklevel=2) |
| |
|
| | function = wrap_function_to_error_out_if_called_directly(function, self) |
| |
|
| | name = self.name or function.__name__ |
| | if name == "request": |
| | location = getlocation(function) |
| | fail( |
| | f"'request' is a reserved word for fixtures, use another name:\n {location}", |
| | pytrace=False, |
| | ) |
| |
|
| | |
| | function._pytestfixturefunction = self |
| | return function |
| |
|
| |
|
| | @overload |
| | def fixture( |
| | fixture_function: FixtureFunction, |
| | *, |
| | 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: ... |
| |
|
| |
|
| | @overload |
| | def fixture( |
| | fixture_function: None = ..., |
| | *, |
| | scope: _ScopeName | Callable[[str, Config], _ScopeName] = ..., |
| | params: Iterable[object] | None = ..., |
| | autouse: bool = ..., |
| | ids: Sequence[object | None] | Callable[[Any], object | None] | None = ..., |
| | name: str | None = None, |
| | ) -> FixtureFunctionMarker: ... |
| |
|
| |
|
| | def fixture( |
| | fixture_function: FixtureFunction | 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: |
| | """Decorator to mark a fixture factory function. |
| | |
| | This decorator can be used, with or without parameters, to define a |
| | fixture function. |
| | |
| | The name of the fixture function can later be referenced to cause its |
| | invocation ahead of running tests: test modules or classes can use the |
| | ``pytest.mark.usefixtures(fixturename)`` marker. |
| | |
| | Test functions can directly use fixture names as input arguments in which |
| | case the fixture instance returned from the fixture function will be |
| | injected. |
| | |
| | Fixtures can provide their values to test functions using ``return`` or |
| | ``yield`` statements. When using ``yield`` the code block after the |
| | ``yield`` statement is executed as teardown code regardless of the test |
| | outcome, and must yield exactly once. |
| | |
| | :param scope: |
| | The scope for which this fixture is shared; one of ``"function"`` |
| | (default), ``"class"``, ``"module"``, ``"package"`` or ``"session"``. |
| | |
| | This parameter may also be a callable which receives ``(fixture_name, config)`` |
| | as parameters, and must return a ``str`` with one of the values mentioned above. |
| | |
| | See :ref:`dynamic scope` in the docs for more information. |
| | |
| | :param params: |
| | An optional list of parameters which will cause multiple invocations |
| | of the fixture function and all of the tests using it. The current |
| | parameter is available in ``request.param``. |
| | |
| | :param autouse: |
| | If True, the fixture func is activated for all tests that can see it. |
| | If False (the default), an explicit reference is needed to activate |
| | the fixture. |
| | |
| | :param ids: |
| | Sequence of ids each corresponding to the params so that they are |
| | part of the test id. If no ids are provided they will be generated |
| | automatically from the params. |
| | |
| | :param name: |
| | The name of the fixture. This defaults to the name of the decorated |
| | function. If a fixture is used in the same module in which it is |
| | defined, the function name of the fixture will be shadowed by the |
| | function arg that requests the fixture; one way to resolve this is to |
| | name the decorated function ``fixture_<fixturename>`` and then use |
| | ``@pytest.fixture(name='<fixturename>')``. |
| | """ |
| | fixture_marker = FixtureFunctionMarker( |
| | scope=scope, |
| | params=tuple(params) if params is not None else None, |
| | autouse=autouse, |
| | ids=None if ids is None else ids if callable(ids) else tuple(ids), |
| | name=name, |
| | _ispytest=True, |
| | ) |
| |
|
| | |
| | if fixture_function: |
| | return fixture_marker(fixture_function) |
| |
|
| | return fixture_marker |
| |
|
| |
|
| | def yield_fixture( |
| | fixture_function=None, |
| | *args, |
| | scope="function", |
| | params=None, |
| | autouse=False, |
| | ids=None, |
| | name=None, |
| | ): |
| | """(Return a) decorator to mark a yield-fixture factory function. |
| | |
| | .. deprecated:: 3.0 |
| | Use :py:func:`pytest.fixture` directly instead. |
| | """ |
| | warnings.warn(YIELD_FIXTURE, stacklevel=2) |
| | return fixture( |
| | fixture_function, |
| | *args, |
| | scope=scope, |
| | params=params, |
| | autouse=autouse, |
| | ids=ids, |
| | name=name, |
| | ) |
| |
|
| |
|
| | @fixture(scope="session") |
| | def pytestconfig(request: FixtureRequest) -> Config: |
| | """Session-scoped fixture that returns the session's :class:`pytest.Config` |
| | object. |
| | |
| | Example:: |
| | |
| | def test_foo(pytestconfig): |
| | if pytestconfig.get_verbosity() > 0: |
| | ... |
| | |
| | """ |
| | return request.config |
| |
|
| |
|
| | def pytest_addoption(parser: Parser) -> None: |
| | parser.addini( |
| | "usefixtures", |
| | type="args", |
| | default=[], |
| | help="List of default fixtures to be used with this project", |
| | ) |
| | group = parser.getgroup("general") |
| | group.addoption( |
| | "--fixtures", |
| | "--funcargs", |
| | action="store_true", |
| | dest="showfixtures", |
| | default=False, |
| | help="Show available fixtures, sorted by plugin appearance " |
| | "(fixtures with leading '_' are only shown with '-v')", |
| | ) |
| | group.addoption( |
| | "--fixtures-per-test", |
| | action="store_true", |
| | dest="show_fixtures_per_test", |
| | default=False, |
| | help="Show fixtures per test", |
| | ) |
| |
|
| |
|
| | def pytest_cmdline_main(config: Config) -> int | ExitCode | None: |
| | if config.option.showfixtures: |
| | showfixtures(config) |
| | return 0 |
| | if config.option.show_fixtures_per_test: |
| | show_fixtures_per_test(config) |
| | return 0 |
| | return None |
| |
|
| |
|
| | def _get_direct_parametrize_args(node: nodes.Node) -> set[str]: |
| | """Return all direct parametrization arguments of a node, so we don't |
| | mistake them for fixtures. |
| | |
| | Check https://github.com/pytest-dev/pytest/issues/5036. |
| | |
| | These things are done later as well when dealing with parametrization |
| | so this could be improved. |
| | """ |
| | parametrize_argnames: set[str] = set() |
| | for marker in node.iter_markers(name="parametrize"): |
| | if not marker.kwargs.get("indirect", False): |
| | p_argnames, _ = ParameterSet._parse_parametrize_args( |
| | *marker.args, **marker.kwargs |
| | ) |
| | parametrize_argnames.update(p_argnames) |
| | return parametrize_argnames |
| |
|
| |
|
| | def deduplicate_names(*seqs: Iterable[str]) -> tuple[str, ...]: |
| | """De-duplicate the sequence of names while keeping the original order.""" |
| | |
| | return tuple(dict.fromkeys(name for seq in seqs for name in seq)) |
| |
|
| |
|
| | class FixtureManager: |
| | """pytest fixture definitions and information is stored and managed |
| | from this class. |
| | |
| | During collection fm.parsefactories() is called multiple times to parse |
| | fixture function definitions into FixtureDef objects and internal |
| | data structures. |
| | |
| | During collection of test functions, metafunc-mechanics instantiate |
| | a FuncFixtureInfo object which is cached per node/func-name. |
| | This FuncFixtureInfo object is later retrieved by Function nodes |
| | which themselves offer a fixturenames attribute. |
| | |
| | The FuncFixtureInfo object holds information about fixtures and FixtureDefs |
| | relevant for a particular function. An initial list of fixtures is |
| | assembled like this: |
| | |
| | - ini-defined usefixtures |
| | - autouse-marked fixtures along the collection chain up from the function |
| | - usefixtures markers at module/class/function level |
| | - test function funcargs |
| | |
| | Subsequently the funcfixtureinfo.fixturenames attribute is computed |
| | as the closure of the fixtures needed to setup the initial fixtures, |
| | i.e. fixtures needed by fixture functions themselves are appended |
| | to the fixturenames list. |
| | |
| | Upon the test-setup phases all fixturenames are instantiated, retrieved |
| | by a lookup of their FuncFixtureInfo. |
| | """ |
| |
|
| | def __init__(self, session: Session) -> None: |
| | self.session = session |
| | self.config: Config = session.config |
| | |
| | |
| | |
| | |
| | self._arg2fixturedefs: Final[dict[str, list[FixtureDef[Any]]]] = {} |
| | self._holderobjseen: Final[set[object]] = set() |
| | |
| | self._nodeid_autousenames: Final[dict[str, list[str]]] = { |
| | "": self.config.getini("usefixtures"), |
| | } |
| | session.config.pluginmanager.register(self, "funcmanage") |
| |
|
| | def getfixtureinfo( |
| | self, |
| | node: nodes.Item, |
| | func: Callable[..., object] | None, |
| | cls: type | None, |
| | ) -> FuncFixtureInfo: |
| | """Calculate the :class:`FuncFixtureInfo` for an item. |
| | |
| | If ``func`` is None, or if the item sets an attribute |
| | ``nofuncargs = True``, then ``func`` is not examined at all. |
| | |
| | :param node: |
| | The item requesting the fixtures. |
| | :param func: |
| | The item's function. |
| | :param cls: |
| | If the function is a method, the method's class. |
| | """ |
| | if func is not None and not getattr(node, "nofuncargs", False): |
| | argnames = getfuncargnames(func, name=node.name, cls=cls) |
| | else: |
| | argnames = () |
| | usefixturesnames = self._getusefixturesnames(node) |
| | autousenames = self._getautousenames(node) |
| | initialnames = deduplicate_names(autousenames, usefixturesnames, argnames) |
| |
|
| | direct_parametrize_args = _get_direct_parametrize_args(node) |
| |
|
| | names_closure, arg2fixturedefs = self.getfixtureclosure( |
| | parentnode=node, |
| | initialnames=initialnames, |
| | ignore_args=direct_parametrize_args, |
| | ) |
| |
|
| | return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs) |
| |
|
| | def pytest_plugin_registered(self, plugin: _PluggyPlugin, plugin_name: str) -> None: |
| | |
| | |
| | |
| | |
| | if plugin_name and plugin_name.endswith("conftest.py"): |
| | |
| | |
| | |
| | |
| | conftestpath = absolutepath(plugin_name) |
| | try: |
| | nodeid = str(conftestpath.parent.relative_to(self.config.rootpath)) |
| | except ValueError: |
| | nodeid = "" |
| | if nodeid == ".": |
| | nodeid = "" |
| | if os.sep != nodes.SEP: |
| | nodeid = nodeid.replace(os.sep, nodes.SEP) |
| | else: |
| | nodeid = None |
| |
|
| | self.parsefactories(plugin, nodeid) |
| |
|
| | def _getautousenames(self, node: nodes.Node) -> Iterator[str]: |
| | """Return the names of autouse fixtures applicable to node.""" |
| | for parentnode in node.listchain(): |
| | basenames = self._nodeid_autousenames.get(parentnode.nodeid) |
| | if basenames: |
| | yield from basenames |
| |
|
| | def _getusefixturesnames(self, node: nodes.Item) -> Iterator[str]: |
| | """Return the names of usefixtures fixtures applicable to node.""" |
| | for mark in node.iter_markers(name="usefixtures"): |
| | yield from mark.args |
| |
|
| | def getfixtureclosure( |
| | self, |
| | parentnode: nodes.Node, |
| | initialnames: tuple[str, ...], |
| | ignore_args: AbstractSet[str], |
| | ) -> tuple[list[str], dict[str, Sequence[FixtureDef[Any]]]]: |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | fixturenames_closure = list(initialnames) |
| |
|
| | arg2fixturedefs: dict[str, Sequence[FixtureDef[Any]]] = {} |
| | lastlen = -1 |
| | while lastlen != len(fixturenames_closure): |
| | lastlen = len(fixturenames_closure) |
| | for argname in fixturenames_closure: |
| | if argname in ignore_args: |
| | continue |
| | if argname in arg2fixturedefs: |
| | continue |
| | fixturedefs = self.getfixturedefs(argname, parentnode) |
| | if fixturedefs: |
| | arg2fixturedefs[argname] = fixturedefs |
| | for arg in fixturedefs[-1].argnames: |
| | if arg not in fixturenames_closure: |
| | fixturenames_closure.append(arg) |
| |
|
| | def sort_by_scope(arg_name: str) -> Scope: |
| | try: |
| | fixturedefs = arg2fixturedefs[arg_name] |
| | except KeyError: |
| | return Scope.Function |
| | else: |
| | return fixturedefs[-1]._scope |
| |
|
| | fixturenames_closure.sort(key=sort_by_scope, reverse=True) |
| | return fixturenames_closure, arg2fixturedefs |
| |
|
| | def pytest_generate_tests(self, metafunc: Metafunc) -> None: |
| | """Generate new tests based on parametrized fixtures used by the given metafunc""" |
| |
|
| | def get_parametrize_mark_argnames(mark: Mark) -> Sequence[str]: |
| | args, _ = ParameterSet._parse_parametrize_args(*mark.args, **mark.kwargs) |
| | return args |
| |
|
| | for argname in metafunc.fixturenames: |
| | |
| | fixture_defs = metafunc._arg2fixturedefs.get(argname) |
| | if not fixture_defs: |
| | |
| | |
| | continue |
| |
|
| | |
| | |
| | if any( |
| | argname in get_parametrize_mark_argnames(mark) |
| | for mark in metafunc.definition.iter_markers("parametrize") |
| | ): |
| | continue |
| |
|
| | |
| | |
| | |
| | |
| | for fixturedef in reversed(fixture_defs): |
| | |
| | if fixturedef.params is not None: |
| | metafunc.parametrize( |
| | argname, |
| | fixturedef.params, |
| | indirect=True, |
| | scope=fixturedef.scope, |
| | ids=fixturedef.ids, |
| | ) |
| | break |
| |
|
| | |
| | if argname not in fixturedef.argnames: |
| | break |
| |
|
| | |
| |
|
| | def pytest_collection_modifyitems(self, items: list[nodes.Item]) -> None: |
| | |
| | items[:] = reorder_items(items) |
| |
|
| | def _register_fixture( |
| | self, |
| | *, |
| | name: str, |
| | func: _FixtureFunc[object], |
| | nodeid: str | None, |
| | scope: Scope | _ScopeName | Callable[[str, Config], _ScopeName] = "function", |
| | params: Sequence[object] | None = None, |
| | ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None, |
| | autouse: bool = False, |
| | ) -> None: |
| | """Register a fixture |
| | |
| | :param name: |
| | The fixture's name. |
| | :param func: |
| | The fixture's implementation function. |
| | :param nodeid: |
| | The visibility of the fixture. The fixture will be available to the |
| | node with this nodeid and its children in the collection tree. |
| | None means that the fixture is visible to the entire collection tree, |
| | e.g. a fixture defined for general use in a plugin. |
| | :param scope: |
| | The fixture's scope. |
| | :param params: |
| | The fixture's parametrization params. |
| | :param ids: |
| | The fixture's IDs. |
| | :param autouse: |
| | Whether this is an autouse fixture. |
| | """ |
| | fixture_def = FixtureDef( |
| | config=self.config, |
| | baseid=nodeid, |
| | argname=name, |
| | func=func, |
| | scope=scope, |
| | params=params, |
| | ids=ids, |
| | _ispytest=True, |
| | ) |
| |
|
| | faclist = self._arg2fixturedefs.setdefault(name, []) |
| | if fixture_def.has_location: |
| | faclist.append(fixture_def) |
| | else: |
| | |
| | |
| | |
| | |
| | i = len([f for f in faclist if not f.has_location]) |
| | faclist.insert(i, fixture_def) |
| | if autouse: |
| | self._nodeid_autousenames.setdefault(nodeid or "", []).append(name) |
| |
|
| | @overload |
| | def parsefactories( |
| | self, |
| | node_or_obj: nodes.Node, |
| | ) -> None: |
| | raise NotImplementedError() |
| |
|
| | @overload |
| | def parsefactories( |
| | self, |
| | node_or_obj: object, |
| | nodeid: str | None, |
| | ) -> None: |
| | raise NotImplementedError() |
| |
|
| | def parsefactories( |
| | self, |
| | node_or_obj: nodes.Node | object, |
| | nodeid: str | NotSetType | None = NOTSET, |
| | ) -> None: |
| | """Collect fixtures from a collection node or object. |
| | |
| | Found fixtures are parsed into `FixtureDef`s and saved. |
| | |
| | If `node_or_object` is a collection node (with an underlying Python |
| | object), the node's object is traversed and the node's nodeid is used to |
| | determine the fixtures' visibility. `nodeid` must not be specified in |
| | this case. |
| | |
| | If `node_or_object` is an object (e.g. a plugin), the object is |
| | traversed and the given `nodeid` is used to determine the fixtures' |
| | visibility. `nodeid` must be specified in this case; None and "" mean |
| | total visibility. |
| | """ |
| | if nodeid is not NOTSET: |
| | holderobj = node_or_obj |
| | else: |
| | assert isinstance(node_or_obj, nodes.Node) |
| | holderobj = cast(object, node_or_obj.obj) |
| | assert isinstance(node_or_obj.nodeid, str) |
| | nodeid = node_or_obj.nodeid |
| | if holderobj in self._holderobjseen: |
| | return |
| |
|
| | |
| | if not safe_isclass(holderobj) and not isinstance(holderobj, types.ModuleType): |
| | holderobj_tp: object = type(holderobj) |
| | else: |
| | holderobj_tp = holderobj |
| |
|
| | self._holderobjseen.add(holderobj) |
| | for name in dir(holderobj): |
| | |
| | |
| | obj_ub = safe_getattr(holderobj_tp, name, None) |
| | marker = getfixturemarker(obj_ub) |
| | if not isinstance(marker, FixtureFunctionMarker): |
| | |
| | |
| | continue |
| |
|
| | |
| | obj = getattr(holderobj, name) |
| |
|
| | if marker.name: |
| | name = marker.name |
| |
|
| | |
| | |
| | |
| | |
| | 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, |
| | ) |
| |
|
| | def getfixturedefs( |
| | self, argname: str, node: nodes.Node |
| | ) -> Sequence[FixtureDef[Any]] | None: |
| | """Get FixtureDefs for a fixture name which are applicable |
| | to a given node. |
| | |
| | Returns None if there are no fixtures at all defined with the given |
| | name. (This is different from the case in which there are fixtures |
| | with the given name, but none applicable to the node. In this case, |
| | an empty result is returned). |
| | |
| | :param argname: Name of the fixture to search for. |
| | :param node: The requesting Node. |
| | """ |
| | try: |
| | fixturedefs = self._arg2fixturedefs[argname] |
| | except KeyError: |
| | return None |
| | return tuple(self._matchfactories(fixturedefs, node)) |
| |
|
| | def _matchfactories( |
| | 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: |
| | yield fixturedef |
| |
|
| |
|
| | def show_fixtures_per_test(config: Config) -> int | ExitCode: |
| | from _pytest.main import wrap_session |
| |
|
| | return wrap_session(config, _show_fixtures_per_test) |
| |
|
| |
|
| | _PYTEST_DIR = Path(_pytest.__file__).parent |
| |
|
| |
|
| | def _pretty_fixture_path(invocation_dir: Path, func) -> str: |
| | loc = Path(getlocation(func, invocation_dir)) |
| | prefix = Path("...", "_pytest") |
| | try: |
| | return str(prefix / loc.relative_to(_PYTEST_DIR)) |
| | except ValueError: |
| | return bestrelpath(invocation_dir, loc) |
| |
|
| |
|
| | def _show_fixtures_per_test(config: Config, session: Session) -> None: |
| | import _pytest.config |
| |
|
| | session.perform_collect() |
| | invocation_dir = config.invocation_params.dir |
| | tw = _pytest.config.create_terminal_writer(config) |
| | verbose = config.get_verbosity() |
| |
|
| | 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: |
| | argname = fixture_def.argname |
| | if verbose <= 0 and argname.startswith("_"): |
| | return |
| | prettypath = _pretty_fixture_path(invocation_dir, fixture_def.func) |
| | tw.write(f"{argname}", green=True) |
| | tw.write(f" -- {prettypath}", yellow=True) |
| | tw.write("\n") |
| | fixture_doc = inspect.getdoc(fixture_def.func) |
| | if fixture_doc: |
| | write_docstring( |
| | tw, |
| | fixture_doc.split("\n\n", maxsplit=1)[0] |
| | if verbose <= 0 |
| | else fixture_doc, |
| | ) |
| | else: |
| | tw.line(" no docstring available", red=True) |
| |
|
| | def write_item(item: nodes.Item) -> None: |
| | |
| | info: FuncFixtureInfo | None = getattr(item, "_fixtureinfo", None) |
| | if info is None or not info.name2fixturedefs: |
| | |
| | return |
| | tw.line() |
| | tw.sep("-", f"fixtures used by {item.name}") |
| | |
| | tw.sep("-", f"({get_best_relpath(item.function)})") |
| | |
| | for _, fixturedefs in sorted(info.name2fixturedefs.items()): |
| | assert fixturedefs is not None |
| | if not fixturedefs: |
| | continue |
| | |
| | write_fixture(fixturedefs[-1]) |
| |
|
| | for session_item in session.items: |
| | write_item(session_item) |
| |
|
| |
|
| | def showfixtures(config: Config) -> int | ExitCode: |
| | from _pytest.main import wrap_session |
| |
|
| | return wrap_session(config, _showfixtures_main) |
| |
|
| |
|
| | def _showfixtures_main(config: Config, session: Session) -> None: |
| | import _pytest.config |
| |
|
| | session.perform_collect() |
| | invocation_dir = config.invocation_params.dir |
| | tw = _pytest.config.create_terminal_writer(config) |
| | verbose = config.get_verbosity() |
| |
|
| | fm = session._fixturemanager |
| |
|
| | available = [] |
| | seen: set[tuple[str, str]] = set() |
| |
|
| | for argname, fixturedefs in fm._arg2fixturedefs.items(): |
| | assert fixturedefs is not None |
| | if not fixturedefs: |
| | continue |
| | for fixturedef in fixturedefs: |
| | loc = getlocation(fixturedef.func, invocation_dir) |
| | if (fixturedef.argname, loc) in seen: |
| | continue |
| | seen.add((fixturedef.argname, loc)) |
| | available.append( |
| | ( |
| | len(fixturedef.baseid), |
| | fixturedef.func.__module__, |
| | _pretty_fixture_path(invocation_dir, fixturedef.func), |
| | fixturedef.argname, |
| | fixturedef, |
| | ) |
| | ) |
| |
|
| | available.sort() |
| | currentmodule = None |
| | for baseid, module, prettypath, argname, fixturedef in available: |
| | if currentmodule != module: |
| | if not module.startswith("_pytest."): |
| | tw.line() |
| | tw.sep("-", f"fixtures defined from {module}") |
| | currentmodule = module |
| | if verbose <= 0 and argname.startswith("_"): |
| | continue |
| | tw.write(f"{argname}", green=True) |
| | if fixturedef.scope != "function": |
| | tw.write(f" [{fixturedef.scope} scope]", cyan=True) |
| | tw.write(f" -- {prettypath}", yellow=True) |
| | tw.write("\n") |
| | doc = inspect.getdoc(fixturedef.func) |
| | if doc: |
| | write_docstring( |
| | tw, doc.split("\n\n", maxsplit=1)[0] if verbose <= 0 else doc |
| | ) |
| | else: |
| | tw.line(" no docstring available", red=True) |
| | tw.line() |
| |
|
| |
|
| | def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None: |
| | for line in doc.split("\n"): |
| | tw.line(indent + line) |
| |
|