test_pluginmanager.py 14 KB

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