From 73586be08fe33c483ae3c3509a05459969ba2ab9 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 18 Dec 2020 20:33:39 +0200 Subject: [PATCH 01/10] terminal: remove unused union arm in WarningReport.fslocation --- src/_pytest/terminal.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index f5d4e1f8ddc..d3d1a4b666e 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -285,15 +285,13 @@ class WarningReport: User friendly message about the warning. :ivar str|None nodeid: nodeid that generated the warning (see ``get_location``). - :ivar tuple|py.path.local fslocation: + :ivar tuple fslocation: File system location of the source of the warning (see ``get_location``). """ message = attr.ib(type=str) nodeid = attr.ib(type=Optional[str], default=None) - fslocation = attr.ib( - type=Optional[Union[Tuple[str, int], py.path.local]], default=None - ) + fslocation = attr.ib(type=Optional[Tuple[str, int]], default=None) count_towards_summary = True def get_location(self, config: Config) -> Optional[str]: @@ -301,14 +299,9 @@ def get_location(self, config: Config) -> Optional[str]: if self.nodeid: return self.nodeid if self.fslocation: - if isinstance(self.fslocation, tuple) and len(self.fslocation) >= 2: - filename, linenum = self.fslocation[:2] - relpath = bestrelpath( - config.invocation_params.dir, absolutepath(filename) - ) - return f"{relpath}:{linenum}" - else: - return str(self.fslocation) + filename, linenum = self.fslocation + relpath = bestrelpath(config.invocation_params.dir, absolutepath(filename)) + return f"{relpath}:{linenum}" return None From 2c05a7babb4fa31775730e596a908f7c1f861765 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 18 Dec 2020 20:39:33 +0200 Subject: [PATCH 02/10] config: let main() accept any os.PathLike instead of just py.path.local Really it ought to only take the List[str], but for backward compatibility, at least get rid of the explicit py.path.local check. --- src/_pytest/config/__init__.py | 10 ++++++---- testing/test_collection.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 0df4ffa01c1..c9a0e78bfcf 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -128,7 +128,7 @@ def filter_traceback_for_conftest_import_failure( def main( - args: Optional[Union[List[str], py.path.local]] = None, + args: Optional[Union[List[str], "os.PathLike[str]"]] = None, plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None, ) -> Union[int, ExitCode]: """Perform an in-process test run. @@ -295,13 +295,15 @@ def get_plugin_manager() -> "PytestPluginManager": def _prepareconfig( - args: Optional[Union[py.path.local, List[str]]] = None, + args: Optional[Union[List[str], "os.PathLike[str]"]] = None, plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None, ) -> "Config": if args is None: args = sys.argv[1:] - elif isinstance(args, py.path.local): - args = [str(args)] + # TODO: Remove type-ignore after next mypy release. + # https://github.com/python/typeshed/commit/076983eec45e739c68551cb6119fd7d85fd4afa9 + elif isinstance(args, os.PathLike): # type: ignore[misc] + args = [os.fspath(args)] elif not isinstance(args, list): msg = "`args` parameter expected to be a list of strings, got: {!r} (type: {})" raise TypeError(msg.format(args, type(args))) diff --git a/testing/test_collection.py b/testing/test_collection.py index 2d03fda39de..9733b4fbd47 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -277,7 +277,7 @@ def pytest_collect_file(self, path): wascalled.append(path) pytester.makefile(".abc", "xyz") - pytest.main(py.path.local(pytester.path), plugins=[Plugin()]) + pytest.main(pytester.path, plugins=[Plugin()]) assert len(wascalled) == 1 assert wascalled[0].ext == ".abc" From 042d12fae6e03f97ac25311504f6697154eff08e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 19 Dec 2020 14:02:24 +0200 Subject: [PATCH 03/10] doctest: use Path instead of py.path where possible --- src/_pytest/doctest.py | 21 +++++++++++---------- testing/test_doctest.py | 17 ++++++++--------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index d0b6b4c4185..24f8882579b 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -36,6 +36,7 @@ from _pytest.fixtures import FixtureRequest from _pytest.nodes import Collector from _pytest.outcomes import OutcomeException +from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import import_path from _pytest.python_api import approx from _pytest.warning_types import PytestWarning @@ -120,32 +121,32 @@ def pytest_unconfigure() -> None: def pytest_collect_file( - path: py.path.local, parent: Collector, + fspath: Path, path: py.path.local, parent: Collector, ) -> Optional[Union["DoctestModule", "DoctestTextfile"]]: config = parent.config - if path.ext == ".py": - if config.option.doctestmodules and not _is_setup_py(path): + if fspath.suffix == ".py": + if config.option.doctestmodules and not _is_setup_py(fspath): mod: DoctestModule = DoctestModule.from_parent(parent, fspath=path) return mod - elif _is_doctest(config, path, parent): + elif _is_doctest(config, fspath, parent): txt: DoctestTextfile = DoctestTextfile.from_parent(parent, fspath=path) return txt return None -def _is_setup_py(path: py.path.local) -> bool: - if path.basename != "setup.py": +def _is_setup_py(path: Path) -> bool: + if path.name != "setup.py": return False - contents = path.read_binary() + contents = path.read_bytes() return b"setuptools" in contents or b"distutils" in contents -def _is_doctest(config: Config, path: py.path.local, parent) -> bool: - if path.ext in (".txt", ".rst") and parent.session.isinitpath(path): +def _is_doctest(config: Config, path: Path, parent: Collector) -> bool: + if path.suffix in (".txt", ".rst") and parent.session.isinitpath(path): return True globs = config.getoption("doctestglob") or ["test*.txt"] for glob in globs: - if path.check(fnmatch=glob): + if fnmatch_ex(glob, path): return True return False diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 6e3880330a9..08d0aacf68c 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -1,10 +1,9 @@ import inspect import textwrap +from pathlib import Path from typing import Callable from typing import Optional -import py - import pytest from _pytest.doctest import _get_checker from _pytest.doctest import _is_mocked @@ -1496,25 +1495,25 @@ def test_warning_on_unwrap_of_broken_object( assert inspect.unwrap.__module__ == "inspect" -def test_is_setup_py_not_named_setup_py(tmp_path): +def test_is_setup_py_not_named_setup_py(tmp_path: Path) -> None: not_setup_py = tmp_path.joinpath("not_setup.py") not_setup_py.write_text('from setuptools import setup; setup(name="foo")') - assert not _is_setup_py(py.path.local(str(not_setup_py))) + assert not _is_setup_py(not_setup_py) @pytest.mark.parametrize("mod", ("setuptools", "distutils.core")) -def test_is_setup_py_is_a_setup_py(tmpdir, mod): - setup_py = tmpdir.join("setup.py") - setup_py.write(f'from {mod} import setup; setup(name="foo")') +def test_is_setup_py_is_a_setup_py(tmp_path: Path, mod: str) -> None: + setup_py = tmp_path.joinpath("setup.py") + setup_py.write_text(f'from {mod} import setup; setup(name="foo")', "utf-8") assert _is_setup_py(setup_py) @pytest.mark.parametrize("mod", ("setuptools", "distutils.core")) -def test_is_setup_py_different_encoding(tmp_path, mod): +def test_is_setup_py_different_encoding(tmp_path: Path, mod: str) -> None: setup_py = tmp_path.joinpath("setup.py") contents = ( "# -*- coding: cp1252 -*-\n" 'from {} import setup; setup(name="foo", description="€")\n'.format(mod) ) setup_py.write_bytes(contents.encode("cp1252")) - assert _is_setup_py(py.path.local(str(setup_py))) + assert _is_setup_py(setup_py) From 7aa224083205adb650a7b1132e6b9e861361426e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 19 Dec 2020 14:06:17 +0200 Subject: [PATCH 04/10] testing/test_nodes: fix fake session to be more accurate The type of _initialpaths is `FrozenSet[Path]`. --- testing/test_nodes.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/testing/test_nodes.py b/testing/test_nodes.py index bae31f0a39c..59d9f409eac 100644 --- a/testing/test_nodes.py +++ b/testing/test_nodes.py @@ -1,3 +1,4 @@ +from pathlib import Path from typing import cast from typing import List from typing import Type @@ -69,23 +70,23 @@ def test(): def test__check_initialpaths_for_relpath() -> None: """Ensure that it handles dirs, and does not always use dirname.""" - cwd = py.path.local() + cwd = Path.cwd() class FakeSession1: - _initialpaths = [cwd] + _initialpaths = frozenset({cwd}) session = cast(pytest.Session, FakeSession1) - assert nodes._check_initialpaths_for_relpath(session, cwd) == "" + assert nodes._check_initialpaths_for_relpath(session, py.path.local(cwd)) == "" - sub = cwd.join("file") + sub = cwd / "file" class FakeSession2: - _initialpaths = [cwd] + _initialpaths = frozenset({cwd}) session = cast(pytest.Session, FakeSession2) - assert nodes._check_initialpaths_for_relpath(session, sub) == "file" + assert nodes._check_initialpaths_for_relpath(session, py.path.local(sub)) == "file" outside = py.path.local("/outside") assert nodes._check_initialpaths_for_relpath(session, outside) is None From 92ba96b0612e6b06bb8f4ab05bd75481d2504806 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 19 Dec 2020 14:11:00 +0200 Subject: [PATCH 05/10] code: convert from py.path to pathlib --- changelog/8174.trivial.rst | 5 +++ src/_pytest/_code/code.py | 55 +++++++++++++++------------- src/_pytest/fixtures.py | 8 +++- src/_pytest/nodes.py | 8 ++-- src/_pytest/python.py | 6 ++- testing/code/test_excinfo.py | 71 ++++++++++++++++++------------------ testing/code/test_source.py | 11 +++--- 7 files changed, 90 insertions(+), 74 deletions(-) create mode 100644 changelog/8174.trivial.rst diff --git a/changelog/8174.trivial.rst b/changelog/8174.trivial.rst new file mode 100644 index 00000000000..001ae4cb193 --- /dev/null +++ b/changelog/8174.trivial.rst @@ -0,0 +1,5 @@ +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.TracebackEntry`` returns ``Path`` instead of ``py.path.local``. +- The ``_pytest.code.getfslineno()`` function returns ``Path`` instead of ``py.path.local``. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 423069330a5..043a23a79af 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -43,6 +43,8 @@ from _pytest._io.saferepr import saferepr from _pytest.compat import final from _pytest.compat import get_real_func +from _pytest.pathlib import absolutepath +from _pytest.pathlib import bestrelpath if TYPE_CHECKING: from typing_extensions import Literal @@ -78,16 +80,16 @@ def name(self) -> str: return self.raw.co_name @property - def path(self) -> Union[py.path.local, str]: + def path(self) -> Union[Path, str]: """Return a path object pointing to source code, or an ``str`` in case of ``OSError`` / non-existing file.""" if not self.raw.co_filename: return "" try: - p = py.path.local(self.raw.co_filename) + p = absolutepath(self.raw.co_filename) # maybe don't try this checking - if not p.check(): - raise OSError("py.path check failed.") + if not p.exists(): + raise OSError("path check failed.") return p except OSError: # XXX maybe try harder like the weird logic @@ -223,7 +225,7 @@ def statement(self) -> "Source": return source.getstatement(self.lineno) @property - def path(self) -> Union[py.path.local, str]: + def path(self) -> Union[Path, str]: """Path to the source code.""" return self.frame.code.path @@ -336,10 +338,10 @@ def f(cur: TracebackType) -> Iterable[TracebackEntry]: def cut( self, - path=None, + path: Optional[Union[Path, str]] = None, lineno: Optional[int] = None, firstlineno: Optional[int] = None, - excludepath: Optional[py.path.local] = None, + excludepath: Optional[Path] = None, ) -> "Traceback": """Return a Traceback instance wrapping part of this Traceback. @@ -353,17 +355,19 @@ def cut( for x in self: code = x.frame.code codepath = code.path + if path is not None and codepath != path: + continue if ( - (path is None or codepath == path) - and ( - excludepath is None - or not isinstance(codepath, py.path.local) - or not codepath.relto(excludepath) - ) - and (lineno is None or x.lineno == lineno) - and (firstlineno is None or x.frame.code.firstlineno == firstlineno) + excludepath is not None + and isinstance(codepath, Path) + and excludepath in codepath.parents ): - return Traceback(x._rawentry, self._excinfo) + continue + if lineno is not None and x.lineno != lineno: + continue + if firstlineno is not None and x.frame.code.firstlineno != firstlineno: + continue + return Traceback(x._rawentry, self._excinfo) return self @overload @@ -801,7 +805,8 @@ def repr_traceback_entry( message = "in %s" % (entry.name) else: message = excinfo and excinfo.typename or "" - path = self._makepath(entry.path) + entry_path = entry.path + path = self._makepath(entry_path) reprfileloc = ReprFileLocation(path, entry.lineno + 1, message) localsrepr = self.repr_locals(entry.locals) return ReprEntry(lines, reprargs, localsrepr, reprfileloc, style) @@ -814,15 +819,15 @@ def repr_traceback_entry( lines.extend(self.get_exconly(excinfo, indent=4)) return ReprEntry(lines, None, None, None, style) - def _makepath(self, path): - if not self.abspath: + def _makepath(self, path: Union[Path, str]) -> str: + if not self.abspath and isinstance(path, Path): try: - np = py.path.local().bestrelpath(path) + np = bestrelpath(Path.cwd(), path) except OSError: - return path + return str(path) if len(np) < len(str(path)): - path = np - return path + return np + return str(path) def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback": traceback = excinfo.traceback @@ -1181,7 +1186,7 @@ def toterminal(self, tw: TerminalWriter) -> None: tw.line("") -def getfslineno(obj: object) -> Tuple[Union[str, py.path.local], int]: +def getfslineno(obj: object) -> Tuple[Union[str, Path], int]: """Return source location (path, lineno) for the given object. If the source cannot be determined return ("", -1). @@ -1203,7 +1208,7 @@ def getfslineno(obj: object) -> Tuple[Union[str, py.path.local], int]: except TypeError: return "", -1 - fspath = fn and py.path.local(fn) or "" + fspath = fn and absolutepath(fn) or "" lineno = -1 if fspath: try: diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index c24ab7069cb..6db1c5906f0 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -5,6 +5,7 @@ import warnings from collections import defaultdict from collections import deque +from pathlib import Path from types import TracebackType from typing import Any from typing import Callable @@ -58,6 +59,7 @@ from _pytest.outcomes import fail from _pytest.outcomes import TEST_OUTCOME from _pytest.pathlib import absolutepath +from _pytest.pathlib import bestrelpath from _pytest.store import StoreKey if TYPE_CHECKING: @@ -718,7 +720,11 @@ def _factorytraceback(self) -> List[str]: for fixturedef in self._get_fixturestack(): factory = fixturedef.func fs, lineno = getfslineno(factory) - p = self._pyfuncitem.session.fspath.bestrelpath(fs) + if isinstance(fs, Path): + session: Session = self._pyfuncitem.session + p = bestrelpath(Path(session.fspath), fs) + else: + p = fs args = _format_args(factory) lines.append("%s:%d: def %s%s" % (p, lineno + 1, factory.__name__, args)) return lines diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 1b3ec5571b1..da2a0a7ea79 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -39,7 +39,7 @@ SEP = "/" -tracebackcutdir = py.path.local(_pytest.__file__).dirpath() +tracebackcutdir = Path(_pytest.__file__).parent def iterparentnodeids(nodeid: str) -> Iterator[str]: @@ -416,9 +416,7 @@ def repr_failure( return self._repr_failure_py(excinfo, style) -def get_fslocation_from_item( - node: "Node", -) -> Tuple[Union[str, py.path.local], Optional[int]]: +def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[int]]: """Try to extract the actual location from a node, depending on available attributes: * "location": a pair (path, lineno) @@ -474,7 +472,7 @@ def repr_failure( # type: ignore[override] def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: if hasattr(self, "fspath"): traceback = excinfo.traceback - ntraceback = traceback.cut(path=self.fspath) + ntraceback = traceback.cut(path=Path(self.fspath)) if ntraceback == traceback: ntraceback = ntraceback.cut(excludepath=tracebackcutdir) excinfo.traceback = ntraceback.filter() diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 3ff04455fbf..27bbb24fe2c 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -340,7 +340,11 @@ def reportinfo(self) -> Tuple[Union[py.path.local, str], int, str]: fspath: Union[py.path.local, str] = file_path lineno = compat_co_firstlineno else: - fspath, lineno = getfslineno(obj) + path, lineno = getfslineno(obj) + if isinstance(path, Path): + fspath = py.path.local(path) + else: + fspath = path modpath = self.getmodpath() assert isinstance(lineno, int) return fspath, lineno, modpath diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 44d7ab549e8..19c888403f2 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -1,7 +1,6 @@ import importlib import io import operator -import os import queue import sys import textwrap @@ -12,14 +11,14 @@ from typing import TYPE_CHECKING from typing import Union -import py - import _pytest import pytest from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo from _pytest._code.code import FormattedExcinfo from _pytest._io import TerminalWriter +from _pytest.monkeypatch import MonkeyPatch +from _pytest.pathlib import bestrelpath from _pytest.pathlib import import_path from _pytest.pytester import LineMatcher from _pytest.pytester import Pytester @@ -150,9 +149,10 @@ def xyz(): " except somenoname: # type: ignore[name-defined] # noqa: F821", ] - def test_traceback_cut(self): + def test_traceback_cut(self) -> None: co = _pytest._code.Code.from_function(f) path, firstlineno = co.path, co.firstlineno + assert isinstance(path, Path) traceback = self.excinfo.traceback newtraceback = traceback.cut(path=path, firstlineno=firstlineno) assert len(newtraceback) == 1 @@ -163,11 +163,11 @@ def test_traceback_cut_excludepath(self, pytester: Pytester) -> None: p = pytester.makepyfile("def f(): raise ValueError") with pytest.raises(ValueError) as excinfo: import_path(p).f() # type: ignore[attr-defined] - basedir = py.path.local(pytest.__file__).dirpath() + basedir = Path(pytest.__file__).parent newtraceback = excinfo.traceback.cut(excludepath=basedir) for x in newtraceback: - if hasattr(x, "path"): - assert not py.path.local(x.path).relto(basedir) + assert isinstance(x.path, Path) + assert basedir not in x.path.parents assert newtraceback[-1].frame.code.path == p def test_traceback_filter(self): @@ -376,7 +376,7 @@ def test_excinfo_no_python_sourcecode(tmpdir): for item in excinfo.traceback: print(item) # XXX: for some reason jinja.Template.render is printed in full item.source # shouldn't fail - if isinstance(item.path, py.path.local) and item.path.basename == "test.txt": + if isinstance(item.path, Path) and item.path.name == "test.txt": assert str(item.source) == "{{ h()}}:" @@ -392,16 +392,16 @@ def test_entrysource_Queue_example(): assert s.startswith("def get") -def test_codepath_Queue_example(): +def test_codepath_Queue_example() -> None: try: queue.Queue().get(timeout=0.001) except queue.Empty: excinfo = _pytest._code.ExceptionInfo.from_current() entry = excinfo.traceback[-1] path = entry.path - assert isinstance(path, py.path.local) - assert path.basename.lower() == "queue.py" - assert path.check() + assert isinstance(path, Path) + assert path.name.lower() == "queue.py" + assert path.exists() def test_match_succeeds(): @@ -805,21 +805,21 @@ def entry(): raised = 0 - orig_getcwd = os.getcwd + orig_path_cwd = Path.cwd def raiseos(): nonlocal raised upframe = sys._getframe().f_back assert upframe is not None - if upframe.f_code.co_name == "checked_call": + if upframe.f_code.co_name == "_makepath": # Only raise with expected calls, but not via e.g. inspect for # py38-windows. raised += 1 raise OSError(2, "custom_oserror") - return orig_getcwd() + return orig_path_cwd() - monkeypatch.setattr(os, "getcwd", raiseos) - assert p._makepath(__file__) == __file__ + monkeypatch.setattr(Path, "cwd", raiseos) + assert p._makepath(Path(__file__)) == __file__ assert raised == 1 repr_tb = p.repr_traceback(excinfo) @@ -1015,7 +1015,9 @@ def f(): assert line.endswith("mod.py") assert tw_mock.lines[10] == ":3: ValueError" - def test_toterminal_long_filenames(self, importasmod, tw_mock): + def test_toterminal_long_filenames( + self, importasmod, tw_mock, monkeypatch: MonkeyPatch + ) -> None: mod = importasmod( """ def f(): @@ -1023,25 +1025,22 @@ def f(): """ ) excinfo = pytest.raises(ValueError, mod.f) - path = py.path.local(mod.__file__) - old = path.dirpath().chdir() - try: - repr = excinfo.getrepr(abspath=False) - repr.toterminal(tw_mock) - x = py.path.local().bestrelpath(path) - if len(x) < len(str(path)): - msg = tw_mock.get_write_msg(-2) - assert msg == "mod.py" - assert tw_mock.lines[-1] == ":3: ValueError" - - repr = excinfo.getrepr(abspath=True) - repr.toterminal(tw_mock) + path = Path(mod.__file__) + monkeypatch.chdir(path.parent) + repr = excinfo.getrepr(abspath=False) + repr.toterminal(tw_mock) + x = bestrelpath(Path.cwd(), path) + if len(x) < len(str(path)): msg = tw_mock.get_write_msg(-2) - assert msg == path - line = tw_mock.lines[-1] - assert line == ":3: ValueError" - finally: - old.chdir() + assert msg == "mod.py" + assert tw_mock.lines[-1] == ":3: ValueError" + + repr = excinfo.getrepr(abspath=True) + repr.toterminal(tw_mock) + msg = tw_mock.get_write_msg(-2) + assert msg == str(path) + line = tw_mock.lines[-1] + assert line == ":3: ValueError" @pytest.mark.parametrize( "reproptions", diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 04d0ea9323d..6b8443fd243 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -6,13 +6,12 @@ import linecache import sys import textwrap +from pathlib import Path from types import CodeType from typing import Any from typing import Dict from typing import Optional -import py.path - import pytest from _pytest._code import Code from _pytest._code import Frame @@ -352,8 +351,8 @@ def f(x) -> None: fspath, lineno = getfslineno(f) - assert isinstance(fspath, py.path.local) - assert fspath.basename == "test_source.py" + assert isinstance(fspath, Path) + assert fspath.name == "test_source.py" assert lineno == f.__code__.co_firstlineno - 1 # see findsource class A: @@ -362,8 +361,8 @@ class A: fspath, lineno = getfslineno(A) _, A_lineno = inspect.findsource(A) - assert isinstance(fspath, py.path.local) - assert fspath.basename == "test_source.py" + assert isinstance(fspath, Path) + assert fspath.name == "test_source.py" assert lineno == A_lineno assert getfslineno(3) == ("", -1) From 8b220fad4de5e36d3b62d57ca0121b4865f7e518 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 19 Dec 2020 14:48:56 +0200 Subject: [PATCH 06/10] testing/test_helpconfig: remove unclear comment --- testing/test_helpconfig.py | 1 - 1 file changed, 1 deletion(-) diff --git a/testing/test_helpconfig.py b/testing/test_helpconfig.py index c2533ef304a..9a433b1b17e 100644 --- a/testing/test_helpconfig.py +++ b/testing/test_helpconfig.py @@ -16,7 +16,6 @@ def test_version_less_verbose(pytester: Pytester, pytestconfig, monkeypatch) -> monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") result = pytester.runpytest("--version") assert result.ret == 0 - # p = py.path.local(py.__file__).dirpath() result.stderr.fnmatch_lines([f"pytest {pytest.__version__}"]) From 170a2c50408c551c31dbe51e9572c174d0c5cdde Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 19 Dec 2020 14:49:24 +0200 Subject: [PATCH 07/10] testing/test_config: check inipath instead of inifile inifile is soft-deprecated in favor of inipath. --- testing/test_config.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/testing/test_config.py b/testing/test_config.py index eacc9c9ebdd..8c1441e0680 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -11,7 +11,6 @@ from typing import Union import attr -import py.path import _pytest._code import pytest @@ -28,6 +27,7 @@ from _pytest.config.findpaths import get_common_ancestor from _pytest.config.findpaths import locate_config from _pytest.monkeypatch import MonkeyPatch +from _pytest.pathlib import absolutepath from _pytest.pytester import Pytester @@ -854,8 +854,8 @@ def test_inifilename(self, tmp_path: Path) -> None: ) ) - inifile = "../../foo/bar.ini" - option_dict = {"inifilename": inifile, "capture": "no"} + inifilename = "../../foo/bar.ini" + option_dict = {"inifilename": inifilename, "capture": "no"} cwd = tmp_path.joinpath("a/b") cwd.mkdir(parents=True) @@ -873,14 +873,14 @@ def test_inifilename(self, tmp_path: Path) -> None: with MonkeyPatch.context() as mp: mp.chdir(cwd) config = Config.fromdictargs(option_dict, ()) - inipath = py.path.local(inifile) + inipath = absolutepath(inifilename) assert config.args == [str(cwd)] - assert config.option.inifilename == inifile + assert config.option.inifilename == inifilename assert config.option.capture == "no" # this indicates this is the file used for getting configuration values - assert config.inifile == inipath + assert config.inipath == inipath assert config.inicfg.get("name") == "value" assert config.inicfg.get("should_not_be_set") is None From a21841300826fd67b43e5bb3ff1a04e11759dcc1 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 19 Dec 2020 14:51:23 +0200 Subject: [PATCH 08/10] pathlib: missing type annotation for fnmatch_ex --- src/_pytest/pathlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 2e452eb1cc9..d3908a3fdc0 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -387,7 +387,7 @@ def resolve_from_str(input: str, rootpath: Path) -> Path: return rootpath.joinpath(input) -def fnmatch_ex(pattern: str, path) -> bool: +def fnmatch_ex(pattern: str, path: Union[str, "os.PathLike[str]"]) -> bool: """A port of FNMatcher from py.path.common which works with PurePath() instances. The difference between this algorithm and PurePath.match() is that the From 4faed282613dbbe7196543cc8b45885269f39d4a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 19 Dec 2020 15:16:01 +0200 Subject: [PATCH 09/10] testing: convert some tmpdir to tmp_path The tmpdir fixture (and its factory variant) is soft-deprecated in favor of the tmp_path fixture. --- testing/test_cacheprovider.py | 15 ++- testing/test_pathlib.py | 204 +++++++++++++++++++--------------- 2 files changed, 124 insertions(+), 95 deletions(-) diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index ebd455593f3..2cb657efc16 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -8,6 +8,7 @@ from _pytest.config import ExitCode from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import Pytester +from _pytest.tmpdir import TempPathFactory pytest_plugins = ("pytester",) @@ -139,9 +140,11 @@ def test_custom_rel_cache_dir(self, pytester: Pytester) -> None: pytester.runpytest() assert pytester.path.joinpath(rel_cache_dir).is_dir() - def test_custom_abs_cache_dir(self, pytester: Pytester, tmpdir_factory) -> None: - tmp = str(tmpdir_factory.mktemp("tmp")) - abs_cache_dir = os.path.join(tmp, "custom_cache_dir") + def test_custom_abs_cache_dir( + self, pytester: Pytester, tmp_path_factory: TempPathFactory + ) -> None: + tmp = tmp_path_factory.mktemp("tmp") + abs_cache_dir = tmp / "custom_cache_dir" pytester.makeini( """ [pytest] @@ -152,7 +155,7 @@ def test_custom_abs_cache_dir(self, pytester: Pytester, tmpdir_factory) -> None: ) pytester.makepyfile(test_errored="def test_error():\n assert False") pytester.runpytest() - assert Path(abs_cache_dir).is_dir() + assert abs_cache_dir.is_dir() def test_custom_cache_dir_with_env_var( self, pytester: Pytester, monkeypatch: MonkeyPatch @@ -185,9 +188,9 @@ def test_cache_reportheader(env, pytester: Pytester, monkeypatch: MonkeyPatch) - def test_cache_reportheader_external_abspath( - pytester: Pytester, tmpdir_factory + pytester: Pytester, tmp_path_factory: TempPathFactory ) -> None: - external_cache = tmpdir_factory.mktemp( + external_cache = tmp_path_factory.mktemp( "test_cache_reportheader_external_abspath_abs" ) diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index f60b9f26369..48149084ece 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -1,8 +1,11 @@ import os.path +import pickle import sys import unittest.mock from pathlib import Path from textwrap import dedent +from types import ModuleType +from typing import Generator import py @@ -20,6 +23,7 @@ from _pytest.pathlib import resolve_package_path from _pytest.pathlib import symlink_or_skip from _pytest.pathlib import visit +from _pytest.tmpdir import TempPathFactory class TestFNMatcherPort: @@ -96,38 +100,40 @@ class TestImportPath: """ @pytest.fixture(scope="session") - def path1(self, tmpdir_factory): - path = tmpdir_factory.mktemp("path") + def path1(self, tmp_path_factory: TempPathFactory) -> Generator[Path, None, None]: + path = tmp_path_factory.mktemp("path") self.setuptestfs(path) yield path - assert path.join("samplefile").check() + assert path.joinpath("samplefile").exists() - def setuptestfs(self, path): + def setuptestfs(self, path: Path) -> None: # print "setting up test fs for", repr(path) - samplefile = path.ensure("samplefile") - samplefile.write("samplefile\n") + samplefile = path / "samplefile" + samplefile.write_text("samplefile\n") - execfile = path.ensure("execfile") - execfile.write("x=42") + execfile = path / "execfile" + execfile.write_text("x=42") - execfilepy = path.ensure("execfile.py") - execfilepy.write("x=42") + execfilepy = path / "execfile.py" + execfilepy.write_text("x=42") d = {1: 2, "hello": "world", "answer": 42} - path.ensure("samplepickle").dump(d) - - sampledir = path.ensure("sampledir", dir=1) - sampledir.ensure("otherfile") - - otherdir = path.ensure("otherdir", dir=1) - otherdir.ensure("__init__.py") - - module_a = otherdir.ensure("a.py") - module_a.write("from .b import stuff as result\n") - module_b = otherdir.ensure("b.py") - module_b.write('stuff="got it"\n') - module_c = otherdir.ensure("c.py") - module_c.write( + path.joinpath("samplepickle").write_bytes(pickle.dumps(d, 1)) + + sampledir = path / "sampledir" + sampledir.mkdir() + sampledir.joinpath("otherfile").touch() + + otherdir = path / "otherdir" + otherdir.mkdir() + otherdir.joinpath("__init__.py").touch() + + module_a = otherdir / "a.py" + module_a.write_text("from .b import stuff as result\n") + module_b = otherdir / "b.py" + module_b.write_text('stuff="got it"\n') + module_c = otherdir / "c.py" + module_c.write_text( dedent( """ import py; @@ -136,8 +142,8 @@ def setuptestfs(self, path): """ ) ) - module_d = otherdir.ensure("d.py") - module_d.write( + module_d = otherdir / "d.py" + module_d.write_text( dedent( """ import py; @@ -147,122 +153,141 @@ def setuptestfs(self, path): ) ) - def test_smoke_test(self, path1): - obj = import_path(path1.join("execfile.py")) + def test_smoke_test(self, path1: Path) -> None: + obj = import_path(path1 / "execfile.py") assert obj.x == 42 # type: ignore[attr-defined] assert obj.__name__ == "execfile" - def test_renamed_dir_creates_mismatch(self, tmpdir, monkeypatch): - p = tmpdir.ensure("a", "test_x123.py") + def test_renamed_dir_creates_mismatch( + self, tmp_path: Path, monkeypatch: MonkeyPatch + ) -> None: + tmp_path.joinpath("a").mkdir() + p = tmp_path.joinpath("a", "test_x123.py") + p.touch() import_path(p) - tmpdir.join("a").move(tmpdir.join("b")) + tmp_path.joinpath("a").rename(tmp_path.joinpath("b")) with pytest.raises(ImportPathMismatchError): - import_path(tmpdir.join("b", "test_x123.py")) + import_path(tmp_path.joinpath("b", "test_x123.py")) # Errors can be ignored. monkeypatch.setenv("PY_IGNORE_IMPORTMISMATCH", "1") - import_path(tmpdir.join("b", "test_x123.py")) + import_path(tmp_path.joinpath("b", "test_x123.py")) # PY_IGNORE_IMPORTMISMATCH=0 does not ignore error. monkeypatch.setenv("PY_IGNORE_IMPORTMISMATCH", "0") with pytest.raises(ImportPathMismatchError): - import_path(tmpdir.join("b", "test_x123.py")) + import_path(tmp_path.joinpath("b", "test_x123.py")) - def test_messy_name(self, tmpdir): + def test_messy_name(self, tmp_path: Path) -> None: # http://bitbucket.org/hpk42/py-trunk/issue/129 - path = tmpdir.ensure("foo__init__.py") + path = tmp_path / "foo__init__.py" + path.touch() module = import_path(path) assert module.__name__ == "foo__init__" - def test_dir(self, tmpdir): - p = tmpdir.join("hello_123") - p_init = p.ensure("__init__.py") + def test_dir(self, tmp_path: Path) -> None: + p = tmp_path / "hello_123" + p.mkdir() + p_init = p / "__init__.py" + p_init.touch() m = import_path(p) assert m.__name__ == "hello_123" m = import_path(p_init) assert m.__name__ == "hello_123" - def test_a(self, path1): - otherdir = path1.join("otherdir") - mod = import_path(otherdir.join("a.py")) + def test_a(self, path1: Path) -> None: + otherdir = path1 / "otherdir" + mod = import_path(otherdir / "a.py") assert mod.result == "got it" # type: ignore[attr-defined] assert mod.__name__ == "otherdir.a" - def test_b(self, path1): - otherdir = path1.join("otherdir") - mod = import_path(otherdir.join("b.py")) + def test_b(self, path1: Path) -> None: + otherdir = path1 / "otherdir" + mod = import_path(otherdir / "b.py") assert mod.stuff == "got it" # type: ignore[attr-defined] assert mod.__name__ == "otherdir.b" - def test_c(self, path1): - otherdir = path1.join("otherdir") - mod = import_path(otherdir.join("c.py")) + def test_c(self, path1: Path) -> None: + otherdir = path1 / "otherdir" + mod = import_path(otherdir / "c.py") assert mod.value == "got it" # type: ignore[attr-defined] - def test_d(self, path1): - otherdir = path1.join("otherdir") - mod = import_path(otherdir.join("d.py")) + def test_d(self, path1: Path) -> None: + otherdir = path1 / "otherdir" + mod = import_path(otherdir / "d.py") assert mod.value2 == "got it" # type: ignore[attr-defined] - def test_import_after(self, tmpdir): - tmpdir.ensure("xxxpackage", "__init__.py") - mod1path = tmpdir.ensure("xxxpackage", "module1.py") + def test_import_after(self, tmp_path: Path) -> None: + tmp_path.joinpath("xxxpackage").mkdir() + tmp_path.joinpath("xxxpackage", "__init__.py").touch() + mod1path = tmp_path.joinpath("xxxpackage", "module1.py") + mod1path.touch() mod1 = import_path(mod1path) assert mod1.__name__ == "xxxpackage.module1" from xxxpackage import module1 assert module1 is mod1 - def test_check_filepath_consistency(self, monkeypatch, tmpdir): + def test_check_filepath_consistency( + self, monkeypatch: MonkeyPatch, tmp_path: Path + ) -> None: name = "pointsback123" - ModuleType = type(os) - p = tmpdir.ensure(name + ".py") + p = tmp_path.joinpath(name + ".py") + p.touch() for ending in (".pyc", ".pyo"): mod = ModuleType(name) - pseudopath = tmpdir.ensure(name + ending) + pseudopath = tmp_path.joinpath(name + ending) + pseudopath.touch() mod.__file__ = str(pseudopath) monkeypatch.setitem(sys.modules, name, mod) newmod = import_path(p) assert mod == newmod monkeypatch.undo() mod = ModuleType(name) - pseudopath = tmpdir.ensure(name + "123.py") + pseudopath = tmp_path.joinpath(name + "123.py") + pseudopath.touch() mod.__file__ = str(pseudopath) monkeypatch.setitem(sys.modules, name, mod) with pytest.raises(ImportPathMismatchError) as excinfo: import_path(p) modname, modfile, orig = excinfo.value.args assert modname == name - assert modfile == pseudopath + assert modfile == str(pseudopath) assert orig == p assert issubclass(ImportPathMismatchError, ImportError) - def test_issue131_on__init__(self, tmpdir): + def test_issue131_on__init__(self, tmp_path: Path) -> None: # __init__.py files may be namespace packages, and thus the # __file__ of an imported module may not be ourselves # see issue - p1 = tmpdir.ensure("proja", "__init__.py") - p2 = tmpdir.ensure("sub", "proja", "__init__.py") + tmp_path.joinpath("proja").mkdir() + p1 = tmp_path.joinpath("proja", "__init__.py") + p1.touch() + tmp_path.joinpath("sub", "proja").mkdir(parents=True) + p2 = tmp_path.joinpath("sub", "proja", "__init__.py") + p2.touch() m1 = import_path(p1) m2 = import_path(p2) assert m1 == m2 - def test_ensuresyspath_append(self, tmpdir): - root1 = tmpdir.mkdir("root1") - file1 = root1.ensure("x123.py") + def test_ensuresyspath_append(self, tmp_path: Path) -> None: + root1 = tmp_path / "root1" + root1.mkdir() + file1 = root1 / "x123.py" + file1.touch() assert str(root1) not in sys.path import_path(file1, mode="append") assert str(root1) == sys.path[-1] assert str(root1) not in sys.path[:-1] - def test_invalid_path(self, tmpdir): + def test_invalid_path(self, tmp_path: Path) -> None: with pytest.raises(ImportError): - import_path(tmpdir.join("invalid.py")) + import_path(tmp_path / "invalid.py") @pytest.fixture - def simple_module(self, tmpdir): - fn = tmpdir.join("mymod.py") - fn.write( + def simple_module(self, tmp_path: Path) -> Path: + fn = tmp_path / "mymod.py" + fn.write_text( dedent( """ def foo(x): return 40 + x @@ -271,19 +296,21 @@ def foo(x): return 40 + x ) return fn - def test_importmode_importlib(self, simple_module): + def test_importmode_importlib(self, simple_module: Path) -> None: """`importlib` mode does not change sys.path.""" module = import_path(simple_module, mode="importlib") assert module.foo(2) == 42 # type: ignore[attr-defined] - assert simple_module.dirname not in sys.path + assert str(simple_module.parent) not in sys.path - def test_importmode_twice_is_different_module(self, simple_module): + def test_importmode_twice_is_different_module(self, simple_module: Path) -> None: """`importlib` mode always returns a new module.""" module1 = import_path(simple_module, mode="importlib") module2 = import_path(simple_module, mode="importlib") assert module1 is not module2 - def test_no_meta_path_found(self, simple_module, monkeypatch): + def test_no_meta_path_found( + self, simple_module: Path, monkeypatch: MonkeyPatch + ) -> None: """Even without any meta_path should still import module.""" monkeypatch.setattr(sys, "meta_path", []) module = import_path(simple_module, mode="importlib") @@ -299,7 +326,7 @@ def test_no_meta_path_found(self, simple_module, monkeypatch): import_path(simple_module, mode="importlib") -def test_resolve_package_path(tmp_path): +def test_resolve_package_path(tmp_path: Path) -> None: pkg = tmp_path / "pkg1" pkg.mkdir() (pkg / "__init__.py").touch() @@ -309,7 +336,7 @@ def test_resolve_package_path(tmp_path): assert resolve_package_path(pkg.joinpath("subdir", "__init__.py")) == pkg -def test_package_unimportable(tmp_path): +def test_package_unimportable(tmp_path: Path) -> None: pkg = tmp_path / "pkg1-1" pkg.mkdir() pkg.joinpath("__init__.py").touch() @@ -323,7 +350,7 @@ def test_package_unimportable(tmp_path): assert not resolve_package_path(pkg) -def test_access_denied_during_cleanup(tmp_path, monkeypatch): +def test_access_denied_during_cleanup(tmp_path: Path, monkeypatch: MonkeyPatch) -> None: """Ensure that deleting a numbered dir does not fail because of OSErrors (#4262).""" path = tmp_path / "temp-1" path.mkdir() @@ -338,7 +365,7 @@ def renamed_failed(*args): assert not lock_path.is_file() -def test_long_path_during_cleanup(tmp_path): +def test_long_path_during_cleanup(tmp_path: Path) -> None: """Ensure that deleting long path works (particularly on Windows (#6775)).""" path = (tmp_path / ("a" * 250)).resolve() if sys.platform == "win32": @@ -354,14 +381,14 @@ def test_long_path_during_cleanup(tmp_path): assert not os.path.isdir(extended_path) -def test_get_extended_length_path_str(): +def test_get_extended_length_path_str() -> None: assert get_extended_length_path_str(r"c:\foo") == r"\\?\c:\foo" assert get_extended_length_path_str(r"\\share\foo") == r"\\?\UNC\share\foo" assert get_extended_length_path_str(r"\\?\UNC\share\foo") == r"\\?\UNC\share\foo" assert get_extended_length_path_str(r"\\?\c:\foo") == r"\\?\c:\foo" -def test_suppress_error_removing_lock(tmp_path): +def test_suppress_error_removing_lock(tmp_path: Path) -> None: """ensure_deletable should be resilient if lock file cannot be removed (#5456, #7491)""" path = tmp_path / "dir" path.mkdir() @@ -406,15 +433,14 @@ def test_commonpath() -> None: assert commonpath(path, path.parent.parent) == path.parent.parent -def test_visit_ignores_errors(tmpdir) -> None: - symlink_or_skip("recursive", tmpdir.join("recursive")) - tmpdir.join("foo").write_binary(b"") - tmpdir.join("bar").write_binary(b"") +def test_visit_ignores_errors(tmp_path: Path) -> None: + symlink_or_skip("recursive", tmp_path / "recursive") + tmp_path.joinpath("foo").write_bytes(b"") + tmp_path.joinpath("bar").write_bytes(b"") - assert [entry.name for entry in visit(tmpdir, recurse=lambda entry: False)] == [ - "bar", - "foo", - ] + assert [ + entry.name for entry in visit(str(tmp_path), recurse=lambda entry: False) + ] == ["bar", "foo"] @pytest.mark.skipif(not sys.platform.startswith("win"), reason="Windows only") From ca4effc8225edf7fc828a4291642c82349ed8107 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 19 Dec 2020 14:52:10 +0200 Subject: [PATCH 10/10] Convert most of the collection code from py.path to pathlib --- changelog/8174.trivial.rst | 1 + src/_pytest/config/__init__.py | 2 +- src/_pytest/main.py | 84 +++++++++++++++++----------------- src/_pytest/python.py | 61 ++++++++++++------------ testing/test_collection.py | 4 +- 5 files changed, 77 insertions(+), 75 deletions(-) diff --git a/changelog/8174.trivial.rst b/changelog/8174.trivial.rst index 001ae4cb193..7649764618f 100644 --- a/changelog/8174.trivial.rst +++ b/changelog/8174.trivial.rst @@ -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.TracebackEntry`` 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``. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index c9a0e78bfcf..760b0f55c7b 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -348,7 +348,7 @@ def __init__(self) -> None: self._conftestpath2mod: Dict[Path, types.ModuleType] = {} self._confcutdir: Optional[Path] = None self._noconftest = False - self._duplicatepaths: Set[py.path.local] = set() + self._duplicatepaths: Set[Path] = set() # plugins that were explicitly skipped with pytest.skip # list of (module name, skip reason) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index e7c31ecc1d5..79afdde6155 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -37,6 +37,7 @@ from _pytest.outcomes import exit from _pytest.pathlib import absolutepath from _pytest.pathlib import bestrelpath +from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import visit from _pytest.reports import CollectReport from _pytest.reports import TestReport @@ -353,11 +354,14 @@ def pytest_runtestloop(session: "Session") -> bool: 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 checking for the existence of the appropriate activate script.""" - bindir = path.join("Scripts" if sys.platform.startswith("win") else "bin") - if not bindir.isdir(): + bindir = path.joinpath("Scripts" if sys.platform.startswith("win") else "bin") + try: + if not bindir.is_dir(): + return False + except OSError: return False activates = ( "activate", @@ -367,33 +371,32 @@ def _in_venv(path: py.path.local) -> bool: "Activate.bat", "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]: - path_ = Path(path) - ignore_paths = config._getconftest_pathlist("collect_ignore", path=path_.parent) +def pytest_ignore_collect(fspath: Path, config: Config) -> Optional[bool]: + ignore_paths = config._getconftest_pathlist("collect_ignore", path=fspath.parent) ignore_paths = ignore_paths or [] excludeopt = config.getoption("ignore") if excludeopt: ignore_paths.extend(absolutepath(x) for x in excludeopt) - if path_ in ignore_paths: + if fspath in ignore_paths: return True ignore_globs = config._getconftest_pathlist( - "collect_ignore_glob", path=path_.parent + "collect_ignore_glob", path=fspath.parent ) ignore_globs = ignore_globs or [] excludeglobopt = config.getoption("ignore_glob") if 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 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 None @@ -538,21 +541,21 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool: if ihook.pytest_ignore_collect(fspath=fspath, path=path, config=self.config): return False 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 True def _collectfile( - self, path: py.path.local, handle_dupes: bool = True + self, fspath: Path, handle_dupes: bool = True ) -> Sequence[nodes.Collector]: - fspath = Path(path) + path = py.path.local(fspath) assert ( - path.isfile() + fspath.is_file() ), "{!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) - if not self.isinitpath(path): + ihook = self.gethookproxy(fspath) + if not self.isinitpath(fspath): if ihook.pytest_ignore_collect( fspath=fspath, path=path, config=self.config ): @@ -562,10 +565,10 @@ def _collectfile( keepduplicates = self.config.getoption("keepduplicates") if not keepduplicates: duplicate_paths = self.config.pluginmanager._duplicatepaths - if path in duplicate_paths: + if fspath in duplicate_paths: return () 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] @@ -652,10 +655,8 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: from _pytest.python import Package # Keep track of any collected nodes in here, so we don't duplicate fixtures. - node_cache1: Dict[py.path.local, Sequence[nodes.Collector]] = {} - node_cache2: Dict[ - Tuple[Type[nodes.Collector], py.path.local], nodes.Collector - ] = ({}) + node_cache1: Dict[Path, Sequence[nodes.Collector]] = {} + node_cache2: Dict[Tuple[Type[nodes.Collector], Path], nodes.Collector] = ({}) # Keep track of any collected collectors in matchnodes paths, so they # are not collected more than once. @@ -679,31 +680,31 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: break if parent.is_dir(): - pkginit = py.path.local(parent / "__init__.py") - if pkginit.isfile() and pkginit not in node_cache1: + pkginit = parent / "__init__.py" + if pkginit.is_file() and pkginit not in node_cache1: col = self._collectfile(pkginit, handle_dupes=False) if col: if isinstance(col[0], Package): 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. # Let the Package collector deal with subnodes, don't collect here. if argpath.is_dir(): 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): if not direntry.is_file(): continue - path = py.path.local(direntry.path) - dirpath = path.dirpath() + path = Path(direntry.path) + dirpath = path.parent if dirpath not in seen_dirs: # Collect packages first. seen_dirs.add(dirpath) - pkginit = dirpath.join("__init__.py") + pkginit = dirpath / "__init__.py" if pkginit.exists(): for x in self._collectfile(pkginit): yield x @@ -714,23 +715,22 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: continue for x in self._collectfile(path): - key = (type(x), x.fspath) - if key in node_cache2: - yield node_cache2[key] + key2 = (type(x), Path(x.fspath)) + if key2 in node_cache2: + yield node_cache2[key2] else: - node_cache2[key] = x + node_cache2[key2] = x yield x else: assert argpath.is_file() - argpath_ = py.path.local(argpath) - if argpath_ in node_cache1: - col = node_cache1[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(str(argpath.parent), self) + col = collect_root._collectfile(argpath, handle_dupes=False) if col: - node_cache1[argpath_] = col + node_cache1[argpath] = col matching = [] work: List[ @@ -846,7 +846,7 @@ def resolve_collection_argument( 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 module paths instead of file-system paths: diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 27bbb24fe2c..b4605092000 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -66,6 +66,8 @@ from _pytest.mark.structures import normalize_mark_list from _pytest.outcomes import fail 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 ImportPathMismatchError from _pytest.pathlib import parts @@ -190,11 +192,10 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]: def pytest_collect_file( fspath: Path, path: py.path.local, parent: nodes.Collector ) -> Optional["Module"]: - ext = path.ext - if ext == ".py": + if fspath.suffix == ".py": if not parent.session.isinitpath(fspath): if not path_matches_patterns( - path, parent.config.getini("python_files") + ["__init__.py"] + fspath, parent.config.getini("python_files") + ["__init__.py"] ): return None ihook = parent.session.gethookproxy(fspath) @@ -205,13 +206,13 @@ def pytest_collect_file( 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 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": - if path.basename == "__init__.py": +def pytest_pycollect_makemodule(fspath: Path, path: py.path.local, parent) -> "Module": + if fspath.name == "__init__.py": pkg: Package = Package.from_parent(parent, fspath=path) return pkg mod: Module = Module.from_parent(parent, fspath=path) @@ -677,21 +678,21 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool: if ihook.pytest_ignore_collect(fspath=fspath, path=path, config=self.config): return False 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 True def _collectfile( - self, path: py.path.local, handle_dupes: bool = True + self, fspath: Path, handle_dupes: bool = True ) -> Sequence[nodes.Collector]: - fspath = Path(path) + path = py.path.local(fspath) assert ( - path.isfile() + fspath.is_file() ), "{!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) - if not self.session.isinitpath(path): + ihook = self.session.gethookproxy(fspath) + if not self.session.isinitpath(fspath): if ihook.pytest_ignore_collect( fspath=fspath, path=path, config=self.config ): @@ -701,32 +702,32 @@ def _collectfile( keepduplicates = self.config.getoption("keepduplicates") if not keepduplicates: duplicate_paths = self.config.pluginmanager._duplicatepaths - if path in duplicate_paths: + if fspath in duplicate_paths: return () 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] def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: - this_path = self.fspath.dirpath() - init_module = this_path.join("__init__.py") - if init_module.check(file=1) and path_matches_patterns( + this_path = Path(self.fspath).parent + init_module = this_path / "__init__.py" + if init_module.is_file() and path_matches_patterns( init_module, self.config.getini("python_files") ): - yield Module.from_parent(self, fspath=init_module) - pkg_prefixes: Set[py.path.local] = set() + yield Module.from_parent(self, fspath=py.path.local(init_module)) + pkg_prefixes: Set[Path] = set() 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. 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 parts_ = parts(direntry.path) 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 ): continue @@ -736,7 +737,7 @@ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: elif not direntry.is_dir(): # Broken symlink or invalid/missing file. continue - elif path.join("__init__.py").check(file=1): + elif path.joinpath("__init__.py").is_file(): pkg_prefixes.add(path) @@ -1416,13 +1417,13 @@ def _show_fixtures_per_test(config: Config, session: Session) -> None: import _pytest.config session.perform_collect() - curdir = py.path.local() + curdir = Path.cwd() tw = _pytest.config.create_terminal_writer(config) verbose = config.getvalue("verbose") - def get_best_relpath(func): + def get_best_relpath(func) -> str: 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: argname = fixture_def.argname @@ -1472,7 +1473,7 @@ def _showfixtures_main(config: Config, session: Session) -> None: import _pytest.config session.perform_collect() - curdir = py.path.local() + curdir = Path.cwd() tw = _pytest.config.create_terminal_writer(config) verbose = config.getvalue("verbose") @@ -1494,7 +1495,7 @@ def _showfixtures_main(config: Config, session: Session) -> None: ( len(fixturedef.baseid), fixturedef.func.__module__, - curdir.bestrelpath(py.path.local(loc)), + bestrelpath(curdir, Path(loc)), fixturedef.argname, fixturedef, ) diff --git a/testing/test_collection.py b/testing/test_collection.py index 9733b4fbd47..3dd9283eced 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -212,12 +212,12 @@ def test__in_venv(self, pytester: Pytester, fname: str) -> None: bindir = "Scripts" if sys.platform.startswith("win") else "bin" # no bin/activate, not a virtualenv 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 bin_path = base_path.joinpath(bindir) bin_path.mkdir() 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: pytester.makeini(