Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement Node.path as pathlib.Path #8251

Merged
merged 2 commits into from
Mar 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,9 @@ repos:
xml\.
)
types: [python]
- id: py-path-deprecated
name: py.path usage is deprecated
language: pygrep
entry: \bpy\.path\.local
exclude: docs
types: [python]
1 change: 1 addition & 0 deletions changelog/8251.deprecation.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Deprecate ``Node.fspath`` as we plan to move off `py.path.local <https://py.readthedocs.io/en/latest/path.html>`__ and switch to :mod:``pathlib``.
1 change: 1 addition & 0 deletions changelog/8251.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implement ``Node.path`` as a ``pathlib.Path``.
10 changes: 10 additions & 0 deletions doc/en/deprecations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ Below is a complete list of all pytest features which are considered deprecated.
:class:`PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.


``Node.fspath`` in favor of ``pathlib`` and ``Node.path``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. deprecated:: 6.3

As pytest tries to move off `py.path.local <https://py.readthedocs.io/en/latest/path.html>`__ we ported most of the node internals to :mod:`pathlib`.

Pytest will provide compatibility for quite a while.


Backward compatibilities in ``Parser.addoption``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
3 changes: 1 addition & 2 deletions src/_pytest/_code/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@

import attr
import pluggy
import py

import _pytest
from _pytest._code.source import findsource
Expand Down Expand Up @@ -1230,7 +1229,7 @@ def getfslineno(obj: object) -> Tuple[Union[str, Path], int]:
if _PLUGGY_DIR.name == "__init__.py":
_PLUGGY_DIR = _PLUGGY_DIR.parent
_PYTEST_DIR = Path(_pytest.__file__).parent
_PY_DIR = Path(py.__file__).parent
_PY_DIR = Path(__import__("py").__file__).parent


def filter_traceback(entry: TracebackEntry) -> bool:
Expand Down
20 changes: 12 additions & 8 deletions src/_pytest/cacheprovider.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@
from typing import Union

import attr
import py

from .pathlib import resolve_from_str
from .pathlib import rm_rf
from .reports import CollectReport
from _pytest import nodes
from _pytest._io import TerminalWriter
from _pytest.compat import final
from _pytest.compat import LEGACY_PATH
from _pytest.compat import legacy_path
from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config import hookimpl
Expand Down Expand Up @@ -120,7 +121,7 @@ def warn(self, fmt: str, *, _ispytest: bool = False, **args: object) -> None:
stacklevel=3,
)

