Skip to content

Commit ca4effc

Browse files
committed
Convert most of the collection code from py.path to pathlib
1 parent 4faed28 commit ca4effc

File tree

5 files changed

+77
-75
lines changed

5 files changed

+77
-75
lines changed

changelog/8174.trivial.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ The following changes have been made to internal pytest types/functions:
33
- The ``path`` property of ``_pytest.code.Code`` returns ``Path`` instead of ``py.path.local``.
44
- The ``path`` property of ``_pytest.code.TracebackEntry`` returns ``Path`` instead of ``py.path.local``.
55
- The ``_pytest.code.getfslineno()`` function returns ``Path`` instead of ``py.path.local``.
6+
- The ``_pytest.python.path_matches_patterns()`` function takes ``Path`` instead of ``py.path.local``.

src/_pytest/config/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ def __init__(self) -> None:
348348
self._conftestpath2mod: Dict[Path, types.ModuleType] = {}
349349
self._confcutdir: Optional[Path] = None
350350
self._noconftest = False
351-
self._duplicatepaths: Set[py.path.local] = set()
351+
self._duplicatepaths: Set[Path] = set()
352352

353353
# plugins that were explicitly skipped with pytest.skip
354354
# list of (module name, skip reason)

src/_pytest/main.py

Lines changed: 42 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from _pytest.outcomes import exit
3838
from _pytest.pathlib import absolutepath
3939
from _pytest.pathlib import bestrelpath
40+
from _pytest.pathlib import fnmatch_ex
4041
from _pytest.pathlib import visit
4142
from _pytest.reports import CollectReport
4243
from _pytest.reports import TestReport
@@ -353,11 +354,14 @@ def pytest_runtestloop(session: "Session") -> bool:
353354
return True
354355

355356

356-
def _in_venv(path: py.path.local) -> bool:
357+
def _in_venv(path: Path) -> bool:
357358
"""Attempt to detect if ``path`` is the root of a Virtual Environment by
358359
checking for the existence of the appropriate activate script."""
359-
bindir = path.join("Scripts" if sys.platform.startswith("win") else "bin")
360-
if not bindir.isdir():
360+
bindir = path.joinpath("Scripts" if sys.platform.startswith("win") else "bin")
361+
try:
362+
if not bindir.is_dir():
363+
return False
364+
except OSError:
361365
return False
362366
activates = (
363367
"activate",
@@ -367,33 +371,32 @@ def _in_venv(path: py.path.local) -> bool:
367371
"Activate.bat",
368372
"Activate.ps1",
369373
)
370-
return any([fname.basename in activates for fname in bindir.listdir()])
374+
return any(fname.name in activates for fname in bindir.iterdir())
371375

372376

373-
def pytest_ignore_collect(path: py.path.local, config: Config) -> Optional[bool]:
374-
path_ = Path(path)
375-
ignore_paths = config._getconftest_pathlist("collect_ignore", path=path_.parent)
377+
def pytest_ignore_collect(fspath: Path, config: Config) -> Optional[bool]:
378+
ignore_paths = config._getconftest_pathlist("collect_ignore", path=fspath.parent)
376379
ignore_paths = ignore_paths or []
377380
excludeopt = config.getoption("ignore")
378381
if excludeopt:
379382
ignore_paths.extend(absolutepath(x) for x in excludeopt)
380383

381-
if path_ in ignore_paths:
384+
if fspath in ignore_paths:
382385
return True
383386

384387
ignore_globs = config._getconftest_pathlist(
385-
"collect_ignore_glob", path=path_.parent
388+
"collect_ignore_glob", path=fspath.parent
386389
)
387390
ignore_globs = ignore_globs or []
388391
excludeglobopt = config.getoption("ignore_glob")
389392
if excludeglobopt:
390393
ignore_globs.extend(absolutepath(x) for x in excludeglobopt)
391394

392-
if any(fnmatch.fnmatch(str(path), str(glob)) for glob in ignore_globs):
395+
if any(fnmatch.fnmatch(str(fspath), str(glob)) for glob in ignore_globs):
393396
return True
394397

395398
allow_in_venv = config.getoption("collect_in_virtualenv")
396-
if not allow_in_venv and _in_venv(path):
399+
if not allow_in_venv and _in_venv(fspath):
397400
return True
398401
return None
399402

@@ -538,21 +541,21 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
538541
if ihook.pytest_ignore_collect(fspath=fspath, path=path, config=self.config):
539542
return False
540543
norecursepatterns = self.config.getini("norecursedirs")
541-
if any(path.check(fnmatch=pat) for pat in norecursepatterns):
544+
if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
542545
return False
543546
return True
544547

