Skip to content

Commit 41a90cd

Browse files
Merge pull request #8463 from RonnyPfannschmidt/workaround-8361
address #8361 - introduce hook caller wrappers that enable backward compat
2 parents b706a2c + aa10bff commit 41a90cd

9 files changed

+123
-25
lines changed

.pre-commit-config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ repos:
9090
types: [python]
9191
- id: py-path-deprecated
9292
name: py.path usage is deprecated
93+
exclude: docs|src/_pytest/deprecated.py|testing/deprecated_test.py
9394
language: pygrep
9495
entry: \bpy\.path\.local
95-
exclude: docs
9696
types: [python]

doc/en/deprecations.rst

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

2121

22+
``py.path.local`` arguments for hooks replaced with ``pathlib.Path``
23+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
24+
25+
In order to support the transition to :mod:`pathlib`, the following hooks now receive additional arguments:
26+
27+
* :func:`pytest_ignore_collect(fspath: pathlib.Path) <_pytest.hookspec.pytest_ignore_collect>`
28+
* :func:`pytest_collect_file(fspath: pathlib.Path) <_pytest.hookspec.pytest_collect_file>`
29+
* :func:`pytest_pycollect_makemodule(fspath: pathlib.Path) <_pytest.hookspec.pytest_pycollect_makemodule>`
30+
* :func:`pytest_report_header(startpath: pathlib.Path) <_pytest.hookspec.pytest_report_header>`
31+
* :func:`pytest_report_collectionfinish(startpath: pathlib.Path) <_pytest.hookspec.pytest_report_collectionfinish>`
32+
33+
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.
34+
35+
2236
``Node.fspath`` in favor of ``pathlib`` and ``Node.path``
2337
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2438

