nodes: remove cyclic dependency on _pytest.fixtures

- Change the fixtures plugin to store its one piece of data on the node's
  Store instead of directly.

- Import FixtureLookupError lazily.
This commit is contained in:
Ran Benita 2020-10-03 14:56:19 +03:00
parent bf09e7792f
commit d0a3f1dcbc
2 changed files with 39 additions and 37 deletions

View File

@ -29,6 +29,7 @@ import attr
import py import py
import _pytest import _pytest
from _pytest import nodes
from _pytest._code import getfslineno from _pytest._code import getfslineno
from _pytest._code.code import FormattedExcinfo from _pytest._code.code import FormattedExcinfo
from _pytest._code.code import TerminalRepr from _pytest._code.code import TerminalRepr
@ -56,13 +57,13 @@ from _pytest.mark.structures import MarkDecorator
from _pytest.outcomes import fail from _pytest.outcomes import fail
from _pytest.outcomes import TEST_OUTCOME from _pytest.outcomes import TEST_OUTCOME
from _pytest.pathlib import absolutepath from _pytest.pathlib import absolutepath
from _pytest.store import StoreKey
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Deque from typing import Deque
from typing import NoReturn from typing import NoReturn
from typing_extensions import Literal from typing_extensions import Literal
from _pytest import nodes
from _pytest.main import Session from _pytest.main import Session
from _pytest.python import CallSpec2 from _pytest.python import CallSpec2
from _pytest.python import Function from _pytest.python import Function
@ -124,13 +125,12 @@ def get_scope_package(node, fixturedef: "FixtureDef[object]"):
def get_scope_node( def get_scope_node(
node: "nodes.Node", scope: "_Scope" node: nodes.Node, scope: "_Scope"
) -> Optional[Union["nodes.Item", "nodes.Collector"]]: ) -> Optional[Union[nodes.Item, nodes.Collector]]:
import _pytest.python import _pytest.python
import _pytest.nodes
if scope == "function": if scope == "function":
return node.getparent(_pytest.nodes.Item) return node.getparent(nodes.Item)
elif scope == "class": elif scope == "class":
return node.getparent(_pytest.python.Class) return node.getparent(_pytest.python.Class)
elif scope == "module": elif scope == "module":
@ -143,8 +143,12 @@ def get_scope_node(
assert_never(scope) assert_never(scope)
# Used for storing artificial fixturedefs for direct parametrization.
name2pseudofixturedef_key = StoreKey[Dict[str, "FixtureDef[Any]"]]()
def add_funcarg_pseudo_fixture_def( def add_funcarg_pseudo_fixture_def(
collector, metafunc: "Metafunc", fixturemanager: "FixtureManager" collector: nodes.Collector, metafunc: "Metafunc", fixturemanager: "FixtureManager"
) -> None: ) -> None:
# This function will transform all collected calls to functions # This function will transform all collected calls to functions
# if they use direct funcargs (i.e. direct parametrization) # if they use direct funcargs (i.e. direct parametrization)
@ -186,8 +190,15 @@ def add_funcarg_pseudo_fixture_def(
assert scope == "class" and isinstance(collector, _pytest.python.Module) assert scope == "class" and isinstance(collector, _pytest.python.Module)
# Use module-level collector for class-scope (for now). # Use module-level collector for class-scope (for now).
node = collector node = collector
if node and argname in node._name2pseudofixturedef: if node is None:
arg2fixturedefs[argname] = [node._name2pseudofixturedef[argname]] name2pseudofixturedef = None
else:
default: Dict[str, FixtureDef[Any]] = {}
name2pseudofixturedef = node._store.setdefault(
name2pseudofixturedef_key, default
)
if name2pseudofixturedef is not None and argname in name2pseudofixturedef:
arg2fixturedefs[argname] = [name2pseudofixturedef[argname]]
else: else:
fixturedef = FixtureDef( fixturedef = FixtureDef(
fixturemanager=fixturemanager, fixturemanager=fixturemanager,
@ -200,8 +211,8 @@ def add_funcarg_pseudo_fixture_def(
ids=None, ids=None,
) )
arg2fixturedefs[argname] = [fixturedef] arg2fixturedefs[argname] = [fixturedef]
if node is not None: if name2pseudofixturedef is not None:
node._name2pseudofixturedef[argname] = fixturedef name2pseudofixturedef[argname] = fixturedef
def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]: def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]:
@ -222,7 +233,7 @@ def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]:
_Key = Tuple[object, ...] _Key = Tuple[object, ...]
def get_parametrized_fixture_keys(item: "nodes.Item", scopenum: int) -> Iterator[_Key]: def get_parametrized_fixture_keys(item: nodes.Item, scopenum: int) -> Iterator[_Key]:
"""Return list of keys for all parametrized arguments which match """Return list of keys for all parametrized arguments which match
the specified scope. """ the specified scope. """
assert scopenum < scopenum_function # function assert scopenum < scopenum_function # function
@ -256,7 +267,7 @@ def get_parametrized_fixture_keys(item: "nodes.Item", scopenum: int) -> Iterator
# setups and teardowns. # setups and teardowns.
def reorder_items(items: "Sequence[nodes.Item]") -> "List[nodes.Item]": def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
argkeys_cache = {} # type: Dict[int, Dict[nodes.Item, Dict[_Key, None]]] argkeys_cache = {} # type: Dict[int, Dict[nodes.Item, Dict[_Key, None]]]
items_by_argkey = {} # type: Dict[int, Dict[_Key, Deque[nodes.Item]]] items_by_argkey = {} # type: Dict[int, Dict[_Key, Deque[nodes.Item]]]
for scopenum in range(0, scopenum_function): for scopenum in range(0, scopenum_function):
@ -278,15 +289,15 @@ def reorder_items(items: "Sequence[nodes.Item]") -> "List[nodes.Item]":
item_d[key].append(item) item_d[key].append(item)
# cast is a workaround for https://github.com/python/typeshed/issues/3800. # cast is a workaround for https://github.com/python/typeshed/issues/3800.
items_dict = cast( items_dict = cast(
"Dict[nodes.Item, None]", order_preserving_dict.fromkeys(items, None) Dict[nodes.Item, None], order_preserving_dict.fromkeys(items, None)
) )
return list(reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, 0)) return list(reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, 0))
def fix_cache_order( def fix_cache_order(
item: "nodes.Item", item: nodes.Item,
argkeys_cache: "Dict[int, Dict[nodes.Item, Dict[_Key, None]]]", argkeys_cache: Dict[int, Dict[nodes.Item, Dict[_Key, None]]],
items_by_argkey: "Dict[int, Dict[_Key, Deque[nodes.Item]]]", items_by_argkey: Dict[int, Dict[_Key, "Deque[nodes.Item]"]],
) -> None: ) -> None:
for scopenum in range(0, scopenum_function): for scopenum in range(0, scopenum_function):
for key in argkeys_cache[scopenum].get(item, []): for key in argkeys_cache[scopenum].get(item, []):
@ -294,11 +305,11 @@ def fix_cache_order(
def reorder_items_atscope( def reorder_items_atscope(
items: "Dict[nodes.Item, None]", items: Dict[nodes.Item, None],
argkeys_cache: "Dict[int, Dict[nodes.Item, Dict[_Key, None]]]", argkeys_cache: Dict[int, Dict[nodes.Item, Dict[_Key, None]]],
items_by_argkey: "Dict[int, Dict[_Key, Deque[nodes.Item]]]", items_by_argkey: Dict[int, Dict[_Key, "Deque[nodes.Item]"]],
scopenum: int, scopenum: int,
) -> "Dict[nodes.Item, None]": ) -> Dict[nodes.Item, None]:
if scopenum >= scopenum_function or len(items) < 3: if scopenum >= scopenum_function or len(items) < 3:
return items return items
ignore = set() # type: Set[Optional[_Key]] ignore = set() # type: Set[Optional[_Key]]
@ -711,10 +722,10 @@ class FixtureRequest:
lines.append("%s:%d: def %s%s" % (p, lineno + 1, factory.__name__, args)) lines.append("%s:%d: def %s%s" % (p, lineno + 1, factory.__name__, args))
return lines return lines
def _getscopeitem(self, scope: "_Scope") -> Union["nodes.Item", "nodes.Collector"]: def _getscopeitem(self, scope: "_Scope") -> Union[nodes.Item, nodes.Collector]:
if scope == "function": if scope == "function":
# This might also be a non-function Item despite its attribute name. # This might also be a non-function Item despite its attribute name.
node: Optional[Union["nodes.Item", "nodes.Collector"]] = self._pyfuncitem node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
elif scope == "package": elif scope == "package":
# FIXME: _fixturedef is not defined on FixtureRequest (this class), # FIXME: _fixturedef is not defined on FixtureRequest (this class),
# but on FixtureRequest (a subclass). # but on FixtureRequest (a subclass).
@ -1414,7 +1425,7 @@ class FixtureManager:
] # type: List[Tuple[str, List[str]]] ] # type: List[Tuple[str, List[str]]]
session.config.pluginmanager.register(self, "funcmanage") session.config.pluginmanager.register(self, "funcmanage")
def _get_direct_parametrize_args(self, node: "nodes.Node") -> List[str]: def _get_direct_parametrize_args(self, node: nodes.Node) -> List[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.
@ -1434,7 +1445,7 @@ class FixtureManager:
return parametrize_argnames return parametrize_argnames
def getfixtureinfo( def getfixtureinfo(
self, node: "nodes.Node", func, cls, funcargs: bool = True self, node: nodes.Node, func, cls, funcargs: bool = True
) -> FuncFixtureInfo: ) -> FuncFixtureInfo:
if funcargs and not getattr(node, "nofuncargs", False): if funcargs and not getattr(node, "nofuncargs", False):
argnames = getfuncargnames(func, name=node.name, cls=cls) argnames = getfuncargnames(func, name=node.name, cls=cls)
@ -1458,8 +1469,6 @@ class FixtureManager:
except AttributeError: except AttributeError:
pass pass
else: else:
from _pytest import nodes
# Construct the base nodeid which is later used to check # Construct the base nodeid which is later used to check
# what fixtures are visible for particular tests (as denoted # what fixtures are visible for particular tests (as denoted
# by their test id). # by their test id).
@ -1491,7 +1500,7 @@ class FixtureManager:
def getfixtureclosure( def getfixtureclosure(
self, self,
fixturenames: Tuple[str, ...], fixturenames: Tuple[str, ...],
parentnode: "nodes.Node", parentnode: nodes.Node,
ignore_args: Sequence[str] = (), ignore_args: Sequence[str] = (),
) -> Tuple[Tuple[str, ...], List[str], Dict[str, Sequence[FixtureDef[Any]]]]: ) -> Tuple[Tuple[str, ...], 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
@ -1586,7 +1595,7 @@ class FixtureManager:
# Try next super fixture, if any. # Try next super fixture, if any.
def pytest_collection_modifyitems(self, items: "List[nodes.Item]") -> None: def pytest_collection_modifyitems(self, items: List[nodes.Item]) -> None:
# Separate parametrized setups. # Separate parametrized setups.
items[:] = reorder_items(items) items[:] = reorder_items(items)
@ -1667,8 +1676,6 @@ class FixtureManager:
def _matchfactories( def _matchfactories(
self, fixturedefs: Iterable[FixtureDef[Any]], nodeid: str self, fixturedefs: Iterable[FixtureDef[Any]], nodeid: str
) -> Iterator[FixtureDef[Any]]: ) -> Iterator[FixtureDef[Any]]:
from _pytest import nodes
for fixturedef in fixturedefs: for fixturedef in fixturedefs:
if nodes.ischildnode(fixturedef.baseid, nodeid): if nodes.ischildnode(fixturedef.baseid, nodeid):
yield fixturedef yield fixturedef

View File

@ -2,9 +2,7 @@ import os
import warnings import warnings
from functools import lru_cache from functools import lru_cache
from pathlib import Path from pathlib import Path
from typing import Any
from typing import Callable from typing import Callable
from typing import Dict
from typing import Iterable from typing import Iterable
from typing import Iterator from typing import Iterator
from typing import List from typing import List
@ -27,8 +25,6 @@ from _pytest.compat import cached_property
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import ConftestImportFailure from _pytest.config import ConftestImportFailure
from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
from _pytest.fixtures import FixtureDef
from _pytest.fixtures import FixtureLookupError
from _pytest.mark.structures import Mark from _pytest.mark.structures import Mark
from _pytest.mark.structures import MarkDecorator from _pytest.mark.structures import MarkDecorator
from _pytest.mark.structures import NodeKeywords from _pytest.mark.structures import NodeKeywords
@ -170,9 +166,6 @@ class Node(metaclass=NodeMeta):
#: Allow adding of extra keywords to use for matching. #: Allow adding of extra keywords to use for matching.
self.extra_keyword_matches = set() # type: Set[str] self.extra_keyword_matches = set() # type: Set[str]
# Used for storing artificial fixturedefs for direct parametrization.
self._name2pseudofixturedef = {} # type: Dict[str, FixtureDef[Any]]
if nodeid is not None: if nodeid is not None:
assert "::()" not in nodeid assert "::()" not in nodeid
self._nodeid = nodeid self._nodeid = nodeid
@ -366,6 +359,8 @@ class Node(metaclass=NodeMeta):
excinfo: ExceptionInfo[BaseException], excinfo: ExceptionInfo[BaseException],
style: "Optional[_TracebackStyle]" = None, style: "Optional[_TracebackStyle]" = None,
) -> TerminalRepr: ) -> TerminalRepr:
from _pytest.fixtures import FixtureLookupError
if isinstance(excinfo.value, ConftestImportFailure): if isinstance(excinfo.value, ConftestImportFailure):
excinfo = ExceptionInfo(excinfo.value.excinfo) excinfo = ExceptionInfo(excinfo.value.excinfo)
if isinstance(excinfo.value, fail.Exception): if isinstance(excinfo.value, fail.Exception):