123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713 |
- import dataclasses
- import functools
- import inspect
- import os
- import sys
- import warnings
- from collections import defaultdict
- from collections import deque
- from contextlib import suppress
- from pathlib import Path
- from types import TracebackType
- from typing import Any
- from typing import Callable
- from typing import cast
- from typing import Dict
- from typing import Generator
- from typing import Generic
- from typing import Iterable
- from typing import Iterator
- from typing import List
- from typing import MutableMapping
- from typing import NoReturn
- from typing import Optional
- from typing import Sequence
- from typing import Set
- from typing import Tuple
- from typing import Type
- from typing import TYPE_CHECKING
- from typing import TypeVar
- from typing import Union
- import _pytest
- from _pytest import nodes
- from _pytest._code import getfslineno
- from _pytest._code.code import FormattedExcinfo
- from _pytest._code.code import TerminalRepr
- from _pytest._io import TerminalWriter
- from _pytest.compat import _format_args
- from _pytest.compat import _PytestWrapper
- from _pytest.compat import assert_never
- from _pytest.compat import final
- 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 overload
- from _pytest.compat import safe_getattr
- from _pytest.config import _PluggyPlugin
- from _pytest.config import Config
- from _pytest.config.argparsing import Parser
- from _pytest.deprecated import check_ispytest
- from _pytest.deprecated import YIELD_FIXTURE
- 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 HIGH_SCOPES
- from _pytest.scope import Scope
- from _pytest.stash import StashKey
- if TYPE_CHECKING:
- from typing import Deque
- from _pytest.scope import _ScopeName
- from _pytest.main import Session
- from _pytest.python import CallSpec2
- from _pytest.python import Metafunc
- # The value of the fixture -- return/yield of the fixture function (type variable).
- FixtureValue = TypeVar("FixtureValue")
- # The type of the fixture function (type variable).
- FixtureFunction = TypeVar("FixtureFunction", bound=Callable[..., object])
- # The type of a fixture function (type alias generic in fixture value).
- _FixtureFunc = Union[
- Callable[..., FixtureValue], Callable[..., Generator[FixtureValue, None, None]]
- ]
- # The type of FixtureDef.cached_result (type alias generic in fixture value).
- _FixtureCachedResult = Union[
- Tuple[
- # The result.
- FixtureValue,
- # Cache key.
- object,
- None,
- ],
- Tuple[
- None,
- # Cache key.
- object,
- # Exc info if raised.
- Tuple[Type[BaseException], BaseException, TracebackType],
- ],
- ]
- @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]",
- ) -> Optional[Union[nodes.Item, nodes.Collector]]:
- from _pytest.python import Package
- current: Optional[Union[nodes.Item, nodes.Collector]] = node
- fixture_package_name = "{}/{}".format(fixturedef.baseid, "__init__.py")
- while current and (
- not isinstance(current, Package) or fixture_package_name != current.nodeid
- ):
- current = current.parent # type: ignore[assignment]
- if current is None:
- return node.session
- return current
- def get_scope_node(
- node: nodes.Node, scope: Scope
- ) -> Optional[Union[nodes.Item, nodes.Collector]]:
- 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)
- # Used for storing artificial fixturedefs for direct parametrization.
- name2pseudofixturedef_key = StashKey[Dict[str, "FixtureDef[Any]"]]()
- def add_funcarg_pseudo_fixture_def(
- collector: nodes.Collector, metafunc: "Metafunc", fixturemanager: "FixtureManager"
- ) -> None:
- # This function will transform all collected calls to functions
- # if they use direct funcargs (i.e. direct parametrization)
- # because we want later test execution to be able to rely on
- # an existing FixtureDef structure for all arguments.
- # XXX we can probably avoid this algorithm if we modify CallSpec2
- # to directly care for creating the fixturedefs within its methods.
- if not metafunc._calls[0].funcargs:
- # This function call does not have direct parametrization.
- return
- # Collect funcargs of all callspecs into a list of values.
- arg2params: Dict[str, List[object]] = {}
- arg2scope: Dict[str, Scope] = {}
- for callspec in metafunc._calls:
- for argname, argvalue in callspec.funcargs.items():
- assert argname not in callspec.params
- callspec.params[argname] = argvalue
- arg2params_list = arg2params.setdefault(argname, [])
- callspec.indices[argname] = len(arg2params_list)
- arg2params_list.append(argvalue)
- if argname not in arg2scope:
- scope = callspec._arg2scope.get(argname, Scope.Function)
- arg2scope[argname] = scope
- callspec.funcargs.clear()
- # Register artificial FixtureDef's so that later at test execution
- # time we can rely on a proper FixtureDef to exist for fixture setup.
- arg2fixturedefs = metafunc._arg2fixturedefs
- for argname, valuelist in arg2params.items():
- # If we have a scope that is higher than function, we need
- # to make sure we only ever create an according fixturedef on
- # a per-scope basis. We thus store and cache the fixturedef on the
- # node related to the scope.
- scope = arg2scope[argname]
- node = None
- if scope is not Scope.Function:
- node = get_scope_node(collector, scope)
- if node is None:
- assert scope is Scope.Class and isinstance(
- collector, _pytest.python.Module
- )
- # Use module-level collector for class-scope (for now).
- node = collector
- if node is None:
- name2pseudofixturedef = None
- else:
- default: Dict[str, FixtureDef[Any]] = {}
- name2pseudofixturedef = node.stash.setdefault(
- name2pseudofixturedef_key, default
- )
- if name2pseudofixturedef is not None and argname in name2pseudofixturedef:
- arg2fixturedefs[argname] = [name2pseudofixturedef[argname]]
- else:
- fixturedef = FixtureDef(
- fixturemanager=fixturemanager,
- baseid="",
- argname=argname,
- func=get_direct_param_fixture_func,
- scope=arg2scope[argname],
- params=valuelist,
- unittest=False,
- ids=None,
- )
- arg2fixturedefs[argname] = [fixturedef]
- if name2pseudofixturedef is not None:
- name2pseudofixturedef[argname] = fixturedef
- def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]:
- """Return fixturemarker or None if it doesn't exist or raised
- exceptions."""
- return cast(
- Optional[FixtureFunctionMarker],
- safe_getattr(obj, "_pytestfixturefunction", None),
- )
- # Parametrized fixture key, helper alias for code below.
- _Key = Tuple[object, ...]
- def get_parametrized_fixture_keys(item: nodes.Item, scope: Scope) -> Iterator[_Key]:
- """Return list of keys for all parametrized arguments which match
- the specified scope."""
- assert scope is not Scope.Function
- try:
- callspec = item.callspec # type: ignore[attr-defined]
- except AttributeError:
- pass
- else:
- cs: CallSpec2 = callspec
- # cs.indices.items() is random order of argnames. Need to
- # sort this so that different calls to
- # get_parametrized_fixture_keys will be deterministic.
- for argname, param_index in sorted(cs.indices.items()):
- if cs._arg2scope[argname] != scope:
- continue
- if scope is Scope.Session:
- key: _Key = (argname, param_index)
- elif scope is Scope.Package:
- key = (argname, param_index, item.path.parent)
- elif scope is Scope.Module:
- key = (argname, param_index, item.path)
- elif scope is Scope.Class:
- item_cls = item.cls # type: ignore[attr-defined]
- key = (argname, param_index, item.path, item_cls)
- else:
- assert_never(scope)
- yield key
- # Algorithm for sorting on a per-parametrized resource setup basis.
- # It is called for Session scope first and performs sorting
- # down to the lower scopes such as to minimize number of "high scope"
- # setups and teardowns.
- def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
- argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[_Key, None]]] = {}
- items_by_argkey: Dict[Scope, Dict[_Key, Deque[nodes.Item]]] = {}
- for scope in HIGH_SCOPES:
- d: Dict[nodes.Item, Dict[_Key, None]] = {}
- argkeys_cache[scope] = d
- item_d: Dict[_Key, Deque[nodes.Item]] = defaultdict(deque)
- items_by_argkey[scope] = item_d
- for item in items:
- keys = dict.fromkeys(get_parametrized_fixture_keys(item, scope), None)
- if keys:
- d[item] = keys
- for key in keys:
- item_d[key].append(item)
- items_dict = dict.fromkeys(items, None)
- return list(
- reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, Scope.Session)
- )
- def fix_cache_order(
- item: nodes.Item,
- argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[_Key, None]]],
- items_by_argkey: Dict[Scope, Dict[_Key, "Deque[nodes.Item]"]],
- ) -> None:
- for scope in HIGH_SCOPES:
- for key in argkeys_cache[scope].get(item, []):
- items_by_argkey[scope][key].appendleft(item)
- def reorder_items_atscope(
- items: Dict[nodes.Item, None],
- argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[_Key, None]]],
- items_by_argkey: Dict[Scope, Dict[_Key, "Deque[nodes.Item]"]],
- scope: Scope,
- ) -> Dict[nodes.Item, None]:
- if scope is Scope.Function or len(items) < 3:
- return items
- ignore: Set[Optional[_Key]] = set()
- items_deque = deque(items)
- items_done: Dict[nodes.Item, None] = {}
- scoped_items_by_argkey = items_by_argkey[scope]
- scoped_argkeys_cache = argkeys_cache[scope]
- while items_deque:
- no_argkey_group: Dict[nodes.Item, None] = {}
- slicing_argkey = None
- while items_deque:
- item = items_deque.popleft()
- if item in items_done or item in no_argkey_group:
- continue
- argkeys = dict.fromkeys(
- (k for k in scoped_argkeys_cache.get(item, []) if k not in ignore), None
- )
- if not argkeys:
- no_argkey_group[item] = None
- else:
- slicing_argkey, _ = argkeys.popitem()
- # We don't have to remove relevant items from later in the
- # deque because they'll just be ignored.
- matching_items = [
- i for i in scoped_items_by_argkey[slicing_argkey] if i in items
- ]
- for i in reversed(matching_items):
- fix_cache_order(i, argkeys_cache, items_by_argkey)
- items_deque.appendleft(i)
- break
- if no_argkey_group:
- no_argkey_group = reorder_items_atscope(
- no_argkey_group, argkeys_cache, items_by_argkey, scope.next_lower()
- )
- for item in no_argkey_group:
- items_done[item] = None
- ignore.add(slicing_argkey)
- return items_done
- def get_direct_param_fixture_func(request: "FixtureRequest") -> Any:
- return request.param
- @dataclasses.dataclass
- class FuncFixtureInfo:
- __slots__ = ("argnames", "initialnames", "names_closure", "name2fixturedefs")
- # Original function argument names.
- argnames: Tuple[str, ...]
- # Argnames that function immediately requires. These include argnames +
- # fixture names specified via usefixtures and via autouse=True in fixture
- # definitions.
- 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()
- # Argname may be smth not included in the original names_closure,
- # in which case we ignore it. This currently happens with pseudo
- # FixtureDefs which wrap 'get_direct_param_fixture_func(request)'.
- # So they introduce the new dependency 'request' which might have
- # been missing in the original tree (closure).
- 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:
- """A request for a fixture from a test or fixture function.
- A request object gives access to the requesting test context and has
- an optional ``param`` attribute in case the fixture is parametrized
- indirectly.
- """
- def __init__(self, pyfuncitem, *, _ispytest: bool = False) -> None:
- check_ispytest(_ispytest)
- self._pyfuncitem = pyfuncitem
- #: Fixture for which this request is being performed.
- self.fixturename: Optional[str] = None
- self._scope = Scope.Function
- self._fixture_defs: Dict[str, FixtureDef[Any]] = {}
- fixtureinfo: FuncFixtureInfo = pyfuncitem._fixtureinfo
- self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy()
- self._arg2index: Dict[str, int] = {}
- self._fixturemanager: FixtureManager = pyfuncitem.session._fixturemanager
- # Notes on the type of `param`:
- # -`request.param` is only defined in parametrized fixtures, and will raise
- # AttributeError otherwise. Python typing has no notion of "undefined", so
- # this cannot be reflected in the type.
- # - Technically `param` is only (possibly) defined on SubRequest, not
- # FixtureRequest, but the typing of that is still in flux so this cheats.
- # - In the future we might consider using a generic for the param type, but
- # for now just using Any.
- self.param: Any
- @property
- def scope(self) -> "_ScopeName":
- """Scope string, one of "function", "class", "module", "package", "session"."""
- return self._scope.value
- @property
- def fixturenames(self) -> List[str]:
- """Names of all active fixtures in this request."""
- result = list(self._pyfuncitem._fixtureinfo.names_closure)
- result.extend(set(self._fixture_defs).difference(result))
- return result
- @property
- def node(self):
- """Underlying collection node (depends on current request scope)."""
- scope = self._scope
- if scope is Scope.Function:
- # This might also be a non-function Item despite its attribute name.
- node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
- elif scope is Scope.Package:
- # FIXME: _fixturedef is not defined on FixtureRequest (this class),
- # but on FixtureRequest (a subclass).
- node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
- else:
- node = get_scope_node(self._pyfuncitem, scope)
- if node is None and scope is Scope.Class:
- # Fallback to function item itself.
- node = self._pyfuncitem
- assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
- scope, self._pyfuncitem
- )
- return node
- def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
- fixturedefs = self._arg2fixturedefs.get(argname, None)
- if fixturedefs is None:
- # We arrive here because of a dynamic call to
- # getfixturevalue(argname) usage which was naturally
- # not known at parsing/collection time.
- assert self._pyfuncitem.parent is not None
- parentid = self._pyfuncitem.parent.nodeid
- fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid)
- # TODO: Fix this type ignore. Either add assert or adjust types.
- # Can this be None here?
- self._arg2fixturedefs[argname] = fixturedefs # type: ignore[assignment]
- # fixturedefs list is immutable so we maintain a decreasing index.
- index = self._arg2index.get(argname, 0) - 1
- if fixturedefs is None or (-index > len(fixturedefs)):
- raise FixtureLookupError(argname, self)
- self._arg2index[argname] = index
- return fixturedefs[index]
- @property
- def config(self) -> Config:
- """The pytest config object associated with this request."""
- return self._pyfuncitem.config # type: ignore[no-any-return]
- @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."""
- # unittest support hack, see _pytest.unittest.TestCaseFunction.
- try:
- return self._pyfuncitem._testcase
- except AttributeError:
- function = getattr(self, "function", None)
- return getattr(function, "__self__", 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")
- return self._pyfuncitem.getparent(_pytest.python.Module).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")
- # TODO: Remove ignore once _pyfuncitem is properly typed.
- return self._pyfuncitem.path # type: ignore
- @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 # type: ignore[no-any-return]
- 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."""
- # XXX usually this method is shadowed by fixturedef specific ones.
- self.node.addfinalizer(finalizer)
- def applymarker(self, marker: Union[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: Optional[str]) -> NoReturn:
- """Raise a FixtureLookupError exception.
- :param msg:
- An optional custom error message.
- """
- raise self._fixturemanager.FixtureLookupError(None, self, msg)
- def _fillfixtures(self) -> None:
- item = self._pyfuncitem
- fixturenames = getattr(item, "fixturenames", self.fixturenames)
- for argname in fixturenames:
- if argname not in item.funcargs:
- item.funcargs[argname] = self.getfixturevalue(argname)
- 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 _get_active_fixturedef(
- self, argname: str
- ) -> Union["FixtureDef[object]", PseudoFixtureDef[object]]:
- try:
- return self._fixture_defs[argname]
- except KeyError:
- try:
- fixturedef = self._getnextfixturedef(argname)
- except FixtureLookupError:
- if argname == "request":
- cached_result = (self, [0], None)
- return PseudoFixtureDef(cached_result, Scope.Function)
- raise
- # Remove indent to prevent the python3 exception
- # from leaking into the call.
- self._compute_fixture_value(fixturedef)
- self._fixture_defs[argname] = fixturedef
- return fixturedef
- def _get_fixturestack(self) -> List["FixtureDef[Any]"]:
- current = self
- values: List[FixtureDef[Any]] = []
- while isinstance(current, SubRequest):
- values.append(current._fixturedef) # type: ignore[has-type]
- current = current._parent_request
- values.reverse()
- return values
- def _compute_fixture_value(self, fixturedef: "FixtureDef[object]") -> None:
- """Create a SubRequest based on "self" and call the execute method
- of the given FixtureDef object.
- This will force the FixtureDef object to throw away any previous
- results and compute a new fixture value, which will be stored into
- the FixtureDef object itself.
- """
- # prepare a subrequest object before calling fixture function
- # (latter managed by fixturedef)
- argname = fixturedef.argname
- funcitem = self._pyfuncitem
- scope = fixturedef._scope
- try:
- callspec = funcitem.callspec
- except AttributeError:
- callspec = None
- if callspec is not None and argname in callspec.params:
- param = callspec.params[argname]
- param_index = callspec.indices[argname]
- # If a parametrize invocation set a scope it will override
- # the static scope defined with the fixture function.
- with suppress(KeyError):
- scope = callspec._arg2scope[argname]
- else:
- param = NOTSET
- param_index = 0
- has_params = fixturedef.params is not None
- fixtures_not_supported = getattr(funcitem, "nofuncargs", False)
- if has_params and fixtures_not_supported:
- msg = (
- "{name} does not support fixtures, maybe unittest.TestCase subclass?\n"
- "Node id: {nodeid}\n"
- "Function type: {typename}"
- ).format(
- name=funcitem.name,
- nodeid=funcitem.nodeid,
- typename=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)
- msg = (
- "The requested fixture has no parameter defined for test:\n"
- " {}\n\n"
- "Requested fixture '{}' defined in:\n{}"
- "\n\nRequested here:\n{}:{}".format(
- funcitem.nodeid,
- fixturedef.argname,
- getlocation(fixturedef.func, funcitem.config.rootpath),
- source_path_str,
- source_lineno,
- )
- )
- fail(msg, pytrace=False)
- subrequest = SubRequest(
- self, scope, param, param_index, fixturedef, _ispytest=True
- )
- # Check if a higher-level scoped fixture accesses a lower level one.
- subrequest._check_scope(argname, self._scope, scope)
- try:
- # Call the fixture function.
- fixturedef.execute(request=subrequest)
- finally:
- self._schedule_finalizers(fixturedef, subrequest)
- def _schedule_finalizers(
- self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest"
- ) -> None:
- # If fixture function failed it might have registered finalizers.
- subrequest.node.addfinalizer(lambda: fixturedef.finish(request=subrequest))
- def _check_scope(
- self,
- argname: str,
- invoking_scope: Scope,
- requested_scope: Scope,
- ) -> None:
- if argname == "request":
- return
- if invoking_scope > requested_scope:
- # Try to report something helpful.
- text = "\n".join(self._factorytraceback())
- fail(
- f"ScopeMismatch: You tried to access the {requested_scope.value} scoped "
- f"fixture {argname} with a {invoking_scope.value} scoped request object, "
- f"involved factories:\n{text}",
- pytrace=False,
- )
- def _factorytraceback(self) -> List[str]:
- lines = []
- for fixturedef in self._get_fixturestack():
- factory = fixturedef.func
- fs, lineno = getfslineno(factory)
- if isinstance(fs, Path):
- session: Session = self._pyfuncitem.session
- p = bestrelpath(session.path, fs)
- else:
- p = fs
- args = _format_args(factory)
- lines.append("%s:%d: def %s%s" % (p, lineno + 1, factory.__name__, args))
- return lines
- def __repr__(self) -> str:
- return "<FixtureRequest for %r>" % (self.node)
- @final
- class SubRequest(FixtureRequest):
- """A sub request for handling getting a fixture from a test function/fixture."""
- def __init__(
- self,
- request: "FixtureRequest",
- scope: Scope,
- param: Any,
- param_index: int,
- fixturedef: "FixtureDef[object]",
- *,
- _ispytest: bool = False,
- ) -> None:
- check_ispytest(_ispytest)
- self._parent_request = request
- self.fixturename = fixturedef.argname
- if param is not NOTSET:
- self.param = param
- self.param_index = param_index
- self._scope = scope
- self._fixturedef = fixturedef
- self._pyfuncitem = request._pyfuncitem
- self._fixture_defs = request._fixture_defs
- self._arg2fixturedefs = request._arg2fixturedefs
- self._arg2index = request._arg2index
- self._fixturemanager = request._fixturemanager
- def __repr__(self) -> str:
- return f"<SubRequest {self.fixturename!r} for {self._pyfuncitem!r}>"
- 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."""
- self._fixturedef.addfinalizer(finalizer)
- def _schedule_finalizers(
- self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest"
- ) -> None:
- # If the executing fixturedef was not explicitly requested in the argument list (via
- # getfixturevalue inside the fixture call) then ensure this fixture def will be finished
- # first.
- if fixturedef.argname not in self.fixturenames:
- fixturedef.addfinalizer(
- functools.partial(self._fixturedef.finish, request=self)
- )
- super()._schedule_finalizers(fixturedef, subrequest)
- @final
- class FixtureLookupError(LookupError):
- """Could not return a requested fixture (missing or invalid)."""
- def __init__(
- self, argname: Optional[str], request: FixtureRequest, msg: Optional[str] = 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:
- # The last fixture raise an error, let's present
- # it at the requesting side.
- 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()
- parentid = self.request._pyfuncitem.parent.nodeid
- for name, fixturedefs in fm._arg2fixturedefs.items():
- faclist = list(fm._matchfactories(fixturedefs, parentid))
- if faclist:
- available.add(name)
- if self.argname in available:
- msg = " recursive dependency involving fixture '{}' detected".format(
- self.argname
- )
- 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: Union[str, "os.PathLike[str]"],
- firstlineno: int,
- tblines: Sequence[str],
- errorstring: str,
- argname: Optional[str],
- ) -> None:
- self.tblines = tblines
- self.errorstring = errorstring
- self.filename = filename
- self.firstlineno = firstlineno
- self.argname = argname
- def toterminal(self, tw: TerminalWriter) -> None:
- # tw.line("FixtureLookupError: %s" %(self.argname), red=True)
- 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 fail_fixturefunc(fixturefunc, msg: str) -> NoReturn:
- fs, lineno = getfslineno(fixturefunc)
- location = f"{fs}:{lineno + 1}"
- source = _pytest._code.Source(fixturefunc)
- fail(msg + ":\n\n" + str(source.indent()) + "\n" + location, pytrace=False)
- 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:
- fail_fixturefunc(fixturefunc, "fixture function has more than one 'yield'")
- def _eval_scope_callable(
- scope_callable: "Callable[[str, Config], _ScopeName]",
- fixture_name: str,
- config: Config,
- ) -> "_ScopeName":
- try:
- # Type ignored because there is no typing mechanism to specify
- # keyword arguments, currently.
- result = scope_callable(fixture_name=fixture_name, config=config) # type: ignore[call-arg]
- except Exception as e:
- raise TypeError(
- "Error evaluating {} while defining fixture '{}'.\n"
- "Expected a function with the signature (*, fixture_name, config)".format(
- scope_callable, fixture_name
- )
- ) from e
- if not isinstance(result, str):
- fail(
- "Expected {} to return a 'str' while defining fixture '{}', but it returned:\n"
- "{!r}".format(scope_callable, fixture_name, result),
- pytrace=False,
- )
- return result
- @final
- class FixtureDef(Generic[FixtureValue]):
- """A container for a fixture definition."""
- def __init__(
- self,
- fixturemanager: "FixtureManager",
- baseid: Optional[str],
- argname: str,
- func: "_FixtureFunc[FixtureValue]",
- scope: Union[Scope, "_ScopeName", Callable[[str, Config], "_ScopeName"], None],
- params: Optional[Sequence[object]],
- unittest: bool = False,
- ids: Optional[
- Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
- ] = None,
- ) -> None:
- self._fixturemanager = fixturemanager
- # The "base" node ID for the fixture.
- #
- # This is a node ID prefix. A fixture is only available to a node (e.g.
- # a `Function` item) if the fixture's baseid is a parent of the node's
- # nodeid (see the `iterparentnodeids` function for what constitutes a
- # "parent" and a "prefix" in this context).
- #
- # For a fixture found in a Collector's object (e.g. a `Module`s module,
- # a `Class`'s class), the baseid is the Collector's nodeid.
- #
- # For a fixture found in a conftest plugin, the baseid is the conftest's
- # directory path relative to the rootdir.
- #
- # For other plugins, the baseid is the empty string (always matches).
- self.baseid = baseid or ""
- # Whether the fixture was found from a node or a conftest in the
- # collection tree. Will be false for fixtures defined in non-conftest
- # plugins.
- self.has_location = baseid is not None
- # The fixture factory function.
- self.func = func
- # The name by which the fixture may be requested.
- self.argname = argname
- if scope is None:
- scope = Scope.Function
- elif callable(scope):
- scope = _eval_scope_callable(scope, argname, fixturemanager.config)
- if isinstance(scope, str):
- scope = Scope.from_user(
- scope, descr=f"Fixture '{func.__name__}'", where=baseid
- )
- self._scope = scope
- # If the fixture is directly parametrized, the parameter values.
- self.params: Optional[Sequence[object]] = params
- # If the fixture is directly parametrized, a tuple of explicit IDs to
- # assign to the parameter values, or a callable to generate an ID given
- # a parameter value.
- self.ids = ids
- # The names requested by the fixtures.
- self.argnames = getfuncargnames(func, name=argname, is_method=unittest)
- # Whether the fixture was collected from a unittest TestCase class.
- # Note that it really only makes sense to define autouse fixtures in
- # unittest TestCases.
- self.unittest = unittest
- # If the fixture was executed, the current value of the fixture.
- # Can change if the fixture is executed with different parameters.
- self.cached_result: Optional[_FixtureCachedResult[FixtureValue]] = None
- self._finalizers: 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:
- exc = None
- try:
- while self._finalizers:
- try:
- func = self._finalizers.pop()
- func()
- except BaseException as e:
- # XXX Only first exception will be seen by user,
- # ideally all should be reported.
- if exc is None:
- exc = e
- if exc:
- raise exc
- finally:
- ihook = request.node.ihook
- ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
- # Even if finalization fails, we invalidate the cached fixture
- # value and remove all finalizers because they may be bound methods
- # which will keep instances alive.
- self.cached_result = None
- self._finalizers = []
- def execute(self, request: SubRequest) -> FixtureValue:
- # Get required arguments and register our own finish()
- # with their finalization.
- for argname in self.argnames:
- fixturedef = request._get_active_fixturedef(argname)
- if argname != "request":
- # PseudoFixtureDef is only for "request".
- assert isinstance(fixturedef, FixtureDef)
- fixturedef.addfinalizer(functools.partial(self.finish, request=request))
- my_cache_key = self.cache_key(request)
- if self.cached_result is not None:
- # note: comparison with `==` can fail (or be expensive) for e.g.
- # numpy arrays (#6497).
- cache_key = self.cached_result[1]
- if my_cache_key is cache_key:
- if self.cached_result[2] is not None:
- _, val, tb = self.cached_result[2]
- raise val.with_traceback(tb)
- else:
- result = self.cached_result[0]
- return result
- # We have a previous but differently parametrized fixture instance
- # so we need to tear it down before creating a new one.
- self.finish(request)
- assert self.cached_result is None
- ihook = request.node.ihook
- result = ihook.pytest_fixture_setup(fixturedef=self, request=request)
- return result
- def cache_key(self, request: SubRequest) -> object:
- return request.param_index if not hasattr(request, "param") else request.param
- def __repr__(self) -> str:
- return "<FixtureDef argname={!r} scope={!r} baseid={!r}>".format(
- self.argname, self.scope, self.baseid
- )
- def resolve_fixture_function(
- fixturedef: FixtureDef[FixtureValue], request: FixtureRequest
- ) -> "_FixtureFunc[FixtureValue]":
- """Get the actual callable that can be called to obtain the fixture
- value, dealing with unittest-specific instances and bound methods."""
- fixturefunc = fixturedef.func
- if fixturedef.unittest:
- if request.instance is not None:
- # Bind the unbound method to the TestCase instance.
- fixturefunc = fixturedef.func.__get__(request.instance) # type: ignore[union-attr]
- else:
- # The fixture function needs to be bound to the actual
- # request.instance so that code working with "fixturedef" behaves
- # as expected.
- if request.instance is not None:
- # Handle the case where fixture is defined not in a test class, but some other class
- # (for example a plugin class with a fixture), see #2270.
- if hasattr(fixturefunc, "__self__") and not isinstance(
- request.instance, fixturefunc.__self__.__class__ # type: ignore[union-attr]
- ):
- return fixturefunc
- fixturefunc = getimfunc(fixturedef.func)
- if fixturefunc != fixturedef.func:
- fixturefunc = fixturefunc.__get__(request.instance) # type: ignore[union-attr]
- return fixturefunc
- def pytest_fixture_setup(
- fixturedef: FixtureDef[FixtureValue], request: SubRequest
- ) -> FixtureValue:
- """Execution of fixture setup."""
- kwargs = {}
- for argname in fixturedef.argnames:
- fixdef = request._get_active_fixturedef(argname)
- assert fixdef.cached_result is not None
- result, arg_cache_key, exc = fixdef.cached_result
- request._check_scope(argname, request._scope, fixdef._scope)
- kwargs[argname] = result
- fixturefunc = resolve_fixture_function(fixturedef, request)
- my_cache_key = fixturedef.cache_key(request)
- try:
- result = call_fixture_func(fixturefunc, request, kwargs)
- except TEST_OUTCOME:
- exc_info = sys.exc_info()
- assert exc_info[0] is not None
- if isinstance(
- exc_info[1], skip.Exception
- ) and not fixturefunc.__name__.startswith("xunit_setup"):
- exc_info[1]._use_item_location = True # type: ignore[attr-defined]
- fixturedef.cached_result = (None, my_cache_key, exc_info)
- raise
- fixturedef.cached_result = (result, my_cache_key, None)
- return result
- def _ensure_immutable_ids(
- ids: Optional[Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]]
- ) -> Optional[Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]]:
- if ids is None:
- return None
- if callable(ids):
- return ids
- return tuple(ids)
- def _params_converter(
- params: Optional[Iterable[object]],
- ) -> Optional[Tuple[object, ...]]:
- return tuple(params) if params is not None else None
- 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."""
- message = (
- '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."
- ).format(name=fixture_marker.name or function.__name__)
- @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:
- scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]"
- params: Optional[Tuple[object, ...]]
- autouse: bool = False
- ids: Optional[
- Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
- ] = None
- name: Optional[str] = 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(
- "fixture is being applied more than once to the same function"
- )
- function = wrap_function_to_error_out_if_called_directly(function, self)
- name = self.name or function.__name__
- if name == "request":
- location = getlocation(function)
- fail(
- "'request' is a reserved word for fixtures, use another name:\n {}".format(
- location
- ),
- pytrace=False,
- )
- # Type ignored because https://github.com/python/mypy/issues/2087.
- function._pytestfixturefunction = self # type: ignore[attr-defined]
- return function
- @overload
- def fixture(
- fixture_function: FixtureFunction,
- *,
- scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ...,
- params: Optional[Iterable[object]] = ...,
- autouse: bool = ...,
- ids: Optional[
- Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
- ] = ...,
- name: Optional[str] = ...,
- ) -> FixtureFunction:
- ...
- @overload
- def fixture( # noqa: F811
- fixture_function: None = ...,
- *,
- scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ...,
- params: Optional[Iterable[object]] = ...,
- autouse: bool = ...,
- ids: Optional[
- Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
- ] = ...,
- name: Optional[str] = None,
- ) -> FixtureFunctionMarker:
- ...
- def fixture( # noqa: F811
- fixture_function: Optional[FixtureFunction] = None,
- *,
- scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = "function",
- params: Optional[Iterable[object]] = None,
- autouse: bool = False,
- ids: Optional[
- Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
- ] = None,
- name: Optional[str] = None,
- ) -> Union[FixtureFunctionMarker, FixtureFunction]:
- """Decorator to mark a fixture factory function.
- This decorator can be used, with or without parameters, to define a
- 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,
- )
- # Direct decoration.
- 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.getoption("verbose") > 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",
- )
- 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.
- """
- FixtureLookupError = FixtureLookupError
- FixtureLookupErrorRepr = FixtureLookupErrorRepr
- def __init__(self, session: "Session") -> None:
- self.session = session
- self.config: Config = session.config
- self._arg2fixturedefs: Dict[str, List[FixtureDef[Any]]] = {}
- self._holderobjseen: Set[object] = set()
- # A mapping from a nodeid to a list of autouse fixtures it defines.
- self._nodeid_autousenames: Dict[str, List[str]] = {
- "": self.config.getini("usefixtures"),
- }
- session.config.pluginmanager.register(self, "funcmanage")
- def _get_direct_parametrize_args(self, node: nodes.Node) -> List[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: List[str] = []
- 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.extend(p_argnames)
- return parametrize_argnames
- def getfixtureinfo(
- self, node: nodes.Node, func, cls, funcargs: bool = True
- ) -> FuncFixtureInfo:
- if funcargs and not getattr(node, "nofuncargs", False):
- argnames = getfuncargnames(func, name=node.name, cls=cls)
- else:
- argnames = ()
- usefixtures = tuple(
- arg for mark in node.iter_markers(name="usefixtures") for arg in mark.args
- )
- initialnames = usefixtures + argnames
- fm = node.session._fixturemanager
- initialnames, names_closure, arg2fixturedefs = fm.getfixtureclosure(
- initialnames, node, ignore_args=self._get_direct_parametrize_args(node)
- )
- return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)
- def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
- nodeid = None
- try:
- p = absolutepath(plugin.__file__) # type: ignore[attr-defined]
- except AttributeError:
- pass
- else:
- # Construct the base nodeid which is later used to check
- # what fixtures are visible for particular tests (as denoted
- # by their test id).
- if p.name.startswith("conftest.py"):
- try:
- nodeid = str(p.parent.relative_to(self.config.rootpath))
- except ValueError:
- nodeid = ""
- if nodeid == ".":
- nodeid = ""
- if os.sep != nodes.SEP:
- nodeid = nodeid.replace(os.sep, nodes.SEP)
- self.parsefactories(plugin, nodeid)
- def _getautousenames(self, nodeid: str) -> Iterator[str]:
- """Return the names of autouse fixtures applicable to nodeid."""
- for parentnodeid in nodes.iterparentnodeids(nodeid):
- basenames = self._nodeid_autousenames.get(parentnodeid)
- if basenames:
- yield from basenames
- def getfixtureclosure(
- self,
- fixturenames: Tuple[str, ...],
- parentnode: nodes.Node,
- ignore_args: Sequence[str] = (),
- ) -> Tuple[Tuple[str, ...], List[str], Dict[str, Sequence[FixtureDef[Any]]]]:
- # Collect the closure of all fixtures, starting with the given
- # fixturenames as the initial set. As we have to visit all
- # factory definitions anyway, we also return an arg2fixturedefs
- # mapping so that the caller can reuse it and does not have
- # to re-discover fixturedefs again for each fixturename
- # (discovering matching fixtures for a given name/node is expensive).
- parentid = parentnode.nodeid
- fixturenames_closure = list(self._getautousenames(parentid))
- def merge(otherlist: Iterable[str]) -> None:
- for arg in otherlist:
- if arg not in fixturenames_closure:
- fixturenames_closure.append(arg)
- merge(fixturenames)
- # At this point, fixturenames_closure contains what we call "initialnames",
- # which is a set of fixturenames the function immediately requests. We
- # need to return it as well, so save this.
- initialnames = tuple(fixturenames_closure)
- 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, parentid)
- if fixturedefs:
- arg2fixturedefs[argname] = fixturedefs
- merge(fixturedefs[-1].argnames)
- 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 initialnames, 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:
- # Get the FixtureDefs for the argname.
- fixture_defs = metafunc._arg2fixturedefs.get(argname)
- if not fixture_defs:
- # Will raise FixtureLookupError at setup time if not parametrized somewhere
- # else (e.g @pytest.mark.parametrize)
- continue
- # If the test itself parametrizes using this argname, give it
- # precedence.
- if any(
- argname in get_parametrize_mark_argnames(mark)
- for mark in metafunc.definition.iter_markers("parametrize")
- ):
- continue
- # In the common case we only look at the fixture def with the
- # closest scope (last in the list). But if the fixture overrides
- # another fixture, while requesting the super fixture, keep going
- # in case the super fixture is parametrized (#1953).
- for fixturedef in reversed(fixture_defs):
- # Fixture is parametrized, apply it and stop.
- if fixturedef.params is not None:
- metafunc.parametrize(
- argname,
- fixturedef.params,
- indirect=True,
- scope=fixturedef.scope,
- ids=fixturedef.ids,
- )
- break
- # Not requesting the overridden super fixture, stop.
- if argname not in fixturedef.argnames:
- break
- # Try next super fixture, if any.
- def pytest_collection_modifyitems(self, items: List[nodes.Item]) -> None:
- # Separate parametrized setups.
- items[:] = reorder_items(items)
- @overload
- def parsefactories(
- self,
- node_or_obj: nodes.Node,
- *,
- unittest: bool = ...,
- ) -> None:
- raise NotImplementedError()
- @overload
- def parsefactories( # noqa: F811
- self,
- node_or_obj: object,
- nodeid: Optional[str],
- *,
- unittest: bool = ...,
- ) -> None:
- raise NotImplementedError()
- def parsefactories( # noqa: F811
- self,
- node_or_obj: Union[nodes.Node, object],
- nodeid: Union[str, NotSetType, None] = NOTSET,
- *,
- unittest: bool = False,
- ) -> 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' visibilty. `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) # type: ignore[attr-defined]
- assert isinstance(node_or_obj.nodeid, str)
- nodeid = node_or_obj.nodeid
- if holderobj in self._holderobjseen:
- return
- self._holderobjseen.add(holderobj)
- autousenames = []
- for name in dir(holderobj):
- # ugly workaround for one of the fspath deprecated property of node
- # todo: safely generalize
- if isinstance(holderobj, nodes.Node) and name == "fspath":
- continue
- # The attribute can be an arbitrary descriptor, so the attribute
- # access below can raise. safe_getatt() ignores such exceptions.
- obj = safe_getattr(holderobj, name, None)
- marker = getfixturemarker(obj)
- if not isinstance(marker, FixtureFunctionMarker):
- # Magic globals with __getattr__ might have got us a wrong
- # fixture attribute.
- continue
- 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.
- obj = get_real_method(obj, holderobj)
- fixture_def = FixtureDef(
- fixturemanager=self,
- baseid=nodeid,
- argname=name,
- func=obj,
- scope=marker.scope,
- params=marker.params,
- unittest=unittest,
- ids=marker.ids,
- )
- faclist = self._arg2fixturedefs.setdefault(name, [])
- if fixture_def.has_location:
- faclist.append(fixture_def)
- else:
- # fixturedefs with no location are at the front
- # so this inserts the current fixturedef after the
- # existing fixturedefs from external plugins but
- # before the fixturedefs provided in conftests.
- i = len([f for f in faclist if not f.has_location])
- faclist.insert(i, fixture_def)
- if marker.autouse:
- autousenames.append(name)
- if autousenames:
- self._nodeid_autousenames.setdefault(nodeid or "", []).extend(autousenames)
- def getfixturedefs(
- self, argname: str, nodeid: str
- ) -> Optional[Sequence[FixtureDef[Any]]]:
- """Get a list of fixtures which are applicable to the given node id.
- :param str argname: Name of the fixture to search for.
- :param str nodeid: Full node id of the requesting test.
- :rtype: Sequence[FixtureDef]
- """
- try:
- fixturedefs = self._arg2fixturedefs[argname]
- except KeyError:
- return None
- return tuple(self._matchfactories(fixturedefs, nodeid))
- def _matchfactories(
- self, fixturedefs: Iterable[FixtureDef[Any]], nodeid: str
- ) -> Iterator[FixtureDef[Any]]:
- parentnodeids = set(nodes.iterparentnodeids(nodeid))
- for fixturedef in fixturedefs:
- if fixturedef.baseid in parentnodeids:
- yield fixturedef
|