src/_pytest/config/__init__.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -916,8 +916,10 @@ def __init__(
916916
:type: PytestPluginManager
917917
"""
918918

919+
from .compat import PathAwareHookProxy
920+
919921
self.trace = self.pluginmanager.trace.root.get("config")
920-
self.hook = self.pluginmanager.hook
922+
self.hook = PathAwareHookProxy(self.pluginmanager.hook)
921923
self._inicache: Dict[str, Any] = {}
922924
self._override_ini: Sequence[str] = ()
923925
self._opt2dest: Dict[str, str] = {}

src/_pytest/config/compat.py

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import functools
2+
import warnings
3+
from pathlib import Path
4+
from typing import Optional
5+
6+
from ..compat import LEGACY_PATH
7+
from ..deprecated import HOOK_LEGACY_PATH_ARG
8+
from _pytest.nodes import _imply_path
9+
10+
# hookname: (Path, LEGACY_PATH)
11+
imply_paths_hooks = {
12+
"pytest_ignore_collect": ("fspath", "path"),
13+
"pytest_collect_file": ("fspath", "path"),
14+
"pytest_pycollect_makemodule": ("fspath", "path"),
15+
"pytest_report_header": ("startpath", "startdir"),
16+
"pytest_report_collectionfinish": ("startpath", "startdir"),
17+
}
18+
19+
20+
class PathAwareHookProxy:
21+
"""
22+
this helper wraps around hook callers
23+
until pluggy supports fixingcalls, this one will do
24+
25+
it currently doesnt return full hook caller proxies for fixed hooks,
26+
this may have to be changed later depending on bugs
27+
"""
28+
29+
def __init__(self, hook_caller):
30+
self.__hook_caller = hook_caller
31+
32+
def __dir__(self):
33+
return dir(self.__hook_caller)
34+
35+
def __getattr__(self, key, _wraps=functools.wraps):
36+
hook = getattr(self.__hook_caller, key)
37+
if key not in imply_paths_hooks:
38+
self.__dict__[key] = hook
39+
return hook
40+
else:
41+
path_var, fspath_var = imply_paths_hooks[key]
42+
43+
@_wraps(hook)
44+
def fixed_hook(**kw):
45+
46+
path_value: Optional[Path] = kw.pop(path_var, None)
47+
fspath_value: Optional[LEGACY_PATH] = kw.pop(fspath_var, None)
48+
if fspath_value is not None:
49+
warnings.warn(
50+
HOOK_LEGACY_PATH_ARG.format(
51+
pylib_path_arg=fspath_var, pathlib_path_arg=path_var
52+
),
53+
stacklevel=2,
54+
)
55+
path_value, fspath_value = _imply_path(path_value, fspath_value)
56+
kw[path_var] = path_value
57+
kw[fspath_var] = fspath_value
58+
return hook(**kw)
59+
60+
fixed_hook.__name__ = key
61+
self.__dict__[key] = fixed_hook
62+
return fixed_hook

src/_pytest/deprecated.py

+6
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@
9595
"see https://docs.pytest.org/en/latest/deprecations.html#node-fspath-in-favor-of-pathlib-and-node-path",
9696
)
9797

98+
HOOK_LEGACY_PATH_ARG = UnformattedWarning(
99+
PytestDeprecationWarning,
100+
"The ({pylib_path_arg}: py.path.local) argument is deprecated, please use ({pathlib_path_arg}: pathlib.Path)\n"
101+
"see https://docs.pytest.org/en/latest/deprecations.html"
102+
"#py-path-local-arguments-for-hooks-replaced-with-pathlib-path",
103+
)
98104
# You want to make some `__init__` or function "private".
99105
#
100106
# def my_private_function(some, args):

src/_pytest/main.py

+6-8
Original file line numberDiff line numberDiff line change
@@ -555,7 +555,9 @@ def gethookproxy(self, fspath: "os.PathLike[str]"):
555555
remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
556556
if remove_mods:
557557
# One or more conftests are not in use at this fspath.
558-
proxy = FSHookProxy(pm, remove_mods)
558+
from .config.compat import PathAwareHookProxy
559+
560+
proxy = PathAwareHookProxy(FSHookProxy(pm, remove_mods))
559561
else:
560562
# All plugins are active for this fspath.
561563
proxy = self.config.hook
@@ -565,9 +567,8 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
565567
if direntry.name == "__pycache__":
566568
return False
567569
fspath = Path(direntry.path)
568-
path = legacy_path(fspath)
569570
ihook = self.gethookproxy(fspath.parent)
570-
if ihook.pytest_ignore_collect(fspath=fspath, path=path, config=self.config):
571+
if ihook.pytest_ignore_collect(fspath=fspath, config=self.config):
571572
return False
572573
norecursepatterns = self.config.getini("norecursedirs")
573574
if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
@@ -577,17 +578,14 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
577578
def _collectfile(
578579
self, fspath: Path, handle_dupes: bool = True
579580
) -> Sequence[nodes.Collector]:
580-
path = legacy_path(fspath)
581581
assert (
582582
fspath.is_file()
583583
), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format(
584584
fspath, fspath.is_dir(), fspath.exists(), fspath.is_symlink()
585585
)
586586
ihook = self.gethookproxy(fspath)
587587
if not self.isinitpath(fspath):
588-
if ihook.pytest_ignore_collect(
589-
fspath=fspath, path=path, config=self.config
590-
):
588+
if ihook.pytest_ignore_collect(fspath=fspath, config=self.config):
591589
return ()
592590

593591
if handle_dupes:
@@ -599,7 +597,7 @@ def _collectfile(
599597
else:
600598
duplicate_paths.add(fspath)
601599

602-
return ihook.pytest_collect_file(fspath=fspath, path=path, parent=self) # type: ignore[no-any-return]
600+
return ihook.pytest_collect_file(fspath=fspath, parent=self) # type: ignore[no-any-return]
603601

604602
@overload
605603
def perform_collect(

src/_pytest/python.py

+5-13
Original file line numberDiff line numberDiff line change
@@ -188,19 +188,15 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]:
188188
return True
189189

190190

191-
def pytest_collect_file(
192-
fspath: Path, path: LEGACY_PATH, parent: nodes.Collector
193-
) -> Optional["Module"]:
191+
def pytest_collect_file(fspath: Path, parent: nodes.Collector) -> Optional["Module"]:
194192
if fspath.suffix == ".py":
195193
if not parent.session.isinitpath(fspath):
196194
if not path_matches_patterns(
197195
fspath, parent.config.getini("python_files") + ["__init__.py"]
198196
):
199197
return None
200198
ihook = parent.session.gethookproxy(fspath)
201-
module: Module = ihook.pytest_pycollect_makemodule(
202-
fspath=fspath, path=path, parent=parent
203-
)
199+
module: Module = ihook.pytest_pycollect_makemodule(fspath=fspath, parent=parent)
204200
return module
205201
return None
206202

@@ -675,9 +671,8 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
675671
if direntry.name == "__pycache__":
676672
return False
677673
fspath = Path(direntry.path)
678-
path = legacy_path(fspath)
679674
ihook = self.session.gethookproxy(fspath.parent)
680-
if ihook.pytest_ignore_collect(fspath=fspath, path=path, config=self.config):
675+
if ihook.pytest_ignore_collect(fspath=fspath, config=self.config):
681676
return False
682677
norecursepatterns = self.config.getini("norecursedirs")
683678
if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
@@ -687,17 +682,14 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
687682
def _collectfile(
688683
self, fspath: Path, handle_dupes: bool = True
689684
) -> Sequence[nodes.Collector]:
690-
path = legacy_path(fspath)
691685
assert (
692686
fspath.is_file()
693687
), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format(
694688
fspath, fspath.is_dir(), fspath.exists(), fspath.is_symlink()
695689
)
696690
ihook = self.session.gethookproxy(fspath)
697691
if not self.session.isinitpath(fspath):
698-
if ihook.pytest_ignore_collect(
699-
fspath=fspath, path=path, config=self.config
700-
):
692+
if ihook.pytest_ignore_collect(fspath=fspath, config=self.config):
701693
return ()
702694

703695
if handle_dupes:
@@ -709,7 +701,7 @@ def _collectfile(
709701
else:
710702
duplicate_paths.add(fspath)
711703

712-
return ihook.pytest_collect_file(fspath=fspath, path=path, parent=self) # type: ignore[no-any-return]
704+
return ihook.pytest_collect_file(fspath=fspath, parent=self) # type: ignore[no-any-return]
713705

714706
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
715707
this_path = self.path.parent

src/_pytest/terminal.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -716,7 +716,7 @@ def pytest_sessionstart(self, session: "Session") -> None:
716716
msg += " -- " + str(sys.executable)
717717
self.write_line(msg)
718718
lines = self.config.hook.pytest_report_header(
719-
config=self.config, startpath=self.startpath, startdir=self.startdir
719+
config=self.config, startpath=self.startpath
720720
)
721721
self._write_report_lines_from_hooks(lines)
722722

@@ -753,7 +753,6 @@ def pytest_collection_finish(self, session: "Session") -> None:
753753
lines = self.config.hook.pytest_report_collectionfinish(
754754
config=self.config,
755755
startpath=self.startpath,
756-
startdir=self.startdir,
757756
items=session.items,
758757
)
759758
self._write_report_lines_from_hooks(lines)

testing/deprecated_test.py

+25
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import re
2+
import sys
23
import warnings
34
from unittest import mock
45

56
import pytest
67
from _pytest import deprecated
8+
from _pytest.compat import legacy_path
79
from _pytest.pytester import Pytester
10+
from pytest import PytestDeprecationWarning
811

912

1013
@pytest.mark.parametrize("attribute", pytest.collect.__all__) # type: ignore
@@ -153,3 +156,25 @@ def test_raising_unittest_skiptest_during_collection_is_deprecated(
153156
"*PytestDeprecationWarning: Raising unittest.SkipTest*",
154157
]
155158
)
159+
160+
161+
@pytest.mark.parametrize("hooktype", ["hook", "ihook"])
162+
def test_hookproxy_warnings_for_fspath(tmp_path, hooktype, request):
163+
path = legacy_path(tmp_path)
164+
165+
PATH_WARN_MATCH = r".*path: py\.path\.local\) argument is deprecated, please use \(fspath: pathlib\.Path.*"
166+
if hooktype == "ihook":
167+
hooks = request.node.ihook
168+
else:
169+
hooks = request.config.hook
170+
171+
with pytest.warns(PytestDeprecationWarning, match=PATH_WARN_MATCH) as r:
172+
l1 = sys._getframe().f_lineno
173+
hooks.pytest_ignore_collect(config=request.config, path=path, fspath=tmp_path)
174+
l2 = sys._getframe().f_lineno
175+
176+
(record,) = r
177+
assert record.filename == __file__
178+
assert l1 < record.lineno < l2
179+
180+
hooks.pytest_ignore_collect(config=request.config, fspath=tmp_path)

0 commit comments

Comments
 (0)