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

Multicall to func #90

Merged
merged 5 commits into from
Oct 20, 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
75 changes: 4 additions & 71 deletions pluggy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import inspect
import warnings
from .callers import _MultiCall, HookCallError, _raise_wrapfail, _Result
from .callers import _multicall, HookCallError, _Result, _legacymulticall

__version__ = '0.5.3.dev'

Expand Down Expand Up @@ -166,25 +166,6 @@ def get(self, name):
return self.__class__(self.root, self.tags + (name,))


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 ``_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 = _Result.from_call(func)
try:
wrap_controller.send(call_outcome)
_raise_wrapfail(wrap_controller, "has second yield")
except StopIteration:
pass
return call_outcome.get_result()


class _TracedHookExecution(object):
def __init__(self, pluginmanager, before, after):
self.pluginmanager = pluginmanager
Expand Down Expand Up @@ -232,7 +213,7 @@ def __init__(self, project_name, implprefix=None):
self._inner_hookexec = lambda hook, methods, kwargs: \
hook.multicall(
methods, kwargs, specopts=hook.spec_opts, hook=hook
).execute()
)

def _hookexec(self, hook, methods, kwargs):
# called from all hookcaller instances.
Expand Down Expand Up @@ -485,54 +466,6 @@ def subset_hook_caller(self, name, remove_plugins):
return orig


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

# XXX note that the __multicall__ argument is supported only
# for pytest compatibility reasons. It was never officially
# supported there and is explicitely deprecated since 2.8
# so we can remove it soon, allowing to avoid the below recursion
# in execute() and simplify/speed up the execute loop.

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.caller_kwargs["__multicall__"] = self
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")

while self.hook_impls:
hook_impl = self.hook_impls.pop()
try:
args = [caller_kwargs[argname] for argname in hook_impl.argnames]
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:
return _wrapped_call(hook_impl.function(*args), self.execute)
res = hook_impl.function(*args)
if res is not None:
if firstresult:
return res
results.append(res)

if not firstresult:
return results

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)


def varnames(func):
"""Return tuple of positional and keywrord argument names for a function,
method, class or callable.
Expand Down Expand Up @@ -602,7 +535,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
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 @@ -659,7 +592,7 @@ def _add_hookimpl(self, hookimpl):
"removed in an upcoming release.",
DeprecationWarning
)
self.multicall = _LegacyMultiCall
self.multicall = _legacymulticall

def __repr__(self):
return "<_HookCaller %r>" % (self.name,)
Expand Down
153 changes: 107 additions & 46 deletions pluggy/callers.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,67 +77,128 @@ def get_result(self):
_reraise(*ex) # noqa


class _MultiCall(object):
"""Execute a call into multiple python functions/methods.
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 ``_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 = _Result.from_call(func)
try:
wrap_controller.send(call_outcome)
_raise_wrapfail(wrap_controller, "has second yield")
except StopIteration:
pass
return call_outcome.get_result()


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

# XXX note that the __multicall__ argument is supported only
# for pytest compatibility reasons. It was never officially
# supported there and is explicitely deprecated since 2.8
# so we can remove it soon, allowing to avoid the below recursion
# in execute() and simplify/speed up the execute loop.

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.caller_kwargs["__multicall__"] = self
self.specopts = hook.spec_opts if hook else specopts

def execute(self):
__tracebackhide__ = True
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:
if firstresult: # first result hooks return a single value
outcome = _Result(results[0] if results else None, excinfo)
else:
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

return outcome.get_result()
while self.hook_impls:
hook_impl = self.hook_impls.pop()
try:
args = [caller_kwargs[argname] for argname in hook_impl.argnames]
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:
return _wrapped_call(hook_impl.function(*args), self.execute)
res = hook_impl.function(*args)
if res is not None:
if firstresult:
return res
results.append(res)

if not firstresult:
return results

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)


def _legacymulticall(hook_impls, caller_kwargs, specopts={}, hook=None):
return _LegacyMultiCall(
hook_impls, caller_kwargs, specopts=specopts, hook=hook).execute()


def _multicall(hook_impls, caller_kwargs, specopts={}, hook=None):
"""Execute a call into multiple python functions/methods and return the
result(s).

``caller_kwargs`` comes from _HookCaller.__call__().
"""
__tracebackhide__ = True
specopts = hook.spec_opts if hook else specopts
results = []
firstresult = specopts.get("firstresult")
excinfo = None
try: # run impl and wrapper setup functions in a loop
teardowns = []
try:
for hook_impl in reversed(hook_impls):
try:
args = [caller_kwargs[argname] for argname in hook_impl.argnames]
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:
if firstresult: # first result hooks return a single value
outcome = _Result(results[0] if results else None, excinfo)
else:
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

return outcome.get_result()
10 changes: 5 additions & 5 deletions testing/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Benchmarking and performance tests.
"""
import pytest
from pluggy import (_MultiCall, _LegacyMultiCall, HookImpl, HookspecMarker,
from pluggy import (_multicall, _legacymulticall, HookImpl, HookspecMarker,
HookimplMarker)

hookspec = HookspecMarker("example")
Expand All @@ -28,31 +28,31 @@ def wrapper(arg1, arg2, arg3):


@pytest.fixture(
params=[0, 1, 10, 100],
params=[10, 100],
ids="hooks={}".format,
)
def hooks(request):
return [hook for i in range(request.param)]


@pytest.fixture(
params=[0, 1, 10, 100],
params=[10, 100],
ids="wrappers={}".format,
)
def wrappers(request):
return [wrapper for i in range(request.param)]


@pytest.fixture(
params=[_MultiCall, _LegacyMultiCall],
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()
return MC(methods, {'arg1': 1, 'arg2': 2, 'arg3': 3}, callertype)


def test_hook_and_wrappers_speed(benchmark, hooks, wrappers, callertype):
Expand Down
Loading