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