Merge pull request #11416 from bluetech/fixtures-getfixtureclosure

fixtures: more tweaks
This commit is contained in:
Ronny Pfannschmidt 2023-09-08 19:48:34 +02:00 committed by GitHub
commit dd7beb39d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 70 additions and 75 deletions

View File

@ -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

View File

@ -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"""

View File

@ -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()

View File

@ -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")