test_pluginmanager.py 16 KB

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