Merge pull request #12087 from nicoddemus/revert-path-deprecations

Revert legacy path removals
This commit is contained in:
Bruno Oliveira 2024-03-08 20:06:47 -03:00 committed by GitHub
commit 2ccc73be9a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 329 additions and 119 deletions

View File

@ -0,0 +1,8 @@
Delayed the deprecation of the following features to ``9.0.0``:
* :ref:`node-ctor-fspath-deprecation`.
* :ref:`legacy-path-hooks-deprecated`.
It was discovered after ``8.1.0`` was released that the warnings about the impeding removal were not being displayed, so the team decided to revert the removal.
This was the reason for ``8.1.0`` being yanked.

View File

@ -200,6 +200,7 @@ nitpick_ignore = [
("py:class", "_tracing.TagTracerSub"), ("py:class", "_tracing.TagTracerSub"),
("py:class", "warnings.WarningMessage"), ("py:class", "warnings.WarningMessage"),
# Undocumented type aliases # Undocumented type aliases
("py:class", "LEGACY_PATH"),
("py:class", "_PluggyPlugin"), ("py:class", "_PluggyPlugin"),
# TypeVars # TypeVars
("py:class", "_pytest._code.code.E"), ("py:class", "_pytest._code.code.E"),

View File

@ -19,7 +19,45 @@ Below is a complete list of all pytest features which are considered deprecated.
:class:`~pytest.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`. :class:`~pytest.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.
.. _legacy-path-hooks-deprecated: .. _node-ctor-fspath-deprecation:
``fspath`` argument for Node constructors replaced with ``pathlib.Path``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
In order to support the transition from ``py.path.local`` to :mod:`pathlib`,
the ``fspath`` argument to :class:`~_pytest.nodes.Node` constructors like
:func:`pytest.Function.from_parent()` and :func:`pytest.Class.from_parent()`
is now deprecated.
Plugins which construct nodes should pass the ``path`` argument, of type
:class:`pathlib.Path`, instead of the ``fspath`` argument.
Plugins which implement custom items and collectors are encouraged to replace
``fspath`` parameters (``py.path.local``) with ``path`` parameters
(``pathlib.Path``), and drop any other usage of the ``py`` library if possible.
If possible, plugins with custom items should use :ref:`cooperative
constructors <uncooperative-constructors-deprecated>` to avoid hardcoding
arguments they only pass on to the superclass.
.. note::
The name of the :class:`~_pytest.nodes.Node` arguments and attributes (the
new attribute being ``path``) is **the opposite** of the situation for
hooks, :ref:`outlined below <legacy-path-hooks-deprecated>` (the old
argument being ``path``).
This is an unfortunate artifact due to historical reasons, which should be
resolved in future versions as we slowly get rid of the :pypi:`py`
dependency (see :issue:`9283` for a longer discussion).
Due to the ongoing migration of methods like :meth:`~pytest.Item.reportinfo`
which still is expected to return a ``py.path.local`` object, nodes still have
both ``fspath`` (``py.path.local``) and ``path`` (``pathlib.Path``) attributes,
no matter what argument was used in the constructor. We expect to deprecate the
``fspath`` attribute in a future release.
Configuring hook specs/impls using markers Configuring hook specs/impls using markers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -62,6 +100,33 @@ Changed ``hookwrapper`` attributes:
* ``historic`` * ``historic``
.. _legacy-path-hooks-deprecated:
``py.path.local`` arguments for hooks replaced with ``pathlib.Path``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
In order to support the transition from ``py.path.local`` to :mod:`pathlib`, the following hooks now receive additional arguments:
* :hook:`pytest_ignore_collect(collection_path: pathlib.Path) <pytest_ignore_collect>` as equivalent to ``path``
* :hook:`pytest_collect_file(file_path: pathlib.Path) <pytest_collect_file>` as equivalent to ``path``
* :hook:`pytest_pycollect_makemodule(module_path: pathlib.Path) <pytest_pycollect_makemodule>` as equivalent to ``path``
* :hook:`pytest_report_header(start_path: pathlib.Path) <pytest_report_header>` as equivalent to ``startdir``
* :hook:`pytest_report_collectionfinish(start_path: pathlib.Path) <pytest_report_collectionfinish>` as equivalent to ``startdir``
The accompanying ``py.path.local`` based paths have been deprecated: plugins which manually invoke those hooks should only pass the new ``pathlib.Path`` arguments, and users should change their hook implementations to use the new ``pathlib.Path`` arguments.
.. note::
The name of the :class:`~_pytest.nodes.Node` arguments and attributes,
:ref:`outlined above <node-ctor-fspath-deprecation>` (the new attribute
being ``path``) is **the opposite** of the situation for hooks (the old
argument being ``path``).
This is an unfortunate artifact due to historical reasons, which should be
resolved in future versions as we slowly get rid of the :pypi:`py`
dependency (see :issue:`9283` for a longer discussion).
Directly constructing internal classes Directly constructing internal classes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -208,73 +273,6 @@ an appropriate period of deprecation has passed.
Some breaking changes which could not be deprecated are also listed. Some breaking changes which could not be deprecated are also listed.
.. _node-ctor-fspath-deprecation:
``fspath`` argument for Node constructors replaced with ``pathlib.Path``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
In order to support the transition from ``py.path.local`` to :mod:`pathlib`,
the ``fspath`` argument to :class:`~_pytest.nodes.Node` constructors like
:func:`pytest.Function.from_parent()` and :func:`pytest.Class.from_parent()`
is now deprecated.
Plugins which construct nodes should pass the ``path`` argument, of type
:class:`pathlib.Path`, instead of the ``fspath`` argument.
Plugins which implement custom items and collectors are encouraged to replace
``fspath`` parameters (``py.path.local``) with ``path`` parameters
(``pathlib.Path``), and drop any other usage of the ``py`` library if possible.
If possible, plugins with custom items should use :ref:`cooperative
constructors <uncooperative-constructors-deprecated>` to avoid hardcoding
arguments they only pass on to the superclass.
.. note::
The name of the :class:`~_pytest.nodes.Node` arguments and attributes (the
new attribute being ``path``) is **the opposite** of the situation for
hooks, :ref:`outlined below <legacy-path-hooks-deprecated>` (the old
argument being ``path``).
This is an unfortunate artifact due to historical reasons, which should be
resolved in future versions as we slowly get rid of the :pypi:`py`
dependency (see :issue:`9283` for a longer discussion).
Due to the ongoing migration of methods like :meth:`~pytest.Item.reportinfo`
which still is expected to return a ``py.path.local`` object, nodes still have
both ``fspath`` (``py.path.local``) and ``path`` (``pathlib.Path``) attributes,
no matter what argument was used in the constructor. We expect to deprecate the
``fspath`` attribute in a future release.
``py.path.local`` arguments for hooks replaced with ``pathlib.Path``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
.. versionremoved:: 8.0
In order to support the transition from ``py.path.local`` to :mod:`pathlib`, the following hooks now receive additional arguments:
* :hook:`pytest_ignore_collect(collection_path: pathlib.Path) <pytest_ignore_collect>` as equivalent to ``path``
* :hook:`pytest_collect_file(file_path: pathlib.Path) <pytest_collect_file>` as equivalent to ``path``
* :hook:`pytest_pycollect_makemodule(module_path: pathlib.Path) <pytest_pycollect_makemodule>` as equivalent to ``path``
* :hook:`pytest_report_header(start_path: pathlib.Path) <pytest_report_header>` as equivalent to ``startdir``
* :hook:`pytest_report_collectionfinish(start_path: pathlib.Path) <pytest_report_collectionfinish>` as equivalent to ``startdir``
The accompanying ``py.path.local`` based paths have been deprecated: plugins which manually invoke those hooks should only pass the new ``pathlib.Path`` arguments, and users should change their hook implementations to use the new ``pathlib.Path`` arguments.
.. note::
The name of the :class:`~_pytest.nodes.Node` arguments and attributes,
:ref:`outlined above <node-ctor-fspath-deprecation>` (the new attribute
being ``path``) is **the opposite** of the situation for hooks (the old
argument being ``path``).
This is an unfortunate artifact due to historical reasons, which should be
resolved in future versions as we slowly get rid of the :pypi:`py`
dependency (see :issue:`9283` for a longer discussion).
.. _nose-deprecation: .. _nose-deprecation:
Support for tests written for nose Support for tests written for nose

View File

@ -1,5 +1,6 @@
# mypy: allow-untyped-defs # mypy: allow-untyped-defs
"""Python version compatibility code.""" """Python version compatibility code."""
from __future__ import annotations from __future__ import annotations
import dataclasses import dataclasses
@ -16,6 +17,22 @@ from typing import Callable
from typing import Final from typing import Final
from typing import NoReturn from typing import NoReturn
import py
#: 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: 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

@ -38,12 +38,14 @@ from typing import TYPE_CHECKING
from typing import Union from typing import Union
import warnings import warnings
import pluggy
from pluggy import HookimplMarker from pluggy import HookimplMarker
from pluggy import HookimplOpts from pluggy import HookimplOpts
from pluggy import HookspecMarker from pluggy import HookspecMarker
from pluggy import HookspecOpts from pluggy import HookspecOpts
from pluggy import PluginManager from pluggy import PluginManager
from .compat import PathAwareHookProxy
from .exceptions import PrintHelp as PrintHelp from .exceptions import PrintHelp as PrintHelp
from .exceptions import UsageError as UsageError from .exceptions import UsageError as UsageError
from .findpaths import determine_setup from .findpaths import determine_setup
@ -1068,7 +1070,7 @@ class Config:
self._store = self.stash self._store = self.stash
self.trace = self.pluginmanager.trace.root.get("config") self.trace = self.pluginmanager.trace.root.get("config")
self.hook = self.pluginmanager.hook # type: ignore[assignment] self.hook: pluggy.HookRelay = PathAwareHookProxy(self.pluginmanager.hook) # type: ignore[assignment]
self._inicache: Dict[str, Any] = {} self._inicache: Dict[str, Any] = {}
self._override_ini: Sequence[str] = () self._override_ini: Sequence[str] = ()
self._opt2dest: Dict[str, str] = {} self._opt2dest: Dict[str, str] = {}

View File

@ -0,0 +1,85 @@
from __future__ import annotations
import functools
from pathlib import Path
from typing import Any
from typing import Mapping
import warnings
import pluggy
from ..compat import LEGACY_PATH
from ..compat import legacy_path
from ..deprecated import HOOK_LEGACY_PATH_ARG
# hookname: (Path, LEGACY_PATH)
imply_paths_hooks: Mapping[str, tuple[str, str]] = {
"pytest_ignore_collect": ("collection_path", "path"),
"pytest_collect_file": ("file_path", "path"),
"pytest_pycollect_makemodule": ("module_path", "path"),
"pytest_report_header": ("start_path", "startdir"),
"pytest_report_collectionfinish": ("start_path", "startdir"),
}
def _check_path(path: Path, fspath: LEGACY_PATH) -> 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"
)
class PathAwareHookProxy:
"""
this helper wraps around hook callers
until pluggy supports fixingcalls, this one will do
it currently doesn't return full hook caller proxies for fixed hooks,
this may have to be changed later depending on bugs
"""
def __init__(self, hook_relay: pluggy.HookRelay) -> None:
self._hook_relay = hook_relay
def __dir__(self) -> list[str]:
return dir(self._hook_relay)
def __getattr__(self, key: str) -> pluggy.HookCaller:
hook: pluggy.HookCaller = getattr(self._hook_relay, key)
if key not in imply_paths_hooks:
self.__dict__[key] = hook
return hook
else:
path_var, fspath_var = imply_paths_hooks[key]
@functools.wraps(hook)
def fixed_hook(**kw: Any) -> Any:
path_value: Path | None = kw.pop(path_var, None)
fspath_value: LEGACY_PATH | None = kw.pop(fspath_var, None)
if fspath_value is not None:
warnings.warn(
HOOK_LEGACY_PATH_ARG.format(
pylib_path_arg=fspath_var, pathlib_path_arg=path_var
),
stacklevel=2,
)
if path_value is not None:
if fspath_value is not None:
_check_path(path_value, fspath_value)
else:
fspath_value = legacy_path(path_value)
else:
assert fspath_value is not None
path_value = Path(fspath_value)
kw[path_var] = path_value
kw[fspath_var] = fspath_value
return hook(**kw)
fixed_hook.name = hook.name # type: ignore[attr-defined]
fixed_hook.spec = hook.spec # type: ignore[attr-defined]
fixed_hook.__name__ = key
self.__dict__[key] = fixed_hook
return fixed_hook # type: ignore[return-value]

View File

@ -36,6 +36,21 @@ YIELD_FIXTURE = PytestDeprecationWarning(
PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.") PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.")
HOOK_LEGACY_PATH_ARG = UnformattedWarning(
PytestRemovedIn9Warning,
"The ({pylib_path_arg}: py.path.local) argument is deprecated, please use ({pathlib_path_arg}: pathlib.Path)\n"
"see https://docs.pytest.org/en/latest/deprecations.html"
"#py-path-local-arguments-for-hooks-replaced-with-pathlib-path",
)
NODE_CTOR_FSPATH_ARG = UnformattedWarning(
PytestRemovedIn9Warning,
"The (fspath: py.path.local) argument to {node_type_name} is deprecated. "
"Please use the (path: pathlib.Path) argument instead.\n"
"See https://docs.pytest.org/en/latest/deprecations.html"
"#fspath-argument-for-node-constructors-replaced-with-pathlib-path",
)
HOOK_LEGACY_MARKING = UnformattedWarning( HOOK_LEGACY_MARKING = UnformattedWarning(
PytestDeprecationWarning, PytestDeprecationWarning,
"The hook{type} {fullname} uses old-style configuration options (marks or attributes).\n" "The hook{type} {fullname} uses old-style configuration options (marks or attributes).\n"

View File

@ -22,6 +22,7 @@ if TYPE_CHECKING:
from _pytest._code.code import ExceptionInfo from _pytest._code.code import ExceptionInfo
from _pytest._code.code import ExceptionRepr from _pytest._code.code import ExceptionRepr
from _pytest.compat import LEGACY_PATH
from _pytest.config import _PluggyPlugin from _pytest.config import _PluggyPlugin
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import ExitCode from _pytest.config import ExitCode
@ -296,7 +297,9 @@ def pytest_collection_finish(session: "Session") -> None:
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_ignore_collect(collection_path: Path, config: "Config") -> Optional[bool]: def pytest_ignore_collect(
collection_path: Path, path: "LEGACY_PATH", config: "Config"
) -> Optional[bool]:
"""Return True to prevent considering this path for collection. """Return True to prevent considering this path for collection.
This hook is consulted for all files and directories prior to calling This hook is consulted for all files and directories prior to calling
@ -310,10 +313,8 @@ def pytest_ignore_collect(collection_path: Path, config: "Config") -> Optional[b
.. versionchanged:: 7.0.0 .. versionchanged:: 7.0.0
The ``collection_path`` parameter was added as a :class:`pathlib.Path` The ``collection_path`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``path`` parameter. equivalent of the ``path`` parameter. The ``path`` parameter
has been deprecated.
.. versionchanged:: 8.0.0
The ``path`` parameter has been removed.
Use in conftest plugins Use in conftest plugins
======================= =======================
@ -354,7 +355,9 @@ def pytest_collect_directory(path: Path, parent: "Collector") -> "Optional[Colle
""" """
def pytest_collect_file(file_path: Path, parent: "Collector") -> "Optional[Collector]": def pytest_collect_file(
file_path: Path, path: "LEGACY_PATH", parent: "Collector"
) -> "Optional[Collector]":
"""Create a :class:`~pytest.Collector` for the given path, or None if not relevant. """Create a :class:`~pytest.Collector` for the given path, or None if not relevant.
For best results, the returned collector should be a subclass of For best results, the returned collector should be a subclass of
@ -367,10 +370,8 @@ def pytest_collect_file(file_path: Path, parent: "Collector") -> "Optional[Colle
.. versionchanged:: 7.0.0 .. versionchanged:: 7.0.0
The ``file_path`` parameter was added as a :class:`pathlib.Path` The ``file_path`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``path`` parameter. equivalent of the ``path`` parameter. The ``path`` parameter
has been deprecated.
.. versionchanged:: 8.0.0
The ``path`` parameter was removed.
Use in conftest plugins Use in conftest plugins
======================= =======================
@ -467,7 +468,9 @@ def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectRepor
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_pycollect_makemodule(module_path: Path, parent) -> Optional["Module"]: def pytest_pycollect_makemodule(
module_path: Path, path: "LEGACY_PATH", parent
) -> Optional["Module"]:
"""Return a :class:`pytest.Module` collector or None for the given path. """Return a :class:`pytest.Module` collector or None for the given path.
This hook will be called for each matching test module path. This hook will be called for each matching test module path.
@ -483,8 +486,7 @@ def pytest_pycollect_makemodule(module_path: Path, parent) -> Optional["Module"]
The ``module_path`` parameter was added as a :class:`pathlib.Path` The ``module_path`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``path`` parameter. equivalent of the ``path`` parameter.
.. versionchanged:: 8.0.0 The ``path`` parameter has been deprecated in favor of ``fspath``.
The ``path`` parameter has been removed in favor of ``module_path``.
Use in conftest plugins Use in conftest plugins
======================= =======================
@ -992,7 +994,7 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No
def pytest_report_header( # type:ignore[empty-body] def pytest_report_header( # type:ignore[empty-body]
config: "Config", start_path: Path config: "Config", start_path: 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.
@ -1009,10 +1011,8 @@ def pytest_report_header( # type:ignore[empty-body]
.. versionchanged:: 7.0.0 .. versionchanged:: 7.0.0
The ``start_path`` parameter was added as a :class:`pathlib.Path` The ``start_path`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``startdir`` parameter. equivalent of the ``startdir`` parameter. The ``startdir`` parameter
has been deprecated.
.. versionchanged:: 8.0.0
The ``startdir`` parameter has been removed.
Use in conftest plugins Use in conftest plugins
======================= =======================
@ -1024,6 +1024,7 @@ def pytest_report_header( # type:ignore[empty-body]
def pytest_report_collectionfinish( # type:ignore[empty-body] def pytest_report_collectionfinish( # type:ignore[empty-body]
config: "Config", config: "Config",
start_path: Path, start_path: Path,
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
@ -1047,10 +1048,8 @@ def pytest_report_collectionfinish( # type:ignore[empty-body]
.. versionchanged:: 7.0.0 .. versionchanged:: 7.0.0
The ``start_path`` parameter was added as a :class:`pathlib.Path` The ``start_path`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``startdir`` parameter. equivalent of the ``startdir`` parameter. The ``startdir`` parameter
has been deprecated.
.. versionchanged:: 8.0.0
The ``startdir`` parameter has been removed.
Use in conftest plugins Use in conftest plugins
======================= =======================

View File

@ -1,7 +1,7 @@
# mypy: allow-untyped-defs # mypy: allow-untyped-defs
"""Add backward compatibility support for the legacy py path type.""" """Add backward compatibility support for the legacy py path type."""
import dataclasses import dataclasses
import os
from pathlib import Path from pathlib import Path
import shlex import shlex
import subprocess import subprocess
@ -14,9 +14,9 @@ from typing import Union
from iniconfig import SectionWrapper from iniconfig import SectionWrapper
import py
from _pytest.cacheprovider import Cache from _pytest.cacheprovider import Cache
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 hookimpl from _pytest.config import hookimpl
from _pytest.config import PytestPluginManager from _pytest.config import PytestPluginManager
@ -39,20 +39,6 @@ if TYPE_CHECKING:
import pexpect import pexpect
#: 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)
@final @final
class Testdir: class Testdir:
""" """

View File

@ -37,6 +37,7 @@ from _pytest.config import hookimpl
from _pytest.config import PytestPluginManager from _pytest.config import PytestPluginManager
from _pytest.config import UsageError from _pytest.config import UsageError
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
from _pytest.config.compat import PathAwareHookProxy
from _pytest.fixtures import FixtureManager from _pytest.fixtures import FixtureManager
from _pytest.outcomes import exit from _pytest.outcomes import exit
from _pytest.pathlib import absolutepath from _pytest.pathlib import absolutepath
@ -557,6 +558,7 @@ class Session(nodes.Collector):
super().__init__( super().__init__(
name="", name="",
path=config.rootpath, path=config.rootpath,
fspath=None,
parent=None, parent=None,
config=config, config=config,
session=self, session=self,
@ -694,7 +696,7 @@ class Session(nodes.Collector):
proxy: pluggy.HookRelay proxy: pluggy.HookRelay
if remove_mods: if remove_mods:
# One or more conftests are not in use at this path. # One or more conftests are not in use at this path.
proxy = FSHookProxy(pm, remove_mods) # type: ignore[arg-type,assignment] proxy = PathAwareHookProxy(FSHookProxy(pm, remove_mods)) # type: ignore[arg-type,assignment]
else: else:
# All plugins are active for this fspath. # All plugins are active for this fspath.
proxy = self.config.hook proxy = self.config.hook

View File

@ -3,6 +3,7 @@ import abc
from functools import cached_property from functools import cached_property
from inspect import signature from inspect import signature
import os import os
import pathlib
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from typing import Callable from typing import Callable
@ -29,8 +30,11 @@ 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._code.code import Traceback from _pytest._code.code import Traceback
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.config.compat import _check_path
from _pytest.deprecated import NODE_CTOR_FSPATH_ARG
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
@ -55,6 +59,29 @@ tracebackcutdir = Path(_pytest.__file__).parent
_T = TypeVar("_T") _T = TypeVar("_T")
def _imply_path(
node_type: Type["Node"],
path: Optional[Path],
fspath: Optional[LEGACY_PATH],
) -> Path:
if fspath is not None:
warnings.warn(
NODE_CTOR_FSPATH_ARG.format(
node_type_name=node_type.__name__,
),
stacklevel=6,
)
if path is not None:
if fspath is not None:
_check_path(path, fspath)
return path
else:
assert fspath is not None
return Path(fspath)
_NodeType = TypeVar("_NodeType", bound="Node") _NodeType = TypeVar("_NodeType", bound="Node")
@ -110,6 +137,13 @@ class Node(abc.ABC, metaclass=NodeMeta):
leaf nodes. leaf nodes.
""" """
# Implemented in the legacypath plugin.
#: A ``LEGACY_PATH`` copy of the :attr:`path` attribute. Intended for usage
#: for methods not migrated to ``pathlib.Path`` yet, such as
#: :meth:`Item.reportinfo <pytest.Item.reportinfo>`. Will be deprecated in
#: a future release, prefer using :attr:`path` instead.
fspath: LEGACY_PATH
# Use __slots__ to make attribute access faster. # Use __slots__ to make attribute access faster.
# Note that __dict__ is still available. # Note that __dict__ is still available.
__slots__ = ( __slots__ = (
@ -129,6 +163,7 @@ class Node(abc.ABC, 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[LEGACY_PATH] = None,
path: Optional[Path] = None, path: Optional[Path] = None,
nodeid: Optional[str] = None, nodeid: Optional[str] = None,
) -> None: ) -> None:
@ -154,11 +189,10 @@ class Node(abc.ABC, metaclass=NodeMeta):
raise TypeError("session or parent must be provided") raise TypeError("session or parent must be provided")
self.session = parent.session self.session = parent.session
if path is None: if path is None and fspath is None:
path = getattr(parent, "path", None) path = getattr(parent, "path", None)
assert path is not None
#: Filesystem path where this node was collected from (can be None). #: Filesystem path where this node was collected from (can be None).
self.path = path self.path: pathlib.Path = _imply_path(type(self), path, fspath=fspath)
# 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.
@ -529,6 +563,7 @@ class FSCollector(Collector, abc.ABC):
def __init__( def __init__(
self, self,
fspath: Optional[LEGACY_PATH] = None,
path_or_parent: Optional[Union[Path, Node]] = None, path_or_parent: Optional[Union[Path, Node]] = None,
path: Optional[Path] = None, path: Optional[Path] = None,
name: Optional[str] = None, name: Optional[str] = None,
@ -544,8 +579,8 @@ class FSCollector(Collector, abc.ABC):
elif isinstance(path_or_parent, Path): elif isinstance(path_or_parent, Path):
assert path is None assert path is None
path = path_or_parent path = path_or_parent
assert path is not None
path = _imply_path(type(self), path, fspath=fspath)
if name is None: if name is None:
name = path.name name = path.name
if parent is not None and parent.path != path: if parent is not None and parent.path != path:
@ -585,11 +620,12 @@ class FSCollector(Collector, abc.ABC):
cls, cls,
parent, parent,
*, *,
fspath: Optional[LEGACY_PATH] = None,
path: Optional[Path] = None, path: Optional[Path] = None,
**kw, **kw,
) -> "Self": ) -> "Self":
"""The public constructor.""" """The public constructor."""
return super().from_parent(parent=parent, path=path, **kw) return super().from_parent(parent=parent, fspath=fspath, path=path, **kw)
class File(FSCollector, abc.ABC): class File(FSCollector, abc.ABC):

View File

@ -48,6 +48,7 @@ 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 NOTSET from _pytest.compat import NOTSET
from _pytest.compat import safe_getattr from _pytest.compat import safe_getattr
from _pytest.compat import safe_isclass from _pytest.compat import safe_isclass
@ -665,6 +666,7 @@ class Package(nodes.Directory):
def __init__( def __init__(
self, self,
fspath: Optional[LEGACY_PATH],
parent: nodes.Collector, parent: nodes.Collector,
# NOTE: following args are unused: # NOTE: following args are unused:
config=None, config=None,
@ -676,6 +678,7 @@ class Package(nodes.Directory):
# super().__init__(self, fspath, parent=parent) # super().__init__(self, fspath, parent=parent)
session = parent.session session = parent.session
super().__init__( super().__init__(
fspath=fspath,
path=path, path=path,
parent=parent, parent=parent,
config=config, config=config,

View File

@ -1,5 +1,10 @@
# mypy: allow-untyped-defs # mypy: allow-untyped-defs
from pathlib import Path
import re
import sys
from _pytest import deprecated from _pytest import deprecated
from _pytest.compat import legacy_path
from _pytest.pytester import Pytester from _pytest.pytester import Pytester
import pytest import pytest
from pytest import PytestDeprecationWarning from pytest import PytestDeprecationWarning
@ -85,6 +90,56 @@ def test_private_is_deprecated() -> None:
PrivateInit(10, _ispytest=True) PrivateInit(10, _ispytest=True)
@pytest.mark.parametrize("hooktype", ["hook", "ihook"])
def test_hookproxy_warnings_for_pathlib(tmp_path, hooktype, request):
path = legacy_path(tmp_path)
PATH_WARN_MATCH = r".*path: py\.path\.local\) argument is deprecated, please use \(collection_path: pathlib\.Path.*"
if hooktype == "ihook":
hooks = request.node.ihook
else:
hooks = request.config.hook
with pytest.warns(PytestDeprecationWarning, match=PATH_WARN_MATCH) as r:
l1 = sys._getframe().f_lineno
hooks.pytest_ignore_collect(
config=request.config, path=path, collection_path=tmp_path
)
l2 = sys._getframe().f_lineno
(record,) = r
assert record.filename == __file__
assert l1 < record.lineno < l2
hooks.pytest_ignore_collect(config=request.config, collection_path=tmp_path)
# Passing entirely *different* paths is an outright error.
with pytest.raises(ValueError, match=r"path.*fspath.*need to be equal"):
with pytest.warns(PytestDeprecationWarning, match=PATH_WARN_MATCH) as r:
hooks.pytest_ignore_collect(
config=request.config, path=path, collection_path=Path("/bla/bla")
)
def test_node_ctor_fspath_argument_is_deprecated(pytester: Pytester) -> None:
mod = pytester.getmodulecol("")
class MyFile(pytest.File):
def collect(self):
raise NotImplementedError()
with pytest.warns(
pytest.PytestDeprecationWarning,
match=re.escape(
"The (fspath: py.path.local) argument to MyFile is deprecated."
),
):
MyFile.from_parent(
parent=mod.parent,
fspath=legacy_path("bla"),
)
def test_fixture_disallow_on_marked_functions(): def test_fixture_disallow_on_marked_functions():
"""Test that applying @pytest.fixture to a marked function warns (#3364).""" """Test that applying @pytest.fixture to a marked function warns (#3364)."""
with pytest.warns( with pytest.warns(

View File

@ -1,8 +1,8 @@
# mypy: allow-untyped-defs # mypy: allow-untyped-defs
from pathlib import Path from pathlib import Path
from _pytest.compat import LEGACY_PATH
from _pytest.fixtures import TopRequest from _pytest.fixtures import TopRequest
from _pytest.legacypath import LEGACY_PATH
from _pytest.legacypath import TempdirFactory from _pytest.legacypath import TempdirFactory
from _pytest.legacypath import Testdir from _pytest.legacypath import Testdir
import pytest import pytest
@ -16,7 +16,7 @@ def test_item_fspath(pytester: pytest.Pytester) -> None:
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 # type: ignore[attr-defined] assert item2.fspath == item.fspath
assert item2.path == item.path assert item2.path == item.path

View File

@ -6,6 +6,7 @@ from typing import Type
import warnings import warnings
from _pytest import nodes from _pytest import nodes
from _pytest.compat import legacy_path
from _pytest.outcomes import OutcomeException from _pytest.outcomes import OutcomeException
from _pytest.pytester import Pytester from _pytest.pytester import Pytester
from _pytest.warning_types import PytestWarning from _pytest.warning_types import PytestWarning
@ -44,9 +45,9 @@ def test_subclassing_both_item_and_collector_deprecated(
warnings.simplefilter("error") warnings.simplefilter("error")
class SoWrong(nodes.Item, nodes.File): class SoWrong(nodes.Item, nodes.File):
def __init__(self, path, parent): def __init__(self, fspath, parent):
"""Legacy ctor with legacy call # don't wana see""" """Legacy ctor with legacy call # don't wana see"""
super().__init__(parent, path) super().__init__(fspath, parent)
def collect(self): def collect(self):
raise NotImplementedError() raise NotImplementedError()
@ -55,7 +56,9 @@ def test_subclassing_both_item_and_collector_deprecated(
raise NotImplementedError() raise NotImplementedError()
with pytest.warns(PytestWarning) as rec: with pytest.warns(PytestWarning) as rec:
SoWrong.from_parent(request.session, path=tmp_path / "broken.txt", wrong=10) SoWrong.from_parent(
request.session, fspath=legacy_path(tmp_path / "broken.txt")
)
messages = [str(x.message) for x in rec] messages = [str(x.message) for x in rec]
assert any( assert any(
re.search(".*SoWrong.* not using a cooperative constructor.*", x) re.search(".*SoWrong.* not using a cooperative constructor.*", x)