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

Deprecate __multicall__ #58

Merged
merged 5 commits into from
Jul 16, 2017
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
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 @@ -20,7 +20,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 @@ -40,7 +40,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