test_pluginmanager.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. """
  2. ``PluginManager`` unit and public API testing.
  3. """
  4. import pytest
  5. from pluggy import (
  6. PluginValidationError,
  7. HookCallError,
  8. HookimplMarker,
  9. HookspecMarker,
  10. )
  11. from pluggy._manager import importlib_metadata
  12. hookspec = HookspecMarker("example")
  13. hookimpl = HookimplMarker("example")
  14. def test_plugin_double_register(pm):
  15. """Registering the same plugin more then once isn't allowed"""
  16. pm.register(42, name="abc")
  17. with pytest.raises(ValueError):
  18. pm.register(42, name="abc")
  19. with pytest.raises(ValueError):
  20. pm.register(42, name="def")
  21. def test_pm(pm):
  22. """Basic registration with objects"""
  23. class A:
  24. pass
  25. a1, a2 = A(), A()
  26. pm.register(a1)
  27. assert pm.is_registered(a1)
  28. pm.register(a2, "hello")
  29. assert pm.is_registered(a2)
  30. out = pm.get_plugins()
  31. assert a1 in out
  32. assert a2 in out
  33. assert pm.get_plugin("hello") == a2
  34. assert pm.unregister(a1) == a1
  35. assert not pm.is_registered(a1)
  36. out = pm.list_name_plugin()
  37. assert len(out) == 1
  38. assert out == [("hello", a2)]
  39. def test_has_plugin(pm):
  40. class A:
  41. pass
  42. a1 = A()
  43. pm.register(a1, "hello")
  44. assert pm.is_registered(a1)
  45. assert pm.has_plugin("hello")
  46. def test_register_dynamic_attr(he_pm):
  47. class A:
  48. def __getattr__(self, name):
  49. if name[0] != "_":
  50. return 42
  51. raise AttributeError()
  52. a = A()
  53. he_pm.register(a)
  54. assert not he_pm.get_hookcallers(a)
  55. def test_pm_name(pm):
  56. class A:
  57. pass
  58. a1 = A()
  59. name = pm.register(a1, name="hello")
  60. assert name == "hello"
  61. pm.unregister(a1)
  62. assert pm.get_plugin(a1) is None
  63. assert not pm.is_registered(a1)
  64. assert not pm.get_plugins()
  65. name2 = pm.register(a1, name="hello")
  66. assert name2 == name
  67. pm.unregister(name="hello")
  68. assert pm.get_plugin(a1) is None
  69. assert not pm.is_registered(a1)
  70. assert not pm.get_plugins()
  71. def test_set_blocked(pm):
  72. class A:
  73. pass
  74. a1 = A()
  75. name = pm.register(a1)
  76. assert pm.is_registered(a1)
  77. assert not pm.is_blocked(name)
  78. pm.set_blocked(name)
  79. assert pm.is_blocked(name)
  80. assert not pm.is_registered(a1)
  81. pm.set_blocked("somename")
  82. assert pm.is_blocked("somename")
  83. assert not pm.register(A(), "somename")
  84. pm.unregister(name="somename")
  85. assert pm.is_blocked("somename")
  86. def test_register_mismatch_method(he_pm):
  87. class hello:
  88. @hookimpl
  89. def he_method_notexists(self):
  90. pass
  91. plugin = hello()
  92. he_pm.register(plugin)
  93. with pytest.raises(PluginValidationError) as excinfo:
  94. he_pm.check_pending()
  95. assert excinfo.value.plugin is plugin
  96. def test_register_mismatch_arg(he_pm):
  97. class hello:
  98. @hookimpl
  99. def he_method1(self, qlwkje):
  100. pass
  101. plugin = hello()
  102. with pytest.raises(PluginValidationError) as excinfo:
  103. he_pm.register(plugin)
  104. assert excinfo.value.plugin is plugin
  105. def test_register_hookwrapper_not_a_generator_function(he_pm):
  106. class hello:
  107. @hookimpl(hookwrapper=True)
  108. def he_method1(self):
  109. pass # pragma: no cover
  110. plugin = hello()
  111. with pytest.raises(PluginValidationError, match="generator function") as excinfo:
  112. he_pm.register(plugin)
  113. assert excinfo.value.plugin is plugin
  114. def test_register(pm):
  115. class MyPlugin:
  116. pass
  117. my = MyPlugin()
  118. pm.register(my)
  119. assert my in pm.get_plugins()
  120. my2 = MyPlugin()
  121. pm.register(my2)
  122. assert {my, my2}.issubset(pm.get_plugins())
  123. assert pm.is_registered(my)
  124. assert pm.is_registered(my2)
  125. pm.unregister(my)
  126. assert not pm.is_registered(my)
  127. assert my not in pm.get_plugins()
  128. def test_register_unknown_hooks(pm):
  129. class Plugin1:
  130. @hookimpl
  131. def he_method1(self, arg):
  132. return arg + 1
  133. pname = pm.register(Plugin1())
  134. class Hooks:
  135. @hookspec
  136. def he_method1(self, arg):
  137. pass
  138. pm.add_hookspecs(Hooks)
  139. # assert not pm._unverified_hooks
  140. assert pm.hook.he_method1(arg=1) == [2]
  141. assert len(pm.get_hookcallers(pm.get_plugin(pname))) == 1
  142. def test_register_historic(pm):
  143. class Hooks:
  144. @hookspec(historic=True)
  145. def he_method1(self, arg):
  146. pass
  147. pm.add_hookspecs(Hooks)
  148. pm.hook.he_method1.call_historic(kwargs=dict(arg=1))
  149. out = []
  150. class Plugin:
  151. @hookimpl
  152. def he_method1(self, arg):
  153. out.append(arg)
  154. pm.register(Plugin())
  155. assert out == [1]
  156. class Plugin2:
  157. @hookimpl
  158. def he_method1(self, arg):
  159. out.append(arg * 10)
  160. pm.register(Plugin2())
  161. assert out == [1, 10]
  162. pm.hook.he_method1.call_historic(kwargs=dict(arg=12))
  163. assert out == [1, 10, 120, 12]
  164. @pytest.mark.parametrize("result_callback", [True, False])
  165. def test_with_result_memorized(pm, result_callback):
  166. """Verify that ``_HookCaller._maybe_apply_history()`
  167. correctly applies the ``result_callback`` function, when provided,
  168. to the result from calling each newly registered hook.
  169. """
  170. out = []
  171. if result_callback:
  172. def callback(res):
  173. out.append(res)
  174. else:
  175. callback = None
  176. class Hooks:
  177. @hookspec(historic=True)
  178. def he_method1(self, arg):
  179. pass
  180. pm.add_hookspecs(Hooks)
  181. class Plugin1:
  182. @hookimpl
  183. def he_method1(self, arg):
  184. return arg * 10
  185. pm.register(Plugin1())
  186. he_method1 = pm.hook.he_method1
  187. he_method1.call_historic(result_callback=callback, kwargs=dict(arg=1))
  188. class Plugin2:
  189. @hookimpl
  190. def he_method1(self, arg):
  191. return arg * 10
  192. pm.register(Plugin2())
  193. if result_callback:
  194. assert out == [10, 10]
  195. else:
  196. assert out == []
  197. def test_with_callbacks_immediately_executed(pm):
  198. class Hooks:
  199. @hookspec(historic=True)
  200. def he_method1(self, arg):
  201. pass
  202. pm.add_hookspecs(Hooks)
  203. class Plugin1:
  204. @hookimpl
  205. def he_method1(self, arg):
  206. return arg * 10
  207. class Plugin2:
  208. @hookimpl
  209. def he_method1(self, arg):
  210. return arg * 20
  211. class Plugin3:
  212. @hookimpl
  213. def he_method1(self, arg):
  214. return arg * 30
  215. out = []
  216. pm.register(Plugin1())
  217. pm.register(Plugin2())
  218. he_method1 = pm.hook.he_method1
  219. he_method1.call_historic(lambda res: out.append(res), dict(arg=1))
  220. assert out == [20, 10]
  221. pm.register(Plugin3())
  222. assert out == [20, 10, 30]
  223. def test_register_historic_incompat_hookwrapper(pm):
  224. class Hooks:
  225. @hookspec(historic=True)
  226. def he_method1(self, arg):
  227. pass
  228. pm.add_hookspecs(Hooks)
  229. out = []
  230. class Plugin:
  231. @hookimpl(hookwrapper=True)
  232. def he_method1(self, arg):
  233. out.append(arg)
  234. with pytest.raises(PluginValidationError):
  235. pm.register(Plugin())
  236. def test_call_extra(pm):
  237. class Hooks:
  238. @hookspec
  239. def he_method1(self, arg):
  240. pass
  241. pm.add_hookspecs(Hooks)
  242. def he_method1(arg):
  243. return arg * 10
  244. out = pm.hook.he_method1.call_extra([he_method1], dict(arg=1))
  245. assert out == [10]
  246. def test_call_with_too_few_args(pm):
  247. class Hooks:
  248. @hookspec
  249. def he_method1(self, arg):
  250. pass
  251. pm.add_hookspecs(Hooks)
  252. class Plugin1:
  253. @hookimpl
  254. def he_method1(self, arg):
  255. 0 / 0
  256. pm.register(Plugin1())
  257. with pytest.raises(HookCallError):
  258. with pytest.warns(UserWarning):
  259. pm.hook.he_method1()
  260. def test_subset_hook_caller(pm):
  261. class Hooks:
  262. @hookspec
  263. def he_method1(self, arg):
  264. pass
  265. pm.add_hookspecs(Hooks)
  266. out = []
  267. class Plugin1:
  268. @hookimpl
  269. def he_method1(self, arg):
  270. out.append(arg)
  271. class Plugin2:
  272. @hookimpl
  273. def he_method1(self, arg):
  274. out.append(arg * 10)
  275. class PluginNo:
  276. pass
  277. plugin1, plugin2, plugin3 = Plugin1(), Plugin2(), PluginNo()
  278. pm.register(plugin1)
  279. pm.register(plugin2)
  280. pm.register(plugin3)
  281. pm.hook.he_method1(arg=1)
  282. assert out == [10, 1]
  283. out[:] = []
  284. hc = pm.subset_hook_caller("he_method1", [plugin1])
  285. hc(arg=2)
  286. assert out == [20]
  287. out[:] = []
  288. hc = pm.subset_hook_caller("he_method1", [plugin2])
  289. hc(arg=2)
  290. assert out == [2]
  291. out[:] = []
  292. pm.unregister(plugin1)
  293. hc(arg=2)
  294. assert out == []
  295. out[:] = []
  296. pm.hook.he_method1(arg=1)
  297. assert out == [10]
  298. def test_get_hookimpls(pm):
  299. class Hooks:
  300. @hookspec
  301. def he_method1(self, arg):
  302. pass
  303. pm.add_hookspecs(Hooks)
  304. assert pm.hook.he_method1.get_hookimpls() == []
  305. class Plugin1:
  306. @hookimpl
  307. def he_method1(self, arg):
  308. pass
  309. class Plugin2:
  310. @hookimpl
  311. def he_method1(self, arg):
  312. pass
  313. class PluginNo:
  314. pass
  315. plugin1, plugin2, plugin3 = Plugin1(), Plugin2(), PluginNo()
  316. pm.register(plugin1)
  317. pm.register(plugin2)
  318. pm.register(plugin3)
  319. hookimpls = pm.hook.he_method1.get_hookimpls()
  320. hook_plugins = [item.plugin for item in hookimpls]
  321. assert hook_plugins == [plugin1, plugin2]
  322. def test_add_hookspecs_nohooks(pm):
  323. with pytest.raises(ValueError):
  324. pm.add_hookspecs(10)
  325. def test_load_setuptools_instantiation(monkeypatch, pm):
  326. class EntryPoint:
  327. name = "myname"
  328. group = "hello"
  329. value = "myname:foo"
  330. def load(self):
  331. class PseudoPlugin:
  332. x = 42
  333. return PseudoPlugin()
  334. class Distribution:
  335. entry_points = (EntryPoint(),)
  336. dist = Distribution()
  337. def my_distributions():
  338. return (dist,)
  339. monkeypatch.setattr(importlib_metadata, "distributions", my_distributions)
  340. num = pm.load_setuptools_entrypoints("hello")
  341. assert num == 1
  342. plugin = pm.get_plugin("myname")
  343. assert plugin.x == 42
  344. ret = pm.list_plugin_distinfo()
  345. # poor man's `assert ret == [(plugin, mock.ANY)]`
  346. assert len(ret) == 1
  347. assert len(ret[0]) == 2
  348. assert ret[0][0] == plugin
  349. assert ret[0][1]._dist == dist
  350. num = pm.load_setuptools_entrypoints("hello")
  351. assert num == 0 # no plugin loaded by this call
  352. def test_add_tracefuncs(he_pm):
  353. out = []
  354. class api1:
  355. @hookimpl
  356. def he_method1(self):
  357. out.append("he_method1-api1")
  358. class api2:
  359. @hookimpl
  360. def he_method1(self):
  361. out.append("he_method1-api2")
  362. he_pm.register(api1())
  363. he_pm.register(api2())
  364. def before(hook_name, hook_impls, kwargs):
  365. out.append((hook_name, list(hook_impls), kwargs))
  366. def after(outcome, hook_name, hook_impls, kwargs):
  367. out.append((outcome, hook_name, list(hook_impls), kwargs))
  368. undo = he_pm.add_hookcall_monitoring(before, after)
  369. he_pm.hook.he_method1(arg=1)
  370. assert len(out) == 4
  371. assert out[0][0] == "he_method1"
  372. assert len(out[0][1]) == 2
  373. assert isinstance(out[0][2], dict)
  374. assert out[1] == "he_method1-api2"
  375. assert out[2] == "he_method1-api1"
  376. assert len(out[3]) == 4
  377. assert out[3][1] == out[0][0]
  378. undo()
  379. he_pm.hook.he_method1(arg=1)
  380. assert len(out) == 4 + 2
  381. def test_hook_tracing(he_pm):
  382. saveindent = []
  383. class api1:
  384. @hookimpl
  385. def he_method1(self):
  386. saveindent.append(he_pm.trace.root.indent)
  387. class api2:
  388. @hookimpl
  389. def he_method1(self):
  390. saveindent.append(he_pm.trace.root.indent)
  391. raise ValueError()
  392. he_pm.register(api1())
  393. out = []
  394. he_pm.trace.root.setwriter(out.append)
  395. undo = he_pm.enable_tracing()
  396. try:
  397. indent = he_pm.trace.root.indent
  398. he_pm.hook.he_method1(arg=1)
  399. assert indent == he_pm.trace.root.indent
  400. assert len(out) == 2
  401. assert "he_method1" in out[0]
  402. assert "finish" in out[1]
  403. out[:] = []
  404. he_pm.register(api2())
  405. with pytest.raises(ValueError):
  406. he_pm.hook.he_method1(arg=1)
  407. assert he_pm.trace.root.indent == indent
  408. assert saveindent[0] > indent
  409. finally:
  410. undo()