545548
def _collectfile(
546-
self, path: py.path.local, handle_dupes: bool = True
549+
self, fspath: Path, handle_dupes: bool = True
547550
) -> Sequence[nodes.Collector]:
548-
fspath = Path(path)
551+
path = py.path.local(fspath)
549552
assert (
550-
path.isfile()
553+
fspath.is_file()
551554
), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format(
552-
path, path.isdir(), path.exists(), path.islink()
555+
fspath, fspath.is_dir(), fspath.exists(), fspath.is_symlink()
553556
)
554-
ihook = self.gethookproxy(path)
555-
if not self.isinitpath(path):
557+
ihook = self.gethookproxy(fspath)
558+
if not self.isinitpath(fspath):
556559
if ihook.pytest_ignore_collect(
557560
fspath=fspath, path=path, config=self.config
558561
):
@@ -562,10 +565,10 @@ def _collectfile(
562565
keepduplicates = self.config.getoption("keepduplicates")
563566
if not keepduplicates:
564567
duplicate_paths = self.config.pluginmanager._duplicatepaths
565-
if path in duplicate_paths:
568+
if fspath in duplicate_paths:
566569
return ()
567570
else:
568-
duplicate_paths.add(path)
571+
duplicate_paths.add(fspath)
569572

570573
return ihook.pytest_collect_file(fspath=fspath, path=path, parent=self) # type: ignore[no-any-return]
571574

@@ -652,10 +655,8 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
652655
from _pytest.python import Package
653656

654657
# Keep track of any collected nodes in here, so we don't duplicate fixtures.
655-
node_cache1: Dict[py.path.local, Sequence[nodes.Collector]] = {}
656-
node_cache2: Dict[
657-
Tuple[Type[nodes.Collector], py.path.local], nodes.Collector
658-
] = ({})
658+
node_cache1: Dict[Path, Sequence[nodes.Collector]] = {}
659+
node_cache2: Dict[Tuple[Type[nodes.Collector], Path], nodes.Collector] = ({})
659660

660661
# Keep track of any collected collectors in matchnodes paths, so they
661662
# are not collected more than once.
@@ -679,31 +680,31 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
679680
break
680681

681682
if parent.is_dir():
682-
pkginit = py.path.local(parent / "__init__.py")
683-
if pkginit.isfile() and pkginit not in node_cache1:
683+
pkginit = parent / "__init__.py"
684+
if pkginit.is_file() and pkginit not in node_cache1:
684685
col = self._collectfile(pkginit, handle_dupes=False)
685686
if col:
686687
if isinstance(col[0], Package):
687688
pkg_roots[str(parent)] = col[0]
688-
node_cache1[col[0].fspath] = [col[0]]
689+
node_cache1[Path(col[0].fspath)] = [col[0]]
689690

690691
# If it's a directory argument, recurse and look for any Subpackages.
691692
# Let the Package collector deal with subnodes, don't collect here.
692693
if argpath.is_dir():
693694
assert not names, "invalid arg {!r}".format((argpath, names))
694695

695-
seen_dirs: Set[py.path.local] = set()
696+
seen_dirs: Set[Path] = set()
696697
for direntry in visit(str(argpath), self._recurse):
697698
if not direntry.is_file():
698699
continue
699700

700-
path = py.path.local(direntry.path)
701-
dirpath = path.dirpath()
701+
path = Path(direntry.path)
702+
dirpath = path.parent
702703

703704
if dirpath not in seen_dirs:
704705
# Collect packages first.
705706
seen_dirs.add(dirpath)
706-
pkginit = dirpath.join("__init__.py")
707+
pkginit = dirpath / "__init__.py"
707708
if pkginit.exists():
708709
for x in self._collectfile(pkginit):
709710
yield x
@@ -714,23 +715,22 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
714715
continue
715716

716717
for x in self._collectfile(path):
717-
key = (type(x), x.fspath)
718-
if key in node_cache2:
719-
yield node_cache2[key]
718+
key2 = (type(x), Path(x.fspath))
719+
if key2 in node_cache2:
720+
yield node_cache2[key2]
720721
else:
721-
node_cache2[key] = x
722+
node_cache2[key2] = x
722723
yield x
723724
else:
724725
assert argpath.is_file()
725726

726-
argpath_ = py.path.local(argpath)
727-
if argpath_ in node_cache1:
728-
col = node_cache1[argpath_]
727+
if argpath in node_cache1:
728+
col = node_cache1[argpath]
729729
else:
730-
collect_root = pkg_roots.get(argpath_.dirname, self)
731-
col = collect_root._collectfile(argpath_, handle_dupes=False)
730+
collect_root = pkg_roots.get(str(argpath.parent), self)
731+
col = collect_root._collectfile(argpath, handle_dupes=False)
732732
if col:
733-
node_cache1[argpath_] = col
733+
node_cache1[argpath] = col
734734

735735
matching = []
736736
work: List[
@@ -846,7 +846,7 @@ def resolve_collection_argument(
846846
847847
This function ensures the path exists, and returns a tuple:
848848
849-
(py.path.path("/full/path/to/pkg/tests/test_foo.py"), ["TestClass", "test_foo"])
849+
(Path("/full/path/to/pkg/tests/test_foo.py"), ["TestClass", "test_foo"])
850850
851851
When as_pypath is True, expects that the command-line argument actually contains
852852
module paths instead of file-system paths:

src/_pytest/python.py

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@
6666
from _pytest.mark.structures import normalize_mark_list
6767
from _pytest.outcomes import fail
6868
from _pytest.outcomes import skip
69+
from _pytest.pathlib import bestrelpath
70+
from _pytest.pathlib import fnmatch_ex
6971
from _pytest.pathlib import import_path
7072
from _pytest.pathlib import ImportPathMismatchError
7173
from _pytest.pathlib import parts
@@ -190,11 +192,10 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]:
190192
def pytest_collect_file(
191193
fspath: Path, path: py.path.local, parent: nodes.Collector
192194
) -> Optional["Module"]:
193-
ext = path.ext
194-
if ext == ".py":
195+
if fspath.suffix == ".py":
195196
if not parent.session.isinitpath(fspath):
196197
if not path_matches_patterns(
197-
path, parent.config.getini("python_files") + ["__init__.py"]
198+
fspath, parent.config.getini("python_files") + ["__init__.py"]
198199
):
199200
return None
200201
ihook = parent.session.gethookproxy(fspath)
@@ -205,13 +206,13 @@ def pytest_collect_file(
205206
return None
206207

207208

208-
def path_matches_patterns(path: py.path.local, patterns: Iterable[str]) -> bool:
209+
def path_matches_patterns(path: Path, patterns: Iterable[str]) -> bool:
209210
"""Return whether path matches any of the patterns in the list of globs given."""
210-
return any(path.fnmatch(pattern) for pattern in patterns)
211+
return any(fnmatch_ex(pattern, path) for pattern in patterns)
211212

212213

213-
def pytest_pycollect_makemodule(path: py.path.local, parent) -> "Module":
214-
if path.basename == "__init__.py":
214+
def pytest_pycollect_makemodule(fspath: Path, path: py.path.local, parent) -> "Module":
215+
if fspath.name == "__init__.py":
215216
pkg: Package = Package.from_parent(parent, fspath=path)
216217
return pkg
217218
mod: Module = Module.from_parent(parent, fspath=path)
@@ -677,21 +678,21 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
677678
if ihook.pytest_ignore_collect(fspath=fspath, path=path, config=self.config):
678679
return False
679680
norecursepatterns = self.config.getini("norecursedirs")
680-
if any(path.check(fnmatch=pat) for pat in norecursepatterns):
681+
if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
681682
return False
682683
return True
683684

684685
def _collectfile(
685-
self, path: py.path.local, handle_dupes: bool = True
686+
self, fspath: Path, handle_dupes: bool = True
686687
) -> Sequence[nodes.Collector]:
687-
fspath = Path(path)
688+
path = py.path.local(fspath)
688689
assert (
689-
path.isfile()
690+
fspath.is_file()
690691
), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format(
691-
path, path.isdir(), path.exists(), path.islink()
692+
path, fspath.is_dir(), fspath.exists(), fspath.is_symlink()
692693
)
693-
ihook = self.session.gethookproxy(path)
694-
if not self.session.isinitpath(path):
694+
ihook = self.session.gethookproxy(fspath)
695+
if not self.session.isinitpath(fspath):
695696
if ihook.pytest_ignore_collect(
696697
fspath=fspath, path=path, config=self.config
697698
):
@@ -701,32 +702,32 @@ def _collectfile(
701702
keepduplicates = self.config.getoption("keepduplicates")
702703
if not keepduplicates:
703704
duplicate_paths = self.config.pluginmanager._duplicatepaths
704-
if path in duplicate_paths:
705+
if fspath in duplicate_paths:
705706
return ()
706707
else:
707-
duplicate_paths.add(path)
708+
duplicate_paths.add(fspath)
708709

709710
return ihook.pytest_collect_file(fspath=fspath, path=path, parent=self) # type: ignore[no-any-return]
710711

711712
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
712-
this_path = self.fspath.dirpath()
713-
init_module = this_path.join("__init__.py")
714-
if init_module.check(file=1) and path_matches_patterns(
713+
this_path = Path(self.fspath).parent
714+
init_module = this_path / "__init__.py"
715+
if init_module.is_file() and path_matches_patterns(
715716
init_module, self.config.getini("python_files")
716717
):
717-
yield Module.from_parent(self, fspath=init_module)
718-
pkg_prefixes: Set[py.path.local] = set()
718+
yield Module.from_parent(self, fspath=py.path.local(init_module))
719+
pkg_prefixes: Set[Path] = set()
719720
for direntry in visit(str(this_path), recurse=self._recurse):
720-
path = py.path.local(direntry.path)
721+
path = Path(direntry.path)
721722

722723
# We will visit our own __init__.py file, in which case we skip it.
723724
if direntry.is_file():
724-
if direntry.name == "__init__.py" and path.dirpath() == this_path:
725+
if direntry.name == "__init__.py" and path.parent == this_path:
725726
continue
726727

727728
parts_ = parts(direntry.path)
728729
if any(
729-
str(pkg_prefix) in parts_ and pkg_prefix.join("__init__.py") != path
730+
str(pkg_prefix) in parts_ and pkg_prefix / "__init__.py" != path
730731
for pkg_prefix in pkg_prefixes
731732
):
732733
continue
@@ -736,7 +737,7 @@ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
736737
elif not direntry.is_dir():
737738
# Broken symlink or invalid/missing file.
738739
continue
739-
elif path.join("__init__.py").check(file=1):
740+
elif path.joinpath("__init__.py").is_file():
740741
pkg_prefixes.add(path)
741742

742743

@@ -1416,13 +1417,13 @@ def _show_fixtures_per_test(config: Config, session: Session) -> None:
14161417
import _pytest.config
14171418

14181419
session.perform_collect()
1419-
curdir = py.path.local()
1420+
curdir = Path.cwd()
14201421
tw = _pytest.config.create_terminal_writer(config)
14211422
verbose = config.getvalue("verbose")
14221423

1423-
def get_best_relpath(func):
1424+
def get_best_relpath(func) -> str:
14241425
loc = getlocation(func, str(curdir))
1425-
return curdir.bestrelpath(py.path.local(loc))
1426+
return bestrelpath(curdir, Path(loc))
14261427

14271428
def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None:
14281429
argname = fixture_def.argname
@@ -1472,7 +1473,7 @@ def _showfixtures_main(config: Config, session: Session) -> None:
14721473
import _pytest.config
14731474

14741475
session.perform_collect()
1475-
curdir = py.path.local()
1476+
curdir = Path.cwd()
14761477
tw = _pytest.config.create_terminal_writer(config)
14771478
verbose = config.getvalue("verbose")
14781479

@@ -1494,7 +1495,7 @@ def _showfixtures_main(config: Config, session: Session) -> None:
14941495
(
14951496
len(fixturedef.baseid),
14961497
fixturedef.func.__module__,
1497-
curdir.bestrelpath(py.path.local(loc)),
1498+
bestrelpath(curdir, Path(loc)),
14981499
fixturedef.argname,
14991500
fixturedef,
15001501
)

testing/test_collection.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,12 +212,12 @@ def test__in_venv(self, pytester: Pytester, fname: str) -> None:
212212
bindir = "Scripts" if sys.platform.startswith("win") else "bin"
213213
# no bin/activate, not a virtualenv
214214
base_path = pytester.mkdir("venv")
215-
assert _in_venv(py.path.local(base_path)) is False
215+
assert _in_venv(base_path) is False
216216
# with bin/activate, totally a virtualenv
217217
bin_path = base_path.joinpath(bindir)
218218
bin_path.mkdir()
219219
bin_path.joinpath(fname).touch()
220-
assert _in_venv(py.path.local(base_path)) is True
220+
assert _in_venv(base_path) is True
221221

222222
def test_custom_norecursedirs(self, pytester: Pytester) -> None:
223223
pytester.makeini(

0 commit comments

Comments
 (0)