def makedir(self, name: str) -> py.path.local:
def makedir(self, name: str) -> LEGACY_PATH:
"""Return a directory path object with the given name.

If the directory does not yet exist, it will be created. You can use
Expand All @@ -137,7 +138,7 @@ def makedir(self, name: str) -> py.path.local:
raise ValueError("name is not allowed to contain path separators")
res = self._cachedir.joinpath(self._CACHE_PREFIX_DIRS, path)
res.mkdir(exist_ok=True, parents=True)
return py.path.local(res)
return legacy_path(res)

def _getvaluepath(self, key: str) -> Path:
return self._cachedir.joinpath(self._CACHE_PREFIX_VALUES, Path(key))
Expand Down Expand Up @@ -218,14 +219,17 @@ def pytest_make_collect_report(self, collector: nodes.Collector):

# Sort any lf-paths to the beginning.
lf_paths = self.lfplugin._last_failed_paths

res.result = sorted(
res.result,
key=lambda x: 0 if Path(str(x.fspath)) in lf_paths else 1,
# use stable sort to priorize last failed
key=lambda x: x.path in lf_paths,
reverse=True,
)
return

elif isinstance(collector, Module):
if Path(str(collector.fspath)) in self.lfplugin._last_failed_paths:
if collector.path in self.lfplugin._last_failed_paths:
out = yield
res = out.get_result()
result = res.result
Expand All @@ -246,7 +250,7 @@ def pytest_make_collect_report(self, collector: nodes.Collector):
for x in result
if x.nodeid in lastfailed
# Include any passed arguments (not trivial to filter).
or session.isinitpath(x.fspath)
or session.isinitpath(x.path)
# Keep all sub-collectors.
or isinstance(x, nodes.Collector)
]
Expand All @@ -266,7 +270,7 @@ def pytest_make_collect_report(
# test-bearing paths and doesn't try to include the paths of their
# packages, so don't filter them.
if isinstance(collector, Module) and not isinstance(collector, Package):
if Path(str(collector.fspath)) not in self.lfplugin._last_failed_paths:
if collector.path not in self.lfplugin._last_failed_paths:
self.lfplugin._skipped_files += 1

return CollectReport(
Expand Down Expand Up @@ -415,7 +419,7 @@ def pytest_collection_modifyitems(
self.cached_nodeids.update(item.nodeid for item in items)

def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item]:
return sorted(items, key=lambda item: item.fspath.mtime(), reverse=True)
return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True) # type: ignore[no-any-return]

def pytest_sessionfinish(self) -> None:
config = self.config
Expand Down
15 changes: 15 additions & 0 deletions src/_pytest/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import enum
import functools
import inspect
import os
import re
import sys
from contextlib import contextmanager
Expand All @@ -18,6 +19,7 @@
from typing import Union

import attr
import py

from _pytest.outcomes import fail
from _pytest.outcomes import TEST_OUTCOME
Expand All @@ -30,6 +32,19 @@
_T = TypeVar("_T")
_S = TypeVar("_S")

#: constant to prepare valuing pylib path replacements/lazy proxies later on
# intended for removal in pytest 8.0 or 9.0

# fmt: off
# intentional space to create a fake difference for the verification
LEGACY_PATH = py.path. local
# fmt: on


def legacy_path(path: Union[str, "os.PathLike[str]"]) -> LEGACY_PATH:
"""Internal wrapper to prepare lazy proxies for legacy_path instances"""
return LEGACY_PATH(path)


# fmt: off
# Singleton type for NOTSET, as described in:
Expand Down
25 changes: 13 additions & 12 deletions src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
from typing import Union

import attr
import py
from pluggy import HookimplMarker
from pluggy import HookspecMarker
from pluggy import PluginManager
Expand All @@ -48,6 +47,8 @@
from _pytest._io import TerminalWriter
from _pytest.compat import final
from _pytest.compat import importlib_metadata
from _pytest.compat import LEGACY_PATH
from _pytest.compat import legacy_path
from _pytest.outcomes import fail
from _pytest.outcomes import Skipped
from _pytest.pathlib import absolutepath
Expand Down Expand Up @@ -937,15 +938,15 @@ def __init__(
self.cache: Optional[Cache] = None

@property
def invocation_dir(self) -> py.path.local:
def invocation_dir(self) -> LEGACY_PATH:
"""The directory from which pytest was invoked.

Prefer to use :attr:`invocation_params.dir <InvocationParams.dir>`,
which is a :class:`pathlib.Path`.

:type: py.path.local
:type: LEGACY_PATH
"""
return py.path.local(str(self.invocation_params.dir))
return legacy_path(str(self.invocation_params.dir))

@property
def rootpath(self) -> Path:
Expand All @@ -958,14 +959,14 @@ def rootpath(self) -> Path:
return self._rootpath

@property
def rootdir(self) -> py.path.local:
def rootdir(self) -> LEGACY_PATH:
"""The path to the :ref:`rootdir <rootdir>`.

Prefer to use :attr:`rootpath`, which is a :class:`pathlib.Path`.

:type: py.path.local
:type: LEGACY_PATH
"""
return py.path.local(str(self.rootpath))
return legacy_path(str(self.rootpath))

@property
def inipath(self) -> Optional[Path]:
Expand All @@ -978,14 +979,14 @@ def inipath(self) -> Optional[Path]:
return self._inipath

@property
def inifile(self) -> Optional[py.path.local]:
def inifile(self) -> Optional[LEGACY_PATH]:
"""The path to the :ref:`configfile <configfiles>`.

Prefer to use :attr:`inipath`, which is a :class:`pathlib.Path`.

