test_invocations.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. from typing import Iterator
  2. import pytest
  3. from pluggy import HookimplMarker
  4. from pluggy import HookspecMarker
  5. from pluggy import PluginManager
  6. from pluggy import PluginValidationError
  7. hookspec = HookspecMarker("example")
  8. hookimpl = HookimplMarker("example")
  9. def test_argmismatch(pm: PluginManager) -> None:
  10. class Api:
  11. @hookspec
  12. def hello(self, arg):
  13. "api hook 1"
  14. pm.add_hookspecs(Api)
  15. class Plugin:
  16. @hookimpl
  17. def hello(self, argwrong):
  18. pass
  19. with pytest.raises(PluginValidationError) as exc:
  20. pm.register(Plugin())
  21. assert "argwrong" in str(exc.value)
  22. def test_only_kwargs(pm: PluginManager) -> None:
  23. class Api:
  24. @hookspec
  25. def hello(self, arg):
  26. "api hook 1"
  27. pm.add_hookspecs(Api)
  28. with pytest.raises(TypeError) as exc:
  29. pm.hook.hello(3) # type: ignore[call-arg]
  30. message = "__call__() takes 1 positional argument but 2 were given"
  31. assert message in str(exc.value)
  32. def test_opt_in_args(pm: PluginManager) -> None:
  33. """Verify that two hookimpls with mutex args can serve
  34. under the same spec.
  35. """
  36. class Api:
  37. @hookspec
  38. def hello(self, arg1, arg2, common_arg):
  39. "api hook 1"
  40. class Plugin1:
  41. @hookimpl
  42. def hello(self, arg1, common_arg):
  43. return arg1 + common_arg
  44. class Plugin2:
  45. @hookimpl
  46. def hello(self, arg2, common_arg):
  47. return arg2 + common_arg
  48. pm.add_hookspecs(Api)
  49. pm.register(Plugin1())
  50. pm.register(Plugin2())
  51. results = pm.hook.hello(arg1=1, arg2=2, common_arg=0)
  52. assert results == [2, 1]
  53. def test_call_order(pm: PluginManager) -> None:
  54. class Api:
  55. @hookspec
  56. def hello(self, arg):
  57. "api hook 1"
  58. pm.add_hookspecs(Api)
  59. class Plugin1:
  60. @hookimpl
  61. def hello(self, arg):
  62. return 1
  63. class Plugin2:
  64. @hookimpl
  65. def hello(self, arg):
  66. return 2
  67. class Plugin3:
  68. @hookimpl
  69. def hello(self, arg):
  70. return 3
  71. class Plugin4:
  72. @hookimpl(hookwrapper=True)
  73. def hello(self, arg):
  74. assert arg == 0
  75. outcome = yield
  76. assert outcome.get_result() == [3, 2, 1]
  77. assert outcome.exception is None
  78. assert outcome.excinfo is None
  79. class Plugin5:
  80. @hookimpl(wrapper=True)
  81. def hello(self, arg):
  82. assert arg == 0
  83. result = yield
  84. assert result == [3, 2, 1]
  85. return result
  86. pm.register(Plugin1())
  87. pm.register(Plugin2())
  88. pm.register(Plugin3())
  89. pm.register(Plugin4()) # hookwrapper should get same list result
  90. pm.register(Plugin5()) # hookwrapper should get same list result
  91. res = pm.hook.hello(arg=0)
  92. assert res == [3, 2, 1]
  93. def test_firstresult_definition(pm: PluginManager) -> None:
  94. class Api:
  95. @hookspec(firstresult=True)
  96. def hello(self, arg):
  97. "api hook 1"
  98. pm.add_hookspecs(Api)
  99. class Plugin1:
  100. @hookimpl
  101. def hello(self, arg):
  102. return arg + 1
  103. class Plugin2:
  104. @hookimpl
  105. def hello(self, arg):
  106. return arg - 1
  107. class Plugin3:
  108. @hookimpl
  109. def hello(self, arg):
  110. return None
  111. class Plugin4:
  112. @hookimpl(wrapper=True)
  113. def hello(self, arg):
  114. assert arg == 3
  115. outcome = yield
  116. assert outcome == 2
  117. return outcome
  118. class Plugin5:
  119. @hookimpl(hookwrapper=True)
  120. def hello(self, arg):
  121. assert arg == 3
  122. outcome = yield
  123. assert outcome.get_result() == 2
  124. pm.register(Plugin1()) # discarded - not the last registered plugin
  125. pm.register(Plugin2()) # used as result
  126. pm.register(Plugin3()) # None result is ignored
  127. pm.register(Plugin4()) # wrapper should get same non-list result
  128. pm.register(Plugin5()) # hookwrapper should get same non-list result
  129. res = pm.hook.hello(arg=3)
  130. assert res == 2
  131. def test_firstresult_force_result_hookwrapper(pm: PluginManager) -> None:
  132. """Verify forcing a result in a wrapper."""
  133. class Api:
  134. @hookspec(firstresult=True)
  135. def hello(self, arg):
  136. "api hook 1"
  137. pm.add_hookspecs(Api)
  138. class Plugin1:
  139. @hookimpl
  140. def hello(self, arg):
  141. return arg + 1
  142. class Plugin2:
  143. @hookimpl(hookwrapper=True)
  144. def hello(self, arg):
  145. assert arg == 3
  146. outcome = yield
  147. assert outcome.get_result() == 4
  148. outcome.force_result(0)
  149. class Plugin3:
  150. @hookimpl
  151. def hello(self, arg):
  152. return None
  153. pm.register(Plugin1())
  154. pm.register(Plugin2()) # wrapper
  155. pm.register(Plugin3()) # ignored since returns None
  156. res = pm.hook.hello(arg=3)
  157. assert res == 0 # this result is forced and not a list
  158. def test_firstresult_force_result(pm: PluginManager) -> None:
  159. """Verify forcing a result in a wrapper."""
  160. class Api:
  161. @hookspec(firstresult=True)
  162. def hello(self, arg):
  163. "api hook 1"
  164. pm.add_hookspecs(Api)
  165. class Plugin1:
  166. @hookimpl
  167. def hello(self, arg):
  168. return arg + 1
  169. class Plugin2:
  170. @hookimpl(wrapper=True)
  171. def hello(self, arg):
  172. assert arg == 3
  173. outcome = yield
  174. assert outcome == 4
  175. return 0
  176. class Plugin3:
  177. @hookimpl
  178. def hello(self, arg):
  179. return None
  180. pm.register(Plugin1())
  181. pm.register(Plugin2()) # wrapper
  182. pm.register(Plugin3()) # ignored since returns None
  183. res = pm.hook.hello(arg=3)
  184. assert res == 0 # this result is forced and not a list
  185. def test_firstresult_returns_none(pm: PluginManager) -> None:
  186. """If None results are returned by underlying implementations ensure
  187. the multi-call loop returns a None value.
  188. """
  189. class Api:
  190. @hookspec(firstresult=True)
  191. def hello(self, arg):
  192. "api hook 1"
  193. pm.add_hookspecs(Api)
  194. class Plugin1:
  195. @hookimpl
  196. def hello(self, arg):
  197. return None
  198. pm.register(Plugin1())
  199. res = pm.hook.hello(arg=3)
  200. assert res is None
  201. def test_firstresult_no_plugin(pm: PluginManager) -> None:
  202. """If no implementations/plugins have been registered for a firstresult
  203. hook the multi-call loop should return a None value.
  204. """
  205. class Api:
  206. @hookspec(firstresult=True)
  207. def hello(self, arg):
  208. "api hook 1"
  209. pm.add_hookspecs(Api)
  210. res = pm.hook.hello(arg=3)
  211. assert res is None
  212. def test_no_hookspec(pm: PluginManager) -> None:
  213. """A hook with hookimpls can still be called even if no hookspec
  214. was registered for it (and call_pending wasn't called to check
  215. against it).
  216. """
  217. class Plugin:
  218. @hookimpl
  219. def hello(self, arg):
  220. return "Plugin.hello"
  221. pm.register(Plugin())
  222. assert pm.hook.hello(arg=10, extra=20) == ["Plugin.hello"]
  223. def test_non_wrapper_generator(pm: PluginManager) -> None:
  224. """A hookimpl can be a generator without being a wrapper,
  225. meaning it returns an iterator result."""
  226. class Api:
  227. @hookspec
  228. def hello(self) -> Iterator[int]:
  229. raise NotImplementedError()
  230. pm.add_hookspecs(Api)
  231. class Plugin1:
  232. @hookimpl
  233. def hello(self):
  234. yield 1
  235. class Plugin2:
  236. @hookimpl
  237. def hello(self):
  238. yield 2
  239. yield 3
  240. class Plugin3:
  241. @hookimpl(wrapper=True)
  242. def hello(self):
  243. return (yield)
  244. pm.register(Plugin1())
  245. pm.register(Plugin2()) # wrapper
  246. res = pm.hook.hello()
  247. assert [y for x in res for y in x] == [2, 3, 1]
  248. pm.register(Plugin3())
  249. res = pm.hook.hello()
  250. assert [y for x in res for y in x] == [2, 3, 1]