Skip to content

Commit 46b2c82

Browse files
committed
pathlib: make visit() independent of py.path.local, use os.scandir
`os.scandir()`, introduced in Python 3.5, is much faster than `os.listdir()`. See https://www.python.org/dev/peps/pep-0471/. It also has a `DirEntry` which can be used to further reduce syscalls in some cases.
1 parent c15bb5d commit 46b2c82

File tree

4 files changed

+25
-23
lines changed

4 files changed

+25
-23
lines changed

src/_pytest/main.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -618,11 +618,13 @@ def _collect(
618618
assert not names, "invalid arg {!r}".format((argpath, names))
619619

620620
seen_dirs = set() # type: Set[py.path.local]
621-
for path in visit(argpath, self._recurse):
622-
if not path.check(file=1):
621+
for direntry in visit(str(argpath), self._recurse):
622+
if not direntry.is_file():
623623
continue
624624

625+
path = py.path.local(direntry.path)
625626
dirpath = path.dirpath()
627+
626628
if dirpath not in seen_dirs:
627629
# Collect packages first.
628630
seen_dirs.add(dirpath)

src/_pytest/nodes.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -562,17 +562,18 @@ def _gethookproxy(self, fspath: py.path.local):
562562
def gethookproxy(self, fspath: py.path.local):
563563
raise NotImplementedError()
564564

565-
def _recurse(self, dirpath: py.path.local) -> bool:
566-
if dirpath.basename == "__pycache__":
565+
def _recurse(self, direntry: os.DirEntry) -> bool:
566+
if direntry.name == "__pycache__":
567567
return False
568-
ihook = self._gethookproxy(dirpath.dirpath())
569-
if ihook.pytest_ignore_collect(path=dirpath, config=self.config):
568+
path = py.path.local(direntry.path)
569+
ihook = self._gethookproxy(path.dirpath())
570+
if ihook.pytest_ignore_collect(path=path, config=self.config):
570571
return False
571572
for pat in self._norecursepatterns:
572-
if dirpath.check(fnmatch=pat):
573+
if path.check(fnmatch=pat):
573574
return False
574-
ihook = self._gethookproxy(dirpath)
575-
ihook.pytest_collect_directory(path=dirpath, parent=self)
575+
ihook = self._gethookproxy(path)
576+
ihook.pytest_collect_directory(path=path, parent=self)
576577
return True
577578

578579
def isinitpath(self, path: py.path.local) -> bool:

src/_pytest/pathlib.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -559,15 +559,13 @@ def resolve_package_path(path: Path) -> Optional[Path]:
559559
return result
560560

561561

562-
def visit(
563-
path: py.path.local, recurse: Callable[[py.path.local], bool],
564-
) -> Iterator[py.path.local]:
565-
"""Walk path recursively, in breadth-first order.
562+
def visit(path: str, recurse: Callable[[os.DirEntry], bool]) -> Iterator[os.DirEntry]:
563+
"""Walk a directory recursively, in breadth-first order.
566564
567565
Entries at each directory level are sorted.
568566
"""
569-
entries = sorted(path.listdir())
567+
entries = sorted(os.scandir(path), key=lambda entry: entry.name)
570568
yield from entries
571569
for entry in entries:
572-
if entry.check(dir=1) and recurse(entry):
573-
yield from visit(entry, recurse)
570+
if entry.is_dir(follow_symlinks=False) and recurse(entry):
571+
yield from visit(entry.path, recurse)

src/_pytest/python.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -642,23 +642,24 @@ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
642642
):
643643
yield Module.from_parent(self, fspath=init_module)
644644
pkg_prefixes = set() # type: Set[py.path.local]
645-
for path in visit(this_path, recurse=self._recurse):
645+
for direntry in visit(str(this_path), recurse=self._recurse):
646+
path = py.path.local(direntry.path)
647+
646648
# We will visit our own __init__.py file, in which case we skip it.
647-
is_file = path.isfile()
648-
if is_file:
649-
if path.basename == "__init__.py" and path.dirpath() == this_path:
649+
if direntry.is_file():
650+
if direntry.name == "__init__.py" and path.dirpath() == this_path:
650651
continue
651652

652-
parts_ = parts(path.strpath)
653+
parts_ = parts(direntry.path)
653654
if any(
654655
str(pkg_prefix) in parts_ and pkg_prefix.join("__init__.py") != path
655656
for pkg_prefix in pkg_prefixes
656657
):
657658
continue
658659

659-
if is_file:
660+
if direntry.is_file():
660661
yield from self._collectfile(path)
661-
elif not path.isdir():
662+
elif not direntry.is_dir():
662663
# Broken symlink or invalid/missing file.
663664
continue
664665
elif path.join("__init__.py").check(file=1):

0 commit comments

Comments
 (0)