Merge pull request #8463 from RonnyPfannschmidt/workaround-8361
address #8361 - introduce hook caller wrappers that enable backward compat
This commit is contained in:
commit
41a90cd9fe
|
@ -90,7 +90,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]
|
||||
|
|
|
@ -19,6 +19,20 @@ 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>`.
|
||||
|
||||
|
||||
``py.path.local`` arguments for hooks replaced with ``pathlib.Path``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
In order to support the transition to :mod:`pathlib`, the following hooks now receive additional arguments:
|
||||
|
||||
* :func:`pytest_ignore_collect(fspath: pathlib.Path) <_pytest.hookspec.pytest_ignore_collect>`
|
||||
* :func:`pytest_collect_file(fspath: pathlib.Path) <_pytest.hookspec.pytest_collect_file>`
|
||||
* :func:`pytest_pycollect_makemodule(fspath: pathlib.Path) <_pytest.hookspec.pytest_pycollect_makemodule>`
|
||||
* :func:`pytest_report_header(startpath: pathlib.Path) <_pytest.hookspec.pytest_report_header>`
|
||||
* :func:`pytest_report_collectionfinish(startpath: pathlib.Path) <_pytest.hookspec.pytest_report_collectionfinish>`
|
||||
|
||||
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.
|
||||
|
||||
|
||||
``Node.fspath`` in favor of ``pathlib`` and ``Node.path``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -916,8 +916,10 @@ class Config:
|
|||
: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] = {}
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
import functools
|
||||
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:
|
||||
"""
|
||||
this helper wraps around hook callers
|
||||
until pluggy supports fixingcalls, this one will do
|
||||
|
||||
it currently doesnt return full hook caller proxies for fixed hooks,
|
||||
this may have to be changed later depending on bugs
|
||||
"""
|
||||
|
||||
def __init__(self, hook_caller):
|
||||
self.__hook_caller = hook_caller
|
||||
|
||||
def __dir__(self):
|
||||
return dir(self.__hook_caller)
|
||||
|
||||
def __getattr__(self, key, _wraps=functools.wraps):
|
||||
hook = getattr(self.__hook_caller, key)
|
||||
if key not in imply_paths_hooks:
|
||||
self.__dict__[key] = hook
|
||||
return hook
|
||||
else:
|
||||
path_var, fspath_var = imply_paths_hooks[key]
|
||||
|
||||
@_wraps(hook)
|
||||
def fixed_hook(**kw):
|
||||
|
||||
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
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
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
|
||||
self.__dict__[key] = fixed_hook
|
||||
return fixed_hook
|
|
@ -95,6 +95,12 @@ NODE_FSPATH = UnformattedWarning(
|
|||
"see https://docs.pytest.org/en/latest/deprecations.html#node-fspath-in-favor-of-pathlib-and-node-path",
|
||||
)
|
||||
|
||||
HOOK_LEGACY_PATH_ARG = UnformattedWarning(
|
||||
PytestDeprecationWarning,
|
||||
"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",
|
||||
)
|
||||
# You want to make some `__init__` or function "private".
|
||||
#
|
||||
# def my_private_function(some, args):
|
||||
|
|
|
@ -555,7 +555,9 @@ class Session(nodes.FSCollector):
|
|||
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))
|
||||
else:
|
||||
# All plugins are active for this fspath.
|
||||
proxy = self.config.hook
|
||||
|
@ -565,9 +567,8 @@ class Session(nodes.FSCollector):
|
|||
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):
|
||||
|
@ -577,7 +578,6 @@ class Session(nodes.FSCollector):
|
|||
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(
|
||||
|
@ -585,9 +585,7 @@ class Session(nodes.FSCollector):
|
|||
)
|
||||
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:
|
||||
|
@ -599,7 +597,7 @@ class Session(nodes.FSCollector):
|
|||
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(
|
||||
|
|
|
@ -188,9 +188,7 @@ 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(
|
||||
|
@ -198,9 +196,7 @@ def pytest_collect_file(
|
|||
):
|
||||
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
|
||||
|
||||
|
@ -675,9 +671,8 @@ class Package(Module):
|
|||
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):
|
||||
|
@ -687,7 +682,6 @@ class Package(Module):
|
|||
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(
|
||||
|
@ -695,9 +689,7 @@ class Package(Module):
|
|||
)
|
||||
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:
|
||||
|
@ -709,7 +701,7 @@ class Package(Module):
|
|||
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
|
||||
|
|
|
@ -716,7 +716,7 @@ class TerminalReporter:
|
|||
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)
|
||||
|
||||
|
@ -753,7 +753,6 @@ class TerminalReporter:
|
|||
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)
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import re
|
||||
import sys
|
||||
import warnings
|
||||
from unittest import mock
|
||||
|
||||
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
|
||||
|
@ -153,3 +156,25 @@ def test_raising_unittest_skiptest_during_collection_is_deprecated(
|
|||
"*PytestDeprecationWarning: Raising unittest.SkipTest*",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("hooktype", ["hook", "ihook"])
|
||||
def test_hookproxy_warnings_for_fspath(tmp_path, hooktype, request):
|
||||
path = legacy_path(tmp_path)
|
||||
|
||||
PATH_WARN_MATCH = r".*path: py\.path\.local\) argument is deprecated, please use \(fspath: 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, fspath=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, fspath=tmp_path)
|
||||
|
|
Loading…
Reference in New Issue