Skip to content

Commit 15fbe57

Browse files
[8.1.x] Revert legacy path removals (#12093)
Co-authored-by: Bruno Oliveira <[email protected]>
1 parent 86c3aab commit 15fbe57

15 files changed

+329
-119
lines changed

changelog/12069.trivial.rst

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Delayed the deprecation of the following features to ``9.0.0``:
2+
3+
* :ref:`node-ctor-fspath-deprecation`.
4+
* :ref:`legacy-path-hooks-deprecated`.
5+
6+
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.
7+
8+
This was the reason for ``8.1.0`` being yanked.

doc/en/conf.py

+1
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@
200200
("py:class", "_tracing.TagTracerSub"),
201201
("py:class", "warnings.WarningMessage"),
202202
# Undocumented type aliases
203+
("py:class", "LEGACY_PATH"),
203204
("py:class", "_PluggyPlugin"),
204205
# TypeVars
205206
("py:class", "_pytest._code.code.E"),

doc/en/deprecations.rst

+66-68
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,45 @@ Below is a complete list of all pytest features which are considered deprecated.
1919
:class:`~pytest.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.
2020

2121

22-
.. _legacy-path-hooks-deprecated:
22+
.. _node-ctor-fspath-deprecation:
23+
24+
``fspath`` argument for Node constructors replaced with ``pathlib.Path``
25+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
26+
27+
.. deprecated:: 7.0
28+
29+
In order to support the transition from ``py.path.local`` to :mod:`pathlib`,
30+
the ``fspath`` argument to :class:`~_pytest.nodes.Node` constructors like
31+
:func:`pytest.Function.from_parent()` and :func:`pytest.Class.from_parent()`
32+
is now deprecated.
33+
34+
Plugins which construct nodes should pass the ``path`` argument, of type
35+
:class:`pathlib.Path`, instead of the ``fspath`` argument.
36+
37+
Plugins which implement custom items and collectors are encouraged to replace
38+
``fspath`` parameters (``py.path.local``) with ``path`` parameters
39+
(``pathlib.Path``), and drop any other usage of the ``py`` library if possible.
40+
41+
If possible, plugins with custom items should use :ref:`cooperative
42+
constructors <uncooperative-constructors-deprecated>` to avoid hardcoding
43+
arguments they only pass on to the superclass.
44+
45+
.. note::
46+
The name of the :class:`~_pytest.nodes.Node` arguments and attributes (the
47+
new attribute being ``path``) is **the opposite** of the situation for
48+
hooks, :ref:`outlined below <legacy-path-hooks-deprecated>` (the old
49+
argument being ``path``).
50+
51+
This is an unfortunate artifact due to historical reasons, which should be
52+
resolved in future versions as we slowly get rid of the :pypi:`py`
53+
dependency (see :issue:`9283` for a longer discussion).
54+
55+
Due to the ongoing migration of methods like :meth:`~pytest.Item.reportinfo`
56+
which still is expected to return a ``py.path.local`` object, nodes still have
57+
both ``fspath`` (``py.path.local``) and ``path`` (``pathlib.Path``) attributes,
58+
no matter what argument was used in the constructor. We expect to deprecate the
59+
``fspath`` attribute in a future release.
60+
2361

2462
Configuring hook specs/impls using markers
2563
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -62,6 +100,33 @@ Changed ``hookwrapper`` attributes:
62100
* ``historic``
63101

64102

103+
.. _legacy-path-hooks-deprecated:
104+
105+
``py.path.local`` arguments for hooks replaced with ``pathlib.Path``
106+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
107+
108+
.. deprecated:: 7.0
109+
110+
In order to support the transition from ``py.path.local`` to :mod:`pathlib`, the following hooks now receive additional arguments:
111+
112+
* :hook:`pytest_ignore_collect(collection_path: pathlib.Path) <pytest_ignore_collect>` as equivalent to ``path``
113+
* :hook:`pytest_collect_file(file_path: pathlib.Path) <pytest_collect_file>` as equivalent to ``path``
114+
* :hook:`pytest_pycollect_makemodule(module_path: pathlib.Path) <pytest_pycollect_makemodule>` as equivalent to ``path``
115+
* :hook:`pytest_report_header(start_path: pathlib.Path) <pytest_report_header>` as equivalent to ``startdir``
116+
* :hook:`pytest_report_collectionfinish(start_path: pathlib.Path) <pytest_report_collectionfinish>` as equivalent to ``startdir``
117+
118+
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.
119+
120+
.. note::
121+
The name of the :class:`~_pytest.nodes.Node` arguments and attributes,
122+
:ref:`outlined above <node-ctor-fspath-deprecation>` (the new attribute
123+
being ``path``) is **the opposite** of the situation for hooks (the old
124+
argument being ``path``).
125+
126+
This is an unfortunate artifact due to historical reasons, which should be
127+
resolved in future versions as we slowly get rid of the :pypi:`py`
128+
dependency (see :issue:`9283` for a longer discussion).
129+
65130
Directly constructing internal classes
66131
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
67132

@@ -208,73 +273,6 @@ an appropriate period of deprecation has passed.
208273

209274
Some breaking changes which could not be deprecated are also listed.
210275

211-
.. _node-ctor-fspath-deprecation:
212-
213-
``fspath`` argument for Node constructors replaced with ``pathlib.Path``
214-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
215-
216-
.. deprecated:: 7.0
217-
218-
In order to support the transition from ``py.path.local`` to :mod:`pathlib`,
219-
the ``fspath`` argument to :class:`~_pytest.nodes.Node` constructors like
220-
:func:`pytest.Function.from_parent()` and :func:`pytest.Class.from_parent()`
221-
is now deprecated.
222-
223-
Plugins which construct nodes should pass the ``path`` argument, of type
224-
:class:`pathlib.Path`, instead of the ``fspath`` argument.
225-
226-
Plugins which implement custom items and collectors are encouraged to replace
227-
``fspath`` parameters (``py.path.local``) with ``path`` parameters
228-
(``pathlib.Path``), and drop any other usage of the ``py`` library if possible.
229-
230-
If possible, plugins with custom items should use :ref:`cooperative
231-
constructors <uncooperative-constructors-deprecated>` to avoid hardcoding
232-
arguments they only pass on to the superclass.
233-
234-
.. note::
235-
The name of the :class:`~_pytest.nodes.Node` arguments and attributes (the
236-
new attribute being ``path``) is **the opposite** of the situation for
237-
hooks, :ref:`outlined below <legacy-path-hooks-deprecated>` (the old
238-
argument being ``path``).
239-
240-
This is an unfortunate artifact due to historical reasons, which should be
241-
resolved in future versions as we slowly get rid of the :pypi:`py`
242-
dependency (see :issue:`9283` for a longer discussion).
243-
244-
Due to the ongoing migration of methods like :meth:`~pytest.Item.reportinfo`
245-
which still is expected to return a ``py.path.local`` object, nodes still have
246-
both ``fspath`` (``py.path.local``) and ``path`` (``pathlib.Path``) attributes,
247-
no matter what argument was used in the constructor. We expect to deprecate the
248-
``fspath`` attribute in a future release.
249-
250-
251-
``py.path.local`` arguments for hooks replaced with ``pathlib.Path``
252-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
253-
254-
.. deprecated:: 7.0
255-
.. versionremoved:: 8.0
256-
257-
In order to support the transition from ``py.path.local`` to :mod:`pathlib`, the following hooks now receive additional arguments:
258-
259-
* :hook:`pytest_ignore_collect(collection_path: pathlib.Path) <pytest_ignore_collect>` as equivalent to ``path``
260-
* :hook:`pytest_collect_file(file_path: pathlib.Path) <pytest_collect_file>` as equivalent to ``path``
261-
* :hook:`pytest_pycollect_makemodule(module_path: pathlib.Path) <pytest_pycollect_makemodule>` as equivalent to ``path``
262-
* :hook:`pytest_report_header(start_path: pathlib.Path) <pytest_report_header>` as equivalent to ``startdir``
263-
* :hook:`pytest_report_collectionfinish(start_path: pathlib.Path) <pytest_report_collectionfinish>` as equivalent to ``startdir``
264-
265-
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.
266-
267-
.. note::
268-
The name of the :class:`~_pytest.nodes.Node` arguments and attributes,
269-
:ref:`outlined above <node-ctor-fspath-deprecation>` (the new attribute
270-
being ``path``) is **the opposite** of the situation for hooks (the old
271-
argument being ``path``).
272-
273-
This is an unfortunate artifact due to historical reasons, which should be
274-
resolved in future versions as we slowly get rid of the :pypi:`py`
275-
dependency (see :issue:`9283` for a longer discussion).
276-
277-
278276
.. _nose-deprecation:
279277

280278
Support for tests written for nose

src/_pytest/compat.py

+17
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# mypy: allow-untyped-defs
22
"""Python version compatibility code."""
3+
34
from __future__ import annotations
45

56
import dataclasses
@@ -16,6 +17,22 @@
1617
from typing import Final
1718
from typing import NoReturn
1819

20+
import py
21+
22+
23+
#: constant to prepare valuing pylib path replacements/lazy proxies later on
24+
# intended for removal in pytest 8.0 or 9.0
25+
26+
# fmt: off
27+
# intentional space to create a fake difference for the verification
28+
LEGACY_PATH = py.path. local
29+
# fmt: on
30+
31+
32+
def legacy_path(path: str | os.PathLike[str]) -> LEGACY_PATH:
33+
"""Internal wrapper to prepare lazy proxies for legacy_path instances"""
34+
return LEGACY_PATH(path)
35+
1936

2037
# fmt: off
2138
# Singleton type for NOTSET, as described in:

src/_pytest/config/__init__.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,14 @@
3838
from typing import Union
3939
import warnings
4040

41+
import pluggy
4142
from pluggy import HookimplMarker
4243
from pluggy import HookimplOpts
4344
from pluggy import HookspecMarker
4445
from pluggy import HookspecOpts
4546
from pluggy import PluginManager
4647

48+
from .compat import PathAwareHookProxy
4749
from .exceptions import PrintHelp as PrintHelp
4850
from .exceptions import UsageError as UsageError
4951
from .findpaths import determine_setup
@@ -1068,7 +1070,7 @@ def __init__(
10681070
self._store = self.stash
10691071

10701072
self.trace = self.pluginmanager.trace.root.get("config")
1071-
self.hook = self.pluginmanager.hook # type: ignore[assignment]
1073+
self.hook: pluggy.HookRelay = PathAwareHookProxy(self.pluginmanager.hook) # type: ignore[assignment]
10721074
self._inicache: Dict[str, Any] = {}
10731075
self._override_ini: Sequence[str] = ()
10741076
self._opt2dest: Dict[str, str] = {}

src/_pytest/config/compat.py

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
from __future__ import annotations
2+
3+
import functools
4+
from pathlib import Path
5+
from typing import Any
6+
from typing import Mapping
7+
import warnings
8+
9+
import pluggy
10+
11+
from ..compat import LEGACY_PATH
12+
from ..compat import legacy_path
13+
from ..deprecated import HOOK_LEGACY_PATH_ARG
14+
15+
16+
# hookname: (Path, LEGACY_PATH)
17+
imply_paths_hooks: Mapping[str, tuple[str, str]] = {
18+
"pytest_ignore_collect": ("collection_path", "path"),
19+
"pytest_collect_file": ("file_path", "path"),
20+
"pytest_pycollect_makemodule": ("module_path", "path"),
21+
"pytest_report_header": ("start_path", "startdir"),
22+
"pytest_report_collectionfinish": ("start_path", "startdir"),
23+
}
24+
25+
26+
def _check_path(path: Path, fspath: LEGACY_PATH) -> None:
27+
if Path(fspath) != path:
28+
raise ValueError(
29+
f"Path({fspath!r}) != {path!r}\n"
30+
"if both path and fspath are given they need to be equal"
31+
)
32+
33+
34+
class PathAwareHookProxy:
35+
"""
36+
this helper wraps around hook callers
37+
until pluggy supports fixingcalls, this one will do
38+
39+
it currently doesn't return full hook caller proxies for fixed hooks,
40+
this may have to be changed later depending on bugs
41+
"""
42+
43+
def __init__(self, hook_relay: pluggy.HookRelay) -> None:
44+
self._hook_relay = hook_relay
45+
46+
def __dir__(self) -> list[str]:
47+
return dir(self._hook_relay)
48+
49+
def __getattr__(self, key: str) -> pluggy.HookCaller:
50+
hook: pluggy.HookCaller = getattr(self._hook_relay, key)
51+
if key not in imply_paths_hooks:
52+
self.__dict__[key] = hook
53+
return hook
54+
else:
55+
path_var, fspath_var = imply_paths_hooks[key]
56+
57+
@functools.wraps(hook)
58+
def fixed_hook(**kw: Any) -> Any:
59+
path_value: Path | None = kw.pop(path_var, None)
60+
fspath_value: LEGACY_PATH | None = kw.pop(fspath_var, None)
61+
if fspath_value is not None:
62+
warnings.warn(
63+
HOOK_LEGACY_PATH_ARG.format(
64+
pylib_path_arg=fspath_var, pathlib_path_arg=path_var
65+
),
66+
stacklevel=2,
67+
)
68+
if path_value is not None:
69+
if fspath_value is not None:
70+
_check_path(path_value, fspath_value)
71+
else:
72+
fspath_value = legacy_path(path_value)
73+
else:
74+
assert fspath_value is not None
75+
path_value = Path(fspath_value)
76+
77+
kw[path_var] = path_value
78+
kw[fspath_var] = fspath_value
79+
return hook(**kw)
80+
81+
fixed_hook.name = hook.name # type: ignore[attr-defined]
82+
fixed_hook.spec = hook.spec # type: ignore[attr-defined]
83+
fixed_hook.__name__ = key
84+
self.__dict__[key] = fixed_hook
85+
return fixed_hook # type: ignore[return-value]

src/_pytest/deprecated.py

+15
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,21 @@
3636
PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.")
3737

3838

39+
HOOK_LEGACY_PATH_ARG = UnformattedWarning(
40+
PytestRemovedIn9Warning,
41+
"The ({pylib_path_arg}: py.path.local) argument is deprecated, please use ({pathlib_path_arg}: pathlib.Path)\n"
42+
"see https://docs.pytest.org/en/latest/deprecations.html"
43+
"#py-path-local-arguments-for-hooks-replaced-with-pathlib-path",
44+
)
45+
46+
NODE_CTOR_FSPATH_ARG = UnformattedWarning(
47+
PytestRemovedIn9Warning,
48+
"The (fspath: py.path.local) argument to {node_type_name} is deprecated. "
49+
"Please use the (path: pathlib.Path) argument instead.\n"
50+
"See https://docs.pytest.org/en/latest/deprecations.html"
51+
"#fspath-argument-for-node-constructors-replaced-with-pathlib-path",
52+
)
53+
3954
HOOK_LEGACY_MARKING = UnformattedWarning(
4055
PytestDeprecationWarning,
4156
"The hook{type} {fullname} uses old-style configuration options (marks or attributes).\n"

0 commit comments

Comments
 (0)