Skip to content

Commit a400a2c

Browse files
support adding per implementation warnings for hookspecs
1 parent fb2f96c commit a400a2c

File tree

4 files changed

+58
-5
lines changed

4 files changed

+58
-5
lines changed

docs/index.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,21 @@ dynamically loaded plugins.
384384
For more info see :ref:`call_historic`.
385385

386386

387+
Warnings on hook implementation
388+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
389+
390+
As projects evolve new hooks may be introduced and/or deprecated.
391+
392+
if a hookspec specifies a ``warn_on_impl``, pluggy will trigger it for any plugin implementing the hook.
393+
394+
395+
.. code-block:: python
396+
397+
@hookspec(warn_on_impl=DeprecationWarning("oldhook is deprecated and will be removed soon"))
398+
def oldhook():
399+
pass
400+
401+
387402
.. links
388403
.. _@contextlib.contextmanager:
389404
https://docs.python.org/3.6/library/contextlib.html#contextlib.contextmanager

pluggy/hooks.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class HookspecMarker(object):
1717
def __init__(self, project_name):
1818
self.project_name = project_name
1919

20-
def __call__(self, function=None, firstresult=False, historic=False):
20+
def __call__(self, function=None, firstresult=False, historic=False, warn_on_impl=None):
2121
""" if passed a function, directly sets attributes on the function
2222
which will make it discoverable to add_hookspecs(). If passed no
2323
function, returns a decorator which can be applied to a function
@@ -35,7 +35,8 @@ def setattr_hookspec_opts(func):
3535
if historic and firstresult:
3636
raise ValueError("cannot have a historic firstresult hook")
3737
setattr(func, self.project_name + "_spec",
38-
dict(firstresult=firstresult, historic=historic))
38+
dict(firstresult=firstresult, historic=historic,
39+
warn_on_impl=warn_on_impl,))
3940
return func
4041

4142
if function is not None:
@@ -195,6 +196,7 @@ def set_specification(self, specmodule_or_class, spec_opts):
195196
self.spec_opts.update(spec_opts)
196197
if spec_opts.get("historic"):
197198
self._call_history = []
199+
self.warn_on_impl = spec_opts.get('warn_on_impl')
198200

199201
def is_historic(self):
200202
return hasattr(self, "_call_history")

pluggy/manager.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
import inspect
22
from . import _tracing
33
from .hooks import HookImpl, _HookRelay, _HookCaller, normalize_hookimpl_opts
4+
import warnings
5+
6+
7+
def _warn_for_function(warning, function):
8+
warnings.warn_explicit(
9+
warning,
10+
type(warning),
11+
lineno=function.__code__.co_firstlineno,
12+
filename=function.__code__.co_filename,
13+
)
414

515

616
class PluginValidationError(Exception):
7-
""" plugin failed validation.
17+
""" plugin failed validation.
818
9-
:param object plugin: the plugin which failed validation, may be a module or an arbitrary object.
19+
:param object plugin: the plugin which failed validation,
20+
may be a module or an arbitrary object.
1021
"""
1122

1223
def __init__(self, plugin, message):
@@ -188,7 +199,8 @@ def _verify_hook(self, hook, hookimpl):
188199
hookimpl.plugin,
189200
"Plugin %r\nhook %r\nhistoric incompatible to hookwrapper" %
190201
(hookimpl.plugin_name, hook.name))
191-
202+
if hook.warn_on_impl:
203+
_warn_for_function(hook.warn_on_impl, hookimpl.function)
192204
# positional arg checking
193205
notinspec = set(hookimpl.argnames) - set(hook.argnames)
194206
if notinspec:

testing/test_details.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,30 @@ def x1meth2(self):
4646
assert pm.hook.x1meth2._wrappers[0].hookwrapper
4747

4848

49+
def test_warn_when_deprecated_specified(recwarn):
50+
warning = DeprecationWarning("foo is deprecated")
51+
52+
class Spec(object):
53+
@hookspec(warn_on_impl=warning)
54+
def foo(self):
55+
pass
56+
57+
class Plugin(object):
58+
@hookimpl
59+
def foo(self):
60+
pass
61+
62+
pm = PluginManager(hookspec.project_name)
63+
pm.add_hookspecs(Spec)
64+
65+
with pytest.warns(DeprecationWarning) as records:
66+
pm.register(Plugin())
67+
(record,) = records
68+
assert record.message is warning
69+
assert record.filename == Plugin.foo.__code__.co_filename
70+
assert record.lineno == Plugin.foo.__code__.co_firstlineno
71+
72+
4973
def test_plugin_getattr_raises_errors():
5074
"""Pluggy must be able to handle plugins which raise weird exceptions
5175
when getattr() gets called (#11).

0 commit comments

Comments
 (0)