Convert most of the collection code from py.path to pathlib

This commit is contained in:
Ran Benita 2020-12-19 14:52:10 +02:00
parent 4faed28261
commit ca4effc822
5 changed files with 77 additions and 75 deletions

View File

@ -3,3 +3,4 @@ The following changes have been made to internal pytest types/functions:
- The ``path`` property of ``_pytest.code.Code`` returns ``Path`` instead of ``py.path.local``. - The ``path`` property of ``_pytest.code.Code`` returns ``Path`` instead of ``py.path.local``.
- The ``path`` property of ``_pytest.code.TracebackEntry`` returns ``Path`` instead of ``py.path.local``. - The ``path`` property of ``_pytest.code.TracebackEntry`` returns ``Path`` instead of ``py.path.local``.
- The ``_pytest.code.getfslineno()`` function returns ``Path`` instead of ``py.path.local``. - The ``_pytest.code.getfslineno()`` function returns ``Path`` instead of ``py.path.local``.
- The ``_pytest.python.path_matches_patterns()`` function takes ``Path`` instead of ``py.path.local``.

View File

@ -348,7 +348,7 @@ class PytestPluginManager(PluginManager):
self._conftestpath2mod: Dict[Path, types.ModuleType] = {} self._conftestpath2mod: Dict[Path, types.ModuleType] = {}
self._confcutdir: Optional[Path] = None self._confcutdir: Optional[Path] = None
self._noconftest = False self._noconftest = False
self._duplicatepaths: Set[py.path.local] = set() self._duplicatepaths: Set[Path] = set()
# plugins that were explicitly skipped with pytest.skip # plugins that were explicitly skipped with pytest.skip
# list of (module name, skip reason) # list of (module name, skip reason)

View File

