fixtures: expand comments and annotations on fixture internals
This commit is contained in:
parent
ecfab4dc8b
commit
01f38aca44
|
@ -68,7 +68,7 @@ def is_async_function(func: object) -> bool:
|
|||
return iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
|
||||
|
||||
|
||||
def getlocation(function, curdir: str | None = None) -> str:
|
||||
def getlocation(function, curdir: str | os.PathLike[str] | None = None) -> str:
|
||||
function = get_real_func(function)
|
||||
fn = Path(inspect.getfile(function))
|
||||
lineno = function.__code__.co_firstlineno
|
||||
|
|
|
@ -582,7 +582,7 @@ def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest:
|
|||
doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined]
|
||||
node=doctest_item, func=func, cls=None, funcargs=False
|
||||
)
|
||||
fixture_request = FixtureRequest(doctest_item, _ispytest=True)
|
||||
fixture_request = FixtureRequest(doctest_item, _ispytest=True) # type: ignore[arg-type]
|
||||
fixture_request._fillfixtures()
|
||||
return fixture_request
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ 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
|
||||
|
@ -73,6 +74,7 @@ if TYPE_CHECKING:
|
|||
from _pytest.scope import _ScopeName
|
||||
from _pytest.main import Session
|
||||
from _pytest.python import CallSpec2
|
||||
from _pytest.python import Function
|
||||
from _pytest.python import Metafunc
|
||||
|
||||
|
||||
|
@ -352,17 +354,24 @@ def get_direct_param_fixture_func(request: "FixtureRequest") -> Any:
|
|||
return request.param
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class FuncFixtureInfo:
|
||||
__slots__ = ("argnames", "initialnames", "names_closure", "name2fixturedefs")
|
||||
|
||||
# Original function argument names.
|
||||
# Original function argument names, i.e. fixture names that the function
|
||||
# requests directly.
|
||||
argnames: Tuple[str, ...]
|
||||
# Argnames that function immediately requires. These include argnames +
|
||||
# fixture names specified via usefixtures and via autouse=True in fixture
|
||||
# definitions.
|
||||
# Fixture names that the function immediately requires. These include
|
||||
# argnames + fixture names specified via usefixtures and via autouse=True in
|
||||
# fixture definitions.
|
||||
initialnames: Tuple[str, ...]
|
||||
# The transitive closure of the fixture names that the function requires.
|
||||
# Note: can't include dynamic dependencies (`request.getfixturevalue` calls).
|
||||
names_closure: List[str]
|
||||
# A map from a fixture name in the transitive closure to the FixtureDefs
|
||||
# matching the name which are applicable to this function.
|
||||
# There may be multiple overriding fixtures with the same name. The
|
||||
# sequence is ordered from furthest to closes to the function.
|
||||
name2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]]
|
||||
|
||||
def prune_dependency_tree(self) -> None:
|
||||
|
@ -401,17 +410,31 @@ class FixtureRequest:
|
|||
indirectly.
|
||||
"""
|
||||
|
||||
def __init__(self, pyfuncitem, *, _ispytest: bool = False) -> None:
|
||||
def __init__(self, pyfuncitem: "Function", *, _ispytest: bool = False) -> None:
|
||||
check_ispytest(_ispytest)
|
||||
self._pyfuncitem = pyfuncitem
|
||||
#: Fixture for which this request is being performed.
|
||||
self.fixturename: Optional[str] = None
|
||||
self._pyfuncitem = pyfuncitem
|
||||
self._fixturemanager = pyfuncitem.session._fixturemanager
|
||||
self._scope = Scope.Function
|
||||
self._fixture_defs: Dict[str, FixtureDef[Any]] = {}
|
||||
fixtureinfo: FuncFixtureInfo = pyfuncitem._fixtureinfo
|
||||
self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy()
|
||||
# The FixtureDefs for each fixture name requested by this item.
|
||||
# Starts from the statically-known fixturedefs resolved during
|
||||
# collection. Dynamically requested fixtures (using
|
||||
# `request.getfixturevalue("foo")`) are added dynamically.
|
||||
self._arg2fixturedefs = pyfuncitem._fixtureinfo.name2fixturedefs.copy()
|
||||
# A fixture may override another fixture with the same name, e.g. a fixture
|
||||
# in a module can override a fixture in a conftest, a fixture in a class can
|
||||
# override a fixture in the module, and so on.
|
||||
# An overriding fixture can request its own name; in this case it gets
|
||||
# the value of the fixture it overrides, one level up.
|
||||
# The _arg2index state keeps the current depth in the overriding chain.
|
||||
# The fixturedefs list in _arg2fixturedefs for a given name is ordered from
|
||||
# furthest to closest, so we use negative indexing -1, -2, ... to go from
|
||||
# last to first.
|
||||
self._arg2index: Dict[str, int] = {}
|
||||
self._fixturemanager: FixtureManager = pyfuncitem.session._fixturemanager
|
||||
# The evaluated argnames so far, mapping to the FixtureDef they resolved
|
||||
# to.
|
||||
self._fixture_defs: Dict[str, FixtureDef[Any]] = {}
|
||||
# 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
|
||||
|
@ -466,10 +489,14 @@ class FixtureRequest:
|
|||
fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid)
|
||||
if fixturedefs is not None:
|
||||
self._arg2fixturedefs[argname] = fixturedefs
|
||||
# No fixtures defined with this name.
|
||||
if fixturedefs is None:
|
||||
raise FixtureLookupError(argname, self)
|
||||
# fixturedefs list is immutable so we maintain a decreasing index.
|
||||
# The are no fixtures with this name applicable for the function.
|
||||
if not fixturedefs:
|
||||
raise FixtureLookupError(argname, self)
|
||||
index = self._arg2index.get(argname, 0) - 1
|
||||
# The fixture requested its own name, but no remaining to override.
|
||||
if -index > len(fixturedefs):
|
||||
raise FixtureLookupError(argname, self)
|
||||
self._arg2index[argname] = index
|
||||
|
@ -503,7 +530,7 @@ class FixtureRequest:
|
|||
"""Instance (can be None) on which test function was collected."""
|
||||
# unittest support hack, see _pytest.unittest.TestCaseFunction.
|
||||
try:
|
||||
return self._pyfuncitem._testcase
|
||||
return self._pyfuncitem._testcase # type: ignore[attr-defined]
|
||||
except AttributeError:
|
||||
function = getattr(self, "function", None)
|
||||
return getattr(function, "__self__", None)
|
||||
|
@ -513,7 +540,9 @@ class FixtureRequest:
|
|||
"""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
|
||||
mod = self._pyfuncitem.getparent(_pytest.python.Module)
|
||||
assert mod is not None
|
||||
return mod.obj
|
||||
|
||||
@property
|
||||
def path(self) -> Path:
|
||||
|
@ -829,7 +858,9 @@ class FixtureLookupError(LookupError):
|
|||
if msg is None:
|
||||
fm = self.request._fixturemanager
|
||||
available = set()
|
||||
parentid = self.request._pyfuncitem.parent.nodeid
|
||||
parent = self.request._pyfuncitem.parent
|
||||
assert parent is not None
|
||||
parentid = parent.nodeid
|
||||
for name, fixturedefs in fm._arg2fixturedefs.items():
|
||||
faclist = list(fm._matchfactories(fixturedefs, parentid))
|
||||
if faclist:
|
||||
|
@ -976,15 +1007,15 @@ class FixtureDef(Generic[FixtureValue]):
|
|||
# directory path relative to the rootdir.
|
||||
#
|
||||
# For other plugins, the baseid is the empty string (always matches).
|
||||
self.baseid = baseid or ""
|
||||
self.baseid: Final = 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
|
||||
self.has_location: Final = baseid is not None
|
||||
# The fixture factory function.
|
||||
self.func = func
|
||||
self.func: Final = func
|
||||
# The name by which the fixture may be requested.
|
||||
self.argname = argname
|
||||
self.argname: Final = argname
|
||||
if scope is None:
|
||||
scope = Scope.Function
|
||||
elif callable(scope):
|
||||
|
@ -993,23 +1024,23 @@ class FixtureDef(Generic[FixtureValue]):
|
|||
scope = Scope.from_user(
|
||||
scope, descr=f"Fixture '{func.__name__}'", where=baseid
|
||||
)
|
||||
self._scope = scope
|
||||
self._scope: Final = scope
|
||||
# If the fixture is directly parametrized, the parameter values.
|
||||
self.params: Optional[Sequence[object]] = params
|
||||
self.params: Final = 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
|
||||
self.ids: Final = ids
|
||||
# The names requested by the fixtures.
|
||||
self.argnames = getfuncargnames(func, name=argname, is_method=unittest)
|
||||
self.argnames: Final = 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
|
||||
self.unittest: Final = 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]] = []
|
||||
self._finalizers: Final[List[Callable[[], object]]] = []
|
||||
|
||||
@property
|
||||
def scope(self) -> "_ScopeName":
|
||||
|
@ -1040,7 +1071,7 @@ class FixtureDef(Generic[FixtureValue]):
|
|||
# value and remove all finalizers because they may be bound methods
|
||||
# which will keep instances alive.
|
||||
self.cached_result = None
|
||||
self._finalizers = []
|
||||
self._finalizers.clear()
|
||||
|
||||
def execute(self, request: SubRequest) -> FixtureValue:
|
||||
# Get required arguments and register our own finish()
|
||||
|
@ -1417,10 +1448,14 @@ class FixtureManager:
|
|||
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()
|
||||
# Maps a fixture name (argname) to all of the FixtureDefs in the test
|
||||
# suite/plugins defined with this name. Populated by parsefactories().
|
||||
# TODO: The order of the FixtureDefs list of each arg is significant,
|
||||
# explain.
|
||||
self._arg2fixturedefs: Final[Dict[str, List[FixtureDef[Any]]]] = {}
|
||||
self._holderobjseen: Final[Set[object]] = set()
|
||||
# A mapping from a nodeid to a list of autouse fixtures it defines.
|
||||
self._nodeid_autousenames: Dict[str, List[str]] = {
|
||||
self._nodeid_autousenames: Final[Dict[str, List[str]]] = {
|
||||
"": self.config.getini("usefixtures"),
|
||||
}
|
||||
session.config.pluginmanager.register(self, "funcmanage")
|
||||
|
@ -1699,11 +1734,16 @@ class FixtureManager:
|
|||
def getfixturedefs(
|
||||
self, argname: str, nodeid: str
|
||||
) -> Optional[Sequence[FixtureDef[Any]]]:
|
||||
"""Get a list of fixtures which are applicable to the given node id.
|
||||
"""Get FixtureDefs for a fixture name which are applicable
|
||||
to a given node.
|
||||
|
||||
:param str argname: Name of the fixture to search for.
|
||||
:param str nodeid: Full node id of the requesting test.
|
||||
:rtype: Sequence[FixtureDef]
|
||||
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 nodeid: Full node id of the requesting test.
|
||||
"""
|
||||
try:
|
||||
fixturedefs = self._arg2fixturedefs[argname]
|
||||
|
|
|
@ -699,6 +699,7 @@ class TestRequestBasic:
|
|||
"""
|
||||
)
|
||||
(item1,) = pytester.genitems([modcol])
|
||||
assert isinstance(item1, Function)
|
||||
assert item1.name == "test_method"
|
||||
arg2fixturedefs = fixtures.FixtureRequest(
|
||||
item1, _ispytest=True
|
||||
|
@ -967,6 +968,7 @@ class TestRequestBasic:
|
|||
def test_request_getmodulepath(self, pytester: Pytester) -> None:
|
||||
modcol = pytester.getmodulecol("def test_somefunc(): pass")
|
||||
(item,) = pytester.genitems([modcol])
|
||||
assert isinstance(item, Function)
|
||||
req = fixtures.FixtureRequest(item, _ispytest=True)
|
||||
assert req.path == modcol.path
|
||||
|
||||
|
@ -1125,6 +1127,7 @@ class TestRequestMarking:
|
|||
pass
|
||||
"""
|
||||
)
|
||||
assert isinstance(item1, Function)
|
||||
req1 = fixtures.FixtureRequest(item1, _ispytest=True)
|
||||
assert "xfail" not in item1.keywords
|
||||
req1.applymarker(pytest.mark.xfail)
|
||||
|
@ -4009,6 +4012,7 @@ class TestScopeOrdering:
|
|||
"""
|
||||
)
|
||||
items, _ = pytester.inline_genitems()
|
||||
assert isinstance(items[0], Function)
|
||||
request = FixtureRequest(items[0], _ispytest=True)
|
||||
assert request.fixturenames == "m1 f1".split()
|
||||
|
||||
|
@ -4057,6 +4061,7 @@ class TestScopeOrdering:
|
|||
"""
|
||||
)
|
||||
items, _ = pytester.inline_genitems()
|
||||
assert isinstance(items[0], Function)
|
||||
request = FixtureRequest(items[0], _ispytest=True)
|
||||
# order of fixtures based on their scope and position in the parameter list
|
||||
assert (
|
||||
|
@ -4084,6 +4089,7 @@ class TestScopeOrdering:
|
|||
"""
|
||||
)
|
||||
items, _ = pytester.inline_genitems()
|
||||
assert isinstance(items[0], Function)
|
||||
request = FixtureRequest(items[0], _ispytest=True)
|
||||
assert request.fixturenames == "m1 f1".split()
|
||||
|
||||
|
@ -4117,6 +4123,7 @@ class TestScopeOrdering:
|
|||
"""
|
||||
)
|
||||
items, _ = pytester.inline_genitems()
|
||||
assert isinstance(items[0], Function)
|
||||
request = FixtureRequest(items[0], _ispytest=True)
|
||||
assert request.fixturenames == "s1 m1 c1 f2 f1".split()
|
||||
|
||||
|
@ -4159,6 +4166,7 @@ class TestScopeOrdering:
|
|||
}
|
||||
)
|
||||
items, _ = pytester.inline_genitems()
|
||||
assert isinstance(items[0], Function)
|
||||
request = FixtureRequest(items[0], _ispytest=True)
|
||||
assert request.fixturenames == "p_sub m_conf m_sub m_test f1".split()
|
||||
|
||||
|
@ -4203,6 +4211,7 @@ class TestScopeOrdering:
|
|||
"""
|
||||
)
|
||||
items, _ = pytester.inline_genitems()
|
||||
assert isinstance(items[0], Function)
|
||||
request = FixtureRequest(items[0], _ispytest=True)
|
||||
assert request.fixturenames == "s1 p1 m1 m2 c1 f2 f1".split()
|
||||
|
||||
|
|
|
@ -90,6 +90,7 @@ def test_cache_makedir(cache: pytest.Cache) -> None:
|
|||
def test_fixturerequest_getmodulepath(pytester: pytest.Pytester) -> None:
|
||||
modcol = pytester.getmodulecol("def test_somefunc(): pass")
|
||||
(item,) = pytester.genitems([modcol])
|
||||
assert isinstance(item, pytest.Function)
|
||||
req = pytest.FixtureRequest(item, _ispytest=True)
|
||||
assert req.path == modcol.path
|
||||
assert req.fspath == modcol.fspath # type: ignore[attr-defined]
|
||||
|
|
Loading…
Reference in New Issue