Merge pull request #8463 from RonnyPfannschmidt/workaround-8361

address #8361 - introduce hook caller wrappers that enable backward compat
This commit is contained in:
Ronny Pfannschmidt 2021-04-05 22:50:31 +02:00 committed by GitHub
commit 41a90cd9fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 123 additions and 25 deletions

View File

@ -90,7 +90,7 @@ repos:
types: [python] types: [python]
- id: py-path-deprecated - id: py-path-deprecated
name: py.path usage is deprecated name: py.path usage is deprecated
exclude: docs|src/_pytest/deprecated.py|testing/deprecated_test.py
language: pygrep language: pygrep
entry: \bpy\.path\.local entry: \bpy\.path\.local
exclude: docs
types: [python] types: [python]

View File

@ -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>`. :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`` ``Node.fspath`` in favor of ``pathlib`` and ``Node.path``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -916,8 +916,10 @@ class Config:
:type: PytestPluginManager :type: PytestPluginManager
""" """
from .compat import PathAwareHookProxy
self.trace = self.pluginmanager.trace.root.get("config") 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._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,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

View File

@ -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", "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". # You want to make some `__init__` or function "private".
# #
# def my_private_function(some, args): # def my_private_function(some, args):

View File

@ -555,7 +555,9 @@ class Session(nodes.FSCollector):
remove_mods = pm._conftest_plugins.difference(my_conftestmodules) remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
if remove_mods: if remove_mods:
# One or more conftests are not in use at this fspath. # 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: else:
# All plugins are active for this fspath. # All plugins are active for this fspath.
proxy = self.config.hook proxy = self.config.hook
@ -565,9 +567,8 @@ class Session(nodes.FSCollector):
if direntry.name == "__pycache__": if direntry.name == "__pycache__":
return False return False
fspath = Path(direntry.path) fspath = Path(direntry.path)
path = legacy_path(fspath)
ihook = self.gethookproxy(fspath.parent) 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 return False
norecursepatterns = self.config.getini("norecursedirs") norecursepatterns = self.config.getini("norecursedirs")
if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns): if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
@ -577,7 +578,6 @@ class Session(nodes.FSCollector):
def _collectfile( def _collectfile(
self, fspath: Path, handle_dupes: bool = True self, fspath: Path, handle_dupes: bool = True
) -> Sequence[nodes.Collector]: ) -> Sequence[nodes.Collector]:
path = legacy_path(fspath)
assert ( assert (
fspath.is_file() fspath.is_file()
), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format( ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format(
@ -585,9 +585,7 @@ class Session(nodes.FSCollector):
) )
ihook = self.gethookproxy(fspath) ihook = self.gethookproxy(fspath)
if not self.isinitpath(fspath): if not self.isinitpath(fspath):
if ihook.pytest_ignore_collect( if ihook.pytest_ignore_collect(fspath=fspath, config=self.config):
fspath=fspath, path=path, config=self.config
):
return () return ()
if handle_dupes: if handle_dupes:
@ -599,7 +597,7 @@ class Session(nodes.FSCollector):
else: else:
duplicate_paths.add(fspath) 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 @overload
def perform_collect( def perform_collect(

View File

@ -188,9 +188,7 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]:
return True return True
def pytest_collect_file( def pytest_collect_file(fspath: Path, parent: nodes.Collector) -> Optional["Module"]:
fspath: Path, path: LEGACY_PATH, parent: nodes.Collector
) -> Optional["Module"]:
if fspath.suffix == ".py": if fspath.suffix == ".py":
if not parent.session.isinitpath(fspath): if not parent.session.isinitpath(fspath):
if not path_matches_patterns( if not path_matches_patterns(
@ -198,9 +196,7 @@ def pytest_collect_file(
): ):
return None return None
ihook = parent.session.gethookproxy(fspath) ihook = parent.session.gethookproxy(fspath)
module: Module = ihook.pytest_pycollect_makemodule( module: Module = ihook.pytest_pycollect_makemodule(fspath=fspath, parent=parent)
fspath=fspath, path=path, parent=parent
)
return module return module
return None return None
@ -675,9 +671,8 @@ class Package(Module):
if direntry.name == "__pycache__": if direntry.name == "__pycache__":
return False return False
fspath = Path(direntry.path) fspath = Path(direntry.path)
path = legacy_path(fspath)
ihook = self.session.gethookproxy(fspath.parent) 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 return False
norecursepatterns = self.config.getini("norecursedirs") norecursepatterns = self.config.getini("norecursedirs")
if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns): if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
@ -687,7 +682,6 @@ class Package(Module):
def _collectfile( def _collectfile(
self, fspath: Path, handle_dupes: bool = True self, fspath: Path, handle_dupes: bool = True
) -> Sequence[nodes.Collector]: ) -> Sequence[nodes.Collector]:
path = legacy_path(fspath)
assert ( assert (
fspath.is_file() fspath.is_file()
), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format( ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format(
@ -695,9 +689,7 @@ class Package(Module):
) )
ihook = self.session.gethookproxy(fspath) ihook = self.session.gethookproxy(fspath)
if not self.session.isinitpath(fspath): if not self.session.isinitpath(fspath):
if ihook.pytest_ignore_collect( if ihook.pytest_ignore_collect(fspath=fspath, config=self.config):
fspath=fspath, path=path, config=self.config
):
return () return ()
if handle_dupes: if handle_dupes:
@ -709,7 +701,7 @@ class Package(Module):
else: else:
duplicate_paths.add(fspath) 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]]: def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
this_path = self.path.parent this_path = self.path.parent

View File

@ -716,7 +716,7 @@ class TerminalReporter:
msg += " -- " + str(sys.executable) msg += " -- " + str(sys.executable)
self.write_line(msg) self.write_line(msg)
lines = self.config.hook.pytest_report_header( 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) self._write_report_lines_from_hooks(lines)
@ -753,7 +753,6 @@ class TerminalReporter:
lines = self.config.hook.pytest_report_collectionfinish( lines = self.config.hook.pytest_report_collectionfinish(
config=self.config, config=self.config,
startpath=self.startpath, startpath=self.startpath,
startdir=self.startdir,
items=session.items, items=session.items,
) )
self._write_report_lines_from_hooks(lines) self._write_report_lines_from_hooks(lines)

View File

@ -1,10 +1,13 @@
import re import re
import sys
import warnings import warnings
from unittest import mock from unittest import mock
import pytest import pytest
from _pytest import deprecated from _pytest import deprecated
from _pytest.compat import legacy_path
from _pytest.pytester import Pytester from _pytest.pytester import Pytester
from pytest import PytestDeprecationWarning
@pytest.mark.parametrize("attribute", pytest.collect.__all__) # type: ignore @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*", "*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)