From 92ba96b0612e6b06bb8f4ab05bd75481d2504806 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 19 Dec 2020 14:11:00 +0200 Subject: [PATCH] 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 000000000..001ae4cb1 --- /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 423069330..043a23a79 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -43,6 +43,8 @@ from _pytest._io.saferepr import safeformat 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 @@ class Code: 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 @@ class TracebackEntry: 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 @@ class Traceback(List[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 @@ class Traceback(List[TracebackEntry]): 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 @@ class FormattedExcinfo: 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 @@ class FormattedExcinfo: 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 @@ class ReprFuncArgs(TerminalRepr): 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 c24ab7069..6db1c5906 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -5,6 +5,7 @@ import sys 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.mark.structures import MarkDecorator 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 @@ class FixtureRequest: 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 1b3ec5571..da2a0a7ea 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -39,7 +39,7 @@ if TYPE_CHECKING: SEP = "/" -tracebackcutdir = py.path.local(_pytest.__file__).dirpath() +tracebackcutdir = Path(_pytest.__file__).parent def iterparentnodeids(nodeid: str) -> Iterator[str]: @@ -416,9 +416,7 @@ class Node(metaclass=NodeMeta): 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 @@ class Collector(Node): 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 3ff04455f..27bbb24fe 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -340,7 +340,11 @@ class PyobjMixin: 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 44d7ab549..19c888403 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 Tuple 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 @@ class TestTraceback_f_g_h: " 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 @@ class TestTraceback_f_g_h: 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 @@ raise ValueError() 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 @@ raise ValueError() 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 @@ raise ValueError() """ ) 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 04d0ea932..6b8443fd2 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -6,13 +6,12 @@ import inspect 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 test_getfslineno() -> 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 @@ def test_getfslineno() -> None: 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)