Skip to content

Commit

Permalink
Merge pull request pytest-dev#90 from tgoodlet/multicall_to_func
Browse files Browse the repository at this point in the history
Multicall to func
  • Loading branch information
RonnyPfannschmidt authored Oct 20, 2017
2 parents a62cff6 + 7791b9c commit 4fb708b
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 150 deletions.
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

0 comments on commit 4fb708b

Please sign in to comment.