Skip to content

Commit

Permalink
Deprecate __multicall__ support
Browse files Browse the repository at this point in the history
Add a new `pluggy.callers._MultiCall` implementation which removes
support the implicit `__multicall__` special argument and drops the
recursion required to handle hook wrappers. Rename the original
implementation `_LegacyMultiCall` and load it only when the
`__multicall__` argument is detected in a hookimpl function
signature. Add a deprecation warning whenever the legacy fallback
occurs.

Resolves #23
  • Loading branch information
Tyler Goodlet committed Jul 9, 2017
1 parent fb00c4e commit 845c502
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 12 deletions.
24 changes: 12 additions & 12 deletions pluggy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import sys
import inspect
import warnings
from .callers import _MultiCall, HookCallError, _raise_wrapfail

__version__ = '0.5.0'

Expand All @@ -14,10 +15,6 @@ class PluginValidationError(Exception):
""" plugin failed validation. """


class HookCallError(Exception):
""" Hook was called wrongly. """


class HookspecMarker(object):
""" Decorator helper class for marking functions as hook specifications.
Expand Down Expand Up @@ -172,12 +169,6 @@ def get(self, name):
return self.__class__(self.root, self.tags + (name,))


def _raise_wrapfail(wrap_controller, msg):
co = wrap_controller.gi_code
raise RuntimeError("wrap_controller at %r %s:%d %s" %
(co.co_name, co.co_filename, co.co_firstlineno, msg))


def _wrapped_call(wrap_controller, func):
""" Wrap calling to a function with a generator which needs to yield
exactly once. The yield point will trigger calling the wrapped function
Expand Down Expand Up @@ -275,7 +266,7 @@ def __init__(self, project_name, implprefix=None):
self.hook = _HookRelay(self.trace.root.get("hook"))
self._implprefix = implprefix
self._inner_hookexec = lambda hook, methods, kwargs: \
_MultiCall(
hook.multicall(
methods, kwargs, specopts=hook.spec_opts, hook=hook
).execute()

Expand Down Expand Up @@ -530,7 +521,7 @@ def subset_hook_caller(self, name, remove_plugins):
return orig


class _MultiCall(object):
class _LegacyMultiCall(object):
""" execute a call into multiple python functions/methods. """

# XXX note that the __multicall__ argument is supported only
Expand Down Expand Up @@ -647,6 +638,7 @@ def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None)
self._hookexec = hook_execute
self.argnames = None
self.kwargnames = None
self.multicall = _MultiCall
if specmodule_or_class is not None:
assert spec_opts is not None
self.set_specification(specmodule_or_class, spec_opts)
Expand Down Expand Up @@ -697,6 +689,14 @@ def _add_hookimpl(self, hookimpl):
i -= 1
methods.insert(i + 1, hookimpl)

if '__multicall__' in hookimpl.argnames:
warnings.warn(
"Support for __multicall__ is now deprecated and will be"
"removed in an upcoming release.",
warnings.DeprecationWarning
)
self.multicall = _LegacyMultiCall

def __repr__(self):
return "<_HookCaller %r>" % (self.name,)

Expand Down
108 changes: 108 additions & 0 deletions pluggy/callers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
'''
Call loop machinery
'''
import sys


_py3 = sys.version_info > (3, 0)


if not _py3:
exec("""
def _reraise(cls, val, tb):
raise cls, val, tb
""")


def _raise_wrapfail(wrap_controller, msg):
co = wrap_controller.gi_code
raise RuntimeError("wrap_controller at %r %s:%d %s" %
(co.co_name, co.co_filename, co.co_firstlineno, msg))


class HookCallError(Exception):
""" Hook was called wrongly. """


class Result(object):
def __init__(self, result, excinfo):
self.result = result
self.excinfo = excinfo

def force_result(self, result):
self.result = result
self.excinfo = None

def get_result(self):
if self.excinfo is None:
return self.result
else:
ex = self.excinfo
if _py3:
raise ex[1].with_traceback(ex[2])
_reraise(*ex) # noqa


class _MultiCall(object):
"""Execute a call into multiple python functions/methods.
"""
def __init__(self, hook_impls, kwargs, specopts={}, hook=None):
self.hook = hook
self.hook_impls = hook_impls
self.caller_kwargs = kwargs # come from _HookCaller.__call__()
self.specopts = hook.spec_opts if hook else specopts

def execute(self):
caller_kwargs = self.caller_kwargs
self.results = results = []
firstresult = self.specopts.get("firstresult")
excinfo = None
try: # run impl and wrapper setup functions in a loop
teardowns = []
try:
for hook_impl in reversed(self.hook_impls):
try:
args = [caller_kwargs[argname] for argname in hook_impl.argnames]
# args = operator.itemgetter(hookimpl.argnames)(caller_kwargs)
except KeyError:
for argname in hook_impl.argnames:
if argname not in caller_kwargs:
raise HookCallError(
"hook call must provide argument %r" % (argname,))

if hook_impl.hookwrapper:
try:
gen = hook_impl.function(*args)
next(gen) # first yield
teardowns.append(gen)
except StopIteration:
_raise_wrapfail(gen, "did not yield")
else:
res = hook_impl.function(*args)
if res is not None:
results.append(res)
if firstresult: # halt further impl calls
break
except BaseException:
excinfo = sys.exc_info()
finally:
outcome = Result(results, excinfo)

# run all wrapper post-yield blocks
for gen in reversed(teardowns):
try:
gen.send(outcome)
_raise_wrapfail(gen, "has second yield")
except StopIteration:
pass

if firstresult:
return outcome.get_result()[0]

return outcome.get_result()

def __repr__(self):
status = "%d meths" % (len(self.hook_impls),)
if hasattr(self, "results"):
status = ("%d results, " % len(self.results)) + status
return "<_MultiCall %s, kwargs=%r>" % (status, self.caller_kwargs)

0 comments on commit 845c502

Please sign in to comment.