Skip to content

Commit af124c7

Browse files
authored
Merge pull request #11026 from bluetech/small-fixes
Small fixes and improvements
2 parents 4e6d53f + 63f258f commit af124c7

File tree

3 files changed

+99
-58
lines changed

3 files changed

+99
-58
lines changed

src/_pytest/fixtures.py

Lines changed: 68 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
from _pytest.compat import getlocation
4747
from _pytest.compat import is_generator
4848
from _pytest.compat import NOTSET
49+
from _pytest.compat import NotSetType
4950
from _pytest.compat import overload
5051
from _pytest.compat import safe_getattr
5152
from _pytest.config import _PluggyPlugin
@@ -112,16 +113,18 @@ def pytest_sessionstart(session: "Session") -> None:
112113
session._fixturemanager = FixtureManager(session)
113114

114115

115-
def get_scope_package(node, fixturedef: "FixtureDef[object]"):
116-
import pytest
116+
def get_scope_package(
117+
node: nodes.Item,
118+
fixturedef: "FixtureDef[object]",
119+
) -> Optional[Union[nodes.Item, nodes.Collector]]:
120+
from _pytest.python import Package
117121

118-
cls = pytest.Package
119-
current = node
122+
current: Optional[Union[nodes.Item, nodes.Collector]] = node
120123
fixture_package_name = "{}/{}".format(fixturedef.baseid, "__init__.py")
121124
while current and (
122-
type(current) is not cls or fixture_package_name != current.nodeid
125+
not isinstance(current, Package) or fixture_package_name != current.nodeid
123126
):
124-
current = current.parent
127+
current = current.parent # type: ignore[assignment]
125128
if current is None:
126129
return node.session
127130
return current
@@ -434,7 +437,23 @@ def fixturenames(self) -> List[str]:
434437
@property
435438
def node(self):
436439
"""Underlying collection node (depends on current request scope)."""
437-
return self._getscopeitem(self._scope)
440+
scope = self._scope
441+
if scope is Scope.Function:
442+
# This might also be a non-function Item despite its attribute name.
443+
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
444+
elif scope is Scope.Package:
445+
# FIXME: _fixturedef is not defined on FixtureRequest (this class),
446+
# but on FixtureRequest (a subclass).
447+
node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
448+
else:
449+
node = get_scope_node(self._pyfuncitem, scope)
450+
if node is None and scope is Scope.Class:
451+
# Fallback to function item itself.
452+
node = self._pyfuncitem
453+
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
454+
scope, self._pyfuncitem
455+
)
456+
return node
438457

439458
def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
440459
fixturedefs = self._arg2fixturedefs.get(argname, None)
@@ -518,11 +537,7 @@ def addfinalizer(self, finalizer: Callable[[], object]) -> None:
518537
"""Add finalizer/teardown function to be called without arguments after
519538
the last test within the requesting test context finished execution."""
520539
# XXX usually this method is shadowed by fixturedef specific ones.
521-
self._addfinalizer(finalizer, scope=self.scope)
522-
523-
def _addfinalizer(self, finalizer: Callable[[], object], scope) -> None:
524-
node = self._getscopeitem(scope)
525-
node.addfinalizer(finalizer)
540+
self.node.addfinalizer(finalizer)
526541

