Merge pull request #11026 from bluetech/small-fixes

Small fixes and improvements
This commit is contained in:
Ran Benita 2023-05-24 21:44:03 +03:00 committed by GitHub
commit af124c7f21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 99 additions and 58 deletions

View File

@ -46,6 +46,7 @@ from _pytest.compat import getimfunc
from _pytest.compat import getlocation from _pytest.compat import getlocation
from _pytest.compat import is_generator from _pytest.compat import is_generator
from _pytest.compat import NOTSET from _pytest.compat import NOTSET
from _pytest.compat import NotSetType
from _pytest.compat import overload from _pytest.compat import overload
from _pytest.compat import safe_getattr from _pytest.compat import safe_getattr
from _pytest.config import _PluggyPlugin from _pytest.config import _PluggyPlugin
@ -112,16 +113,18 @@ def pytest_sessionstart(session: "Session") -> None:
session._fixturemanager = FixtureManager(session) session._fixturemanager = FixtureManager(session)
def get_scope_package(node, fixturedef: "FixtureDef[object]"): def get_scope_package(
import pytest node: nodes.Item,
fixturedef: "FixtureDef[object]",
) -> Optional[Union[nodes.Item, nodes.Collector]]:
from _pytest.python import Package
cls = pytest.Package current: Optional[Union[nodes.Item, nodes.Collector]] = node
current = node
fixture_package_name = "{}/{}".format(fixturedef.baseid, "__init__.py") fixture_package_name = "{}/{}".format(fixturedef.baseid, "__init__.py")
while current and ( while current and (
type(current) is not cls or fixture_package_name != current.nodeid not isinstance(current, Package) or fixture_package_name != current.nodeid
): ):
current = current.parent current = current.parent # type: ignore[assignment]
if current is None: if current is None:
return node.session return node.session
return current return current
@ -434,7 +437,23 @@ class FixtureRequest:
@property @property
def node(self): def node(self):
"""Underlying collection node (depends on current request scope).""" """Underlying collection node (depends on current request scope)."""
return self._getscopeitem(self._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]": def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
fixturedefs = self._arg2fixturedefs.get(argname, None) fixturedefs = self._arg2fixturedefs.get(argname, None)
@ -518,11 +537,7 @@ class FixtureRequest:
"""Add finalizer/teardown function to be called without arguments after """Add finalizer/teardown function to be called without arguments after
the last test within the requesting test context finished execution.""" the last test within the requesting test context finished execution."""
# XXX usually this method is shadowed by fixturedef specific ones. # XXX usually this method is shadowed by fixturedef specific ones.
self._addfinalizer(finalizer, scope=self.scope) self.node.addfinalizer(finalizer)
def _addfinalizer(self, finalizer: Callable[[], object], scope) -> None:
node = self._getscopeitem(scope)
node.addfinalizer(finalizer)
def applymarker(self, marker: Union[str, MarkDecorator]) -> None: def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
"""Apply a marker to a single test function invocation. """Apply a marker to a single test function invocation.
@ -717,28 +732,6 @@ 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: Union[Scope, "_ScopeName"]
) -> Union[nodes.Item, nodes.Collector]:
if isinstance(scope, str):
scope = Scope(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 __repr__(self) -> str: def __repr__(self) -> str:
return "<FixtureRequest for %r>" % (self.node) return "<FixtureRequest for %r>" % (self.node)
@ -1593,13 +1586,52 @@ class FixtureManager:
# Separate parametrized setups. # Separate parametrized setups.
items[:] = reorder_items(items) items[:] = reorder_items(items)
@overload
def parsefactories( def parsefactories(
self, node_or_obj, nodeid=NOTSET, unittest: bool = False self,
node_or_obj: nodes.Node,
*,
unittest: bool = ...,
) -> None: ) -> 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: if nodeid is not NOTSET:
holderobj = node_or_obj holderobj = node_or_obj
else: else:
holderobj = node_or_obj.obj 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 nodeid = node_or_obj.nodeid
if holderobj in self._holderobjseen: if holderobj in self._holderobjseen:
return return

View File

@ -27,6 +27,7 @@ from typing import Callable
from typing import Dict 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 Optional from typing import Optional
from typing import Set from typing import Set
from typing import Tuple from typing import Tuple
@ -669,30 +670,38 @@ def resolve_package_path(path: Path) -> Optional[Path]:
return result return result
def scandir(path: Union[str, "os.PathLike[str]"]) -> List["os.DirEntry[str]"]:
"""Scan a directory recursively, in breadth-first order.
The returned entries are sorted.
"""
entries = []
with os.scandir(path) as s:
# Skip entries with symlink loops and other brokenness, so the caller
# doesn't have to deal with it.
for entry in s:
try:
entry.is_file()
except OSError as err:
if _ignore_error(err):
continue
raise
entries.append(entry)
entries.sort(key=lambda entry: entry.name)
return entries
def visit( def visit(
path: Union[str, "os.PathLike[str]"], recurse: Callable[["os.DirEntry[str]"], bool] path: Union[str, "os.PathLike[str]"], recurse: Callable[["os.DirEntry[str]"], bool]
) -> Iterator["os.DirEntry[str]"]: ) -> Iterator["os.DirEntry[str]"]:
"""Walk a directory recursively, in breadth-first order. """Walk a directory recursively, in breadth-first order.
The `recurse` predicate determines whether a directory is recursed.
Entries at each directory level are sorted. Entries at each directory level are sorted.
""" """
entries = scandir(path)
# Skip entries with symlink loops and other brokenness, so the caller doesn't
# have to deal with it.
entries = []
for entry in os.scandir(path):
try:
entry.is_file()
except OSError as err:
if _ignore_error(err):
continue
raise
entries.append(entry)
entries.sort(key=lambda entry: entry.name)
yield from entries yield from entries
for entry in entries: for entry in entries:
if entry.is_dir() and recurse(entry): if entry.is_dir() and recurse(entry):
yield from visit(entry.path, recurse) yield from visit(entry.path, recurse)

View File

@ -667,7 +667,7 @@ class Package(Module):
config=None, config=None,
session=None, session=None,
nodeid=None, nodeid=None,
path=Optional[Path], path: Optional[Path] = None,
) -> None: ) -> None:
# NOTE: Could be just the following, but kept as-is for compat. # NOTE: Could be just the following, but kept as-is for compat.
# nodes.FSCollector.__init__(self, fspath, parent=parent) # nodes.FSCollector.__init__(self, fspath, parent=parent)
@ -745,11 +745,11 @@ class Package(Module):
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
this_path = self.path.parent this_path = self.path.parent
init_module = this_path / "__init__.py"
if init_module.is_file() and path_matches_patterns( # Always collect the __init__ first.
init_module, self.config.getini("python_files") if path_matches_patterns(self.path, self.config.getini("python_files")):
): yield Module.from_parent(self, path=self.path)
yield Module.from_parent(self, path=init_module)
pkg_prefixes: Set[Path] = set() pkg_prefixes: Set[Path] = set()
for direntry in visit(str(this_path), recurse=self._recurse): for direntry in visit(str(this_path), recurse=self._recurse):
path = Path(direntry.path) path = Path(direntry.path)