Merge pull request #11416 from bluetech/fixtures-getfixtureclosure
fixtures: more tweaks
This commit is contained in:
commit
dd7beb39d6
|
@ -255,14 +255,20 @@ class DoctestItem(Item):
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
parent: "Union[DoctestTextfile, DoctestModule]",
|
parent: "Union[DoctestTextfile, DoctestModule]",
|
||||||
runner: Optional["doctest.DocTestRunner"] = None,
|
runner: "doctest.DocTestRunner",
|
||||||
dtest: Optional["doctest.DocTest"] = None,
|
dtest: "doctest.DocTest",
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(name, parent)
|
super().__init__(name, parent)
|
||||||
self.runner = runner
|
self.runner = runner
|
||||||
self.dtest = dtest
|
self.dtest = dtest
|
||||||
|
|
||||||
|
# Stuff needed for fixture support.
|
||||||
self.obj = None
|
self.obj = None
|
||||||
self.fixture_request: Optional[TopRequest] = None
|
fm = self.session._fixturemanager
|
||||||
|
fixtureinfo = fm.getfixtureinfo(node=self, func=None, cls=None)
|
||||||
|
self._fixtureinfo = fixtureinfo
|
||||||
|
self.fixturenames = fixtureinfo.names_closure
|
||||||
|
self._initrequest()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_parent( # type: ignore
|
def from_parent( # type: ignore
|
||||||
|
@ -277,19 +283,18 @@ class DoctestItem(Item):
|
||||||
"""The public named constructor."""
|
"""The public named constructor."""
|
||||||
return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
|
return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
|
||||||
|
|
||||||
|
def _initrequest(self) -> None:
|
||||||
|
self.funcargs: Dict[str, object] = {}
|
||||||
|
self._request = TopRequest(self, _ispytest=True) # type: ignore[arg-type]
|
||||||
|
|
||||||
def setup(self) -> None:
|
def setup(self) -> None:
|
||||||
if self.dtest is not None:
|
self._request._fillfixtures()
|
||||||
self.fixture_request = _setup_fixtures(self)
|
globs = dict(getfixture=self._request.getfixturevalue)
|
||||||
globs = dict(getfixture=self.fixture_request.getfixturevalue)
|
for name, value in self._request.getfixturevalue("doctest_namespace").items():
|
||||||
for name, value in self.fixture_request.getfixturevalue(
|
globs[name] = value
|
||||||
"doctest_namespace"
|
self.dtest.globs.update(globs)
|
||||||
).items():
|
|
||||||
globs[name] = value
|
|
||||||
self.dtest.globs.update(globs)
|
|
||||||
|
|
||||||
def runtest(self) -> None:
|
def runtest(self) -> None:
|
||||||
assert self.dtest is not None
|
|
||||||
assert self.runner is not None
|
|
||||||
_check_all_skipped(self.dtest)
|
_check_all_skipped(self.dtest)
|
||||||
self._disable_output_capturing_for_darwin()
|
self._disable_output_capturing_for_darwin()
|
||||||
failures: List["doctest.DocTestFailure"] = []
|
failures: List["doctest.DocTestFailure"] = []
|
||||||
|
@ -376,7 +381,6 @@ class DoctestItem(Item):
|
||||||
return ReprFailDoctest(reprlocation_lines)
|
return ReprFailDoctest(reprlocation_lines)
|
||||||
|
|
||||||
def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
|
def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
|
||||||
assert self.dtest is not None
|
|
||||||
return self.path, self.dtest.lineno, "[doctest] %s" % self.name
|
return self.path, self.dtest.lineno, "[doctest] %s" % self.name
|
||||||
|
|
||||||
|
|
||||||
|
@ -396,8 +400,8 @@ def _get_flag_lookup() -> Dict[str, int]:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_optionflags(parent):
|
def get_optionflags(config: Config) -> int:
|
||||||
optionflags_str = parent.config.getini("doctest_optionflags")
|
optionflags_str = config.getini("doctest_optionflags")
|
||||||
flag_lookup_table = _get_flag_lookup()
|
flag_lookup_table = _get_flag_lookup()
|
||||||
flag_acc = 0
|
flag_acc = 0
|
||||||
for flag in optionflags_str:
|
for flag in optionflags_str:
|
||||||
|
@ -405,8 +409,8 @@ def get_optionflags(parent):
|
||||||
return flag_acc
|
return flag_acc
|
||||||
|
|
||||||
|
|
||||||
def _get_continue_on_failure(config):
|
def _get_continue_on_failure(config: Config) -> bool:
|
||||||
continue_on_failure = config.getvalue("doctest_continue_on_failure")
|
continue_on_failure: bool = config.getvalue("doctest_continue_on_failure")
|
||||||
if continue_on_failure:
|
if continue_on_failure:
|
||||||
# We need to turn off this if we use pdb since we should stop at
|
# We need to turn off this if we use pdb since we should stop at
|
||||||
# the first failure.
|
# the first failure.
|
||||||
|
@ -429,7 +433,7 @@ class DoctestTextfile(Module):
|
||||||
name = self.path.name
|
name = self.path.name
|
||||||
globs = {"__name__": "__main__"}
|
globs = {"__name__": "__main__"}
|
||||||
|
|
||||||
optionflags = get_optionflags(self)
|
optionflags = get_optionflags(self.config)
|
||||||
|
|
||||||
runner = _get_runner(
|
runner = _get_runner(
|
||||||
verbose=False,
|
verbose=False,
|
||||||
|
@ -574,7 +578,7 @@ class DoctestModule(Module):
|
||||||
raise
|
raise
|
||||||
# Uses internal doctest module parsing mechanism.
|
# Uses internal doctest module parsing mechanism.
|
||||||
finder = MockAwareDocTestFinder()
|
finder = MockAwareDocTestFinder()
|
||||||
optionflags = get_optionflags(self)
|
optionflags = get_optionflags(self.config)
|
||||||
runner = _get_runner(
|
runner = _get_runner(
|
||||||
verbose=False,
|
verbose=False,
|
||||||
optionflags=optionflags,
|
optionflags=optionflags,
|
||||||
|
@ -589,24 +593,6 @@ class DoctestModule(Module):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _setup_fixtures(doctest_item: DoctestItem) -> TopRequest:
|
|
||||||
"""Used by DoctestTextfile and DoctestItem to setup fixture information."""
|
|
||||||
|
|
||||||
def func() -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
doctest_item.funcargs = {} # type: ignore[attr-defined]
|
|
||||||
fm = doctest_item.session._fixturemanager
|
|
||||||
fixtureinfo = fm.getfixtureinfo(
|
|
||||||
node=doctest_item, func=func, cls=None, funcargs=False
|
|
||||||
)
|
|
||||||
doctest_item._fixtureinfo = fixtureinfo # type: ignore[attr-defined]
|
|
||||||
doctest_item.fixturenames = fixtureinfo.names_closure # type: ignore[attr-defined]
|
|
||||||
fixture_request = TopRequest(doctest_item, _ispytest=True) # type: ignore[arg-type]
|
|
||||||
fixture_request._fillfixtures()
|
|
||||||
return fixture_request
|
|
||||||
|
|
||||||
|
|
||||||
def _init_checker_class() -> Type["doctest.OutputChecker"]:
|
def _init_checker_class() -> Type["doctest.OutputChecker"]:
|
||||||
import doctest
|
import doctest
|
||||||
import re
|
import re
|
||||||
|
|
|
@ -8,6 +8,7 @@ from collections import defaultdict
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import AbstractSet
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
@ -1382,7 +1383,7 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_direct_parametrize_args(node: nodes.Node) -> List[str]:
|
def _get_direct_parametrize_args(node: nodes.Node) -> Set[str]:
|
||||||
"""Return all direct parametrization arguments of a node, so we don't
|
"""Return all direct parametrization arguments of a node, so we don't
|
||||||
mistake them for fixtures.
|
mistake them for fixtures.
|
||||||
|
|
||||||
|
@ -1391,17 +1392,22 @@ def _get_direct_parametrize_args(node: nodes.Node) -> List[str]:
|
||||||
These things are done later as well when dealing with parametrization
|
These things are done later as well when dealing with parametrization
|
||||||
so this could be improved.
|
so this could be improved.
|
||||||
"""
|
"""
|
||||||
parametrize_argnames: List[str] = []
|
parametrize_argnames: Set[str] = set()
|
||||||
for marker in node.iter_markers(name="parametrize"):
|
for marker in node.iter_markers(name="parametrize"):
|
||||||
if not marker.kwargs.get("indirect", False):
|
if not marker.kwargs.get("indirect", False):
|
||||||
p_argnames, _ = ParameterSet._parse_parametrize_args(
|
p_argnames, _ = ParameterSet._parse_parametrize_args(
|
||||||
*marker.args, **marker.kwargs
|
*marker.args, **marker.kwargs
|
||||||
)
|
)
|
||||||
parametrize_argnames.extend(p_argnames)
|
parametrize_argnames.update(p_argnames)
|
||||||
|
|
||||||
return parametrize_argnames
|
return parametrize_argnames
|
||||||
|
|
||||||
|
|
||||||
|
def deduplicate_names(*seqs: Iterable[str]) -> Tuple[str, ...]:
|
||||||
|
"""De-duplicate the sequence of names while keeping the original order."""
|
||||||
|
# Ideally we would use a set, but it does not preserve insertion order.
|
||||||
|
return tuple(dict.fromkeys(name for seq in seqs for name in seq))
|
||||||
|
|
||||||
|
|
||||||
class FixtureManager:
|
class FixtureManager:
|
||||||
"""pytest fixture definitions and information is stored and managed
|
"""pytest fixture definitions and information is stored and managed
|
||||||
from this class.
|
from this class.
|
||||||
|
@ -1454,13 +1460,12 @@ class FixtureManager:
|
||||||
def getfixtureinfo(
|
def getfixtureinfo(
|
||||||
self,
|
self,
|
||||||
node: nodes.Item,
|
node: nodes.Item,
|
||||||
func: Callable[..., object],
|
func: Optional[Callable[..., object]],
|
||||||
cls: Optional[type],
|
cls: Optional[type],
|
||||||
funcargs: bool = True,
|
|
||||||
) -> FuncFixtureInfo:
|
) -> FuncFixtureInfo:
|
||||||
"""Calculate the :class:`FuncFixtureInfo` for an item.
|
"""Calculate the :class:`FuncFixtureInfo` for an item.
|
||||||
|
|
||||||
If ``funcargs`` is false, or if the item sets an attribute
|
If ``func`` is None, or if the item sets an attribute
|
||||||
``nofuncargs = True``, then ``func`` is not examined at all.
|
``nofuncargs = True``, then ``func`` is not examined at all.
|
||||||
|
|
||||||
:param node:
|
:param node:
|
||||||
|
@ -1469,21 +1474,23 @@ class FixtureManager:
|
||||||
The item's function.
|
The item's function.
|
||||||
:param cls:
|
:param cls:
|
||||||
If the function is a method, the method's class.
|
If the function is a method, the method's class.
|
||||||
:param funcargs:
|
|
||||||
Whether to look into func's parameters as fixture requests.
|
|
||||||
"""
|
"""
|
||||||
if funcargs and not getattr(node, "nofuncargs", False):
|
if func is not None and not getattr(node, "nofuncargs", False):
|
||||||
argnames = getfuncargnames(func, name=node.name, cls=cls)
|
argnames = getfuncargnames(func, name=node.name, cls=cls)
|
||||||
else:
|
else:
|
||||||
argnames = ()
|
argnames = ()
|
||||||
|
usefixturesnames = self._getusefixturesnames(node)
|
||||||
|
autousenames = self._getautousenames(node.nodeid)
|
||||||
|
initialnames = deduplicate_names(autousenames, usefixturesnames, argnames)
|
||||||
|
|
||||||
usefixtures = tuple(
|
direct_parametrize_args = _get_direct_parametrize_args(node)
|
||||||
arg for mark in node.iter_markers(name="usefixtures") for arg in mark.args
|
|
||||||
)
|
names_closure, arg2fixturedefs = self.getfixtureclosure(
|
||||||
initialnames = usefixtures + argnames
|
parentnode=node,
|
||||||
initialnames, names_closure, arg2fixturedefs = self.getfixtureclosure(
|
initialnames=initialnames,
|
||||||
initialnames, node, ignore_args=_get_direct_parametrize_args(node)
|
ignore_args=direct_parametrize_args,
|
||||||
)
|
)
|
||||||
|
|
||||||
return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)
|
return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)
|
||||||
|
|
||||||
def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
|
def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
|
||||||
|
@ -1515,12 +1522,17 @@ class FixtureManager:
|
||||||
if basenames:
|
if basenames:
|
||||||
yield from 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(
|
def getfixtureclosure(
|
||||||
self,
|
self,
|
||||||
fixturenames: Tuple[str, ...],
|
|
||||||
parentnode: nodes.Node,
|
parentnode: nodes.Node,
|
||||||
ignore_args: Sequence[str] = (),
|
initialnames: Tuple[str, ...],
|
||||||
) -> Tuple[Tuple[str, ...], List[str], Dict[str, Sequence[FixtureDef[Any]]]]:
|
ignore_args: AbstractSet[str],
|
||||||
|
) -> Tuple[List[str], Dict[str, Sequence[FixtureDef[Any]]]]:
|
||||||
# Collect the closure of all fixtures, starting with the given
|
# Collect the closure of all fixtures, starting with the given
|
||||||
# fixturenames as the initial set. As we have to visit all
|
# fixturenames as the initial set. As we have to visit all
|
||||||
# factory definitions anyway, we also return an arg2fixturedefs
|
# factory definitions anyway, we also return an arg2fixturedefs
|
||||||
|
@ -1529,19 +1541,7 @@ class FixtureManager:
|
||||||
# (discovering matching fixtures for a given name/node is expensive).
|
# (discovering matching fixtures for a given name/node is expensive).
|
||||||
|
|
||||||
parentid = parentnode.nodeid
|
parentid = parentnode.nodeid
|
||||||
fixturenames_closure = list(self._getautousenames(parentid))
|
fixturenames_closure = list(initialnames)
|
||||||
|
|
||||||
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]]] = {}
|
arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]] = {}
|
||||||
lastlen = -1
|
lastlen = -1
|
||||||
|
@ -1555,7 +1555,9 @@ class FixtureManager:
|
||||||
fixturedefs = self.getfixturedefs(argname, parentid)
|
fixturedefs = self.getfixturedefs(argname, parentid)
|
||||||
if fixturedefs:
|
if fixturedefs:
|
||||||
arg2fixturedefs[argname] = fixturedefs
|
arg2fixturedefs[argname] = fixturedefs
|
||||||
merge(fixturedefs[-1].argnames)
|
for arg in fixturedefs[-1].argnames:
|
||||||
|
if arg not in fixturenames_closure:
|
||||||
|
fixturenames_closure.append(arg)
|
||||||
|
|
||||||
def sort_by_scope(arg_name: str) -> Scope:
|
def sort_by_scope(arg_name: str) -> Scope:
|
||||||
try:
|
try:
|
||||||
|
@ -1566,7 +1568,7 @@ class FixtureManager:
|
||||||
return fixturedefs[-1]._scope
|
return fixturedefs[-1]._scope
|
||||||
|
|
||||||
fixturenames_closure.sort(key=sort_by_scope, reverse=True)
|
fixturenames_closure.sort(key=sort_by_scope, reverse=True)
|
||||||
return initialnames, fixturenames_closure, arg2fixturedefs
|
return fixturenames_closure, arg2fixturedefs
|
||||||
|
|
||||||
def pytest_generate_tests(self, metafunc: "Metafunc") -> None:
|
def pytest_generate_tests(self, metafunc: "Metafunc") -> None:
|
||||||
"""Generate new tests based on parametrized fixtures used by the given metafunc"""
|
"""Generate new tests based on parametrized fixtures used by the given metafunc"""
|
||||||
|
|
|
@ -1800,9 +1800,8 @@ class Function(PyobjMixin, nodes.Item):
|
||||||
self.keywords.update(keywords)
|
self.keywords.update(keywords)
|
||||||
|
|
||||||
if fixtureinfo is None:
|
if fixtureinfo is None:
|
||||||
fixtureinfo = self.session._fixturemanager.getfixtureinfo(
|
fm = self.session._fixturemanager
|
||||||
self, self.obj, self.cls, funcargs=True
|
fixtureinfo = fm.getfixtureinfo(self, self.obj, self.cls)
|
||||||
)
|
|
||||||
self._fixtureinfo: FuncFixtureInfo = fixtureinfo
|
self._fixtureinfo: FuncFixtureInfo = fixtureinfo
|
||||||
self.fixturenames = fixtureinfo.names_closure
|
self.fixturenames = fixtureinfo.names_closure
|
||||||
self._initrequest()
|
self._initrequest()
|
||||||
|
|
|
@ -6,6 +6,7 @@ from pathlib import Path
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest.compat import getfuncargnames
|
from _pytest.compat import getfuncargnames
|
||||||
from _pytest.config import ExitCode
|
from _pytest.config import ExitCode
|
||||||
|
from _pytest.fixtures import deduplicate_names
|
||||||
from _pytest.fixtures import TopRequest
|
from _pytest.fixtures import TopRequest
|
||||||
from _pytest.monkeypatch import MonkeyPatch
|
from _pytest.monkeypatch import MonkeyPatch
|
||||||
from _pytest.pytester import get_public_names
|
from _pytest.pytester import get_public_names
|
||||||
|
@ -4531,3 +4532,10 @@ def test_yield_fixture_with_no_value(pytester: Pytester) -> None:
|
||||||
result.assert_outcomes(errors=1)
|
result.assert_outcomes(errors=1)
|
||||||
result.stdout.fnmatch_lines([expected])
|
result.stdout.fnmatch_lines([expected])
|
||||||
assert result.ret == ExitCode.TESTS_FAILED
|
assert result.ret == ExitCode.TESTS_FAILED
|
||||||
|
|
||||||
|
|
||||||
|
def test_deduplicate_names() -> None:
|
||||||
|
items = deduplicate_names("abacd")
|
||||||
|
assert items == ("a", "b", "c", "d")
|
||||||
|
items = deduplicate_names(items + ("g", "f", "g", "e", "b"))
|
||||||
|
assert items == ("a", "b", "c", "d", "g", "f", "e")
|
||||||
|
|
Loading…
Reference in New Issue