@ -37,6 +37,7 @@ from _pytest.fixtures import FixtureManager
from _pytest.outcomes import exit from _pytest.outcomes import exit
from _pytest.pathlib import absolutepath from _pytest.pathlib import absolutepath
from _pytest.pathlib import bestrelpath from _pytest.pathlib import bestrelpath
from _pytest.pathlib import fnmatch_ex
from _pytest.pathlib import visit from _pytest.pathlib import visit
from _pytest.reports import CollectReport from _pytest.reports import CollectReport
from _pytest.reports import TestReport from _pytest.reports import TestReport
@ -353,11 +354,14 @@ def pytest_runtestloop(session: "Session") -> bool:
return True return True
def _in_venv(path: py.path.local) -> bool: def _in_venv(path: Path) -> bool:
"""Attempt to detect if ``path`` is the root of a Virtual Environment by """Attempt to detect if ``path`` is the root of a Virtual Environment by
checking for the existence of the appropriate activate script.""" checking for the existence of the appropriate activate script."""
bindir = path.join("Scripts" if sys.platform.startswith("win") else "bin") bindir = path.joinpath("Scripts" if sys.platform.startswith("win") else "bin")
if not bindir.isdir(): try:
if not bindir.is_dir():
return False
except OSError:
return False return False
activates = ( activates = (
"activate", "activate",
@ -367,33 +371,32 @@ def _in_venv(path: py.path.local) -> bool:
"Activate.bat", "Activate.bat",
"Activate.ps1", "Activate.ps1",
) )
return any([fname.basename in activates for fname in bindir.listdir()]) return any(fname.name in activates for fname in bindir.iterdir())
def pytest_ignore_collect(path: py.path.local, config: Config) -> Optional[bool]: def pytest_ignore_collect(fspath: Path, config: Config) -> Optional[bool]:
path_ = Path(path) ignore_paths = config._getconftest_pathlist("collect_ignore", path=fspath.parent)
ignore_paths = config._getconftest_pathlist("collect_ignore", path=path_.parent)
ignore_paths = ignore_paths or [] ignore_paths = ignore_paths or []
excludeopt = config.getoption("ignore") excludeopt = config.getoption("ignore")
if excludeopt: if excludeopt:
ignore_paths.extend(absolutepath(x) for x in excludeopt) ignore_paths.extend(absolutepath(x) for x in excludeopt)
if path_ in ignore_paths: if fspath in ignore_paths:
return True return True
ignore_globs = config._getconftest_pathlist( ignore_globs = config._getconftest_pathlist(
"collect_ignore_glob", path=path_.parent "collect_ignore_glob", path=fspath.parent
) )
ignore_globs = ignore_globs or [] ignore_globs = ignore_globs or []
excludeglobopt = config.getoption("ignore_glob") excludeglobopt = config.getoption("ignore_glob")
if excludeglobopt: if excludeglobopt:
ignore_globs.extend(absolutepath(x) for x in excludeglobopt) ignore_globs.extend(absolutepath(x) for x in excludeglobopt)
if any(fnmatch.fnmatch(str(path), str(glob)) for glob in ignore_globs): if any(fnmatch.fnmatch(str(fspath), str(glob)) for glob in ignore_globs):
return True return True
allow_in_venv = config.getoption("collect_in_virtualenv") allow_in_venv = config.getoption("collect_in_virtualenv")
if not allow_in_venv and _in_venv(path): if not allow_in_venv and _in_venv(fspath):
return True return True
return None return None
@ -538,21 +541,21 @@ class Session(nodes.FSCollector):
if ihook.pytest_ignore_collect(fspath=fspath, path=path, config=self.config): if ihook.pytest_ignore_collect(fspath=fspath, path=path, config=self.config):
return False return False
norecursepatterns = self.config.getini("norecursedirs") norecursepatterns = self.config.getini("norecursedirs")
if any(path.check(fnmatch=pat) for pat in norecursepatterns): if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
return False return False
return True return True
def _collectfile( def _collectfile(
self, path: py.path.local, handle_dupes: bool = True self, fspath: Path, handle_dupes: bool = True
) -> Sequence[nodes.Collector]: ) -> Sequence[nodes.Collector]:
fspath = Path(path) path = py.path.local(fspath)
assert ( assert (
path.isfile() fspath.is_file()
), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format( ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format(
path, path.isdir(), path.exists(), path.islink() fspath, fspath.is_dir(), fspath.exists(), fspath.is_symlink()
) )
ihook = self.gethookproxy(path) ihook = self.gethookproxy(fspath)
if not self.isinitpath(path): if not self.isinitpath(fspath):
if ihook.pytest_ignore_collect( if ihook.pytest_ignore_collect(
fspath=fspath, path=path, config=self.config fspath=fspath, path=path, config=self.config
): ):
@ -562,10 +565,10 @@ class Session(nodes.FSCollector):
keepduplicates = self.config.getoption("keepduplicates") keepduplicates = self.config.getoption("keepduplicates")
if not keepduplicates: if not keepduplicates:
duplicate_paths = self.config.pluginmanager._duplicatepaths duplicate_paths = self.config.pluginmanager._duplicatepaths
if path in duplicate_paths: if fspath in duplicate_paths:
return () return ()
else: else:
duplicate_paths.add(path) duplicate_paths.add(fspath)
return ihook.pytest_collect_file(fspath=fspath, path=path, parent=self) # type: ignore[no-any-return] return ihook.pytest_collect_file(fspath=fspath, path=path, parent=self) # type: ignore[no-any-return]
@ -652,10 +655,8 @@ class Session(nodes.FSCollector):
from _pytest.python import Package from _pytest.python import Package
# Keep track of any collected nodes in here, so we don't duplicate fixtures. # Keep track of any collected nodes in here, so we don't duplicate fixtures.
node_cache1: Dict[py.path.local, Sequence[nodes.Collector]] = {} node_cache1: Dict[Path, Sequence[nodes.Collector]] = {}
node_cache2: Dict[ node_cache2: Dict[Tuple[Type[nodes.Collector], Path], nodes.Collector] = ({})
Tuple[Type[nodes.Collector], py.path.local], nodes.Collector
] = ({})
# Keep track of any collected collectors in matchnodes paths, so they # Keep track of any collected collectors in matchnodes paths, so they
# are not collected more than once. # are not collected more than once.
@ -679,31 +680,31 @@ class Session(nodes.FSCollector):
break break
if parent.is_dir(): if parent.is_dir():
pkginit = py.path.local(parent / "__init__.py") pkginit = parent / "__init__.py"
if pkginit.isfile() and pkginit not in node_cache1: if pkginit.is_file() and pkginit not in node_cache1:
col = self._collectfile(pkginit, handle_dupes=False) col = self._collectfile(pkginit, handle_dupes=False)
if col: if col:
if isinstance(col[0], Package): if isinstance(col[0], Package):
pkg_roots[str(parent)] = col[0] pkg_roots[str(parent)] = col[0]
node_cache1[col[0].fspath] = [col[0]] node_cache1[Path(col[0].fspath)] = [col[0]]
# If it's a directory argument, recurse and look for any Subpackages. # If it's a directory argument, recurse and look for any Subpackages.
# Let the Package collector deal with subnodes, don't collect here. # Let the Package collector deal with subnodes, don't collect here.
if argpath.is_dir(): if argpath.is_dir():
assert not names, "invalid arg {!r}".format((argpath, names)) assert not names, "invalid arg {!r}".format((argpath, names))
seen_dirs: Set[py.path.local] = set() seen_dirs: Set[Path] = set()
for direntry in visit(str(argpath), self._recurse): for direntry in visit(str(argpath), self._recurse):
if not direntry.is_file(): if not direntry.is_file():
continue continue
path = py.path.local(direntry.path) path = Path(direntry.path)
dirpath = path.dirpath() dirpath = path.parent
if dirpath not in seen_dirs: if dirpath not in seen_dirs:
# Collect packages first. # Collect packages first.
seen_dirs.add(dirpath) seen_dirs.add(dirpath)
pkginit = dirpath.join("__init__.py") pkginit = dirpath / "__init__.py"
if pkginit.exists(): if pkginit.exists():
for x in self._collectfile(pkginit): for x in self._collectfile(pkginit):
yield x yield x
@ -714,23 +715,22 @@ class Session(nodes.FSCollector):
continue continue
for x in self._collectfile(path): for x in self._collectfile(path):
key = (type(x), x.fspath) key2 = (type(x), Path(x.fspath))
if key in node_cache2: if key2 in node_cache2:
yield node_cache2[key] yield node_cache2[key2]
else: else:
node_cache2[key] = x node_cache2[key2] = x
yield x yield x
else: else:
assert argpath.is_file() assert argpath.is_file()
argpath_ = py.path.local(argpath) if argpath in node_cache1:
if argpath_ in node_cache1: col = node_cache1[argpath]
col = node_cache1[argpath_]
else: else:
collect_root = pkg_roots.get(argpath_.dirname, self) collect_root = pkg_roots.get(str(argpath.parent), self)
col = collect_root._collectfile(argpath_, handle_dupes=False) col = collect_root._collectfile(argpath, handle_dupes=False)
if col: if col:
node_cache1[argpath_] = col node_cache1[argpath] = col
matching = [] matching = []
work: List[ work: List[
@ -846,7 +846,7 @@ def resolve_collection_argument(
This function ensures the path exists, and returns a tuple: This function ensures the path exists, and returns a tuple:
(py.path.path("/full/path/to/pkg/tests/test_foo.py"), ["TestClass", "test_foo"]) (Path("/full/path/to/pkg/tests/test_foo.py"), ["TestClass", "test_foo"])
When as_pypath is True, expects that the command-line argument actually contains When as_pypath is True, expects that the command-line argument actually contains
module paths instead of file-system paths: module paths instead of file-system paths:

View File

@ -66,6 +66,8 @@ from _pytest.mark.structures import MarkDecorator
from _pytest.mark.structures import normalize_mark_list from _pytest.mark.structures import normalize_mark_list
from _pytest.outcomes import fail from _pytest.outcomes import fail
from _pytest.outcomes import skip from _pytest.outcomes import skip
from _pytest.pathlib import bestrelpath
from _pytest.pathlib import fnmatch_ex
from _pytest.pathlib import import_path from _pytest.pathlib import import_path
from _pytest.pathlib import ImportPathMismatchError from _pytest.pathlib import ImportPathMismatchError
from _pytest.pathlib import parts from _pytest.pathlib import parts
@ -190,11 +192,10 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]:
def pytest_collect_file( def pytest_collect_file(
fspath: Path, path: py.path.local, parent: nodes.Collector fspath: Path, path: py.path.local, parent: nodes.Collector
) -> Optional["Module"]: ) -> Optional["Module"]:
ext = path.ext if fspath.suffix == ".py":
if ext == ".py":
if not parent.session.isinitpath(fspath): if not parent.session.isinitpath(fspath):
if not path_matches_patterns( if not path_matches_patterns(
path, parent.config.getini("python_files") + ["__init__.py"] fspath, parent.config.getini("python_files") + ["__init__.py"]
): ):
return None return None
ihook = parent.session.gethookproxy(fspath) ihook = parent.session.gethookproxy(fspath)
@ -205,13 +206,13 @@ def pytest_collect_file(
return None return None
def path_matches_patterns(path: py.path.local, patterns: Iterable[str]) -> bool: def path_matches_patterns(path: Path, patterns: Iterable[str]) -> bool:
"""Return whether path matches any of the patterns in the list of globs given.""" """Return whether path matches any of the patterns in the list of globs given."""
return any(path.fnmatch(pattern) for pattern in patterns) return any(fnmatch_ex(pattern, path) for pattern in patterns)
def pytest_pycollect_makemodule(path: py.path.local, parent) -> "Module": def pytest_pycollect_makemodule(fspath: Path, path: py.path.local, parent) -> "Module":
if path.basename == "__init__.py": if fspath.name == "__init__.py":
pkg: Package = Package.from_parent(parent, fspath=path) pkg: Package = Package.from_parent(parent, fspath=path)
return pkg return pkg
mod: Module = Module.from_parent(parent, fspath=path) mod: Module = Module.from_parent(parent, fspath=path)
@ -677,21 +678,21 @@ class Package(Module):
if ihook.pytest_ignore_collect(fspath=fspath, path=path, config=self.config): if ihook.pytest_ignore_collect(fspath=fspath, path=path, config=self.config):
return False return False
norecursepatterns = self.config.getini("norecursedirs") norecursepatterns = self.config.getini("norecursedirs")
if any(path.check(fnmatch=pat) for pat in norecursepatterns): if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
return False return False
return True return True
def _collectfile( def _collectfile(
self, path: py.path.local, handle_dupes: bool = True self, fspath: Path, handle_dupes: bool = True
) -> Sequence[nodes.Collector]: ) -> Sequence[nodes.Collector]:
fspath = Path(path) path = py.path.local(fspath)
assert ( assert (
path.isfile() fspath.is_file()
), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format( ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format(
path, path.isdir(), path.exists(), path.islink() path, fspath.is_dir(), fspath.exists(), fspath.is_symlink()
) )
ihook = self.session.gethookproxy(path) ihook = self.session.gethookproxy(fspath)
if not self.session.isinitpath(path): if not self.session.isinitpath(fspath):
if ihook.pytest_ignore_collect( if ihook.pytest_ignore_collect(
fspath=fspath, path=path, config=self.config fspath=fspath, path=path, config=self.config
): ):
@ -701,32 +702,32 @@ class Package(Module):
keepduplicates = self.config.getoption("keepduplicates") keepduplicates = self.config.getoption("keepduplicates")
if not keepduplicates: if not keepduplicates:
duplicate_paths = self.config.pluginmanager._duplicatepaths duplicate_paths = self.config.pluginmanager._duplicatepaths
if path in duplicate_paths: if fspath in duplicate_paths:
return () return ()
else: else:
duplicate_paths.add(path) duplicate_paths.add(fspath)
return ihook.pytest_collect_file(fspath=fspath, path=path, parent=self) # type: ignore[no-any-return] return ihook.pytest_collect_file(fspath=fspath, path=path, parent=self) # type: ignore[no-any-return]
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
this_path = self.fspath.dirpath() this_path = Path(self.fspath).parent
init_module = this_path.join("__init__.py") init_module = this_path / "__init__.py"
if init_module.check(file=1) and path_matches_patterns( if init_module.is_file() and path_matches_patterns(
init_module, self.config.getini("python_files") init_module, self.config.getini("python_files")
): ):
yield Module.from_parent(self, fspath=init_module) yield Module.from_parent(self, fspath=py.path.local(init_module))
pkg_prefixes: Set[py.path.local] = 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 = py.path.local(direntry.path) path = Path(direntry.path)
# We will visit our own __init__.py file, in which case we skip it. # We will visit our own __init__.py file, in which case we skip it.
if direntry.is_file(): if direntry.is_file():
if direntry.name == "__init__.py" and path.dirpath() == this_path: if direntry.name == "__init__.py" and path.parent == this_path:
continue continue
parts_ = parts(direntry.path) parts_ = parts(direntry.path)
if any( if any(
str(pkg_prefix) in parts_ and pkg_prefix.join("__init__.py") != path str(pkg_prefix) in parts_ and pkg_prefix / "__init__.py" != path
for pkg_prefix in pkg_prefixes for pkg_prefix in pkg_prefixes
): ):
continue continue
@ -736,7 +737,7 @@ class Package(Module):
elif not direntry.is_dir(): elif not direntry.is_dir():
# Broken symlink or invalid/missing file. # Broken symlink or invalid/missing file.
continue continue
elif path.join("__init__.py").check(file=1): elif path.joinpath("__init__.py").is_file():
pkg_prefixes.add(path) pkg_prefixes.add(path)
@ -1416,13 +1417,13 @@ def _show_fixtures_per_test(config: Config, session: Session) -> None:
import _pytest.config import _pytest.config
session.perform_collect() session.perform_collect()
curdir = py.path.local() curdir = Path.cwd()
tw = _pytest.config.create_terminal_writer(config) tw = _pytest.config.create_terminal_writer(config)
verbose = config.getvalue("verbose") verbose = config.getvalue("verbose")
def get_best_relpath(func): def get_best_relpath(func) -> str:
loc = getlocation(func, str(curdir)) loc = getlocation(func, str(curdir))
return curdir.bestrelpath(py.path.local(loc)) return bestrelpath(curdir, Path(loc))
def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None: def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None:
argname = fixture_def.argname argname = fixture_def.argname
@ -1472,7 +1473,7 @@ def _showfixtures_main(config: Config, session: Session) -> None:
import _pytest.config import _pytest.config
session.perform_collect() session.perform_collect()
curdir = py.path.local() curdir = Path.cwd()
tw = _pytest.config.create_terminal_writer(config) tw = _pytest.config.create_terminal_writer(config)
verbose = config.getvalue("verbose") verbose = config.getvalue("verbose")
@ -1494,7 +1495,7 @@ def _showfixtures_main(config: Config, session: Session) -> None:
( (
len(fixturedef.baseid), len(fixturedef.baseid),
fixturedef.func.__module__, fixturedef.func.__module__,
curdir.bestrelpath(py.path.local(loc)), bestrelpath(curdir, Path(loc)),
fixturedef.argname, fixturedef.argname,
fixturedef, fixturedef,
) )

View File

@ -212,12 +212,12 @@ class TestCollectFS:
bindir = "Scripts" if sys.platform.startswith("win") else "bin" bindir = "Scripts" if sys.platform.startswith("win") else "bin"
# no bin/activate, not a virtualenv # no bin/activate, not a virtualenv
base_path = pytester.mkdir("venv") base_path = pytester.mkdir("venv")
assert _in_venv(py.path.local(base_path)) is False assert _in_venv(base_path) is False
# with bin/activate, totally a virtualenv # with bin/activate, totally a virtualenv
bin_path = base_path.joinpath(bindir) bin_path = base_path.joinpath(bindir)
bin_path.mkdir() bin_path.mkdir()
bin_path.joinpath(fname).touch() bin_path.joinpath(fname).touch()
assert _in_venv(py.path.local(base_path)) is True assert _in_venv(base_path) is True
def test_custom_norecursedirs(self, pytester: Pytester) -> None: def test_custom_norecursedirs(self, pytester: Pytester) -> None:
pytester.makeini( pytester.makeini(