:type: Optional[py.path.local]
:type: Optional[LEGACY_PATH]
"""
return py.path.local(str(self.inipath)) if self.inipath else None
return legacy_path(str(self.inipath)) if self.inipath else None

def add_cleanup(self, func: Callable[[], None]) -> None:
"""Add a function to be called when the config object gets out of
Expand Down Expand Up @@ -1420,7 +1421,7 @@ def _getini(self, name: str):
assert self.inipath is not None
dp = self.inipath.parent
input_values = shlex.split(value) if isinstance(value, str) else value
return [py.path.local(str(dp / x)) for x in input_values]
return [legacy_path(str(dp / x)) for x in input_values]
elif type == "args":
return shlex.split(value) if isinstance(value, str) else value
elif type == "linelist":
Expand All @@ -1446,7 +1447,7 @@ def _getconftest_pathlist(self, name: str, path: Path) -> Optional[List[Path]]:
for relroot in relroots:
if isinstance(relroot, Path):
pass
elif isinstance(relroot, py.path.local):
elif isinstance(relroot, LEGACY_PATH):
relroot = Path(relroot)
else:
relroot = relroot.replace("/", os.sep)
Expand Down
8 changes: 8 additions & 0 deletions src/_pytest/deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@
)


NODE_FSPATH = UnformattedWarning(
PytestDeprecationWarning,
"{type}.fspath is deprecated and will be replaced by {type}.path.\n"
"see https://docs.pytest.org/en/latest/deprecations.html#node-fspath-in-favor-of-pathlib-and-node-path",
)

# You want to make some `__init__` or function "private".
#
# def my_private_function(some, args):
Expand All @@ -106,6 +112,8 @@
#
# All other calls will get the default _ispytest=False and trigger
# the warning (possibly error in the future).


def check_ispytest(ispytest: bool) -> None:
if not ispytest:
warn(PRIVATE, stacklevel=3)
26 changes: 13 additions & 13 deletions src/_pytest/doctest.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@
from typing import TYPE_CHECKING
from typing import Union

import py.path

import pytest
from _pytest import outcomes
from _pytest._code.code import ExceptionInfo
from _pytest._code.code import ReprFileLocation
from _pytest._code.code import TerminalRepr
from _pytest._io import TerminalWriter
from _pytest.compat import LEGACY_PATH
from _pytest.compat import legacy_path
from _pytest.compat import safe_getattr
from _pytest.config import Config
from _pytest.config.argparsing import Parser
Expand Down Expand Up @@ -122,16 +122,16 @@ def pytest_unconfigure() -> None:

def pytest_collect_file(
fspath: Path,
path: py.path.local,
path: LEGACY_PATH,
parent: Collector,
) -> Optional[Union["DoctestModule", "DoctestTextfile"]]:
config = parent.config
if fspath.suffix == ".py":
if config.option.doctestmodules and not _is_setup_py(fspath):
mod: DoctestModule = DoctestModule.from_parent(parent, fspath=path)
mod: DoctestModule = DoctestModule.from_parent(parent, path=fspath)
return mod
elif _is_doctest(config, fspath, parent):
txt: DoctestTextfile = DoctestTextfile.from_parent(parent, fspath=path)
txt: DoctestTextfile = DoctestTextfile.from_parent(parent, path=fspath)
return txt
return None

Expand Down Expand Up @@ -378,7 +378,7 @@ def repr_failure( # type: ignore[override]

def reportinfo(self):
assert self.dtest is not None
return self.fspath, self.dtest.lineno, "[doctest] %s" % self.name
return legacy_path(self.path), self.dtest.lineno, "[doctest] %s" % self.name


def _get_flag_lookup() -> Dict[str, int]:
Expand Down Expand Up @@ -425,9 +425,9 @@ def collect(self) -> Iterable[DoctestItem]:
# Inspired by doctest.testfile; ideally we would use it directly,
# but it doesn't support passing a custom checker.
encoding = self.config.getini("doctest_encoding")
text = self.fspath.read_text(encoding)
filename = str(self.fspath)
name = self.fspath.basename
text = self.path.read_text(encoding)
filename = str(self.path)
name = self.path.name
globs = {"__name__": "__main__"}

optionflags = get_optionflags(self)
Expand Down Expand Up @@ -534,16 +534,16 @@ def _find(
self, tests, obj, name, module, source_lines, globs, seen
)

if self.fspath.basename == "conftest.py":
if self.path.name == "conftest.py":
module = self.config.pluginmanager._importconftest(
Path(self.fspath), self.config.getoption("importmode")
self.path, self.config.getoption("importmode")
)
else:
try:
module = import_path(self.fspath)
module = import_path(self.path)
except ImportError:
if self.config.getvalue("doctest_ignore_import_errors"):
pytest.skip("unable to import module %r" % self.fspath)
pytest.skip("unable to import module %r" % self.path)
else:
raise
# Uses internal doctest module parsing mechanism.
Expand Down
Loading