"""
|
|
Call loop machinery
|
|
"""
|
|
import sys
|
|
import warnings
|
|
|
|
_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
|
|
|
|
@property
|
|
def excinfo(self):
|
|
return self._excinfo
|
|
|
|
@property
|
|
def result(self):
|
|
"""Get the result(s) for this hook call (DEPRECATED in favor of ``get_result()``)."""
|
|
msg = "Use get_result() which forces correct exception handling"
|
|
warnings.warn(DeprecationWarning(msg), stacklevel=2)
|
|
return self._result
|
|
|
|
@classmethod
|
|
def from_call(cls, func):
|
|
__tracebackhide__ = True
|
|
result = excinfo = None
|
|
try:
|
|
result = func()
|
|
except BaseException:
|
|
excinfo = sys.exc_info()
|
|
|
|
return cls(result, excinfo)
|
|
|
|
def force_result(self, result):
|
|
"""Force the result(s) to ``result``.
|
|
|
|
If the hook was marked as a ``firstresult`` a single value should
|
|
be set otherwise set a (modified) list of results. Any exceptions
|
|
found during invocation will be deleted.
|
|
"""
|
|
self._result = result
|
|
self._excinfo = None
|
|
|
|
def get_result(self):
|
|
"""Get the result(s) for this hook call.
|
|
|
|
If the hook was marked as a ``firstresult`` only a single value
|
|
will be returned otherwise a list of results.
|
|
"""
|
|
__tracebackhide__ = True
|
|
if self._excinfo is None:
|
|
return self._result
|
|
else:
|
|
ex = self._excinfo
|
|
if _py3:
|
|
raise ex[1].with_traceback(ex[2])
|
|
_reraise(*ex) # noqa
|
|
|
|
|
|
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, firstresult=False):
|
|
self.hook_impls = hook_impls
|
|
self.caller_kwargs = kwargs # come from _HookCaller.__call__()
|
|
self.caller_kwargs["__multicall__"] = self
|
|
self.firstresult = firstresult
|
|
|
|
def execute(self):
|
|
caller_kwargs = self.caller_kwargs
|
|
self.results = results = []
|
|
firstresult = self.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 _legacymulticall(hook_impls, caller_kwargs, firstresult=False):
|
|
return _LegacyMultiCall(
|
|
hook_impls, caller_kwargs, firstresult=firstresult
|
|
).execute()
|
|
|
|
|
|
def _multicall(hook_impls, caller_kwargs, firstresult=False):
|
|
"""Execute a call into multiple python functions/methods and return the
|
|
result(s).
|
|
|
|
``caller_kwargs`` comes from _HookCaller.__call__().
|
|
"""
|
|
__tracebackhide__ = True
|
|
results = []
|
|
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()
|