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

support adding per implementation warnings for hookspecs #138

Merged
merged 1 commit into from
Apr 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,21 @@ dynamically loaded plugins.
For more info see :ref:`call_historic`.


Warnings on hook implementation
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

As projects evolve new hooks may be introduced and/or deprecated.

if a hookspec specifies a ``warn_on_impl``, pluggy will trigger it for any plugin implementing the hook.


.. code-block:: python

@hookspec(warn_on_impl=DeprecationWarning("oldhook is deprecated and will be removed soon"))
def oldhook():
pass


.. links
.. [email protected]:
https://docs.python.org/3.6/library/contextlib.html#contextlib.contextmanager
Expand Down
6 changes: 4 additions & 2 deletions pluggy/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class HookspecMarker(object):
def __init__(self, project_name):
self.project_name = project_name

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

if function is not None:
Expand Down Expand Up @@ -195,6 +196,7 @@ def set_specification(self, specmodule_or_class, spec_opts):
self.spec_opts.update(spec_opts)
if spec_opts.get("historic"):
self._call_history = []
self.warn_on_impl = spec_opts.get('warn_on_impl')

def is_historic(self):
return hasattr(self, "_call_history")
Expand Down
18 changes: 15 additions & 3 deletions pluggy/manager.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import inspect
from . import _tracing
from .hooks import HookImpl, _HookRelay, _HookCaller, normalize_hookimpl_opts
import warnings


def _warn_for_function(warning, function):
warnings.warn_explicit(
warning,
type(warning),
lineno=function.__code__.co_firstlineno,
filename=function.__code__.co_filename,
)


class PluginValidationError(Exception):
""" plugin failed validation.
""" plugin failed validation.

:param object plugin: the plugin which failed validation, may be a module or an arbitrary object.
:param object plugin: the plugin which failed validation,
may be a module or an arbitrary object.
"""

def __init__(self, plugin, message):
Expand Down Expand Up @@ -188,7 +199,8 @@ def _verify_hook(self, hook, hookimpl):
hookimpl.plugin,
"Plugin %r\nhook %r\nhistoric incompatible to hookwrapper" %
(hookimpl.plugin_name, hook.name))

if hook.warn_on_impl:
_warn_for_function(hook.warn_on_impl, hookimpl.function)
# positional arg checking
notinspec = set(hookimpl.argnames) - set(hook.argnames)
if notinspec:
Expand Down
24 changes: 24 additions & 0 deletions testing/test_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,30 @@ def x1meth2(self):
assert pm.hook.x1meth2._wrappers[0].hookwrapper


def test_warn_when_deprecated_specified(recwarn):
warning = DeprecationWarning("foo is deprecated")

class Spec(object):
@hookspec(warn_on_impl=warning)
def foo(self):
pass

class Plugin(object):
@hookimpl
def foo(self):
pass

pm = PluginManager(hookspec.project_name)
pm.add_hookspecs(Spec)

with pytest.warns(DeprecationWarning) as records:
pm.register(Plugin())
(record,) = records
assert record.message is warning
assert record.filename == Plugin.foo.__code__.co_filename
assert record.lineno == Plugin.foo.__code__.co_firstlineno


def test_plugin_getattr_raises_errors():
"""Pluggy must be able to handle plugins which raise weird exceptions
when getattr() gets called (#11).
Expand Down