test_multicall.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. from typing import Callable
  2. from typing import List
  3. from typing import Mapping
  4. from typing import Sequence
  5. from typing import Type
  6. from typing import Union
  7. import pytest
  8. from pluggy import HookCallError
  9. from pluggy import HookimplMarker
  10. from pluggy import HookspecMarker
  11. from pluggy._callers import _multicall
  12. from pluggy._hooks import HookImpl
  13. hookspec = HookspecMarker("example")
  14. hookimpl = HookimplMarker("example")
  15. def MC(
  16. methods: Sequence[Callable[..., object]],
  17. kwargs: Mapping[str, object],
  18. firstresult: bool = False,
  19. ) -> Union[object, List[object]]:
  20. caller = _multicall
  21. hookfuncs = []
  22. for method in methods:
  23. f = HookImpl(None, "<temp>", method, method.example_impl) # type: ignore[attr-defined]
  24. hookfuncs.append(f)
  25. return caller("foo", hookfuncs, kwargs, firstresult)
  26. def test_keyword_args() -> None:
  27. @hookimpl
  28. def f(x):
  29. return x + 1
  30. class A:
  31. @hookimpl
  32. def f(self, x, y):
  33. return x + y
  34. reslist = MC([f, A().f], dict(x=23, y=24))
  35. assert reslist == [24 + 23, 24]
  36. def test_keyword_args_with_defaultargs() -> None:
  37. @hookimpl
  38. def f(x, z=1):
  39. return x + z
  40. reslist = MC([f], dict(x=23, y=24))
  41. assert reslist == [24]
  42. def test_tags_call_error() -> None:
  43. @hookimpl
  44. def f(x):
  45. return x
  46. with pytest.raises(HookCallError):
  47. MC([f], {})
  48. def test_call_none_is_no_result() -> None:
  49. @hookimpl
  50. def m1():
  51. return 1
  52. @hookimpl
  53. def m2():
  54. return None
  55. res = MC([m1, m2], {}, firstresult=True)
  56. assert res == 1
  57. res = MC([m1, m2], {}, firstresult=False)
  58. assert res == [1]
  59. def test_hookwrapper() -> None:
  60. out = []
  61. @hookimpl(hookwrapper=True)
  62. def m1():
  63. out.append("m1 init")
  64. yield None
  65. out.append("m1 finish")
  66. @hookimpl
  67. def m2():
  68. out.append("m2")
  69. return 2
  70. res = MC([m2, m1], {})
  71. assert res == [2]
  72. assert out == ["m1 init", "m2", "m1 finish"]
  73. out[:] = []
  74. res = MC([m2, m1], {}, firstresult=True)
  75. assert res == 2
  76. assert out == ["m1 init", "m2", "m1 finish"]
  77. def test_hookwrapper_two_yields() -> None:
  78. @hookimpl(hookwrapper=True)
  79. def m():
  80. yield
  81. yield
  82. with pytest.raises(RuntimeError, match="has second yield"):
  83. MC([m], {})
  84. def test_wrapper() -> None:
  85. out = []
  86. @hookimpl(wrapper=True)
  87. def m1():
  88. out.append("m1 init")
  89. result = yield
  90. out.append("m1 finish")
  91. return result * 2
  92. @hookimpl
  93. def m2():
  94. out.append("m2")
  95. return 2
  96. res = MC([m2, m1], {})
  97. assert res == [2, 2]
  98. assert out == ["m1 init", "m2", "m1 finish"]
  99. out[:] = []
  100. res = MC([m2, m1], {}, firstresult=True)
  101. assert res == 4
  102. assert out == ["m1 init", "m2", "m1 finish"]
  103. def test_wrapper_two_yields() -> None:
  104. @hookimpl(wrapper=True)
  105. def m():
  106. yield
  107. yield
  108. with pytest.raises(RuntimeError, match="has second yield"):
  109. MC([m], {})
  110. def test_hookwrapper_order() -> None:
  111. out = []
  112. @hookimpl(hookwrapper=True)
  113. def m1():
  114. out.append("m1 init")
  115. yield 1
  116. out.append("m1 finish")
  117. @hookimpl(wrapper=True)
  118. def m2():
  119. out.append("m2 init")
  120. result = yield 2
  121. out.append("m2 finish")
  122. return result
  123. @hookimpl(hookwrapper=True)
  124. def m3():
  125. out.append("m3 init")
  126. yield 3
  127. out.append("m3 finish")
  128. @hookimpl(hookwrapper=True)
  129. def m4():
  130. out.append("m4 init")
  131. yield 4
  132. out.append("m4 finish")
  133. res = MC([m4, m3, m2, m1], {})
  134. assert res == []
  135. assert out == [
  136. "m1 init",
  137. "m2 init",
  138. "m3 init",
  139. "m4 init",
  140. "m4 finish",
  141. "m3 finish",
  142. "m2 finish",
  143. "m1 finish",
  144. ]
  145. def test_hookwrapper_not_yield() -> None:
  146. @hookimpl(hookwrapper=True)
  147. def m1():
  148. pass
  149. with pytest.raises(TypeError):
  150. MC([m1], {})
  151. def test_hookwrapper_yield_not_executed() -> None:
  152. @hookimpl(hookwrapper=True)
  153. def m1():
  154. if False:
  155. yield # type: ignore[unreachable]
  156. with pytest.raises(RuntimeError, match="did not yield"):
  157. MC([m1], {})
  158. def test_hookwrapper_too_many_yield() -> None:
  159. @hookimpl(hookwrapper=True)
  160. def m1():
  161. yield 1
  162. yield 2
  163. with pytest.raises(RuntimeError) as ex:
  164. MC([m1], {})
  165. assert "m1" in str(ex.value)
  166. assert (__file__ + ":") in str(ex.value)
  167. def test_wrapper_yield_not_executed() -> None:
  168. @hookimpl(wrapper=True)
  169. def m1():
  170. if False:
  171. yield # type: ignore[unreachable]
  172. with pytest.raises(RuntimeError, match="did not yield"):
  173. MC([m1], {})
  174. def test_wrapper_too_many_yield() -> None:
  175. out = []
  176. @hookimpl(wrapper=True)
  177. def m1():
  178. try:
  179. yield 1
  180. yield 2
  181. finally:
  182. out.append("cleanup")
  183. with pytest.raises(RuntimeError) as ex:
  184. try:
  185. MC([m1], {})
  186. finally:
  187. out.append("finally")
  188. assert "m1" in str(ex.value)
  189. assert (__file__ + ":") in str(ex.value)
  190. assert out == ["cleanup", "finally"]
  191. @pytest.mark.parametrize("exc", [ValueError, SystemExit])
  192. def test_hookwrapper_exception(exc: "Type[BaseException]") -> None:
  193. out = []
  194. @hookimpl(hookwrapper=True)
  195. def m1():
  196. out.append("m1 init")
  197. result = yield
  198. assert isinstance(result.exception, exc)
  199. assert result.excinfo[0] == exc
  200. out.append("m1 finish")
  201. @hookimpl
  202. def m2():
  203. raise exc
  204. with pytest.raises(exc):
  205. MC([m2, m1], {})
  206. assert out == ["m1 init", "m1 finish"]
  207. def test_hookwrapper_force_exception() -> None:
  208. out = []
  209. @hookimpl(hookwrapper=True)
  210. def m1():
  211. out.append("m1 init")
  212. result = yield
  213. try:
  214. result.get_result()
  215. except BaseException as exc:
  216. result.force_exception(exc)
  217. out.append("m1 finish")
  218. @hookimpl(hookwrapper=True)
  219. def m2():
  220. out.append("m2 init")
  221. result = yield
  222. try:
  223. result.get_result()
  224. except BaseException as exc:
  225. new_exc = OSError("m2")
  226. new_exc.__cause__ = exc
  227. result.force_exception(new_exc)
  228. out.append("m2 finish")
  229. @hookimpl(hookwrapper=True)
  230. def m3():
  231. out.append("m3 init")
  232. yield
  233. out.append("m3 finish")
  234. @hookimpl
  235. def m4():
  236. raise ValueError("m4")
  237. with pytest.raises(OSError, match="m2") as excinfo:
  238. MC([m4, m3, m2, m1], {})
  239. assert out == [
  240. "m1 init",
  241. "m2 init",
  242. "m3 init",
  243. "m3 finish",
  244. "m2 finish",
  245. "m1 finish",
  246. ]
  247. assert excinfo.value.__cause__ is not None
  248. assert str(excinfo.value.__cause__) == "m4"
  249. @pytest.mark.parametrize("exc", [ValueError, SystemExit])
  250. def test_wrapper_exception(exc: "Type[BaseException]") -> None:
  251. out = []
  252. @hookimpl(wrapper=True)
  253. def m1():
  254. out.append("m1 init")
  255. try:
  256. result = yield
  257. except BaseException as e:
  258. assert isinstance(e, exc)
  259. raise
  260. finally:
  261. out.append("m1 finish")
  262. return result
  263. @hookimpl
  264. def m2():
  265. out.append("m2 init")
  266. raise exc
  267. with pytest.raises(exc):
  268. MC([m2, m1], {})
  269. assert out == ["m1 init", "m2 init", "m1 finish"]
  270. def test_wrapper_exception_chaining() -> None:
  271. @hookimpl
  272. def m1():
  273. raise Exception("m1")
  274. @hookimpl(wrapper=True)
  275. def m2():
  276. try:
  277. yield
  278. except Exception:
  279. raise Exception("m2")
  280. @hookimpl(wrapper=True)
  281. def m3():
  282. yield
  283. return 10
  284. @hookimpl(wrapper=True)
  285. def m4():
  286. try:
  287. yield
  288. except Exception as e:
  289. raise Exception("m4") from e
  290. with pytest.raises(Exception) as excinfo:
  291. MC([m1, m2, m3, m4], {})
  292. assert str(excinfo.value) == "m4"
  293. assert excinfo.value.__cause__ is not None
  294. assert str(excinfo.value.__cause__) == "m2"
  295. assert excinfo.value.__cause__.__context__ is not None
  296. assert str(excinfo.value.__cause__.__context__) == "m1"
  297. def test_unwind_inner_wrapper_teardown_exc() -> None:
  298. out = []
  299. @hookimpl(wrapper=True)
  300. def m1():
  301. out.append("m1 init")
  302. try:
  303. yield
  304. out.append("m1 unreachable")
  305. except BaseException:
  306. out.append("m1 teardown")
  307. raise
  308. finally:
  309. out.append("m1 cleanup")
  310. @hookimpl(wrapper=True)
  311. def m2():
  312. out.append("m2 init")
  313. yield
  314. out.append("m2 raise")
  315. raise ValueError()
  316. with pytest.raises(ValueError):
  317. try:
  318. MC([m2, m1], {})
  319. finally:
  320. out.append("finally")
  321. assert out == [
  322. "m1 init",
  323. "m2 init",
  324. "m2 raise",
  325. "m1 teardown",
  326. "m1 cleanup",
  327. "finally",
  328. ]
  329. def test_suppress_inner_wrapper_teardown_exc() -> None:
  330. out = []
  331. @hookimpl(wrapper=True)
  332. def m1():
  333. out.append("m1 init")
  334. result = yield
  335. out.append("m1 finish")
  336. return result
  337. @hookimpl(wrapper=True)
  338. def m2():
  339. out.append("m2 init")
  340. try:
  341. yield
  342. out.append("m2 unreachable")
  343. except ValueError:
  344. out.append("m2 suppress")
  345. return 22
  346. @hookimpl(wrapper=True)
  347. def m3():
  348. out.append("m3 init")
  349. yield
  350. out.append("m3 raise")
  351. raise ValueError()
  352. assert MC([m3, m2, m1], {}) == 22
  353. assert out == [
  354. "m1 init",
  355. "m2 init",
  356. "m3 init",
  357. "m3 raise",
  358. "m2 suppress",
  359. "m1 finish",
  360. ]