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