Skip to content

Commit

Permalink
Merge pull request #58 from tgoodlet/deprecate__multicall__
Browse files Browse the repository at this point in the history
Deprecate __multicall__
  • Loading branch information
goodboy committed Jul 16, 2017
2 parents 45a5656 + 88f1b73 commit 6689c91
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 64 deletions.
70 changes: 17 additions & 53 deletions pluggy.py → pluggy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
import sys
import inspect
import warnings
from .callers import _MultiCall, HookCallError, _raise_wrapfail, _Result

__version__ = '0.5.0'

__all__ = ["PluginManager", "PluginValidationError", "HookCallError",
"HookspecMarker", "HookimplMarker"]

_py3 = sys.version_info > (3, 0)


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 @@ -86,7 +80,7 @@ def __call__(self, function=None, hookwrapper=False, optionalhook=False,
If hookwrapper is True the hook implementations needs to execute exactly
one "yield". The code before the yield is run early before any non-hookwrapper
function is run. The code after the yield is run after all non-hookwrapper
function have run. The yield receives an ``_CallOutcome`` object representing
function have run. The yield receives a ``_Result`` object representing
the exception or result outcome of the inner calls (including other hookwrapper
calls).
Expand Down Expand Up @@ -172,23 +166,17 @@ 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
and return its _CallOutcome to the yield point. The generator then needs
and return its ``_Result`` to the yield point. The generator then needs
to finish (raise StopIteration) in order for the wrapped call to complete.
"""
try:
next(wrap_controller) # first yield
except StopIteration:
_raise_wrapfail(wrap_controller, "did not yield")
call_outcome = _CallOutcome(func)
call_outcome = _Result.from_call(func)
try:
wrap_controller.send(call_outcome)
_raise_wrapfail(wrap_controller, "has second yield")
Expand All @@ -197,39 +185,6 @@ def _wrapped_call(wrap_controller, func):
return call_outcome.get_result()


class _CallOutcome(object):
""" Outcome of a function call, either an exception or a proper result.
Calling the ``get_result`` method will return the result or reraise
the exception raised when the function was called. """
excinfo = None

def __init__(self, func):
try:
self.result = func()
except BaseException:
self.excinfo = sys.exc_info()

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


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


class _TracedHookExecution(object):
def __init__(self, pluginmanager, before, after):
self.pluginmanager = pluginmanager
Expand All @@ -241,7 +196,7 @@ def __init__(self, pluginmanager, before, after):

def __call__(self, hook, hook_impls, kwargs):
self.before(hook.name, hook_impls, kwargs)
outcome = _CallOutcome(lambda: self.oldcall(hook, hook_impls, kwargs))
outcome = _Result.from_call(lambda: self.oldcall(hook, hook_impls, kwargs))
self.after(outcome, hook.name, hook_impls, kwargs)
return outcome.get_result()

Expand Down Expand Up @@ -275,7 +230,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 @@ -490,7 +445,7 @@ def add_hookcall_monitoring(self, before, after):
of HookImpl instances and the keyword arguments for the hook call.
``after(outcome, hook_name, hook_impls, kwargs)`` receives the
same arguments as ``before`` but also a :py:class:`_CallOutcome`` object
same arguments as ``before`` but also a :py:class:`_Result`` object
which represents the result of the overall hook call.
"""
return _TracedHookExecution(self, before, after).undo
Expand Down Expand Up @@ -530,7 +485,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 +602,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 +653,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
118 changes: 118 additions & 0 deletions pluggy/callers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
'''
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

@classmethod
def from_call(cls, func):
result = excinfo = None
try:
result = func()
except BaseException:
excinfo = sys.exc_info()

return cls(result, 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)
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

def get_version():
p = os.path.join(os.path.dirname(
os.path.abspath(__file__)), "pluggy.py")
os.path.abspath(__file__)), "pluggy/__init__.py")
with open(p) as f:
for line in f.readlines():
if "__version__" in line:
Expand All @@ -42,7 +42,7 @@ def main():
author_email='holger at merlinux.eu',
url='https://github.com/pytest-dev/pluggy',
classifiers=classifiers,
py_modules=['pluggy'],
packages=['pluggy'],
)


Expand Down
23 changes: 16 additions & 7 deletions testing/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@
Benchmarking and performance tests.
"""
import pytest
from pluggy import _MultiCall, HookImpl, HookspecMarker, HookimplMarker
from pluggy import (_MultiCall, _LegacyMultiCall, HookImpl, HookspecMarker,
HookimplMarker)

hookspec = HookspecMarker("example")
hookimpl = HookimplMarker("example")


def MC(methods, kwargs, firstresult=False):
def MC(methods, kwargs, callertype, firstresult=False):
hookfuncs = []
for method in methods:
f = HookImpl(None, "<temp>", method, method.example_impl)
hookfuncs.append(f)
return _MultiCall(hookfuncs, kwargs, {"firstresult": firstresult})
return callertype(hookfuncs, kwargs, {"firstresult": firstresult})


@hookimpl
Expand Down Expand Up @@ -42,9 +43,17 @@ def wrappers(request):
return [wrapper for i in range(request.param)]


def inner_exec(methods):
return MC(methods, {'arg1': 1, 'arg2': 2, 'arg3': 3}).execute()
@pytest.fixture(
params=[_MultiCall, _LegacyMultiCall],
ids=lambda item: item.__name__
)
def callertype(request):
return request.param


def inner_exec(methods, callertype):
return MC(methods, {'arg1': 1, 'arg2': 2, 'arg3': 3}, callertype).execute()


def test_hook_and_wrappers_speed(benchmark, hooks, wrappers):
benchmark(inner_exec, hooks + wrappers)
def test_hook_and_wrappers_speed(benchmark, hooks, wrappers, callertype):
benchmark(inner_exec, hooks + wrappers, callertype)
7 changes: 5 additions & 2 deletions testing/test_multicall.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest

from pluggy import _MultiCall, HookImpl, HookCallError
from pluggy import _MultiCall, HookImpl, HookCallError, _LegacyMultiCall
from pluggy import HookspecMarker, HookimplMarker


Expand All @@ -18,11 +18,14 @@ def test_uses_copy_of_methods():


def MC(methods, kwargs, firstresult=False):
caller = _MultiCall
hookfuncs = []
for method in methods:
f = HookImpl(None, "<temp>", method, method.example_impl)
hookfuncs.append(f)
return _MultiCall(hookfuncs, kwargs, {"firstresult": firstresult})
if '__multicall__' in f.argnames:
caller = _LegacyMultiCall
return caller(hookfuncs, kwargs, {"firstresult": firstresult})


def test_call_passing():
Expand Down

0 comments on commit 6689c91

Please sign in to comment.