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 4 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
24 changes: 12 additions & 12 deletions pluggy.py → 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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a duplication of outcome just to remove the bad ctor
lets just move the exception handling behaviour to a alternate ctor,

so we can have _Result(result=...) or _Result.from_call(func)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I really didn't like the duplication.
@RonnyPfannschmidt are you ok with keeping _Result and being rid of _CallOutcome?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

both are internal classes and the exposed api is the same, so go ahead :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RonnyPfannschmidt good enough?

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)
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