527542
def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
528543
"""Apply a marker to a single test function invocation.
@@ -717,28 +732,6 @@ def _factorytraceback(self) -> List[str]:
717732
lines.append("%s:%d: def %s%s" % (p, lineno + 1, factory.__name__, args))
718733
return lines
719734

720-
def _getscopeitem(
721-
self, scope: Union[Scope, "_ScopeName"]
722-
) -> Union[nodes.Item, nodes.Collector]:
723-
if isinstance(scope, str):
724-
scope = Scope(scope)
725-
if scope is Scope.Function:
726-
# This might also be a non-function Item despite its attribute name.
727-
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
728-
elif scope is Scope.Package:
729-
# FIXME: _fixturedef is not defined on FixtureRequest (this class),
730-
# but on FixtureRequest (a subclass).
731-
node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
732-
else:
733-
node = get_scope_node(self._pyfuncitem, scope)
734-
if node is None and scope is Scope.Class:
735-
# Fallback to function item itself.
736-
node = self._pyfuncitem
737-
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
738-
scope, self._pyfuncitem
739-
)
740-
return node
741-
742735
def __repr__(self) -> str:
743736
return "<FixtureRequest for %r>" % (self.node)
744737

@@ -1593,13 +1586,52 @@ def pytest_collection_modifyitems(self, items: List[nodes.Item]) -> None:
15931586
# Separate parametrized setups.
15941587
items[:] = reorder_items(items)
15951588

1589+
@overload
15961590
def parsefactories(
1597-
self, node_or_obj, nodeid=NOTSET, unittest: bool = False
1591+
self,
1592+
node_or_obj: nodes.Node,
1593+
*,
1594+
unittest: bool = ...,
15981595
) -> None:
1596+
raise NotImplementedError()
1597+
1598+
@overload
1599+
def parsefactories( # noqa: F811
1600+
self,
1601+
node_or_obj: object,
1602+
nodeid: Optional[str],
1603+
*,
1604+
unittest: bool = ...,
1605+
) -> None:
1606+
raise NotImplementedError()
1607+
1608+
def parsefactories( # noqa: F811
1609+
self,
1610+
node_or_obj: Union[nodes.Node, object],
1611+
nodeid: Union[str, NotSetType, None] = NOTSET,
1612+
*,
1613+
unittest: bool = False,
1614+
) -> None:
1615+
"""Collect fixtures from a collection node or object.
1616+
1617+
Found fixtures are parsed into `FixtureDef`s and saved.
1618+
1619+
If `node_or_object` is a collection node (with an underlying Python
1620+
object), the node's object is traversed and the node's nodeid is used to
1621+
determine the fixtures' visibilty. `nodeid` must not be specified in
1622+
this case.
1623+
1624+
If `node_or_object` is an object (e.g. a plugin), the object is
1625+
traversed and the given `nodeid` is used to determine the fixtures'
1626+
visibility. `nodeid` must be specified in this case; None and "" mean
1627+
total visibility.
1628+
"""
15991629
if nodeid is not NOTSET:
16001630
holderobj = node_or_obj
16011631
else:
1602-
holderobj = node_or_obj.obj
1632+
assert isinstance(node_or_obj, nodes.Node)
1633+
holderobj = cast(object, node_or_obj.obj) # type: ignore[attr-defined]
1634+
assert isinstance(node_or_obj.nodeid, str)
16031635
nodeid = node_or_obj.nodeid
16041636
if holderobj in self._holderobjseen:
16051637
return

src/_pytest/pathlib.py

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from typing import Dict
2828
from typing import Iterable
2929
from typing import Iterator
30+
from typing import List
3031
from typing import Optional
3132
from typing import Set
3233
from typing import Tuple
@@ -669,30 +670,38 @@ def resolve_package_path(path: Path) -> Optional[Path]:
669670
return result
670671

671672

673+
def scandir(path: Union[str, "os.PathLike[str]"]) -> List["os.DirEntry[str]"]:
674+
"""Scan a directory recursively, in breadth-first order.
675+
676+
The returned entries are sorted.
677+
"""
678+
entries = []
679+
with os.scandir(path) as s:
680+
# Skip entries with symlink loops and other brokenness, so the caller
681+
# doesn't have to deal with it.
682+
for entry in s:
683+
try:
684+
entry.is_file()
685+
except OSError as err:
686+
if _ignore_error(err):
687+
continue
688+
raise
689+
entries.append(entry)
690+
entries.sort(key=lambda entry: entry.name)
691+
return entries
692+
693+
672694
def visit(
673695
path: Union[str, "os.PathLike[str]"], recurse: Callable[["os.DirEntry[str]"], bool]
674696
) -> Iterator["os.DirEntry[str]"]:
675697
"""Walk a directory recursively, in breadth-first order.
676698
699+
The `recurse` predicate determines whether a directory is recursed.
700+
677701
Entries at each directory level are sorted.
678702
"""
679-
680-
# Skip entries with symlink loops and other brokenness, so the caller doesn't
681-
# have to deal with it.
682-
entries = []
683-
for entry in os.scandir(path):
684-
try:
685-
entry.is_file()
686-
except OSError as err:
687-
if _ignore_error(err):
688-
continue
689-
raise
690-
entries.append(entry)
691-
692-
entries.sort(key=lambda entry: entry.name)
693-
703+
entries = scandir(path)
694704
yield from entries
695-
696705
for entry in entries:
697706
if entry.is_dir() and recurse(entry):
698707
yield from visit(entry.path, recurse)

src/_pytest/python.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -667,7 +667,7 @@ def __init__(
667667
config=None,
668668
session=None,
669669
nodeid=None,
670-
path=Optional[Path],
670+
path: Optional[Path] = None,
671671
) -> None:
672672
# NOTE: Could be just the following, but kept as-is for compat.
673673
# nodes.FSCollector.__init__(self, fspath, parent=parent)
@@ -745,11 +745,11 @@ def _collectfile(
745745

746746
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
747747
this_path = self.path.parent
748-
init_module = this_path / "__init__.py"
749-
if init_module.is_file() and path_matches_patterns(
750-
init_module, self.config.getini("python_files")
751-
):
752-
yield Module.from_parent(self, path=init_module)
748+
749+
# Always collect the __init__ first.
750+
if path_matches_patterns(self.path, self.config.getini("python_files")):
751+
yield Module.from_parent(self, path=self.path)
752+
753753
pkg_prefixes: Set[Path] = set()
754754
for direntry in visit(str(this_path), recurse=self._recurse):
755755
path = Path(direntry.path)

0 commit comments

Comments
 (0)