Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

address #8361 - introduce hook caller wrappers that enable backward compat #8463

Merged
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ repos:
types: [python]
- id: py-path-deprecated
name: py.path usage is deprecated
exclude: docs|src/_pytest/deprecated.py|testing/deprecated_test.py
language: pygrep
entry: \bpy\.path\.local
exclude: docs
types: [python]
4 changes: 3 additions & 1 deletion src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -917,8 +917,10 @@ def __init__(
:type: PytestPluginManager
"""

from .compat import PathAwareHookProxy

self.trace = self.pluginmanager.trace.root.get("config")
self.hook = self.pluginmanager.hook
self.hook = PathAwareHookProxy(self.pluginmanager.hook)
self._inicache: Dict[str, Any] = {}
self._override_ini: Sequence[str] = ()
self._opt2dest: Dict[str, str] = {}
Expand Down
48 changes: 48 additions & 0 deletions src/_pytest/config/compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import warnings
from pathlib import Path
from typing import Optional

from ..compat import LEGACY_PATH
from ..deprecated import HOOK_LEGACY_PATH_ARG
from _pytest.nodes import _imply_path

# hookname: (Path, LEGACY_PATH)
imply_paths_hooks = {
"pytest_ignore_collect": ("fspath", "path"),
"pytest_collect_file": ("fspath", "path"),
"pytest_pycollect_makemodule": ("fspath", "path"),
"pytest_report_header": ("startpath", "startdir"),
"pytest_report_collectionfinish": ("startpath", "startdir"),
}


class PathAwareHookProxy:
RonnyPfannschmidt marked this conversation as resolved.
Show resolved Hide resolved
def __init__(self, hook_caller):
self.__hook_caller = hook_caller

def __dir__(self):
RonnyPfannschmidt marked this conversation as resolved.
Show resolved Hide resolved
return dir(self.__hook_caller)

def __getattr__(self, key):
if key not in imply_paths_hooks:
return getattr(self.__hook_caller, key)
RonnyPfannschmidt marked this conversation as resolved.
Show resolved Hide resolved
else:
hook = getattr(self.__hook_caller, key)
path_var, fspath_var = imply_paths_hooks[key]

def fixed_hook(**kw):
RonnyPfannschmidt marked this conversation as resolved.
Show resolved Hide resolved
path_value: Optional[Path] = kw.pop(path_var, None)
fspath_value: Optional[LEGACY_PATH] = 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
RonnyPfannschmidt marked this conversation as resolved.
Show resolved Hide resolved
)
)
path_value, fspath_value = _imply_path(path_value, fspath_value)
kw[path_var] = path_value
kw[fspath_var] = fspath_value
return hook(**kw)

fixed_hook.__name__ = key
return fixed_hook
RonnyPfannschmidt marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 4 additions & 0 deletions src/_pytest/deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@
"see https://docs.pytest.org/en/latest/deprecations.html#node-fspath-in-favor-of-pathlib-and-node-path",
)

HOOK_LEGACY_PATH_ARG = UnformattedWarning(
PytestDeprecationWarning,
"{pylib_path_arg} : py.path.local is deprecated, please use {pathlib_path_arg} : pathlib.Path",
RonnyPfannschmidt marked this conversation as resolved.
Show resolved Hide resolved
)
# You want to make some `__init__` or function "private".
#
# def my_private_function(some, args):
Expand Down
14 changes: 6 additions & 8 deletions src/_pytest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,9 @@ def gethookproxy(self, fspath: "os.PathLike[str]"):
remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
if remove_mods:
# One or more conftests are not in use at this fspath.
proxy = FSHookProxy(pm, remove_mods)
from .config.compat import PathAwareHookProxy

proxy = PathAwareHookProxy(FSHookProxy(pm, remove_mods))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bluetech here i wrap that one

else:
# All plugins are active for this fspath.
proxy = self.config.hook
Expand All @@ -561,9 +563,8 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
if direntry.name == "__pycache__":
return False
fspath = Path(direntry.path)
path = legacy_path(fspath)
ihook = self.gethookproxy(fspath.parent)
if ihook.pytest_ignore_collect(fspath=fspath, path=path, config=self.config):
if ihook.pytest_ignore_collect(fspath=fspath, config=self.config):
return False
norecursepatterns = self.config.getini("norecursedirs")
if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
Expand All @@ -573,17 +574,14 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
def _collectfile(
self, fspath: Path, handle_dupes: bool = True
) -> Sequence[nodes.Collector]:
path = legacy_path(fspath)
assert (
fspath.is_file()
), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format(
fspath, fspath.is_dir(), fspath.exists(), fspath.is_symlink()
)
ihook = self.gethookproxy(fspath)
if not self.isinitpath(fspath):
if ihook.pytest_ignore_collect(
fspath=fspath, path=path, config=self.config
):
if ihook.pytest_ignore_collect(fspath=fspath, config=self.config):
return ()

if handle_dupes:
Expand All @@ -595,7 +593,7 @@ def _collectfile(
else:
duplicate_paths.add(fspath)

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

@overload
def perform_collect(
Expand Down
18 changes: 5 additions & 13 deletions src/_pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,19 +188,15 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]:
return True


def pytest_collect_file(
fspath: Path, path: LEGACY_PATH, parent: nodes.Collector
) -> Optional["Module"]:
def pytest_collect_file(fspath: Path, parent: nodes.Collector) -> Optional["Module"]:
if fspath.suffix == ".py":
if not parent.session.isinitpath(fspath):
if not path_matches_patterns(
fspath, parent.config.getini("python_files") + ["__init__.py"]
):
return None
ihook = parent.session.gethookproxy(fspath)
module: Module = ihook.pytest_pycollect_makemodule(
fspath=fspath, path=path, parent=parent
)
module: Module = ihook.pytest_pycollect_makemodule(fspath=fspath, parent=parent)
return module
return None

Expand Down Expand Up @@ -675,9 +671,8 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
if direntry.name == "__pycache__":
return False
fspath = Path(direntry.path)
path = legacy_path(fspath)
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, config=self.config):
return False
norecursepatterns = self.config.getini("norecursedirs")
if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
Expand All @@ -687,17 +682,14 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
def _collectfile(
self, fspath: Path, handle_dupes: bool = True
) -> Sequence[nodes.Collector]:
path = legacy_path(fspath)
assert (
fspath.is_file()
), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format(
fspath, fspath.is_dir(), fspath.exists(), fspath.is_symlink()
)
ihook = self.session.gethookproxy(fspath)
if not self.session.isinitpath(fspath):
if ihook.pytest_ignore_collect(
fspath=fspath, path=path, config=self.config
):
if ihook.pytest_ignore_collect(fspath=fspath, config=self.config):
return ()

if handle_dupes:
Expand All @@ -709,7 +701,7 @@ def _collectfile(
else:
duplicate_paths.add(fspath)

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

def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
this_path = self.path.parent
Expand Down
3 changes: 1 addition & 2 deletions src/_pytest/terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -716,7 +716,7 @@ def pytest_sessionstart(self, session: "Session") -> None:
msg += " -- " + str(sys.executable)
self.write_line(msg)
lines = self.config.hook.pytest_report_header(
config=self.config, startpath=self.startpath, startdir=self.startdir
config=self.config, startpath=self.startpath
)
self._write_report_lines_from_hooks(lines)

Expand Down Expand Up @@ -753,7 +753,6 @@ def pytest_collection_finish(self, session: "Session") -> None:
lines = self.config.hook.pytest_report_collectionfinish(
config=self.config,
startpath=self.startpath,
startdir=self.startdir,
items=session.items,
)
self._write_report_lines_from_hooks(lines)
Expand Down
24 changes: 24 additions & 0 deletions testing/deprecated_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

import pytest
from _pytest import deprecated
from _pytest.compat import legacy_path
from _pytest.pytester import Pytester
from pytest import PytestDeprecationWarning


@pytest.mark.parametrize("attribute", pytest.collect.__all__) # type: ignore
Expand Down Expand Up @@ -153,3 +155,25 @@ def test_raising_unittest_skiptest_during_collection_is_deprecated(
"*PytestDeprecationWarning: Raising unittest.SkipTest*",
]
)


def test_hookproxy_warnings_for_fspath(pytestconfig, tmp_path, request):
path = legacy_path(tmp_path)

with pytest.warns(
PytestDeprecationWarning,
match="path : py.path.local is deprecated, please use fspath : pathlib.Path",
):
pytestconfig.hook.pytest_ignore_collect(
config=pytestconfig, path=path, fspath=tmp_path
)
with pytest.warns(
PytestDeprecationWarning,
match="path : py.path.local is deprecated, please use fspath : pathlib.Path",
):
request.node.ihook.pytest_ignore_collect(
config=pytestconfig, path=path, fspath=tmp_path
)

pytestconfig.hook.pytest_ignore_collect(config=pytestconfig, fspath=tmp_path)
request.node.ihook.pytest_ignore_collect(config=pytestconfig, fspath=tmp_path)