Merge pull request #8251 from RonnyPfannschmidt/pathlib-node-path

This commit is contained in:
Bruno Oliveira 2021-03-07 11:10:07 -03:00 committed by GitHub
commit fc651fb158
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 320 additions and 196 deletions

View File

@ -87,3 +87,9 @@ repos:
xml\. xml\.
) )
types: [python] types: [python]
- id: py-path-deprecated
name: py.path usage is deprecated
language: pygrep
entry: \bpy\.path\.local
exclude: docs
types: [python]

View File

@ -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``.

View File

@ -0,0 +1 @@
Implement ``Node.path`` as a ``pathlib.Path``.

View File

@ -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>`. :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`` Backward compatibilities in ``Parser.addoption``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -31,7 +31,6 @@ from weakref import ref
import attr import attr
import pluggy import pluggy
import py
import _pytest import _pytest
from _pytest._code.source import findsource from _pytest._code.source import findsource
@ -1230,7 +1229,7 @@ _PLUGGY_DIR = Path(pluggy.__file__.rstrip("oc"))
if _PLUGGY_DIR.name == "__init__.py": if _PLUGGY_DIR.name == "__init__.py":
_PLUGGY_DIR = _PLUGGY_DIR.parent _PLUGGY_DIR = _PLUGGY_DIR.parent
_PYTEST_DIR = Path(_pytest.__file__).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: def filter_traceback(entry: TracebackEntry) -> bool:

View File

@ -13,7 +13,6 @@ from typing import Set
from typing import Union from typing import Union
import attr import attr
import py
from .pathlib import resolve_from_str from .pathlib import resolve_from_str
from .pathlib import rm_rf from .pathlib import rm_rf
@ -21,6 +20,8 @@ from .reports import CollectReport
from _pytest import nodes from _pytest import nodes
from _pytest._io import TerminalWriter from _pytest._io import TerminalWriter
from _pytest.compat import final 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 Config
from _pytest.config import ExitCode from _pytest.config import ExitCode
from _pytest.config import hookimpl from _pytest.config import hookimpl
@ -120,7 +121,7 @@ class Cache:
stacklevel=3, 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. """Return a directory path object with the given name.
If the directory does not yet exist, it will be created. You can use If the directory does not yet exist, it will be created. You can use
@ -137,7 +138,7 @@ class Cache:
raise ValueError("name is not allowed to contain path separators") raise ValueError("name is not allowed to contain path separators")
res = self._cachedir.joinpath(self._CACHE_PREFIX_DIRS, path) res = self._cachedir.joinpath(self._CACHE_PREFIX_DIRS, path)
res.mkdir(exist_ok=True, parents=True) res.mkdir(exist_ok=True, parents=True)
return py.path.local(res) return legacy_path(res)
def _getvaluepath(self, key: str) -> Path: def _getvaluepath(self, key: str) -> Path:
return self._cachedir.joinpath(self._CACHE_PREFIX_VALUES, Path(key)) return self._cachedir.joinpath(self._CACHE_PREFIX_VALUES, Path(key))
@ -218,14 +219,17 @@ class LFPluginCollWrapper:
# Sort any lf-paths to the beginning. # Sort any lf-paths to the beginning.
lf_paths = self.lfplugin._last_failed_paths lf_paths = self.lfplugin._last_failed_paths
res.result = sorted( res.result = sorted(
res.result, 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 return
elif isinstance(collector, Module): 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 out = yield
res = out.get_result() res = out.get_result()
result = res.result result = res.result
@ -246,7 +250,7 @@ class LFPluginCollWrapper:
for x in result for x in result
if x.nodeid in lastfailed if x.nodeid in lastfailed
# Include any passed arguments (not trivial to filter). # Include any passed arguments (not trivial to filter).
or session.isinitpath(x.fspath) or session.isinitpath(x.path)
# Keep all sub-collectors. # Keep all sub-collectors.
or isinstance(x, nodes.Collector) or isinstance(x, nodes.Collector)
] ]
@ -266,7 +270,7 @@ class LFPluginCollSkipfiles:
# test-bearing paths and doesn't try to include the paths of their # test-bearing paths and doesn't try to include the paths of their
# packages, so don't filter them. # packages, so don't filter them.
if isinstance(collector, Module) and not isinstance(collector, Package): 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 self.lfplugin._skipped_files += 1
return CollectReport( return CollectReport(
@ -415,7 +419,7 @@ class NFPlugin:
self.cached_nodeids.update(item.nodeid for item in items) self.cached_nodeids.update(item.nodeid for item in items)
def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item]: 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: def pytest_sessionfinish(self) -> None:
config = self.config config = self.config

View File

@ -2,6 +2,7 @@
import enum import enum
import functools import functools
import inspect import inspect
import os
import re import re
import sys import sys
from contextlib import contextmanager from contextlib import contextmanager
@ -18,6 +19,7 @@ from typing import TypeVar
from typing import Union from typing import Union
import attr import attr
import py
from _pytest.outcomes import fail from _pytest.outcomes import fail
from _pytest.outcomes import TEST_OUTCOME from _pytest.outcomes import TEST_OUTCOME
@ -30,6 +32,19 @@ if TYPE_CHECKING:
_T = TypeVar("_T") _T = TypeVar("_T")
_S = TypeVar("_S") _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 # fmt: off
# Singleton type for NOTSET, as described in: # Singleton type for NOTSET, as described in:

View File

@ -32,7 +32,6 @@ from typing import TYPE_CHECKING
from typing import Union from typing import Union
import attr import attr
import py
from pluggy import HookimplMarker from pluggy import HookimplMarker
from pluggy import HookspecMarker from pluggy import HookspecMarker
from pluggy import PluginManager from pluggy import PluginManager
@ -48,6 +47,8 @@ from _pytest._code import filter_traceback
from _pytest._io import TerminalWriter from _pytest._io import TerminalWriter
from _pytest.compat import final from _pytest.compat import final
from _pytest.compat import importlib_metadata 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 fail
from _pytest.outcomes import Skipped from _pytest.outcomes import Skipped
from _pytest.pathlib import absolutepath from _pytest.pathlib import absolutepath
@ -937,15 +938,15 @@ class Config:
self.cache: Optional[Cache] = None self.cache: Optional[Cache] = None
@property @property
def invocation_dir(self) -> py.path.local: def invocation_dir(self) -> LEGACY_PATH:
"""The directory from which pytest was invoked. """The directory from which pytest was invoked.
Prefer to use :attr:`invocation_params.dir <InvocationParams.dir>`, Prefer to use :attr:`invocation_params.dir <InvocationParams.dir>`,
which is a :class:`pathlib.Path`. 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 @property
def rootpath(self) -> Path: def rootpath(self) -> Path:
@ -958,14 +959,14 @@ class Config:
return self._rootpath return self._rootpath
@property @property
def rootdir(self) -> py.path.local: def rootdir(self) -> LEGACY_PATH:
"""The path to the :ref:`rootdir <rootdir>`. """The path to the :ref:`rootdir <rootdir>`.
Prefer to use :attr:`rootpath`, which is a :class:`pathlib.Path`. 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 @property
def inipath(self) -> Optional[Path]: def inipath(self) -> Optional[Path]:
@ -978,14 +979,14 @@ class Config:
return self._inipath return self._inipath
@property @property
def inifile(self) -> Optional[py.path.local]: def inifile(self) -> Optional[LEGACY_PATH]:
"""The path to the :ref:`configfile <configfiles>`. """The path to the :ref:`configfile <configfiles>`.
Prefer to use :attr:`inipath`, which is a :class:`pathlib.Path`. 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: def add_cleanup(self, func: Callable[[], None]) -> None:
"""Add a function to be called when the config object gets out of """Add a function to be called when the config object gets out of
@ -1420,7 +1421,7 @@ class Config:
assert self.inipath is not None assert self.inipath is not None
dp = self.inipath.parent dp = self.inipath.parent
input_values = shlex.split(value) if isinstance(value, str) else value 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": elif type == "args":
return shlex.split(value) if isinstance(value, str) else value return shlex.split(value) if isinstance(value, str) else value
elif type == "linelist": elif type == "linelist":
@ -1446,7 +1447,7 @@ class Config:
for relroot in relroots: for relroot in relroots:
if isinstance(relroot, Path): if isinstance(relroot, Path):
pass pass
elif isinstance(relroot, py.path.local): elif isinstance(relroot, LEGACY_PATH):
relroot = Path(relroot) relroot = Path(relroot)
else: else:
relroot = relroot.replace("/", os.sep) relroot = relroot.replace("/", os.sep)

View File

@ -89,6 +89,12 @@ ARGUMENT_TYPE_STR = UnformattedWarning(
) )
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". # You want to make some `__init__` or function "private".
# #
# def my_private_function(some, args): # def my_private_function(some, args):
@ -106,6 +112,8 @@ ARGUMENT_TYPE_STR = UnformattedWarning(
# #
# All other calls will get the default _ispytest=False and trigger # All other calls will get the default _ispytest=False and trigger
# the warning (possibly error in the future). # the warning (possibly error in the future).
def check_ispytest(ispytest: bool) -> None: def check_ispytest(ispytest: bool) -> None:
if not ispytest: if not ispytest:
warn(PRIVATE, stacklevel=3) warn(PRIVATE, stacklevel=3)

View File

@ -22,14 +22,14 @@ from typing import Type
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Union from typing import Union
import py.path
import pytest import pytest
from _pytest import outcomes from _pytest import outcomes
from _pytest._code.code import ExceptionInfo from _pytest._code.code import ExceptionInfo
from _pytest._code.code import ReprFileLocation from _pytest._code.code import ReprFileLocation
from _pytest._code.code import TerminalRepr from _pytest._code.code import TerminalRepr
from _pytest._io import TerminalWriter 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.compat import safe_getattr
from _pytest.config import Config from _pytest.config import Config
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
@ -122,16 +122,16 @@ def pytest_unconfigure() -> None:
def pytest_collect_file( def pytest_collect_file(
fspath: Path, fspath: Path,
path: py.path.local, path: LEGACY_PATH,
parent: Collector, parent: Collector,
) -> Optional[Union["DoctestModule", "DoctestTextfile"]]: ) -> Optional[Union["DoctestModule", "DoctestTextfile"]]:
config = parent.config config = parent.config
if fspath.suffix == ".py": if fspath.suffix == ".py":
if config.option.doctestmodules and not _is_setup_py(fspath): 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 return mod
elif _is_doctest(config, fspath, parent): 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 txt
return None return None
@ -378,7 +378,7 @@ class DoctestItem(pytest.Item):
def reportinfo(self): def reportinfo(self):
assert self.dtest is not None 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]: def _get_flag_lookup() -> Dict[str, int]:
@ -425,9 +425,9 @@ class DoctestTextfile(pytest.Module):
# Inspired by doctest.testfile; ideally we would use it directly, # Inspired by doctest.testfile; ideally we would use it directly,
# but it doesn't support passing a custom checker. # but it doesn't support passing a custom checker.
encoding = self.config.getini("doctest_encoding") encoding = self.config.getini("doctest_encoding")
text = self.fspath.read_text(encoding) text = self.path.read_text(encoding)
filename = str(self.fspath) filename = str(self.path)
name = self.fspath.basename name = self.path.name
globs = {"__name__": "__main__"} globs = {"__name__": "__main__"}
optionflags = get_optionflags(self) optionflags = get_optionflags(self)
@ -534,16 +534,16 @@ class DoctestModule(pytest.Module):
self, tests, obj, name, module, source_lines, globs, seen 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( module = self.config.pluginmanager._importconftest(
Path(self.fspath), self.config.getoption("importmode") self.path, self.config.getoption("importmode")
) )
else: else:
try: try:
module = import_path(self.fspath) module = import_path(self.path)
except ImportError: except ImportError:
if self.config.getvalue("doctest_ignore_import_errors"): 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: else:
raise raise
# Uses internal doctest module parsing mechanism. # Uses internal doctest module parsing mechanism.

View File

@ -28,7 +28,6 @@ from typing import TypeVar
from typing import Union from typing import Union
import attr import attr
import py
import _pytest import _pytest
from _pytest import nodes from _pytest import nodes
@ -46,6 +45,8 @@ from _pytest.compat import getfuncargnames
from _pytest.compat import getimfunc from _pytest.compat import getimfunc
from _pytest.compat import getlocation from _pytest.compat import getlocation
from _pytest.compat import is_generator from _pytest.compat import is_generator
from _pytest.compat import LEGACY_PATH
from _pytest.compat import legacy_path
from _pytest.compat import NOTSET from _pytest.compat import NOTSET
from _pytest.compat import safe_getattr from _pytest.compat import safe_getattr
from _pytest.config import _PluggyPlugin from _pytest.config import _PluggyPlugin
@ -53,6 +54,7 @@ from _pytest.config import Config
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest from _pytest.deprecated import check_ispytest
from _pytest.deprecated import FILLFUNCARGS from _pytest.deprecated import FILLFUNCARGS
from _pytest.deprecated import NODE_FSPATH
from _pytest.deprecated import YIELD_FIXTURE from _pytest.deprecated import YIELD_FIXTURE
from _pytest.mark import Mark from _pytest.mark import Mark
from _pytest.mark import ParameterSet from _pytest.mark import ParameterSet
@ -256,12 +258,12 @@ def get_parametrized_fixture_keys(item: nodes.Item, scopenum: int) -> Iterator[_
if scopenum == 0: # session if scopenum == 0: # session
key: _Key = (argname, param_index) key: _Key = (argname, param_index)
elif scopenum == 1: # package elif scopenum == 1: # package
key = (argname, param_index, item.fspath.dirpath()) key = (argname, param_index, item.path.parent)
elif scopenum == 2: # module elif scopenum == 2: # module
key = (argname, param_index, item.fspath) key = (argname, param_index, item.path)
elif scopenum == 3: # class elif scopenum == 3: # class
item_cls = item.cls # type: ignore[attr-defined] item_cls = item.cls # type: ignore[attr-defined]
key = (argname, param_index, item.fspath, item_cls) key = (argname, param_index, item.path, item_cls)
yield key yield key
@ -519,12 +521,17 @@ class FixtureRequest:
return self._pyfuncitem.getparent(_pytest.python.Module).obj return self._pyfuncitem.getparent(_pytest.python.Module).obj
@property @property
def fspath(self) -> py.path.local: def fspath(self) -> LEGACY_PATH:
"""The file system path of the test module which collected this test.""" """(deprecated) The file system path of the test module which collected this test."""
warnings.warn(NODE_FSPATH.format(type=type(self).__name__), stacklevel=2)
return legacy_path(self.path)
@property
def path(self) -> Path:
if self.scope not in ("function", "class", "module", "package"): if self.scope not in ("function", "class", "module", "package"):
raise AttributeError(f"module not available in {self.scope}-scoped context") raise AttributeError(f"module not available in {self.scope}-scoped context")
# TODO: Remove ignore once _pyfuncitem is properly typed. # TODO: Remove ignore once _pyfuncitem is properly typed.
return self._pyfuncitem.fspath # type: ignore return self._pyfuncitem.path # type: ignore
@property @property
def keywords(self) -> MutableMapping[str, Any]: def keywords(self) -> MutableMapping[str, Any]:
@ -1040,7 +1047,7 @@ class FixtureDef(Generic[_FixtureValue]):
if exc: if exc:
raise exc raise exc
finally: finally:
hook = self._fixturemanager.session.gethookproxy(request.node.fspath) hook = self._fixturemanager.session.gethookproxy(request.node.path)
hook.pytest_fixture_post_finalizer(fixturedef=self, request=request) hook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
# Even if finalization fails, we invalidate the cached fixture # Even if finalization fails, we invalidate the cached fixture
# value and remove all finalizers because they may be bound methods # value and remove all finalizers because they may be bound methods
@ -1075,7 +1082,7 @@ class FixtureDef(Generic[_FixtureValue]):
self.finish(request) self.finish(request)
assert self.cached_result is None assert self.cached_result is None
hook = self._fixturemanager.session.gethookproxy(request.node.fspath) hook = self._fixturemanager.session.gethookproxy(request.node.path)
result = hook.pytest_fixture_setup(fixturedef=self, request=request) result = hook.pytest_fixture_setup(fixturedef=self, request=request)
return result return result
@ -1623,6 +1630,11 @@ class FixtureManager:
self._holderobjseen.add(holderobj) self._holderobjseen.add(holderobj)
autousenames = [] autousenames = []
for name in dir(holderobj): for name in dir(holderobj):
# ugly workaround for one of the fspath deprecated property of node
# todo: safely generalize
if isinstance(holderobj, nodes.Node) and name == "fspath":
continue
# The attribute can be an arbitrary descriptor, so the attribute # The attribute can be an arbitrary descriptor, so the attribute
# access below can raise. safe_getatt() ignores such exceptions. # access below can raise. safe_getatt() ignores such exceptions.
obj = safe_getattr(holderobj, name, None) obj = safe_getattr(holderobj, name, None)

View File

@ -11,7 +11,6 @@ from typing import Tuple
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Union from typing import Union
import py.path
from pluggy import HookspecMarker from pluggy import HookspecMarker
from _pytest.deprecated import WARNING_CAPTURED_HOOK from _pytest.deprecated import WARNING_CAPTURED_HOOK
@ -42,6 +41,7 @@ if TYPE_CHECKING:
from _pytest.reports import TestReport from _pytest.reports import TestReport
from _pytest.runner import CallInfo from _pytest.runner import CallInfo
from _pytest.terminal import TerminalReporter from _pytest.terminal import TerminalReporter
from _pytest.compat import LEGACY_PATH
hookspec = HookspecMarker("pytest") hookspec = HookspecMarker("pytest")
@ -263,7 +263,7 @@ def pytest_collection_finish(session: "Session") -> None:
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_ignore_collect( def pytest_ignore_collect(
fspath: Path, path: py.path.local, config: "Config" fspath: Path, path: "LEGACY_PATH", config: "Config"
) -> Optional[bool]: ) -> Optional[bool]:
"""Return True to prevent considering this path for collection. """Return True to prevent considering this path for collection.
@ -273,7 +273,7 @@ def pytest_ignore_collect(
Stops at first non-None result, see :ref:`firstresult`. Stops at first non-None result, see :ref:`firstresult`.
:param pathlib.Path fspath: The path to analyze. :param pathlib.Path fspath: The path to analyze.
:param py.path.local path: The path to analyze. :param LEGACY_PATH path: The path to analyze.
:param _pytest.config.Config config: The pytest config object. :param _pytest.config.Config config: The pytest config object.
.. versionchanged:: 6.3.0 .. versionchanged:: 6.3.0
@ -283,14 +283,14 @@ def pytest_ignore_collect(
def pytest_collect_file( def pytest_collect_file(
fspath: Path, path: py.path.local, parent: "Collector" fspath: Path, path: "LEGACY_PATH", parent: "Collector"
) -> "Optional[Collector]": ) -> "Optional[Collector]":
"""Create a Collector for the given path, or None if not relevant. """Create a Collector for the given path, or None if not relevant.
The new node needs to have the specified ``parent`` as a parent. The new node needs to have the specified ``parent`` as a parent.
:param pathlib.Path fspath: The path to analyze. :param pathlib.Path fspath: The path to analyze.
:param py.path.local path: The path to collect. :param LEGACY_PATH path: The path to collect.
.. versionchanged:: 6.3.0 .. versionchanged:: 6.3.0
The ``fspath`` parameter was added as a :class:`pathlib.Path` The ``fspath`` parameter was added as a :class:`pathlib.Path`
@ -335,7 +335,7 @@ def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectRepor
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_pycollect_makemodule( def pytest_pycollect_makemodule(
fspath: Path, path: py.path.local, parent fspath: Path, path: "LEGACY_PATH", parent
) -> Optional["Module"]: ) -> Optional["Module"]:
"""Return a Module collector or None for the given path. """Return a Module collector or None for the given path.
@ -346,7 +346,7 @@ def pytest_pycollect_makemodule(
Stops at first non-None result, see :ref:`firstresult`. Stops at first non-None result, see :ref:`firstresult`.
:param pathlib.Path fspath: The path of the module to collect. :param pathlib.Path fspath: The path of the module to collect.
:param py.path.local path: The path of the module to collect. :param legacy_path path: The path of the module to collect.
.. versionchanged:: 6.3.0 .. versionchanged:: 6.3.0
The ``fspath`` parameter was added as a :class:`pathlib.Path` The ``fspath`` parameter was added as a :class:`pathlib.Path`
@ -676,13 +676,13 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No
def pytest_report_header( def pytest_report_header(
config: "Config", startpath: Path, startdir: py.path.local config: "Config", startpath: Path, startdir: "LEGACY_PATH"
) -> Union[str, List[str]]: ) -> Union[str, List[str]]:
"""Return a string or list of strings to be displayed as header info for terminal reporting. """Return a string or list of strings to be displayed as header info for terminal reporting.
:param _pytest.config.Config config: The pytest config object. :param _pytest.config.Config config: The pytest config object.
:param Path startpath: The starting dir. :param Path startpath: The starting dir.
:param py.path.local startdir: The starting dir. :param LEGACY_PATH startdir: The starting dir.
.. note:: .. note::
@ -706,7 +706,7 @@ def pytest_report_header(
def pytest_report_collectionfinish( def pytest_report_collectionfinish(
config: "Config", config: "Config",
startpath: Path, startpath: Path,
startdir: py.path.local, startdir: "LEGACY_PATH",
items: Sequence["Item"], items: Sequence["Item"],
) -> Union[str, List[str]]: ) -> Union[str, List[str]]:
"""Return a string or list of strings to be displayed after collection """Return a string or list of strings to be displayed after collection
@ -718,7 +718,7 @@ def pytest_report_collectionfinish(
:param _pytest.config.Config config: The pytest config object. :param _pytest.config.Config config: The pytest config object.
:param Path startpath: The starting path. :param Path startpath: The starting path.
:param py.path.local startdir: The starting dir. :param LEGACY_PATH startdir: The starting dir.
:param items: List of pytest items that are going to be executed; this list should not be modified. :param items: List of pytest items that are going to be executed; this list should not be modified.
.. note:: .. note::

View File

@ -21,11 +21,11 @@ from typing import TYPE_CHECKING
from typing import Union from typing import Union
import attr import attr
import py
import _pytest._code import _pytest._code
from _pytest import nodes from _pytest import nodes
from _pytest.compat import final from _pytest.compat import final
from _pytest.compat import legacy_path
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import directory_arg from _pytest.config import directory_arg
from _pytest.config import ExitCode from _pytest.config import ExitCode
@ -464,7 +464,12 @@ class Session(nodes.FSCollector):
def __init__(self, config: Config) -> None: def __init__(self, config: Config) -> None:
super().__init__( super().__init__(
config.rootdir, parent=None, config=config, session=self, nodeid="" path=config.rootpath,
fspath=config.rootdir,
parent=None,
config=config,
session=self,
nodeid="",
) )
self.testsfailed = 0 self.testsfailed = 0
self.testscollected = 0 self.testscollected = 0
@ -538,7 +543,7 @@ class Session(nodes.FSCollector):
if direntry.name == "__pycache__": if direntry.name == "__pycache__":
return False return False
fspath = Path(direntry.path) fspath = Path(direntry.path)
path = py.path.local(fspath) path = legacy_path(fspath)
ihook = self.gethookproxy(fspath.parent) ihook = self.gethookproxy(fspath.parent)
if ihook.pytest_ignore_collect(fspath=fspath, path=path, config=self.config): if ihook.pytest_ignore_collect(fspath=fspath, path=path, config=self.config):
return False return False
@ -550,7 +555,7 @@ class Session(nodes.FSCollector):
def _collectfile( def _collectfile(
self, fspath: Path, handle_dupes: bool = True self, fspath: Path, handle_dupes: bool = True
) -> Sequence[nodes.Collector]: ) -> Sequence[nodes.Collector]:
path = py.path.local(fspath) path = legacy_path(fspath)
assert ( assert (
fspath.is_file() fspath.is_file()
), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format( ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format(
@ -688,7 +693,7 @@ class Session(nodes.FSCollector):
if col: if col:
if isinstance(col[0], Package): if isinstance(col[0], Package):
pkg_roots[str(parent)] = col[0] pkg_roots[str(parent)] = col[0]
node_cache1[Path(col[0].fspath)] = [col[0]] node_cache1[col[0].path] = [col[0]]
# If it's a directory argument, recurse and look for any Subpackages. # If it's a directory argument, recurse and look for any Subpackages.
# Let the Package collector deal with subnodes, don't collect here. # Let the Package collector deal with subnodes, don't collect here.
@ -717,7 +722,7 @@ class Session(nodes.FSCollector):
continue continue
for x in self._collectfile(path): for x in self._collectfile(path):
key2 = (type(x), Path(x.fspath)) key2 = (type(x), x.path)
if key2 in node_cache2: if key2 in node_cache2:
yield node_cache2[key2] yield node_cache2[key2]
else: else:

View File

@ -16,16 +16,17 @@ from typing import TYPE_CHECKING
from typing import TypeVar from typing import TypeVar
from typing import Union from typing import Union
import py
import _pytest._code import _pytest._code
from _pytest._code import getfslineno from _pytest._code import getfslineno
from _pytest._code.code import ExceptionInfo from _pytest._code.code import ExceptionInfo
from _pytest._code.code import TerminalRepr from _pytest._code.code import TerminalRepr
from _pytest.compat import cached_property from _pytest.compat import cached_property
from _pytest.compat import LEGACY_PATH
from _pytest.compat import legacy_path
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import ConftestImportFailure from _pytest.config import ConftestImportFailure
from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
from _pytest.deprecated import NODE_FSPATH
from _pytest.mark.structures import Mark from _pytest.mark.structures import Mark
from _pytest.mark.structures import MarkDecorator from _pytest.mark.structures import MarkDecorator
from _pytest.mark.structures import NodeKeywords from _pytest.mark.structures import NodeKeywords
@ -79,6 +80,26 @@ def iterparentnodeids(nodeid: str) -> Iterator[str]:
pos = at + len(sep) pos = at + len(sep)
def _imply_path(
path: Optional[Path], fspath: Optional[LEGACY_PATH]
) -> Tuple[Path, LEGACY_PATH]:
if path is not None:
if fspath is not None:
if Path(fspath) != path:
raise ValueError(
f"Path({fspath!r}) != {path!r}\n"
"if both path and fspath are given they need to be equal"
)
assert Path(fspath) == path, f"{fspath} != {path}"
else:
fspath = legacy_path(path)
return path, fspath
else:
assert fspath is not None
return Path(fspath), fspath
_NodeType = TypeVar("_NodeType", bound="Node") _NodeType = TypeVar("_NodeType", bound="Node")
@ -110,7 +131,7 @@ class Node(metaclass=NodeMeta):
"parent", "parent",
"config", "config",
"session", "session",
"fspath", "path",
"_nodeid", "_nodeid",
"_store", "_store",
"__dict__", "__dict__",
@ -122,7 +143,8 @@ class Node(metaclass=NodeMeta):
parent: "Optional[Node]" = None, parent: "Optional[Node]" = None,
config: Optional[Config] = None, config: Optional[Config] = None,
session: "Optional[Session]" = None, session: "Optional[Session]" = None,
fspath: Optional[py.path.local] = None, fspath: Optional[LEGACY_PATH] = None,
path: Optional[Path] = None,
nodeid: Optional[str] = None, nodeid: Optional[str] = None,
) -> None: ) -> None:
#: A unique name within the scope of the parent node. #: A unique name within the scope of the parent node.
@ -148,7 +170,7 @@ class Node(metaclass=NodeMeta):
self.session = parent.session self.session = parent.session
#: Filesystem path where this node was collected from (can be None). #: Filesystem path where this node was collected from (can be None).
self.fspath = fspath or getattr(parent, "fspath", None) self.path = _imply_path(path or getattr(parent, "path", None), fspath=fspath)[0]
# The explicit annotation is to avoid publicly exposing NodeKeywords. # The explicit annotation is to avoid publicly exposing NodeKeywords.
#: Keywords/markers collected from all scopes. #: Keywords/markers collected from all scopes.
@ -174,6 +196,17 @@ class Node(metaclass=NodeMeta):
# own use. Currently only intended for internal plugins. # own use. Currently only intended for internal plugins.
self._store = Store() self._store = Store()
@property
def fspath(self) -> LEGACY_PATH:
"""(deprecated) returns a legacy_path copy of self.path"""
warnings.warn(NODE_FSPATH.format(type=type(self).__name__), stacklevel=2)
return legacy_path(self.path)
@fspath.setter
def fspath(self, value: LEGACY_PATH) -> None:
warnings.warn(NODE_FSPATH.format(type=type(self).__name__), stacklevel=2)
self.path = Path(value)
@classmethod @classmethod
def from_parent(cls, parent: "Node", **kw): def from_parent(cls, parent: "Node", **kw):
"""Public constructor for Nodes. """Public constructor for Nodes.
@ -195,7 +228,7 @@ class Node(metaclass=NodeMeta):
@property @property
def ihook(self): def ihook(self):
"""fspath-sensitive hook proxy used to call pytest hooks.""" """fspath-sensitive hook proxy used to call pytest hooks."""
return self.session.gethookproxy(self.fspath) return self.session.gethookproxy(self.path)
def __repr__(self) -> str: def __repr__(self) -> str:
return "<{} {}>".format(self.__class__.__name__, getattr(self, "name", None)) return "<{} {}>".format(self.__class__.__name__, getattr(self, "name", None))
@ -429,7 +462,7 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i
* "obj": a Python object that the node wraps. * "obj": a Python object that the node wraps.
* "fspath": just a path * "fspath": just a path
:rtype: A tuple of (str|py.path.local, int) with filename and line number. :rtype: A tuple of (str|Path, int) with filename and line number.
""" """
# See Item.location. # See Item.location.
location: Optional[Tuple[str, Optional[int], str]] = getattr(node, "location", None) location: Optional[Tuple[str, Optional[int], str]] = getattr(node, "location", None)
@ -476,19 +509,19 @@ class Collector(Node):
return self._repr_failure_py(excinfo, style=tbstyle) return self._repr_failure_py(excinfo, style=tbstyle)
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
if hasattr(self, "fspath"): if hasattr(self, "path"):
traceback = excinfo.traceback traceback = excinfo.traceback
ntraceback = traceback.cut(path=Path(self.fspath)) ntraceback = traceback.cut(path=self.path)
if ntraceback == traceback: if ntraceback == traceback:
ntraceback = ntraceback.cut(excludepath=tracebackcutdir) ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
excinfo.traceback = ntraceback.filter() excinfo.traceback = ntraceback.filter()
def _check_initialpaths_for_relpath( def _check_initialpaths_for_relpath(
session: "Session", fspath: py.path.local session: "Session", fspath: LEGACY_PATH
) -> Optional[str]: ) -> Optional[str]:
for initial_path in session._initialpaths: for initial_path in session._initialpaths:
initial_path_ = py.path.local(initial_path) initial_path_ = legacy_path(initial_path)
if fspath.common(initial_path_) == initial_path_: if fspath.common(initial_path_) == initial_path_:
return fspath.relto(initial_path_) return fspath.relto(initial_path_)
return None return None
@ -497,36 +530,52 @@ def _check_initialpaths_for_relpath(
class FSCollector(Collector): class FSCollector(Collector):
def __init__( def __init__(
self, self,
fspath: py.path.local, fspath: Optional[LEGACY_PATH],
path: Optional[Path],
parent=None, parent=None,
config: Optional[Config] = None, config: Optional[Config] = None,
session: Optional["Session"] = None, session: Optional["Session"] = None,
nodeid: Optional[str] = None, nodeid: Optional[str] = None,
) -> None: ) -> None:
path, fspath = _imply_path(path, fspath=fspath)
name = fspath.basename name = fspath.basename
if parent is not None: if parent is not None and parent.path != path:
rel = fspath.relto(parent.fspath) try:
if rel: rel = path.relative_to(parent.path)
name = rel except ValueError:
pass
else:
name = str(rel)
name = name.replace(os.sep, SEP) name = name.replace(os.sep, SEP)
self.fspath = fspath self.path = Path(fspath)
session = session or parent.session session = session or parent.session
if nodeid is None: if nodeid is None:
nodeid = self.fspath.relto(session.config.rootdir) try:
nodeid = str(self.path.relative_to(session.config.rootpath))
if not nodeid: except ValueError:
nodeid = _check_initialpaths_for_relpath(session, fspath) nodeid = _check_initialpaths_for_relpath(session, fspath)
if nodeid and os.sep != SEP: if nodeid and os.sep != SEP:
nodeid = nodeid.replace(os.sep, SEP) nodeid = nodeid.replace(os.sep, SEP)
super().__init__(name, parent, config, session, nodeid=nodeid, fspath=fspath) super().__init__(
name, parent, config, session, nodeid=nodeid, fspath=fspath, path=path
)
@classmethod @classmethod
def from_parent(cls, parent, *, fspath, **kw): def from_parent(
cls,
parent,
*,
fspath: Optional[LEGACY_PATH] = None,
path: Optional[Path] = None,
**kw,
):
"""The public constructor.""" """The public constructor."""
return super().from_parent(parent=parent, fspath=fspath, **kw) path, fspath = _imply_path(path, fspath=fspath)
return super().from_parent(parent=parent, fspath=fspath, path=path, **kw)
def gethookproxy(self, fspath: "os.PathLike[str]"): def gethookproxy(self, fspath: "os.PathLike[str]"):
warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
@ -587,8 +636,10 @@ class Item(Node):
if content: if content:
self._report_sections.append((when, key, content)) self._report_sections.append((when, key, content))
def reportinfo(self) -> Tuple[Union[py.path.local, str], Optional[int], str]: def reportinfo(self) -> Tuple[Union[LEGACY_PATH, str], Optional[int], str]:
return self.fspath, None, ""
# TODO: enable Path objects in reportinfo
return legacy_path(self.path), None, ""
@cached_property @cached_property
def location(self) -> Tuple[str, Optional[int], str]: def location(self) -> Tuple[str, Optional[int], str]:

View File

@ -34,7 +34,6 @@ from typing import Union
from weakref import WeakKeyDictionary from weakref import WeakKeyDictionary
import attr import attr
import py
from iniconfig import IniConfig from iniconfig import IniConfig
from iniconfig import SectionWrapper from iniconfig import SectionWrapper
@ -42,6 +41,8 @@ from _pytest import timing
from _pytest._code import Source from _pytest._code import Source
from _pytest.capture import _get_multicapture from _pytest.capture import _get_multicapture
from _pytest.compat import final from _pytest.compat import final
from _pytest.compat import LEGACY_PATH
from _pytest.compat import legacy_path
from _pytest.compat import NOTSET from _pytest.compat import NOTSET
from _pytest.compat import NotSetType from _pytest.compat import NotSetType
from _pytest.config import _PluggyPlugin from _pytest.config import _PluggyPlugin
@ -61,6 +62,7 @@ from _pytest.nodes import Item
from _pytest.outcomes import fail from _pytest.outcomes import fail
from _pytest.outcomes import importorskip from _pytest.outcomes import importorskip
from _pytest.outcomes import skip from _pytest.outcomes import skip
from _pytest.pathlib import bestrelpath
from _pytest.pathlib import make_numbered_dir from _pytest.pathlib import make_numbered_dir
from _pytest.reports import CollectReport from _pytest.reports import CollectReport
from _pytest.reports import TestReport from _pytest.reports import TestReport
@ -474,7 +476,7 @@ def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "Pyt
def testdir(pytester: "Pytester") -> "Testdir": def testdir(pytester: "Pytester") -> "Testdir":
""" """
Identical to :fixture:`pytester`, and provides an instance whose methods return Identical to :fixture:`pytester`, and provides an instance whose methods return
legacy ``py.path.local`` objects instead when applicable. legacy ``LEGACY_PATH`` objects instead when applicable.
New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`. New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`.
""" """
@ -933,10 +935,10 @@ class Pytester:
example_path = example_dir.joinpath(name) example_path = example_dir.joinpath(name)
if example_path.is_dir() and not example_path.joinpath("__init__.py").is_file(): if example_path.is_dir() and not example_path.joinpath("__init__.py").is_file():
# TODO: py.path.local.copy can copy files to existing directories, # TODO: legacy_path.copy can copy files to existing directories,
# while with shutil.copytree the destination directory cannot exist, # while with shutil.copytree the destination directory cannot exist,
# we will need to roll our own in order to drop py.path.local completely # we will need to roll our own in order to drop legacy_path completely
py.path.local(example_path).copy(py.path.local(self.path)) legacy_path(example_path).copy(legacy_path(self.path))
return self.path return self.path
elif example_path.is_file(): elif example_path.is_file():
result = self.path.joinpath(example_path.name) result = self.path.joinpath(example_path.name)
@ -957,12 +959,12 @@ class Pytester:
:param _pytest.config.Config config: :param _pytest.config.Config config:
A pytest config. A pytest config.
See :py:meth:`parseconfig` and :py:meth:`parseconfigure` for creating it. See :py:meth:`parseconfig` and :py:meth:`parseconfigure` for creating it.
:param py.path.local arg: :param os.PathLike[str] arg:
Path to the file. Path to the file.
""" """
session = Session.from_config(config) session = Session.from_config(config)
assert "::" not in str(arg) assert "::" not in str(arg)
p = py.path.local(arg) p = legacy_path(arg)
config.hook.pytest_sessionstart(session=session) config.hook.pytest_sessionstart(session=session)
res = session.perform_collect([str(p)], genitems=False)[0] res = session.perform_collect([str(p)], genitems=False)[0]
config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK) config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK)
@ -974,12 +976,12 @@ class Pytester:
This is like :py:meth:`getnode` but uses :py:meth:`parseconfigure` to This is like :py:meth:`getnode` but uses :py:meth:`parseconfigure` to
create the (configured) pytest Config instance. create the (configured) pytest Config instance.
:param py.path.local path: Path to the file. :param os.PathLike[str] path: Path to the file.
""" """
path = py.path.local(path) path = Path(path)
config = self.parseconfigure(path) config = self.parseconfigure(path)
session = Session.from_config(config) session = Session.from_config(config)
x = session.fspath.bestrelpath(path) x = bestrelpath(session.path, path)
config.hook.pytest_sessionstart(session=session) config.hook.pytest_sessionstart(session=session)
res = session.perform_collect([x], genitems=False)[0] res = session.perform_collect([x], genitems=False)[0]
config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK) config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK)
@ -1519,10 +1521,10 @@ class LineComp:
@attr.s(repr=False, str=False, init=False) @attr.s(repr=False, str=False, init=False)
class Testdir: class Testdir:
""" """
Similar to :class:`Pytester`, but this class works with legacy py.path.local objects instead. Similar to :class:`Pytester`, but this class works with legacy legacy_path objects instead.
All methods just forward to an internal :class:`Pytester` instance, converting results All methods just forward to an internal :class:`Pytester` instance, converting results
to `py.path.local` objects as necessary. to `legacy_path` objects as necessary.
""" """
__test__ = False __test__ = False
@ -1536,13 +1538,13 @@ class Testdir:
self._pytester = pytester self._pytester = pytester
@property @property
def tmpdir(self) -> py.path.local: def tmpdir(self) -> LEGACY_PATH:
"""Temporary directory where tests are executed.""" """Temporary directory where tests are executed."""
return py.path.local(self._pytester.path) return legacy_path(self._pytester.path)
@property @property
def test_tmproot(self) -> py.path.local: def test_tmproot(self) -> LEGACY_PATH:
return py.path.local(self._pytester._test_tmproot) return legacy_path(self._pytester._test_tmproot)
@property @property
def request(self): def request(self):
@ -1572,7 +1574,7 @@ class Testdir:
"""See :meth:`Pytester._finalize`.""" """See :meth:`Pytester._finalize`."""
return self._pytester._finalize() return self._pytester._finalize()
def makefile(self, ext, *args, **kwargs) -> py.path.local: def makefile(self, ext, *args, **kwargs) -> LEGACY_PATH:
"""See :meth:`Pytester.makefile`.""" """See :meth:`Pytester.makefile`."""
if ext and not ext.startswith("."): if ext and not ext.startswith("."):
# pytester.makefile is going to throw a ValueError in a way that # pytester.makefile is going to throw a ValueError in a way that
@ -1582,47 +1584,47 @@ class Testdir:
# allowed this, we will prepend "." as a workaround to avoid breaking # allowed this, we will prepend "." as a workaround to avoid breaking
# testdir usage that worked before # testdir usage that worked before
ext = "." + ext ext = "." + ext
return py.path.local(str(self._pytester.makefile(ext, *args, **kwargs))) return legacy_path(self._pytester.makefile(ext, *args, **kwargs))
def makeconftest(self, source) -> py.path.local: def makeconftest(self, source) -> LEGACY_PATH:
"""See :meth:`Pytester.makeconftest`.""" """See :meth:`Pytester.makeconftest`."""
return py.path.local(str(self._pytester.makeconftest(source))) return legacy_path(self._pytester.makeconftest(source))
def makeini(self, source) -> py.path.local: def makeini(self, source) -> LEGACY_PATH:
"""See :meth:`Pytester.makeini`.""" """See :meth:`Pytester.makeini`."""
return py.path.local(str(self._pytester.makeini(source))) return legacy_path(self._pytester.makeini(source))
def getinicfg(self, source: str) -> SectionWrapper: def getinicfg(self, source: str) -> SectionWrapper:
"""See :meth:`Pytester.getinicfg`.""" """See :meth:`Pytester.getinicfg`."""
return self._pytester.getinicfg(source) return self._pytester.getinicfg(source)
def makepyprojecttoml(self, source) -> py.path.local: def makepyprojecttoml(self, source) -> LEGACY_PATH:
"""See :meth:`Pytester.makepyprojecttoml`.""" """See :meth:`Pytester.makepyprojecttoml`."""
return py.path.local(str(self._pytester.makepyprojecttoml(source))) return legacy_path(self._pytester.makepyprojecttoml(source))
def makepyfile(self, *args, **kwargs) -> py.path.local: def makepyfile(self, *args, **kwargs) -> LEGACY_PATH:
"""See :meth:`Pytester.makepyfile`.""" """See :meth:`Pytester.makepyfile`."""
return py.path.local(str(self._pytester.makepyfile(*args, **kwargs))) return legacy_path(self._pytester.makepyfile(*args, **kwargs))
def maketxtfile(self, *args, **kwargs) -> py.path.local: def maketxtfile(self, *args, **kwargs) -> LEGACY_PATH:
"""See :meth:`Pytester.maketxtfile`.""" """See :meth:`Pytester.maketxtfile`."""
return py.path.local(str(self._pytester.maketxtfile(*args, **kwargs))) return legacy_path(self._pytester.maketxtfile(*args, **kwargs))
def syspathinsert(self, path=None) -> None: def syspathinsert(self, path=None) -> None:
"""See :meth:`Pytester.syspathinsert`.""" """See :meth:`Pytester.syspathinsert`."""
return self._pytester.syspathinsert(path) return self._pytester.syspathinsert(path)
def mkdir(self, name) -> py.path.local: def mkdir(self, name) -> LEGACY_PATH:
"""See :meth:`Pytester.mkdir`.""" """See :meth:`Pytester.mkdir`."""
return py.path.local(str(self._pytester.mkdir(name))) return legacy_path(self._pytester.mkdir(name))
def mkpydir(self, name) -> py.path.local: def mkpydir(self, name) -> LEGACY_PATH:
"""See :meth:`Pytester.mkpydir`.""" """See :meth:`Pytester.mkpydir`."""
return py.path.local(str(self._pytester.mkpydir(name))) return legacy_path(self._pytester.mkpydir(name))
def copy_example(self, name=None) -> py.path.local: def copy_example(self, name=None) -> LEGACY_PATH:
"""See :meth:`Pytester.copy_example`.""" """See :meth:`Pytester.copy_example`."""
return py.path.local(str(self._pytester.copy_example(name))) return legacy_path(self._pytester.copy_example(name))
def getnode(self, config: Config, arg) -> Optional[Union[Item, Collector]]: def getnode(self, config: Config, arg) -> Optional[Union[Item, Collector]]:
"""See :meth:`Pytester.getnode`.""" """See :meth:`Pytester.getnode`."""

View File

@ -26,8 +26,6 @@ from typing import Tuple
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Union from typing import Union
import py
import _pytest import _pytest
from _pytest import fixtures from _pytest import fixtures
from _pytest import nodes from _pytest import nodes
@ -45,6 +43,8 @@ from _pytest.compat import getimfunc
from _pytest.compat import getlocation from _pytest.compat import getlocation
from _pytest.compat import is_async_function from _pytest.compat import is_async_function
from _pytest.compat import is_generator from _pytest.compat import is_generator
from _pytest.compat import LEGACY_PATH
from _pytest.compat import legacy_path
from _pytest.compat import NOTSET from _pytest.compat import NOTSET
from _pytest.compat import REGEX_TYPE from _pytest.compat import REGEX_TYPE
from _pytest.compat import safe_getattr from _pytest.compat import safe_getattr
@ -189,7 +189,7 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]:
def pytest_collect_file( def pytest_collect_file(
fspath: Path, path: py.path.local, parent: nodes.Collector fspath: Path, path: LEGACY_PATH, parent: nodes.Collector
) -> Optional["Module"]: ) -> Optional["Module"]:
if fspath.suffix == ".py": if fspath.suffix == ".py":
if not parent.session.isinitpath(fspath): if not parent.session.isinitpath(fspath):
@ -210,7 +210,7 @@ def path_matches_patterns(path: Path, patterns: Iterable[str]) -> bool:
return any(fnmatch_ex(pattern, path) for pattern in patterns) return any(fnmatch_ex(pattern, path) for pattern in patterns)
def pytest_pycollect_makemodule(fspath: Path, path: py.path.local, parent) -> "Module": def pytest_pycollect_makemodule(fspath: Path, path: LEGACY_PATH, parent) -> "Module":
if fspath.name == "__init__.py": if fspath.name == "__init__.py":
pkg: Package = Package.from_parent(parent, fspath=path) pkg: Package = Package.from_parent(parent, fspath=path)
return pkg return pkg
@ -321,7 +321,7 @@ class PyobjMixin(nodes.Node):
parts.reverse() parts.reverse()
return ".".join(parts) return ".".join(parts)
def reportinfo(self) -> Tuple[Union[py.path.local, str], int, str]: def reportinfo(self) -> Tuple[Union[LEGACY_PATH, str], int, str]:
# XXX caching? # XXX caching?
obj = self.obj obj = self.obj
compat_co_firstlineno = getattr(obj, "compat_co_firstlineno", None) compat_co_firstlineno = getattr(obj, "compat_co_firstlineno", None)
@ -330,12 +330,12 @@ class PyobjMixin(nodes.Node):
file_path = sys.modules[obj.__module__].__file__ file_path = sys.modules[obj.__module__].__file__
if file_path.endswith(".pyc"): if file_path.endswith(".pyc"):
file_path = file_path[:-1] file_path = file_path[:-1]
fspath: Union[py.path.local, str] = file_path fspath: Union[LEGACY_PATH, str] = file_path
lineno = compat_co_firstlineno lineno = compat_co_firstlineno
else: else:
path, lineno = getfslineno(obj) path, lineno = getfslineno(obj)
if isinstance(path, Path): if isinstance(path, Path):
fspath = py.path.local(path) fspath = legacy_path(path)
else: else:
fspath = path fspath = path
modpath = self.getmodpath() modpath = self.getmodpath()
@ -577,7 +577,7 @@ class Module(nodes.File, PyCollector):
# We assume we are only called once per module. # We assume we are only called once per module.
importmode = self.config.getoption("--import-mode") importmode = self.config.getoption("--import-mode")
try: try:
mod = import_path(self.fspath, mode=importmode) mod = import_path(self.path, mode=importmode)
except SyntaxError as e: except SyntaxError as e:
raise self.CollectError( raise self.CollectError(
ExceptionInfo.from_current().getrepr(style="short") ExceptionInfo.from_current().getrepr(style="short")
@ -603,10 +603,10 @@ class Module(nodes.File, PyCollector):
) )
formatted_tb = str(exc_repr) formatted_tb = str(exc_repr)
raise self.CollectError( raise self.CollectError(
"ImportError while importing test module '{fspath}'.\n" "ImportError while importing test module '{path}'.\n"
"Hint: make sure your test modules/packages have valid Python names.\n" "Hint: make sure your test modules/packages have valid Python names.\n"
"Traceback:\n" "Traceback:\n"
"{traceback}".format(fspath=self.fspath, traceback=formatted_tb) "{traceback}".format(path=self.path, traceback=formatted_tb)
) from e ) from e
except skip.Exception as e: except skip.Exception as e:
if e.allow_module_level: if e.allow_module_level:
@ -624,18 +624,26 @@ class Module(nodes.File, PyCollector):
class Package(Module): class Package(Module):
def __init__( def __init__(
self, self,
fspath: py.path.local, fspath: Optional[LEGACY_PATH],
parent: nodes.Collector, parent: nodes.Collector,
# NOTE: following args are unused: # NOTE: following args are unused:
config=None, config=None,
session=None, session=None,
nodeid=None, nodeid=None,
path=Optional[Path],
) -> None: ) -> None:
# NOTE: Could be just the following, but kept as-is for compat. # NOTE: Could be just the following, but kept as-is for compat.
# nodes.FSCollector.__init__(self, fspath, parent=parent) # nodes.FSCollector.__init__(self, fspath, parent=parent)
path, fspath = nodes._imply_path(path, fspath=fspath)
session = parent.session session = parent.session
nodes.FSCollector.__init__( nodes.FSCollector.__init__(
self, fspath, parent=parent, config=config, session=session, nodeid=nodeid self,
fspath=fspath,
path=path,
parent=parent,
config=config,
session=session,
nodeid=nodeid,
) )
self.name = os.path.basename(str(fspath.dirname)) self.name = os.path.basename(str(fspath.dirname))
@ -667,7 +675,7 @@ class Package(Module):
if direntry.name == "__pycache__": if direntry.name == "__pycache__":
return False return False
fspath = Path(direntry.path) fspath = Path(direntry.path)
path = py.path.local(fspath) path = legacy_path(fspath)
ihook = self.session.gethookproxy(fspath.parent) ihook = self.session.gethookproxy(fspath.parent)
if ihook.pytest_ignore_collect(fspath=fspath, path=path, config=self.config): if ihook.pytest_ignore_collect(fspath=fspath, path=path, config=self.config):
return False return False
@ -679,7 +687,7 @@ class Package(Module):
def _collectfile( def _collectfile(
self, fspath: Path, handle_dupes: bool = True self, fspath: Path, handle_dupes: bool = True
) -> Sequence[nodes.Collector]: ) -> Sequence[nodes.Collector]:
path = py.path.local(fspath) path = legacy_path(fspath)
assert ( assert (
fspath.is_file() fspath.is_file()
), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format( ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format(
@ -704,12 +712,12 @@ class Package(Module):
return ihook.pytest_collect_file(fspath=fspath, path=path, parent=self) # type: ignore[no-any-return] return ihook.pytest_collect_file(fspath=fspath, path=path, parent=self) # type: ignore[no-any-return]
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
this_path = Path(self.fspath).parent this_path = self.path.parent
init_module = this_path / "__init__.py" init_module = this_path / "__init__.py"
if init_module.is_file() and path_matches_patterns( if init_module.is_file() and path_matches_patterns(
init_module, self.config.getini("python_files") init_module, self.config.getini("python_files")
): ):
yield Module.from_parent(self, fspath=py.path.local(init_module)) yield Module.from_parent(self, path=init_module)
pkg_prefixes: Set[Path] = set() pkg_prefixes: Set[Path] = set()
for direntry in visit(str(this_path), recurse=self._recurse): for direntry in visit(str(this_path), recurse=self._recurse):
path = Path(direntry.path) path = Path(direntry.path)

View File

@ -15,7 +15,6 @@ from typing import TypeVar
from typing import Union from typing import Union
import attr import attr
import py
from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionChainRepr
from _pytest._code.code import ExceptionInfo from _pytest._code.code import ExceptionInfo
@ -30,6 +29,7 @@ from _pytest._code.code import ReprTraceback
from _pytest._code.code import TerminalRepr from _pytest._code.code import TerminalRepr
from _pytest._io import TerminalWriter from _pytest._io import TerminalWriter
from _pytest.compat import final from _pytest.compat import final
from _pytest.compat import LEGACY_PATH
from _pytest.config import Config from _pytest.config import Config
from _pytest.nodes import Collector from _pytest.nodes import Collector
from _pytest.nodes import Item from _pytest.nodes import Item
@ -500,7 +500,7 @@ def _report_to_json(report: BaseReport) -> Dict[str, Any]:
else: else:
d["longrepr"] = report.longrepr d["longrepr"] = report.longrepr
for name in d: for name in d:
if isinstance(d[name], (py.path.local, Path)): if isinstance(d[name], (LEGACY_PATH, Path)):
d[name] = str(d[name]) d[name] = str(d[name])
elif name == "result": elif name == "result":
d[name] = None # for now d[name] = None # for now

View File

@ -6,13 +6,14 @@ from pathlib import Path
from typing import Optional from typing import Optional
import attr import attr
import py
from .pathlib import ensure_reset_dir from .pathlib import ensure_reset_dir
from .pathlib import LOCK_TIMEOUT from .pathlib import LOCK_TIMEOUT
from .pathlib import make_numbered_dir from .pathlib import make_numbered_dir
from .pathlib import make_numbered_dir_with_cleanup from .pathlib import make_numbered_dir_with_cleanup
from _pytest.compat import final 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 Config
from _pytest.deprecated import check_ispytest from _pytest.deprecated import check_ispytest
from _pytest.fixtures import fixture from _pytest.fixtures import fixture
@ -133,7 +134,7 @@ class TempPathFactory:
@final @final
@attr.s(init=False) @attr.s(init=False)
class TempdirFactory: class TempdirFactory:
"""Backward comptibility wrapper that implements :class:``py.path.local`` """Backward comptibility wrapper that implements :class:``_pytest.compat.LEGACY_PATH``
for :class:``TempPathFactory``.""" for :class:``TempPathFactory``."""
_tmppath_factory = attr.ib(type=TempPathFactory) _tmppath_factory = attr.ib(type=TempPathFactory)
@ -144,13 +145,13 @@ class TempdirFactory:
check_ispytest(_ispytest) check_ispytest(_ispytest)
self._tmppath_factory = tmppath_factory self._tmppath_factory = tmppath_factory
def mktemp(self, basename: str, numbered: bool = True) -> py.path.local: def mktemp(self, basename: str, numbered: bool = True) -> LEGACY_PATH:
"""Same as :meth:`TempPathFactory.mktemp`, but returns a ``py.path.local`` object.""" """Same as :meth:`TempPathFactory.mktemp`, but returns a ``_pytest.compat.LEGACY_PATH`` object."""
return py.path.local(self._tmppath_factory.mktemp(basename, numbered).resolve()) return legacy_path(self._tmppath_factory.mktemp(basename, numbered).resolve())
def getbasetemp(self) -> py.path.local: def getbasetemp(self) -> LEGACY_PATH:
"""Backward compat wrapper for ``_tmppath_factory.getbasetemp``.""" """Backward compat wrapper for ``_tmppath_factory.getbasetemp``."""
return py.path.local(self._tmppath_factory.getbasetemp().resolve()) return legacy_path(self._tmppath_factory.getbasetemp().resolve())
def get_user() -> Optional[str]: def get_user() -> Optional[str]:
@ -202,7 +203,7 @@ def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path:
@fixture @fixture
def tmpdir(tmp_path: Path) -> py.path.local: def tmpdir(tmp_path: Path) -> LEGACY_PATH:
"""Return a temporary directory path object which is unique to each test """Return a temporary directory path object which is unique to each test
function invocation, created as a sub directory of the base temporary function invocation, created as a sub directory of the base temporary
directory. directory.
@ -212,11 +213,11 @@ def tmpdir(tmp_path: Path) -> py.path.local:
``--basetemp`` is used then it is cleared each session. See :ref:`base ``--basetemp`` is used then it is cleared each session. See :ref:`base
temporary directory`. temporary directory`.
The returned object is a `py.path.local`_ path object. The returned object is a `legacy_path`_ object.
.. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html .. _legacy_path: https://py.readthedocs.io/en/latest/path.html
""" """
return py.path.local(tmp_path) return legacy_path(tmp_path)
@fixture @fixture

View File

@ -2,3 +2,4 @@
addopts = --strict-markers addopts = --strict-markers
filterwarnings = filterwarnings =
error::pytest.PytestWarning error::pytest.PytestWarning
ignore:.*.fspath is deprecated and will be replaced by .*.path.*:pytest.PytestDeprecationWarning

View File

@ -1125,7 +1125,8 @@ class TestReportInfo:
def test_func_reportinfo(self, pytester: Pytester) -> None: def test_func_reportinfo(self, pytester: Pytester) -> None:
item = pytester.getitem("def test_func(): pass") item = pytester.getitem("def test_func(): pass")
fspath, lineno, modpath = item.reportinfo() fspath, lineno, modpath = item.reportinfo()
assert fspath == item.fspath with pytest.warns(DeprecationWarning):
assert fspath == item.fspath
assert lineno == 0 assert lineno == 0
assert modpath == "test_func" assert modpath == "test_func"
@ -1140,7 +1141,8 @@ class TestReportInfo:
classcol = pytester.collect_by_name(modcol, "TestClass") classcol = pytester.collect_by_name(modcol, "TestClass")
assert isinstance(classcol, Class) assert isinstance(classcol, Class)
fspath, lineno, msg = classcol.reportinfo() fspath, lineno, msg = classcol.reportinfo()
assert fspath == modcol.fspath with pytest.warns(DeprecationWarning):
assert fspath == modcol.fspath
assert lineno == 1 assert lineno == 1
assert msg == "TestClass" assert msg == "TestClass"

View File

@ -966,7 +966,9 @@ class TestRequestBasic:
modcol = pytester.getmodulecol("def test_somefunc(): pass") modcol = pytester.getmodulecol("def test_somefunc(): pass")
(item,) = pytester.genitems([modcol]) (item,) = pytester.genitems([modcol])
req = fixtures.FixtureRequest(item, _ispytest=True) req = fixtures.FixtureRequest(item, _ispytest=True)
assert req.fspath == modcol.fspath assert req.path == modcol.path
with pytest.warns(pytest.PytestDeprecationWarning):
assert req.fspath == modcol.fspath
def test_request_fixturenames(self, pytester: Pytester) -> None: def test_request_fixturenames(self, pytester: Pytester) -> None:
pytester.makepyfile( pytester.makepyfile(

View File

@ -6,8 +6,6 @@ import textwrap
from pathlib import Path from pathlib import Path
from typing import List from typing import List
import py.path
import pytest import pytest
from _pytest.config import ExitCode from _pytest.config import ExitCode
from _pytest.fixtures import FixtureRequest from _pytest.fixtures import FixtureRequest
@ -369,9 +367,10 @@ class TestCustomConftests:
def test_collectignore_exclude_on_option(self, pytester: Pytester) -> None: def test_collectignore_exclude_on_option(self, pytester: Pytester) -> None:
pytester.makeconftest( pytester.makeconftest(
""" """
import py # potentially avoid dependency on pylib
from _pytest.compat import legacy_path
from pathlib import Path from pathlib import Path
collect_ignore = [py.path.local('hello'), 'test_world.py', Path('bye')] collect_ignore = [legacy_path('hello'), 'test_world.py', Path('bye')]
def pytest_addoption(parser): def pytest_addoption(parser):
parser.addoption("--XX", action="store_true", default=False) parser.addoption("--XX", action="store_true", default=False)
def pytest_configure(config): def pytest_configure(config):
@ -464,13 +463,13 @@ class TestSession:
config = pytester.parseconfig(id) config = pytester.parseconfig(id)
topdir = pytester.path topdir = pytester.path
rcol = Session.from_config(config) rcol = Session.from_config(config)
assert topdir == rcol.fspath assert topdir == rcol.path
# rootid = rcol.nodeid # rootid = rcol.nodeid
# root2 = rcol.perform_collect([rcol.nodeid], genitems=False)[0] # root2 = rcol.perform_collect([rcol.nodeid], genitems=False)[0]
# assert root2 == rcol, rootid # assert root2 == rcol, rootid
colitems = rcol.perform_collect([rcol.nodeid], genitems=False) colitems = rcol.perform_collect([rcol.nodeid], genitems=False)
assert len(colitems) == 1 assert len(colitems) == 1
assert colitems[0].fspath == p assert colitems[0].path == p
def get_reported_items(self, hookrec: HookRecorder) -> List[Item]: def get_reported_items(self, hookrec: HookRecorder) -> List[Item]:
"""Return pytest.Item instances reported by the pytest_collectreport hook""" """Return pytest.Item instances reported by the pytest_collectreport hook"""
@ -494,10 +493,10 @@ class TestSession:
topdir = pytester.path # noqa topdir = pytester.path # noqa
hookrec.assert_contains( hookrec.assert_contains(
[ [
("pytest_collectstart", "collector.fspath == topdir"), ("pytest_collectstart", "collector.path == topdir"),
("pytest_make_collect_report", "collector.fspath == topdir"), ("pytest_make_collect_report", "collector.path == topdir"),
("pytest_collectstart", "collector.fspath == p"), ("pytest_collectstart", "collector.path == p"),
("pytest_make_collect_report", "collector.fspath == p"), ("pytest_make_collect_report", "collector.path == p"),
("pytest_pycollect_makeitem", "name == 'test_func'"), ("pytest_pycollect_makeitem", "name == 'test_func'"),
("pytest_collectreport", "report.result[0].name == 'test_func'"), ("pytest_collectreport", "report.result[0].name == 'test_func'"),
] ]
@ -547,7 +546,7 @@ class TestSession:
assert len(items) == 2 assert len(items) == 2
hookrec.assert_contains( hookrec.assert_contains(
[ [
("pytest_collectstart", "collector.fspath == collector.session.fspath"), ("pytest_collectstart", "collector.path == collector.session.path"),
( (
"pytest_collectstart", "pytest_collectstart",
"collector.__class__.__name__ == 'SpecialFile'", "collector.__class__.__name__ == 'SpecialFile'",
@ -570,7 +569,7 @@ class TestSession:
pprint.pprint(hookrec.calls) pprint.pprint(hookrec.calls)
hookrec.assert_contains( hookrec.assert_contains(
[ [
("pytest_collectstart", "collector.fspath == test_aaa"), ("pytest_collectstart", "collector.path == test_aaa"),
("pytest_pycollect_makeitem", "name == 'test_func'"), ("pytest_pycollect_makeitem", "name == 'test_func'"),
("pytest_collectreport", "report.nodeid.startswith('aaa/test_aaa.py')"), ("pytest_collectreport", "report.nodeid.startswith('aaa/test_aaa.py')"),
] ]
@ -592,10 +591,10 @@ class TestSession:
pprint.pprint(hookrec.calls) pprint.pprint(hookrec.calls)
hookrec.assert_contains( hookrec.assert_contains(
[ [
("pytest_collectstart", "collector.fspath == test_aaa"), ("pytest_collectstart", "collector.path == test_aaa"),
("pytest_pycollect_makeitem", "name == 'test_func'"), ("pytest_pycollect_makeitem", "name == 'test_func'"),
("pytest_collectreport", "report.nodeid == 'aaa/test_aaa.py'"), ("pytest_collectreport", "report.nodeid == 'aaa/test_aaa.py'"),
("pytest_collectstart", "collector.fspath == test_bbb"), ("pytest_collectstart", "collector.path == test_bbb"),
("pytest_pycollect_makeitem", "name == 'test_func'"), ("pytest_pycollect_makeitem", "name == 'test_func'"),
("pytest_collectreport", "report.nodeid == 'bbb/test_bbb.py'"), ("pytest_collectreport", "report.nodeid == 'bbb/test_bbb.py'"),
] ]
@ -609,7 +608,9 @@ class TestSession:
items2, hookrec = pytester.inline_genitems(item.nodeid) items2, hookrec = pytester.inline_genitems(item.nodeid)
(item2,) = items2 (item2,) = items2
assert item2.name == item.name assert item2.name == item.name
assert item2.fspath == item.fspath with pytest.warns(DeprecationWarning):
assert item2.fspath == item.fspath
assert item2.path == item.path
def test_find_byid_without_instance_parents(self, pytester: Pytester) -> None: def test_find_byid_without_instance_parents(self, pytester: Pytester) -> None:
p = pytester.makepyfile( p = pytester.makepyfile(
@ -1345,18 +1346,15 @@ def test_fscollector_from_parent(pytester: Pytester, request: FixtureRequest) ->
Context: https://github.com/pytest-dev/pytest-cpp/pull/47 Context: https://github.com/pytest-dev/pytest-cpp/pull/47
""" """
from _pytest.compat import legacy_path
class MyCollector(pytest.File): class MyCollector(pytest.File):
def __init__(self, fspath, parent, x): def __init__(self, *k, x, **kw):
super().__init__(fspath, parent) super().__init__(*k, **kw)
self.x = x self.x = x
@classmethod
def from_parent(cls, parent, *, fspath, x):
return super().from_parent(parent=parent, fspath=fspath, x=x)
collector = MyCollector.from_parent( collector = MyCollector.from_parent(
parent=request.session, fspath=py.path.local(pytester.path) / "foo", x=10 parent=request.session, fspath=legacy_path(pytester.path) / "foo", x=10
) )
assert collector.x == 10 assert collector.x == 10

View File

@ -205,7 +205,7 @@ class TestResolveCollectionArgument:
def test_module_full_path_without_drive(pytester: Pytester) -> None: def test_module_full_path_without_drive(pytester: Pytester) -> None:
"""Collect and run test using full path except for the drive letter (#7628). """Collect and run test using full path except for the drive letter (#7628).
Passing a full path without a drive letter would trigger a bug in py.path.local Passing a full path without a drive letter would trigger a bug in legacy_path
where it would keep the full path without the drive letter around, instead of resolving where it would keep the full path without the drive letter around, instead of resolving
to the full path, resulting in fixtures node ids not matching against test node ids correctly. to the full path, resulting in fixtures node ids not matching against test node ids correctly.
""" """

View File

@ -1048,11 +1048,12 @@ def test_mark_expressions_no_smear(pytester: Pytester) -> None:
# assert skipped_k == failed_k == 0 # assert skipped_k == failed_k == 0
def test_addmarker_order() -> None: def test_addmarker_order(pytester) -> None:
session = mock.Mock() session = mock.Mock()
session.own_markers = [] session.own_markers = []
session.parent = None session.parent = None
session.nodeid = "" session.nodeid = ""
session.path = pytester.path
node = Node.from_parent(session, name="Test") node = Node.from_parent(session, name="Test")
node.add_marker("foo") node.add_marker("foo")
node.add_marker("bar") node.add_marker("bar")

View File

@ -3,10 +3,9 @@ from typing import cast
from typing import List from typing import List
from typing import Type from typing import Type
import py
import pytest import pytest
from _pytest import nodes from _pytest import nodes
from _pytest.compat import legacy_path
from _pytest.pytester import Pytester from _pytest.pytester import Pytester
from _pytest.warning_types import PytestWarning from _pytest.warning_types import PytestWarning
@ -77,7 +76,7 @@ def test__check_initialpaths_for_relpath() -> None:
session = cast(pytest.Session, FakeSession1) session = cast(pytest.Session, FakeSession1)
assert nodes._check_initialpaths_for_relpath(session, py.path.local(cwd)) == "" assert nodes._check_initialpaths_for_relpath(session, legacy_path(cwd)) == ""
sub = cwd / "file" sub = cwd / "file"
@ -86,9 +85,9 @@ def test__check_initialpaths_for_relpath() -> None:
session = cast(pytest.Session, FakeSession2) session = cast(pytest.Session, FakeSession2)
assert nodes._check_initialpaths_for_relpath(session, py.path.local(sub)) == "file" assert nodes._check_initialpaths_for_relpath(session, legacy_path(sub)) == "file"
outside = py.path.local("/outside") outside = legacy_path("/outside")
assert nodes._check_initialpaths_for_relpath(session, outside) is None assert nodes._check_initialpaths_for_relpath(session, outside) is None

View File

@ -4,9 +4,8 @@ import shlex
import subprocess import subprocess
import sys import sys
import py
import pytest import pytest
from _pytest.compat import legacy_path
from _pytest.config import argparsing as parseopt from _pytest.config import argparsing as parseopt
from _pytest.config.exceptions import UsageError from _pytest.config.exceptions import UsageError
from _pytest.monkeypatch import MonkeyPatch from _pytest.monkeypatch import MonkeyPatch
@ -124,11 +123,11 @@ class TestParser:
assert not getattr(args, parseopt.FILE_OR_DIR) assert not getattr(args, parseopt.FILE_OR_DIR)
def test_parse2(self, parser: parseopt.Parser) -> None: def test_parse2(self, parser: parseopt.Parser) -> None:
args = parser.parse([py.path.local()]) args = parser.parse([legacy_path(".")])
assert getattr(args, parseopt.FILE_OR_DIR)[0] == py.path.local() assert getattr(args, parseopt.FILE_OR_DIR)[0] == legacy_path(".")
def test_parse_known_args(self, parser: parseopt.Parser) -> None: def test_parse_known_args(self, parser: parseopt.Parser) -> None:
parser.parse_known_args([py.path.local()]) parser.parse_known_args([legacy_path(".")])
parser.addoption("--hello", action="store_true") parser.addoption("--hello", action="store_true")
ns = parser.parse_known_args(["x", "--y", "--hello", "this"]) ns = parser.parse_known_args(["x", "--y", "--hello", "this"])
assert ns.hello assert ns.hello

View File

@ -7,9 +7,8 @@ from textwrap import dedent
from types import ModuleType from types import ModuleType
from typing import Generator from typing import Generator
import py
import pytest import pytest
from _pytest.compat import legacy_path
from _pytest.monkeypatch import MonkeyPatch from _pytest.monkeypatch import MonkeyPatch
from _pytest.pathlib import bestrelpath from _pytest.pathlib import bestrelpath
from _pytest.pathlib import commonpath from _pytest.pathlib import commonpath
@ -28,14 +27,14 @@ from _pytest.tmpdir import TempPathFactory
class TestFNMatcherPort: class TestFNMatcherPort:
"""Test that our port of py.common.FNMatcher (fnmatch_ex) produces the """Test that our port of py.common.FNMatcher (fnmatch_ex) produces the
same results as the original py.path.local.fnmatch method.""" same results as the original legacy_path.fnmatch method."""
@pytest.fixture(params=["pathlib", "py.path"]) @pytest.fixture(params=["pathlib", "py.path"])
def match(self, request): def match(self, request):
if request.param == "py.path": if request.param == "py.path":
def match_(pattern, path): def match_(pattern, path):
return py.path.local(path).fnmatch(pattern) return legacy_path(path).fnmatch(pattern)
else: else:
assert request.param == "pathlib" assert request.param == "pathlib"

View File

@ -1,11 +1,10 @@
from typing import Sequence from typing import Sequence
from typing import Union from typing import Union
import py.path
import pytest import pytest
from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionChainRepr
from _pytest._code.code import ExceptionRepr from _pytest._code.code import ExceptionRepr
from _pytest.compat import legacy_path
from _pytest.config import Config from _pytest.config import Config
from _pytest.pytester import Pytester from _pytest.pytester import Pytester
from _pytest.reports import CollectReport from _pytest.reports import CollectReport
@ -237,7 +236,7 @@ class TestReportSerialization:
reports = reprec.getreports("pytest_runtest_logreport") reports = reprec.getreports("pytest_runtest_logreport")
assert len(reports) == 3 assert len(reports) == 3
test_a_call = reports[1] test_a_call = reports[1]
test_a_call.path1 = py.path.local(pytester.path) # type: ignore[attr-defined] test_a_call.path1 = legacy_path(pytester.path) # type: ignore[attr-defined]
test_a_call.path2 = pytester.path # type: ignore[attr-defined] test_a_call.path2 = pytester.path # type: ignore[attr-defined]
data = test_a_call._to_json() data = test_a_call._to_json()
assert data["path1"] == str(pytester.path) assert data["path1"] == str(pytester.path)

View File

@ -447,9 +447,9 @@ class TestSessionReports:
assert not rep.skipped assert not rep.skipped
assert rep.passed assert rep.passed
locinfo = rep.location locinfo = rep.location
assert locinfo[0] == col.fspath.basename assert locinfo[0] == col.path.name
assert not locinfo[1] assert not locinfo[1]
assert locinfo[2] == col.fspath.basename assert locinfo[2] == col.path.name
res = rep.result res = rep.result
assert len(res) == 2 assert len(res) == 2
assert res[0].name == "test_func1" assert res[0].name == "test_func1"

View File

@ -133,7 +133,7 @@ class TestTerminal:
item.config.pluginmanager.register(tr) item.config.pluginmanager.register(tr)
location = item.reportinfo() location = item.reportinfo()
tr.config.hook.pytest_runtest_logstart( tr.config.hook.pytest_runtest_logstart(
nodeid=item.nodeid, location=location, fspath=str(item.fspath) nodeid=item.nodeid, location=location, fspath=str(item.path)
) )
linecomp.assert_contains_lines(["*test_show_runtest_logstart.py*"]) linecomp.assert_contains_lines(["*test_show_runtest_logstart.py*"])