diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 805d4c8b3..a01be76b4 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -27,8 +27,6 @@ from typing import Tuple from typing import TYPE_CHECKING from typing import Union -import py - from _pytest._io.saferepr import saferepr from _pytest._version import version from _pytest.assertion import util @@ -37,6 +35,7 @@ from _pytest.assertion.util import ( # noqa: F401 ) from _pytest.config import Config from _pytest.main import Session +from _pytest.pathlib import absolutepath from _pytest.pathlib import fnmatch_ex from _pytest.store import StoreKey @@ -215,7 +214,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader) return True if self.session is not None: - if self.session.isinitpath(py.path.local(fn)): + if self.session.isinitpath(absolutepath(fn)): state.trace(f"matched test file (was specified on cmdline): {fn!r}") return True diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 9a4819655..5a09ea781 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -1,4 +1,5 @@ import argparse +import os import sys import warnings from gettext import gettext @@ -14,8 +15,6 @@ from typing import Tuple from typing import TYPE_CHECKING from typing import Union -import py - import _pytest._io from _pytest.compat import final from _pytest.config.exceptions import UsageError @@ -97,14 +96,14 @@ class Parser: def parse( self, - args: Sequence[Union[str, py.path.local]], + args: Sequence[Union[str, "os.PathLike[str]"]], namespace: Optional[argparse.Namespace] = None, ) -> argparse.Namespace: from _pytest._argcomplete import try_argcomplete self.optparser = self._getparser() try_argcomplete(self.optparser) - strargs = [str(x) if isinstance(x, py.path.local) else x for x in args] + strargs = [os.fspath(x) for x in args] return self.optparser.parse_args(strargs, namespace=namespace) def _getparser(self) -> "MyOptionParser": @@ -128,7 +127,7 @@ class Parser: def parse_setoption( self, - args: Sequence[Union[str, py.path.local]], + args: Sequence[Union[str, "os.PathLike[str]"]], option: argparse.Namespace, namespace: Optional[argparse.Namespace] = None, ) -> List[str]: @@ -139,7 +138,7 @@ class Parser: def parse_known_args( self, - args: Sequence[Union[str, py.path.local]], + args: Sequence[Union[str, "os.PathLike[str]"]], namespace: Optional[argparse.Namespace] = None, ) -> argparse.Namespace: """Parse and return a namespace object with known arguments at this point.""" @@ -147,13 +146,13 @@ class Parser: def parse_known_and_unknown_args( self, - args: Sequence[Union[str, py.path.local]], + args: Sequence[Union[str, "os.PathLike[str]"]], namespace: Optional[argparse.Namespace] = None, ) -> Tuple[argparse.Namespace, List[str]]: """Parse and return a namespace object with known arguments, and the remaining arguments unknown at this point.""" optparser = self._getparser() - strargs = [str(x) if isinstance(x, py.path.local) else x for x in args] + strargs = [os.fspath(x) for x in args] return optparser.parse_known_args(strargs, namespace=namespace) def addini( diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 273bcafd3..c24ab7069 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -648,12 +648,13 @@ class FixtureRequest: if has_params: frame = inspect.stack()[3] frameinfo = inspect.getframeinfo(frame[0]) - source_path = py.path.local(frameinfo.filename) + source_path = absolutepath(frameinfo.filename) source_lineno = frameinfo.lineno - rel_source_path = source_path.relto(funcitem.config.rootdir) - if rel_source_path: - source_path_str = rel_source_path - else: + try: + source_path_str = str( + source_path.relative_to(funcitem.config.rootpath) + ) + except ValueError: source_path_str = str(source_path) msg = ( "The requested fixture has no parameter defined for test:\n" @@ -876,7 +877,7 @@ class FixtureLookupError(LookupError): class FixtureLookupErrorRepr(TerminalRepr): def __init__( self, - filename: Union[str, py.path.local], + filename: Union[str, "os.PathLike[str]"], firstlineno: int, tblines: Sequence[str], errorstring: str, @@ -903,7 +904,7 @@ class FixtureLookupErrorRepr(TerminalRepr): f"{FormattedExcinfo.flow_marker} {line.strip()}", red=True, ) tw.line() - tw.line("%s:%d" % (self.filename, self.firstlineno + 1)) + tw.line("%s:%d" % (os.fspath(self.filename), self.firstlineno + 1)) def fail_fixturefunc(fixturefunc, msg: str) -> "NoReturn": diff --git a/src/_pytest/main.py b/src/_pytest/main.py index eab3c9afd..d536f9d80 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -467,7 +467,7 @@ class Session(nodes.FSCollector): self.shouldfail: Union[bool, str] = False self.trace = config.trace.root.get("collection") self.startdir = config.invocation_dir - self._initialpaths: FrozenSet[py.path.local] = frozenset() + self._initialpaths: FrozenSet[Path] = frozenset() self._bestrelpathcache: Dict[Path, str] = _bestrelpath_cache(config.rootpath) @@ -510,8 +510,8 @@ class Session(nodes.FSCollector): pytest_collectreport = pytest_runtest_logreport - def isinitpath(self, path: py.path.local) -> bool: - return path in self._initialpaths + def isinitpath(self, path: Union[str, "os.PathLike[str]"]) -> bool: + return Path(path) in self._initialpaths def gethookproxy(self, fspath: "os.PathLike[str]"): # Check if we have the common case of running @@ -601,14 +601,14 @@ class Session(nodes.FSCollector): self.trace.root.indent += 1 self._notfound: List[Tuple[str, Sequence[nodes.Collector]]] = [] - self._initial_parts: List[Tuple[py.path.local, List[str]]] = [] + self._initial_parts: List[Tuple[Path, List[str]]] = [] self.items: List[nodes.Item] = [] hook = self.config.hook items: Sequence[Union[nodes.Item, nodes.Collector]] = self.items try: - initialpaths: List[py.path.local] = [] + initialpaths: List[Path] = [] for arg in args: fspath, parts = resolve_collection_argument( self.config.invocation_params.dir, @@ -669,13 +669,13 @@ class Session(nodes.FSCollector): # No point in finding packages when collecting doctests. if not self.config.getoption("doctestmodules", False): pm = self.config.pluginmanager - confcutdir = py.path.local(pm._confcutdir) if pm._confcutdir else None - for parent in reversed(argpath.parts()): - if confcutdir and confcutdir.relto(parent): + confcutdir = pm._confcutdir + for parent in (argpath, *argpath.parents): + if confcutdir and parent in confcutdir.parents: break - if parent.isdir(): - pkginit = parent.join("__init__.py") + if parent.is_dir(): + pkginit = py.path.local(parent / "__init__.py") if pkginit.isfile() and pkginit not in node_cache1: col = self._collectfile(pkginit, handle_dupes=False) if col: @@ -685,7 +685,7 @@ class Session(nodes.FSCollector): # If it's a directory argument, recurse and look for any Subpackages. # Let the Package collector deal with subnodes, don't collect here. - if argpath.check(dir=1): + if argpath.is_dir(): assert not names, "invalid arg {!r}".format((argpath, names)) seen_dirs: Set[py.path.local] = set() @@ -717,15 +717,16 @@ class Session(nodes.FSCollector): node_cache2[key] = x yield x else: - assert argpath.check(file=1) + assert argpath.is_file() - if argpath in node_cache1: - col = node_cache1[argpath] + argpath_ = py.path.local(argpath) + if argpath_ in node_cache1: + col = node_cache1[argpath_] else: - collect_root = pkg_roots.get(argpath.dirname, self) - col = collect_root._collectfile(argpath, handle_dupes=False) + collect_root = pkg_roots.get(argpath_.dirname, self) + col = collect_root._collectfile(argpath_, handle_dupes=False) if col: - node_cache1[argpath] = col + node_cache1[argpath_] = col matching = [] work: List[ @@ -782,9 +783,7 @@ class Session(nodes.FSCollector): # first yielded item will be the __init__ Module itself, so # just use that. If this special case isn't taken, then all the # files in the package will be yielded. - if argpath.basename == "__init__.py" and isinstance( - matching[0], Package - ): + if argpath.name == "__init__.py" and isinstance(matching[0], Package): try: yield next(iter(matching[0].collect())) except StopIteration: @@ -833,7 +832,7 @@ def search_pypath(module_name: str) -> str: def resolve_collection_argument( invocation_path: Path, arg: str, *, as_pypath: bool = False -) -> Tuple[py.path.local, List[str]]: +) -> Tuple[Path, List[str]]: """Parse path arguments optionally containing selection parts and return (fspath, names). Command-line arguments can point to files and/or directories, and optionally contain @@ -875,4 +874,4 @@ def resolve_collection_argument( else "directory argument cannot contain :: selection parts: {arg}" ) raise UsageError(msg.format(arg=arg)) - return py.path.local(str(fspath)), parts + return fspath, parts diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index a052f693a..d012b8a53 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -4,7 +4,6 @@ import re import sys import warnings from contextlib import contextmanager -from pathlib import Path from typing import Any from typing import Generator from typing import List @@ -325,20 +324,14 @@ class MonkeyPatch: invalidate_caches() - def chdir(self, path) -> None: + def chdir(self, path: Union[str, "os.PathLike[str]"]) -> None: """Change the current working directory to the specified path. - Path can be a string or a py.path.local object. + Path can be a string or a path object. """ if self._cwd is None: self._cwd = os.getcwd() - if hasattr(path, "chdir"): - path.chdir() - elif isinstance(path, Path): - # Modern python uses the fspath protocol here LEGACY - os.chdir(str(path)) - else: - os.chdir(path) + os.chdir(path) def undo(self) -> None: """Undo previous changes. diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 98bd581b9..fee0770eb 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -480,10 +480,14 @@ class Collector(Node): excinfo.traceback = ntraceback.filter() -def _check_initialpaths_for_relpath(session, fspath): +def _check_initialpaths_for_relpath( + session: "Session", fspath: py.path.local +) -> Optional[str]: for initial_path in session._initialpaths: - if fspath.common(initial_path) == initial_path: - return fspath.relto(initial_path) + initial_path_ = py.path.local(initial_path) + if fspath.common(initial_path_) == initial_path_: + return fspath.relto(initial_path_) + return None class FSCollector(Collector): diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 8875a28f8..2e452eb1c 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -30,8 +30,6 @@ from typing import Set from typing import TypeVar from typing import Union -import py - from _pytest.compat import assert_never from _pytest.outcomes import skip from _pytest.warning_types import PytestWarning @@ -456,7 +454,7 @@ class ImportPathMismatchError(ImportError): def import_path( - p: Union[str, py.path.local, Path], + p: Union[str, "os.PathLike[str]"], *, mode: Union[str, ImportMode] = ImportMode.prepend, ) -> ModuleType: @@ -482,7 +480,7 @@ def import_path( """ mode = ImportMode(mode) - path = Path(str(p)) + path = Path(p) if not path.exists(): raise ImportError(path) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 84d5276e7..ffe18260f 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -17,8 +17,6 @@ from typing import Mapping from typing import Optional from typing import Set -import py - import _pytest._code import pytest from _pytest.assertion import util @@ -1311,7 +1309,7 @@ class TestEarlyRewriteBailout: import importlib.machinery self.find_spec_calls: List[str] = [] - self.initial_paths: Set[py.path.local] = set() + self.initial_paths: Set[Path] = set() class StubSession: _initialpaths = self.initial_paths @@ -1346,7 +1344,7 @@ class TestEarlyRewriteBailout: pytester.makepyfile(test_foo="def test_foo(): pass") pytester.makepyfile(bar="def bar(): pass") foobar_path = pytester.makepyfile(foobar="def foobar(): pass") - self.initial_paths.add(py.path.local(foobar_path)) + self.initial_paths.add(foobar_path) # conftest files should always be rewritten assert hook.find_spec("conftest") is not None diff --git a/testing/test_main.py b/testing/test_main.py index 3e94668e8..f45607abc 100644 --- a/testing/test_main.py +++ b/testing/test_main.py @@ -4,13 +4,12 @@ import re from pathlib import Path from typing import Optional -import py.path - import pytest from _pytest.config import ExitCode from _pytest.config import UsageError from _pytest.main import resolve_collection_argument from _pytest.main import validate_basetemp +from _pytest.pytester import Pytester from _pytest.pytester import Testdir @@ -109,40 +108,37 @@ def test_validate_basetemp_integration(testdir): class TestResolveCollectionArgument: @pytest.fixture - def invocation_dir(self, testdir: Testdir) -> py.path.local: - testdir.syspathinsert(str(testdir.tmpdir / "src")) - testdir.chdir() + def invocation_path(self, pytester: Pytester) -> Path: + pytester.syspathinsert(pytester.path / "src") + pytester.chdir() - pkg = testdir.tmpdir.join("src/pkg").ensure_dir() - pkg.join("__init__.py").ensure() - pkg.join("test.py").ensure() - return testdir.tmpdir + pkg = pytester.path.joinpath("src/pkg") + pkg.mkdir(parents=True) + pkg.joinpath("__init__.py").touch() + pkg.joinpath("test.py").touch() + return pytester.path - @pytest.fixture - def invocation_path(self, invocation_dir: py.path.local) -> Path: - return Path(str(invocation_dir)) - - def test_file(self, invocation_dir: py.path.local, invocation_path: Path) -> None: + def test_file(self, invocation_path: Path) -> None: """File and parts.""" assert resolve_collection_argument(invocation_path, "src/pkg/test.py") == ( - invocation_dir / "src/pkg/test.py", + invocation_path / "src/pkg/test.py", [], ) assert resolve_collection_argument(invocation_path, "src/pkg/test.py::") == ( - invocation_dir / "src/pkg/test.py", + invocation_path / "src/pkg/test.py", [""], ) assert resolve_collection_argument( invocation_path, "src/pkg/test.py::foo::bar" - ) == (invocation_dir / "src/pkg/test.py", ["foo", "bar"]) + ) == (invocation_path / "src/pkg/test.py", ["foo", "bar"]) assert resolve_collection_argument( invocation_path, "src/pkg/test.py::foo::bar::" - ) == (invocation_dir / "src/pkg/test.py", ["foo", "bar", ""]) + ) == (invocation_path / "src/pkg/test.py", ["foo", "bar", ""]) - def test_dir(self, invocation_dir: py.path.local, invocation_path: Path) -> None: + def test_dir(self, invocation_path: Path) -> None: """Directory and parts.""" assert resolve_collection_argument(invocation_path, "src/pkg") == ( - invocation_dir / "src/pkg", + invocation_path / "src/pkg", [], ) @@ -156,16 +152,16 @@ class TestResolveCollectionArgument: ): resolve_collection_argument(invocation_path, "src/pkg::foo::bar") - def test_pypath(self, invocation_dir: py.path.local, invocation_path: Path) -> None: + def test_pypath(self, invocation_path: Path) -> None: """Dotted name and parts.""" assert resolve_collection_argument( invocation_path, "pkg.test", as_pypath=True - ) == (invocation_dir / "src/pkg/test.py", []) + ) == (invocation_path / "src/pkg/test.py", []) assert resolve_collection_argument( invocation_path, "pkg.test::foo::bar", as_pypath=True - ) == (invocation_dir / "src/pkg/test.py", ["foo", "bar"]) + ) == (invocation_path / "src/pkg/test.py", ["foo", "bar"]) assert resolve_collection_argument(invocation_path, "pkg", as_pypath=True) == ( - invocation_dir / "src/pkg", + invocation_path / "src/pkg", [], ) @@ -191,13 +187,11 @@ class TestResolveCollectionArgument: ): resolve_collection_argument(invocation_path, "foobar", as_pypath=True) - def test_absolute_paths_are_resolved_correctly( - self, invocation_dir: py.path.local, invocation_path: Path - ) -> None: + def test_absolute_paths_are_resolved_correctly(self, invocation_path: Path) -> None: """Absolute paths resolve back to absolute paths.""" - full_path = str(invocation_dir / "src") + full_path = str(invocation_path / "src") assert resolve_collection_argument(invocation_path, full_path) == ( - py.path.local(os.path.abspath("src")), + Path(os.path.abspath("src")), [], ) @@ -206,7 +200,7 @@ class TestResolveCollectionArgument: drive, full_path_without_drive = os.path.splitdrive(full_path) assert resolve_collection_argument( invocation_path, full_path_without_drive - ) == (py.path.local(os.path.abspath("src")), []) + ) == (Path(os.path.abspath("src")), []) def test_module_full_path_without_drive(testdir): diff --git a/testing/test_nodes.py b/testing/test_nodes.py index f3824c570..bae31f0a3 100644 --- a/testing/test_nodes.py +++ b/testing/test_nodes.py @@ -1,3 +1,4 @@ +from typing import cast from typing import List from typing import Type @@ -73,17 +74,21 @@ def test__check_initialpaths_for_relpath() -> None: class FakeSession1: _initialpaths = [cwd] - assert nodes._check_initialpaths_for_relpath(FakeSession1, cwd) == "" + session = cast(pytest.Session, FakeSession1) + + assert nodes._check_initialpaths_for_relpath(session, cwd) == "" sub = cwd.join("file") class FakeSession2: _initialpaths = [cwd] - assert nodes._check_initialpaths_for_relpath(FakeSession2, sub) == "file" + session = cast(pytest.Session, FakeSession2) + + assert nodes._check_initialpaths_for_relpath(session, sub) == "file" outside = py.path.local("/outside") - assert nodes._check_initialpaths_for_relpath(FakeSession2, outside) is None + assert nodes._check_initialpaths_for_relpath(session, outside) is None def test_failure_with_changed_cwd(pytester: Pytester) -> None: