diff --git a/pluggy.py b/pluggy/__init__.py similarity index 94% rename from pluggy.py rename to pluggy/__init__.py index fa30fcdb..9eff1ac9 100644 --- a/pluggy.py +++ b/pluggy/__init__.py @@ -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. @@ -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). @@ -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") @@ -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 @@ -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() @@ -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() @@ -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 @@ -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 @@ -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) @@ -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,) diff --git a/pluggy/callers.py b/pluggy/callers.py new file mode 100644 index 00000000..abe36e78 --- /dev/null +++ b/pluggy/callers.py @@ -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) diff --git a/setup.py b/setup.py index 3e2219a7..36c12a0e 100644 --- a/setup.py +++ b/setup.py @@ -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: @@ -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'], ) diff --git a/testing/benchmark.py b/testing/benchmark.py index e5888a5f..d2609b86 100644 --- a/testing/benchmark.py +++ b/testing/benchmark.py @@ -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, "", method, method.example_impl) hookfuncs.append(f) - return _MultiCall(hookfuncs, kwargs, {"firstresult": firstresult}) + return callertype(hookfuncs, kwargs, {"firstresult": firstresult}) @hookimpl @@ -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) diff --git a/testing/test_multicall.py b/testing/test_multicall.py index 7aff8c33..898fc21d 100644 --- a/testing/test_multicall.py +++ b/testing/test_multicall.py @@ -1,6 +1,6 @@ import pytest -from pluggy import _MultiCall, HookImpl, HookCallError +from pluggy import _MultiCall, HookImpl, HookCallError, _LegacyMultiCall from pluggy import HookspecMarker, HookimplMarker @@ -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, "", 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():