You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

209 lines
6.7KB

  1. """
  2. Call loop machinery
  3. """
  4. import sys
  5. import warnings
  6. _py3 = sys.version_info > (3, 0)
  7. if not _py3:
  8. exec(
  9. """
  10. def _reraise(cls, val, tb):
  11. raise cls, val, tb
  12. """
  13. )
  14. def _raise_wrapfail(wrap_controller, msg):
  15. co = wrap_controller.gi_code
  16. raise RuntimeError(
  17. "wrap_controller at %r %s:%d %s"
  18. % (co.co_name, co.co_filename, co.co_firstlineno, msg)
  19. )
  20. class HookCallError(Exception):
  21. """ Hook was called wrongly. """
  22. class _Result(object):
  23. def __init__(self, result, excinfo):
  24. self._result = result
  25. self._excinfo = excinfo
  26. @property
  27. def excinfo(self):
  28. return self._excinfo
  29. @property
  30. def result(self):
  31. """Get the result(s) for this hook call (DEPRECATED in favor of ``get_result()``)."""
  32. msg = "Use get_result() which forces correct exception handling"
  33. warnings.warn(DeprecationWarning(msg), stacklevel=2)
  34. return self._result
  35. @classmethod
  36. def from_call(cls, func):
  37. __tracebackhide__ = True
  38. result = excinfo = None
  39. try:
  40. result = func()
  41. except BaseException:
  42. excinfo = sys.exc_info()
  43. return cls(result, excinfo)
  44. def force_result(self, result):
  45. """Force the result(s) to ``result``.
  46. If the hook was marked as a ``firstresult`` a single value should
  47. be set otherwise set a (modified) list of results. Any exceptions
  48. found during invocation will be deleted.
  49. """
  50. self._result = result
  51. self._excinfo = None
  52. def get_result(self):
  53. """Get the result(s) for this hook call.
  54. If the hook was marked as a ``firstresult`` only a single value
  55. will be returned otherwise a list of results.
  56. """
  57. __tracebackhide__ = True
  58. if self._excinfo is None:
  59. return self._result
  60. else:
  61. ex = self._excinfo
  62. if _py3:
  63. raise ex[1].with_traceback(ex[2])
  64. _reraise(*ex) # noqa
  65. def _wrapped_call(wrap_controller, func):
  66. """ Wrap calling to a function with a generator which needs to yield
  67. exactly once. The yield point will trigger calling the wrapped function
  68. and return its ``_Result`` to the yield point. The generator then needs
  69. to finish (raise StopIteration) in order for the wrapped call to complete.
  70. """
  71. try:
  72. next(wrap_controller) # first yield
  73. except StopIteration:
  74. _raise_wrapfail(wrap_controller, "did not yield")
  75. call_outcome = _Result.from_call(func)
  76. try:
  77. wrap_controller.send(call_outcome)
  78. _raise_wrapfail(wrap_controller, "has second yield")
  79. except StopIteration:
  80. pass
  81. return call_outcome.get_result()
  82. class _LegacyMultiCall(object):
  83. """ execute a call into multiple python functions/methods. """
  84. # XXX note that the __multicall__ argument is supported only
  85. # for pytest compatibility reasons. It was never officially
  86. # supported there and is explicitely deprecated since 2.8
  87. # so we can remove it soon, allowing to avoid the below recursion
  88. # in execute() and simplify/speed up the execute loop.
  89. def __init__(self, hook_impls, kwargs, firstresult=False):
  90. self.hook_impls = hook_impls
  91. self.caller_kwargs = kwargs # come from _HookCaller.__call__()
  92. self.caller_kwargs["__multicall__"] = self
  93. self.firstresult = firstresult
  94. def execute(self):
  95. caller_kwargs = self.caller_kwargs
  96. self.results = results = []
  97. firstresult = self.firstresult
  98. while self.hook_impls:
  99. hook_impl = self.hook_impls.pop()
  100. try:
  101. args = [caller_kwargs[argname] for argname in hook_impl.argnames]
  102. except KeyError:
  103. for argname in hook_impl.argnames:
  104. if argname not in caller_kwargs:
  105. raise HookCallError(
  106. "hook call must provide argument %r" % (argname,)
  107. )
  108. if hook_impl.hookwrapper:
  109. return _wrapped_call(hook_impl.function(*args), self.execute)
  110. res = hook_impl.function(*args)
  111. if res is not None:
  112. if firstresult:
  113. return res
  114. results.append(res)
  115. if not firstresult:
  116. return results
  117. def __repr__(self):
  118. status = "%d meths" % (len(self.hook_impls),)
  119. if hasattr(self, "results"):
  120. status = ("%d results, " % len(self.results)) + status
  121. return "<_MultiCall %s, kwargs=%r>" % (status, self.caller_kwargs)
  122. def _legacymulticall(hook_impls, caller_kwargs, firstresult=False):
  123. return _LegacyMultiCall(
  124. hook_impls, caller_kwargs, firstresult=firstresult
  125. ).execute()
  126. def _multicall(hook_impls, caller_kwargs, firstresult=False):
  127. """Execute a call into multiple python functions/methods and return the
  128. result(s).
  129. ``caller_kwargs`` comes from _HookCaller.__call__().
  130. """
  131. __tracebackhide__ = True
  132. results = []
  133. excinfo = None
  134. try: # run impl and wrapper setup functions in a loop
  135. teardowns = []
  136. try:
  137. for hook_impl in reversed(hook_impls):
  138. try:
  139. args = [caller_kwargs[argname] for argname in hook_impl.argnames]
  140. except KeyError:
  141. for argname in hook_impl.argnames:
  142. if argname not in caller_kwargs:
  143. raise HookCallError(
  144. "hook call must provide argument %r" % (argname,)
  145. )
  146. if hook_impl.hookwrapper:
  147. try:
  148. gen = hook_impl.function(*args)
  149. next(gen) # first yield
  150. teardowns.append(gen)
  151. except StopIteration:
  152. _raise_wrapfail(gen, "did not yield")
  153. else:
  154. res = hook_impl.function(*args)
  155. if res is not None:
  156. results.append(res)
  157. if firstresult: # halt further impl calls
  158. break
  159. except BaseException:
  160. excinfo = sys.exc_info()
  161. finally:
  162. if firstresult: # first result hooks return a single value
  163. outcome = _Result(results[0] if results else None, excinfo)
  164. else:
  165. outcome = _Result(results, excinfo)
  166. # run all wrapper post-yield blocks
  167. for gen in reversed(teardowns):
  168. try:
  169. gen.send(outcome)
  170. _raise_wrapfail(gen, "has second yield")
  171. except StopIteration:
  172. pass
  173. return outcome.get_result()