diff --git a/.travis.yml b/.travis.yml index 2e32d072..61a58ca1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,25 +1,31 @@ sudo: false language: python python: + + - '2.6' + - '2.7' + - '3.3' + - '3.4' - '3.5' + - '3.6-dev' + - pypy + - nightly # command to install dependencies install: "pip install -U tox" # # command to run tests env: matrix: - - TESTENV=check - - TESTENV=py26-pytest27 - - TESTENV=py26-pytest - - TESTENV=py27-pytest27 - - TESTENV=py27-pytest - - TESTENV=py34-pytest27 - - TESTENV=py34-pytest - - TESTENV=py35-pytest27 - - TESTENV=py35-pytest - - TESTENV=pypy-pytest27 - - TESTENV=pypy-pytest - -script: tox --recreate -e $TESTENV + - TOXENV=py-pytest28 + - TOXENV=py-pytest29 + - TOXENV=py-pytest30 +matrix: + include: + - python: '2.7' + env: TOXENV=check + - python: '3.5' + env: TOXENV=check +script: + - tox --recreate -e $TOXENV notifications: irc: diff --git a/pluggy.py b/pluggy.py index 2f962c6e..08d8c3fc 100644 --- a/pluggy.py +++ b/pluggy.py @@ -104,7 +104,7 @@ def setattr_hookspec_opts(func): if historic and firstresult: raise ValueError("cannot have a historic firstresult hook") setattr(func, self.project_name + "_spec", - dict(firstresult=firstresult, historic=historic)) + dict(firstresult=firstresult, historic=historic)) return func if function is not None: @@ -150,8 +150,8 @@ def __call__(self, function=None, hookwrapper=False, optionalhook=False, """ def setattr_hookimpl_opts(func): setattr(func, self.project_name + "_impl", - dict(hookwrapper=hookwrapper, optionalhook=optionalhook, - tryfirst=tryfirst, trylast=trylast)) + dict(hookwrapper=hookwrapper, optionalhook=optionalhook, + tryfirst=tryfirst, trylast=trylast)) return func if function is None: @@ -232,7 +232,7 @@ def get(self, name): def _raise_wrapfail(wrap_controller, msg): co = wrap_controller.gi_code raise RuntimeError("wrap_controller at %r %s:%d %s" % - (co.co_name, co.co_filename, co.co_firstlineno, msg)) + (co.co_name, co.co_filename, co.co_firstlineno, msg)) def _wrapped_call(wrap_controller, func): @@ -279,6 +279,7 @@ def get_result(self): raise ex[1].with_traceback(ex[2]) _reraise(*ex) # noqa + if not _py3: exec(""" def _reraise(cls, val, tb): @@ -348,7 +349,7 @@ def register(self, plugin, name=None): if self._name2plugin.get(plugin_name, -1) is None: return # blocked plugin, return None to indicate no registration raise ValueError("Plugin already registered: %s=%s\n%s" % - (plugin_name, plugin, self._name2plugin)) + (plugin_name, plugin, self._name2plugin)) # XXX if an error happens we should make sure no state has been # changed at point of return @@ -484,7 +485,7 @@ def _verify_hook(self, hook, hookimpl): "plugin definition: %s\n" "available hookargs: %s" % (hookimpl.plugin_name, hook.name, arg, - _formatdef(hookimpl.function), ", ".join(hook.argnames))) + _formatdef(hookimpl.function), ", ".join(hook.argnames))) def check_pending(self): """ Verify that all hooks which have not been verified against diff --git a/setup.py b/setup.py index 886cd90a..8d74319d 100644 --- a/setup.py +++ b/setup.py @@ -43,5 +43,6 @@ def main(): py_modules=['pluggy'], ) + if __name__ == '__main__': main() diff --git a/testing/conftest.py b/testing/conftest.py new file mode 100644 index 00000000..5334b818 --- /dev/null +++ b/testing/conftest.py @@ -0,0 +1,30 @@ +import pytest + + +@pytest.fixture( + params=[ + lambda spec: spec, + lambda spec: spec() + ], + ids=[ + "spec-is-class", + "spec-is-instance" + ], +) +def he_pm(request, pm): + from pluggy import HookspecMarker + hookspec = HookspecMarker("example") + + class Hooks: + @hookspec + def he_method1(self, arg): + return arg + 1 + + pm.add_hookspecs(request.param(Hooks)) + return pm + + +@pytest.fixture +def pm(): + from pluggy import PluginManager + return PluginManager("example") diff --git a/testing/test_details.py b/testing/test_details.py new file mode 100644 index 00000000..c47a1d9a --- /dev/null +++ b/testing/test_details.py @@ -0,0 +1,64 @@ +from pluggy import PluginManager, HookimplMarker, HookspecMarker + + +hookspec = HookspecMarker("example") +hookimpl = HookimplMarker("example") + + +def test_parse_hookimpl_override(): + class MyPluginManager(PluginManager): + def parse_hookimpl_opts(self, module_or_class, name): + opts = PluginManager.parse_hookimpl_opts( + self, module_or_class, name) + if opts is None: + if name.startswith("x1"): + opts = {} + return opts + + class Plugin: + def x1meth(self): + pass + + @hookimpl(hookwrapper=True, tryfirst=True) + def x1meth2(self): + pass + + class Spec: + @hookspec + def x1meth(self): + pass + + @hookspec + def x1meth2(self): + pass + + pm = MyPluginManager(hookspec.project_name) + pm.register(Plugin()) + pm.add_hookspecs(Spec) + assert not pm.hook.x1meth._nonwrappers[0].hookwrapper + assert not pm.hook.x1meth._nonwrappers[0].tryfirst + assert not pm.hook.x1meth._nonwrappers[0].trylast + assert not pm.hook.x1meth._nonwrappers[0].optionalhook + + assert pm.hook.x1meth2._wrappers[0].tryfirst + assert pm.hook.x1meth2._wrappers[0].hookwrapper + + +def test_plugin_getattr_raises_errors(): + """Pluggy must be able to handle plugins which raise weird exceptions + when getattr() gets called (#11). + """ + class DontTouchMe: + def __getattr__(self, x): + raise Exception('cant touch me') + + class Module: + pass + + module = Module() + module.x = DontTouchMe() + + pm = PluginManager(hookspec.project_name) + # register() would raise an error + pm.register(module, 'donttouch') + assert pm.get_plugin('donttouch') is module diff --git a/testing/test_helpers.py b/testing/test_helpers.py new file mode 100644 index 00000000..bb813bcf --- /dev/null +++ b/testing/test_helpers.py @@ -0,0 +1,68 @@ +from pluggy import _formatdef, varnames + + +def test_varnames(): + def f(x): + i = 3 # noqa + + class A: + def f(self, y): + pass + + class B(object): + def __call__(self, z): + pass + + assert varnames(f) == ("x",) + assert varnames(A().f) == ('y',) + assert varnames(B()) == ('z',) + + +def test_varnames_default(): + def f(x, y=3): + pass + + assert varnames(f) == ("x",) + + +def test_varnames_class(): + class C: + def __init__(self, x): + pass + + class D: + pass + + class E(object): + def __init__(self, x): + pass + + class F(object): + pass + + assert varnames(C) == ("x",) + assert varnames(D) == () + assert varnames(E) == ("x",) + assert varnames(F) == () + + +def test_formatdef(): + def function1(): + pass + + assert _formatdef(function1) == 'function1()' + + def function2(arg1): + pass + + assert _formatdef(function2) == "function2(arg1)" + + def function3(arg1, arg2="qwe"): + pass + + assert _formatdef(function3) == "function3(arg1, arg2='qwe')" + + def function4(arg1, *args, **kwargs): + pass + + assert _formatdef(function4) == "function4(arg1, *args, **kwargs)" diff --git a/testing/test_hookrelay.py b/testing/test_hookrelay.py new file mode 100644 index 00000000..a44a5fc4 --- /dev/null +++ b/testing/test_hookrelay.py @@ -0,0 +1,78 @@ +import pytest +from pluggy import PluginValidationError, HookimplMarker, HookspecMarker + + +hookspec = HookspecMarker("example") +hookimpl = HookimplMarker("example") + + +def test_happypath(pm): + class Api: + @hookspec + def hello(self, arg): + "api hook 1" + + pm.add_hookspecs(Api) + hook = pm.hook + assert hasattr(hook, 'hello') + assert repr(hook.hello).find("hello") != -1 + + class Plugin: + @hookimpl + def hello(self, arg): + return arg + 1 + + plugin = Plugin() + pm.register(plugin) + l = hook.hello(arg=3) + assert l == [4] + assert not hasattr(hook, 'world') + pm.unregister(plugin) + assert hook.hello(arg=3) == [] + + +def test_argmismatch(pm): + class Api: + @hookspec + def hello(self, arg): + "api hook 1" + + pm.add_hookspecs(Api) + + class Plugin: + @hookimpl + def hello(self, argwrong): + pass + + with pytest.raises(PluginValidationError) as exc: + pm.register(Plugin()) + + assert "argwrong" in str(exc.value) + + +def test_only_kwargs(pm): + class Api: + @hookspec + def hello(self, arg): + "api hook 1" + + pm.add_hookspecs(Api) + pytest.raises(TypeError, lambda: pm.hook.hello(3)) + + +def test_firstresult_definition(pm): + class Api: + @hookspec(firstresult=True) + def hello(self, arg): + "api hook 1" + + pm.add_hookspecs(Api) + + class Plugin: + @hookimpl + def hello(self, arg): + return arg + 1 + + pm.register(Plugin()) + res = pm.hook.hello(arg=3) + assert res == 4 diff --git a/testing/test_method_ordering.py b/testing/test_method_ordering.py new file mode 100644 index 00000000..c8bd39b1 --- /dev/null +++ b/testing/test_method_ordering.py @@ -0,0 +1,338 @@ +import pytest + + +import sys +import types + +from pluggy import PluginManager, HookImpl, HookimplMarker, HookspecMarker + +hookspec = HookspecMarker("example") +hookimpl = HookimplMarker("example") + + +@pytest.fixture +def hc(pm): + class Hooks: + @hookspec + def he_method1(self, arg): + pass + pm.add_hookspecs(Hooks) + return pm.hook.he_method1 + + +@pytest.fixture +def addmeth(hc): + def addmeth(tryfirst=False, trylast=False, hookwrapper=False): + def wrap(func): + hookimpl(tryfirst=tryfirst, trylast=trylast, + hookwrapper=hookwrapper)(func) + hc._add_hookimpl(HookImpl(None, "", func, func.example_impl)) + return func + return wrap + return addmeth + + +def funcs(hookmethods): + return [hookmethod.function for hookmethod in hookmethods] + + +def test_adding_nonwrappers(hc, addmeth): + @addmeth() + def he_method1(): + pass + + @addmeth() + def he_method2(): + pass + + @addmeth() + def he_method3(): + pass + assert funcs(hc._nonwrappers) == [he_method1, he_method2, he_method3] + + +def test_adding_nonwrappers_trylast(hc, addmeth): + @addmeth() + def he_method1_middle(): + pass + + @addmeth(trylast=True) + def he_method1(): + pass + + @addmeth() + def he_method1_b(): + pass + assert funcs(hc._nonwrappers) == [he_method1, he_method1_middle, he_method1_b] + + +def test_adding_nonwrappers_trylast3(hc, addmeth): + @addmeth() + def he_method1_a(): + pass + + @addmeth(trylast=True) + def he_method1_b(): + pass + + @addmeth() + def he_method1_c(): + pass + + @addmeth(trylast=True) + def he_method1_d(): + pass + assert funcs(hc._nonwrappers) == \ + [he_method1_d, he_method1_b, he_method1_a, he_method1_c] + + +def test_adding_nonwrappers_trylast2(hc, addmeth): + @addmeth() + def he_method1_middle(): + pass + + @addmeth() + def he_method1_b(): + pass + + @addmeth(trylast=True) + def he_method1(): + pass + assert funcs(hc._nonwrappers) == \ + [he_method1, he_method1_middle, he_method1_b] + + +def test_adding_nonwrappers_tryfirst(hc, addmeth): + @addmeth(tryfirst=True) + def he_method1(): + pass + + @addmeth() + def he_method1_middle(): + pass + + @addmeth() + def he_method1_b(): + pass + assert funcs(hc._nonwrappers) == [ + he_method1_middle, he_method1_b, he_method1] + + +def test_adding_wrappers_ordering(hc, addmeth): + @addmeth(hookwrapper=True) + def he_method1(): + pass + + @addmeth() + def he_method1_middle(): + pass + + @addmeth(hookwrapper=True) + def he_method3(): + pass + + assert funcs(hc._nonwrappers) == [he_method1_middle] + assert funcs(hc._wrappers) == [he_method1, he_method3] + + +def test_adding_wrappers_ordering_tryfirst(hc, addmeth): + @addmeth(hookwrapper=True, tryfirst=True) + def he_method1(): + pass + + @addmeth(hookwrapper=True) + def he_method2(): + pass + + assert hc._nonwrappers == [] + assert funcs(hc._wrappers) == [he_method2, he_method1] + + +def test_hookspec(pm): + class HookSpec: + @hookspec() + def he_myhook1(arg1): + pass + + @hookspec(firstresult=True) + def he_myhook2(arg1): + pass + + @hookspec(firstresult=False) + def he_myhook3(arg1): + pass + + pm.add_hookspecs(HookSpec) + assert not pm.hook.he_myhook1.spec_opts["firstresult"] + assert pm.hook.he_myhook2.spec_opts["firstresult"] + assert not pm.hook.he_myhook3.spec_opts["firstresult"] + + +@pytest.mark.parametrize('name', ["hookwrapper", "optionalhook", "tryfirst", "trylast"]) +@pytest.mark.parametrize('val', [True, False]) +def test_hookimpl(name, val): + @hookimpl(**{name: val}) + def he_myhook1(arg1): + pass + if val: + assert he_myhook1.example_impl.get(name) + else: + assert not hasattr(he_myhook1, name) + + +def test_decorator_functional(pm): + class HookSpec: + @hookspec(firstresult=True) + def he_myhook(self, arg1): + """ add to arg1 """ + + pm.add_hookspecs(HookSpec) + + class Plugin: + @hookimpl() + def he_myhook(self, arg1): + return arg1 + 1 + + pm.register(Plugin()) + results = pm.hook.he_myhook(arg1=17) + assert results == 18 + + +def test_load_setuptools_instantiation(monkeypatch, pm): + pkg_resources = pytest.importorskip("pkg_resources") + + def my_iter(name): + assert name == "hello" + + class EntryPoint: + name = "myname" + dist = None + + def load(self): + class PseudoPlugin: + x = 42 + return PseudoPlugin() + + return iter([EntryPoint()]) + + monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter) + num = pm.load_setuptools_entrypoints("hello") + assert num == 1 + plugin = pm.get_plugin("myname") + assert plugin.x == 42 + assert pm.list_plugin_distinfo() == [(plugin, None)] + + +def test_load_setuptools_not_installed(monkeypatch, pm): + monkeypatch.setitem( + sys.modules, 'pkg_resources', + types.ModuleType("pkg_resources")) + + with pytest.raises(ImportError): + pm.load_setuptools_entrypoints("qwe") + + +def test_add_tracefuncs(he_pm): + l = [] + + class api1: + @hookimpl + def he_method1(self): + l.append("he_method1-api1") + + class api2: + @hookimpl + def he_method1(self): + l.append("he_method1-api2") + + he_pm.register(api1()) + he_pm.register(api2()) + + def before(hook_name, hook_impls, kwargs): + l.append((hook_name, list(hook_impls), kwargs)) + + def after(outcome, hook_name, hook_impls, kwargs): + l.append((outcome, hook_name, list(hook_impls), kwargs)) + + undo = he_pm.add_hookcall_monitoring(before, after) + + he_pm.hook.he_method1() + assert len(l) == 4 + assert l[0][0] == "he_method1" + assert len(l[0][1]) == 2 + assert isinstance(l[0][2], dict) + assert l[1] == "he_method1-api2" + assert l[2] == "he_method1-api1" + assert len(l[3]) == 4 + assert l[3][1] == l[0][0] + + undo() + he_pm.hook.he_method1() + assert len(l) == 4 + 2 + + +def test_hook_tracing(he_pm): + saveindent = [] + + class api1: + @hookimpl + def he_method1(self): + saveindent.append(he_pm.trace.root.indent) + + class api2: + @hookimpl + def he_method1(self): + saveindent.append(he_pm.trace.root.indent) + raise ValueError() + + he_pm.register(api1()) + l = [] + he_pm.trace.root.setwriter(l.append) + undo = he_pm.enable_tracing() + try: + indent = he_pm.trace.root.indent + he_pm.hook.he_method1(arg=1) + assert indent == he_pm.trace.root.indent + assert len(l) == 2 + assert 'he_method1' in l[0] + assert 'finish' in l[1] + + l[:] = [] + he_pm.register(api2()) + + with pytest.raises(ValueError): + he_pm.hook.he_method1(arg=1) + assert he_pm.trace.root.indent == indent + assert saveindent[0] > indent + finally: + undo() + + +def test_prefix_hookimpl(): + pm = PluginManager(hookspec.project_name, "hello_") + + class HookSpec: + @hookspec + def hello_myhook(self, arg1): + """ add to arg1 """ + + pm.add_hookspecs(HookSpec) + + class Plugin: + def hello_myhook(self, arg1): + return arg1 + 1 + + pm.register(Plugin()) + pm.register(Plugin()) + results = pm.hook.hello_myhook(arg1=17) + assert results == [18, 18] + + +def test_prefix_hookimpl_dontmatch_module(): + pm = PluginManager(hookspec.project_name, "hello_") + + class BadPlugin: + hello_module = __import__('email') + + pm.register(BadPlugin()) + pm.check_pending() diff --git a/testing/test_multicall.py b/testing/test_multicall.py new file mode 100644 index 00000000..0d668049 --- /dev/null +++ b/testing/test_multicall.py @@ -0,0 +1,199 @@ +import pytest + +from pluggy import _MultiCall, HookImpl, HookCallError +from pluggy import HookspecMarker, HookimplMarker + + +hookspec = HookspecMarker("example") +hookimpl = HookimplMarker("example") + + +def test_uses_copy_of_methods(): + l = [lambda: 42] + mc = _MultiCall(l, {}) + repr(mc) + l[:] = [] + res = mc.execute() + return res == 42 + + +def MC(methods, kwargs, firstresult=False): + hookfuncs = [] + for method in methods: + f = HookImpl(None, "", method, method.example_impl) + hookfuncs.append(f) + return _MultiCall(hookfuncs, kwargs, {"firstresult": firstresult}) + + +def test_call_passing(): + class P1: + @hookimpl + def m(self, __multicall__, x): + assert len(__multicall__.results) == 1 + assert not __multicall__.hook_impls + return 17 + + class P2: + @hookimpl + def m(self, __multicall__, x): + assert __multicall__.results == [] + assert __multicall__.hook_impls + return 23 + + p1 = P1() + p2 = P2() + multicall = MC([p1.m, p2.m], {"x": 23}) + assert "23" in repr(multicall) + reslist = multicall.execute() + assert len(reslist) == 2 + # ensure reversed order + assert reslist == [23, 17] + + +def test_keyword_args(): + @hookimpl + def f(x): + return x + 1 + + class A: + @hookimpl + def f(self, x, y): + return x + y + + multicall = MC([f, A().f], dict(x=23, y=24)) + assert "'x': 23" in repr(multicall) + assert "'y': 24" in repr(multicall) + reslist = multicall.execute() + assert reslist == [24 + 23, 24] + assert "2 results" in repr(multicall) + + +def test_keyword_args_with_defaultargs(): + @hookimpl + def f(x, z=1): + return x + z + reslist = MC([f], dict(x=23, y=24)).execute() + assert reslist == [24] + + +def test_tags_call_error(): + @hookimpl + def f(x): + return x + multicall = MC([f], {}) + pytest.raises(HookCallError, multicall.execute) + + +def test_call_subexecute(): + @hookimpl + def m(__multicall__): + subresult = __multicall__.execute() + return subresult + 1 + + @hookimpl + def n(): + return 1 + + call = MC([n, m], {}, firstresult=True) + res = call.execute() + assert res == 2 + + +def test_call_none_is_no_result(): + @hookimpl + def m1(): + return 1 + + @hookimpl + def m2(): + return None + + res = MC([m1, m2], {}, {"firstresult": True}).execute() + assert res == 1 + res = MC([m1, m2], {}, {}).execute() + assert res == [1] + + +def test_hookwrapper(): + l = [] + + @hookimpl(hookwrapper=True) + def m1(): + l.append("m1 init") + yield None + l.append("m1 finish") + + @hookimpl + def m2(): + l.append("m2") + return 2 + + res = MC([m2, m1], {}).execute() + assert res == [2] + assert l == ["m1 init", "m2", "m1 finish"] + l[:] = [] + res = MC([m2, m1], {}, {"firstresult": True}).execute() + assert res == 2 + assert l == ["m1 init", "m2", "m1 finish"] + + +def test_hookwrapper_order(): + l = [] + + @hookimpl(hookwrapper=True) + def m1(): + l.append("m1 init") + yield 1 + l.append("m1 finish") + + @hookimpl(hookwrapper=True) + def m2(): + l.append("m2 init") + yield 2 + l.append("m2 finish") + + res = MC([m2, m1], {}).execute() + assert res == [] + assert l == ["m1 init", "m2 init", "m2 finish", "m1 finish"] + + +def test_hookwrapper_not_yield(): + @hookimpl(hookwrapper=True) + def m1(): + pass + + mc = MC([m1], {}) + with pytest.raises(TypeError): + mc.execute() + + +def test_hookwrapper_too_many_yield(): + @hookimpl(hookwrapper=True) + def m1(): + yield 1 + yield 2 + + mc = MC([m1], {}) + with pytest.raises(RuntimeError) as ex: + mc.execute() + assert "m1" in str(ex.value) + assert (__file__ + ':') in str(ex.value) + + +@pytest.mark.parametrize("exc", [ValueError, SystemExit]) +def test_hookwrapper_exception(exc): + l = [] + + @hookimpl(hookwrapper=True) + def m1(): + l.append("m1 init") + yield None + l.append("m1 finish") + + @hookimpl + def m2(): + raise exc + + with pytest.raises(exc): + MC([m2, m1], {}).execute() + assert l == ["m1 init", "m1 finish"] diff --git a/testing/test_pluggy.py b/testing/test_pluggy.py deleted file mode 100644 index e08dada9..00000000 --- a/testing/test_pluggy.py +++ /dev/null @@ -1,1124 +0,0 @@ - -import sys -import types -import pytest - -from pluggy import (PluginManager, varnames, PluginValidationError, - HookCallError, HookimplMarker, HookspecMarker) - -from pluggy import (_MultiCall, _TagTracer, HookImpl, _formatdef) - -hookspec = HookspecMarker("example") -hookimpl = HookimplMarker("example") - - -@pytest.fixture -def pm(): - return PluginManager("example") - - -@pytest.fixture( - params=[ - lambda spec: spec, - lambda spec: spec() - ], - ids=[ - "don't instantiate", - "instatiate" - ], -) -def he_pm(request, pm): - class Hooks: - @hookspec - def he_method1(self, arg): - return arg + 1 - - pm.add_hookspecs(request.param(Hooks)) - return pm - - -class TestPluginManager: - def test_plugin_double_register(self, pm): - pm.register(42, name="abc") - with pytest.raises(ValueError): - pm.register(42, name="abc") - with pytest.raises(ValueError): - pm.register(42, name="def") - - def test_pm(self, pm): - class A: - pass - - a1, a2 = A(), A() - pm.register(a1) - assert pm.is_registered(a1) - pm.register(a2, "hello") - assert pm.is_registered(a2) - l = pm.get_plugins() - assert a1 in l - assert a2 in l - assert pm.get_plugin('hello') == a2 - assert pm.unregister(a1) == a1 - assert not pm.is_registered(a1) - - l = pm.list_name_plugin() - assert len(l) == 1 - assert l == [("hello", a2)] - - def test_has_plugin(self, pm): - class A: - pass - - a1 = A() - pm.register(a1, 'hello') - assert pm.is_registered(a1) - assert pm.has_plugin('hello') - - def test_register_dynamic_attr(self, he_pm): - class A: - def __getattr__(self, name): - if name[0] != "_": - return 42 - raise AttributeError() - - a = A() - he_pm.register(a) - assert not he_pm.get_hookcallers(a) - - def test_pm_name(self, pm): - class A: - pass - - a1 = A() - name = pm.register(a1, name="hello") - assert name == "hello" - pm.unregister(a1) - assert pm.get_plugin(a1) is None - assert not pm.is_registered(a1) - assert not pm.get_plugins() - name2 = pm.register(a1, name="hello") - assert name2 == name - pm.unregister(name="hello") - assert pm.get_plugin(a1) is None - assert not pm.is_registered(a1) - assert not pm.get_plugins() - - def test_set_blocked(self, pm): - class A: - pass - - a1 = A() - name = pm.register(a1) - assert pm.is_registered(a1) - assert not pm.is_blocked(name) - pm.set_blocked(name) - assert pm.is_blocked(name) - assert not pm.is_registered(a1) - - pm.set_blocked("somename") - assert pm.is_blocked("somename") - assert not pm.register(A(), "somename") - pm.unregister(name="somename") - assert pm.is_blocked("somename") - - def test_register_mismatch_method(self, he_pm): - class hello: - @hookimpl - def he_method_notexists(self): - pass - - he_pm.register(hello()) - with pytest.raises(PluginValidationError): - he_pm.check_pending() - - def test_register_mismatch_arg(self, he_pm): - class hello: - @hookimpl - def he_method1(self, qlwkje): - pass - - with pytest.raises(PluginValidationError): - he_pm.register(hello()) - - def test_register(self, pm): - class MyPlugin: - pass - my = MyPlugin() - pm.register(my) - assert my in pm.get_plugins() - my2 = MyPlugin() - pm.register(my2) - assert set([my, my2]).issubset(pm.get_plugins()) - - assert pm.is_registered(my) - assert pm.is_registered(my2) - pm.unregister(my) - assert not pm.is_registered(my) - assert my not in pm.get_plugins() - - def test_register_unknown_hooks(self, pm): - class Plugin1: - @hookimpl - def he_method1(self, arg): - return arg + 1 - - pname = pm.register(Plugin1()) - - class Hooks: - @hookspec - def he_method1(self, arg): - pass - - pm.add_hookspecs(Hooks) - # assert not pm._unverified_hooks - assert pm.hook.he_method1(arg=1) == [2] - assert len(pm.get_hookcallers(pm.get_plugin(pname))) == 1 - - def test_register_historic(self, pm): - class Hooks: - @hookspec(historic=True) - def he_method1(self, arg): - pass - pm.add_hookspecs(Hooks) - - pm.hook.he_method1.call_historic(kwargs=dict(arg=1)) - l = [] - - class Plugin: - @hookimpl - def he_method1(self, arg): - l.append(arg) - - pm.register(Plugin()) - assert l == [1] - - class Plugin2: - @hookimpl - def he_method1(self, arg): - l.append(arg * 10) - - pm.register(Plugin2()) - assert l == [1, 10] - pm.hook.he_method1.call_historic(kwargs=dict(arg=12)) - assert l == [1, 10, 120, 12] - - def test_with_result_memorized(self, pm): - class Hooks: - @hookspec(historic=True) - def he_method1(self, arg): - pass - pm.add_hookspecs(Hooks) - - he_method1 = pm.hook.he_method1 - he_method1.call_historic(lambda res: l.append(res), dict(arg=1)) - l = [] - - class Plugin: - @hookimpl - def he_method1(self, arg): - return arg * 10 - - pm.register(Plugin()) - assert l == [10] - - def test_with_callbacks_immediately_executed(self, pm): - class Hooks: - @hookspec(historic=True) - def he_method1(self, arg): - pass - pm.add_hookspecs(Hooks) - - class Plugin1: - @hookimpl - def he_method1(self, arg): - return arg * 10 - - class Plugin2: - @hookimpl - def he_method1(self, arg): - return arg * 20 - - class Plugin3: - @hookimpl - def he_method1(self, arg): - return arg * 30 - - l = [] - pm.register(Plugin1()) - pm.register(Plugin2()) - - he_method1 = pm.hook.he_method1 - he_method1.call_historic(lambda res: l.append(res), dict(arg=1)) - assert l == [20, 10] - pm.register(Plugin3()) - assert l == [20, 10, 30] - - def test_register_historic_incompat_hookwrapper(self, pm): - class Hooks: - @hookspec(historic=True) - def he_method1(self, arg): - pass - - pm.add_hookspecs(Hooks) - - l = [] - - class Plugin: - @hookimpl(hookwrapper=True) - def he_method1(self, arg): - l.append(arg) - - with pytest.raises(PluginValidationError): - pm.register(Plugin()) - - def test_call_extra(self, pm): - class Hooks: - @hookspec - def he_method1(self, arg): - pass - - pm.add_hookspecs(Hooks) - - def he_method1(arg): - return arg * 10 - - l = pm.hook.he_method1.call_extra([he_method1], dict(arg=1)) - assert l == [10] - - def test_call_with_too_few_args(self, pm): - class Hooks: - @hookspec - def he_method1(self, arg): - pass - - pm.add_hookspecs(Hooks) - - class Plugin1: - @hookimpl - def he_method1(self, arg): - 0 / 0 - pm.register(Plugin1()) - with pytest.raises(HookCallError): - pm.hook.he_method1() - - def test_subset_hook_caller(self, pm): - class Hooks: - @hookspec - def he_method1(self, arg): - pass - - pm.add_hookspecs(Hooks) - - l = [] - - class Plugin1: - @hookimpl - def he_method1(self, arg): - l.append(arg) - - class Plugin2: - @hookimpl - def he_method1(self, arg): - l.append(arg * 10) - - class PluginNo: - pass - - plugin1, plugin2, plugin3 = Plugin1(), Plugin2(), PluginNo() - pm.register(plugin1) - pm.register(plugin2) - pm.register(plugin3) - pm.hook.he_method1(arg=1) - assert l == [10, 1] - l[:] = [] - - hc = pm.subset_hook_caller("he_method1", [plugin1]) - hc(arg=2) - assert l == [20] - l[:] = [] - - hc = pm.subset_hook_caller("he_method1", [plugin2]) - hc(arg=2) - assert l == [2] - l[:] = [] - - pm.unregister(plugin1) - hc(arg=2) - assert l == [] - l[:] = [] - - pm.hook.he_method1(arg=1) - assert l == [10] - - def test_add_hookspecs_nohooks(self, pm): - with pytest.raises(ValueError): - pm.add_hookspecs(10) - - -class TestAddMethodOrdering: - @pytest.fixture - def hc(self, pm): - class Hooks: - @hookspec - def he_method1(self, arg): - pass - pm.add_hookspecs(Hooks) - return pm.hook.he_method1 - - @pytest.fixture - def addmeth(self, hc): - def addmeth(tryfirst=False, trylast=False, hookwrapper=False): - def wrap(func): - hookimpl(tryfirst=tryfirst, trylast=trylast, - hookwrapper=hookwrapper)(func) - hc._add_hookimpl(HookImpl(None, "", func, func.example_impl)) - return func - return wrap - return addmeth - - def funcs(self, hookmethods): - return [hookmethod.function for hookmethod in hookmethods] - - def test_adding_nonwrappers(self, hc, addmeth): - @addmeth() - def he_method1(): - pass - - @addmeth() - def he_method2(): - pass - - @addmeth() - def he_method3(): - pass - assert self.funcs(hc._nonwrappers) == [he_method1, he_method2, he_method3] - - def test_adding_nonwrappers_trylast(self, hc, addmeth): - @addmeth() - def he_method1_middle(): - pass - - @addmeth(trylast=True) - def he_method1(): - pass - - @addmeth() - def he_method1_b(): - pass - assert self.funcs(hc._nonwrappers) == \ - [he_method1, he_method1_middle, he_method1_b] - - def test_adding_nonwrappers_trylast3(self, hc, addmeth): - @addmeth() - def he_method1_a(): - pass - - @addmeth(trylast=True) - def he_method1_b(): - pass - - @addmeth() - def he_method1_c(): - pass - - @addmeth(trylast=True) - def he_method1_d(): - pass - assert self.funcs(hc._nonwrappers) == \ - [he_method1_d, he_method1_b, he_method1_a, he_method1_c] - - def test_adding_nonwrappers_trylast2(self, hc, addmeth): - @addmeth() - def he_method1_middle(): - pass - - @addmeth() - def he_method1_b(): - pass - - @addmeth(trylast=True) - def he_method1(): - pass - assert self.funcs(hc._nonwrappers) == \ - [he_method1, he_method1_middle, he_method1_b] - - def test_adding_nonwrappers_tryfirst(self, hc, addmeth): - @addmeth(tryfirst=True) - def he_method1(): - pass - - @addmeth() - def he_method1_middle(): - pass - - @addmeth() - def he_method1_b(): - pass - assert self.funcs(hc._nonwrappers) == \ - [he_method1_middle, he_method1_b, he_method1] - - def test_adding_wrappers_ordering(self, hc, addmeth): - @addmeth(hookwrapper=True) - def he_method1(): - pass - - @addmeth() - def he_method1_middle(): - pass - - @addmeth(hookwrapper=True) - def he_method3(): - pass - - assert self.funcs(hc._nonwrappers) == [he_method1_middle] - assert self.funcs(hc._wrappers) == [he_method1, he_method3] - - def test_adding_wrappers_ordering_tryfirst(self, hc, addmeth): - @addmeth(hookwrapper=True, tryfirst=True) - def he_method1(): - pass - - @addmeth(hookwrapper=True) - def he_method2(): - pass - - assert hc._nonwrappers == [] - assert self.funcs(hc._wrappers) == [he_method2, he_method1] - - def test_hookspec(self, pm): - class HookSpec: - @hookspec() - def he_myhook1(self, arg1): - pass - - @hookspec(firstresult=True) - def he_myhook2(self, arg1): - pass - - @hookspec(firstresult=False) - def he_myhook3(self, arg1): - pass - - pm.add_hookspecs(HookSpec) - assert not pm.hook.he_myhook1.spec_opts["firstresult"] - assert pm.hook.he_myhook2.spec_opts["firstresult"] - assert not pm.hook.he_myhook3.spec_opts["firstresult"] - - def test_hookimpl(self): - for name in ["hookwrapper", "optionalhook", "tryfirst", "trylast"]: - for val in [True, False]: - @hookimpl(**{name: val}) - def he_myhook1(self, arg1): - pass - if val: - assert he_myhook1.example_impl.get(name) - else: - assert not hasattr(he_myhook1, name) - - def test_decorator_functional(self, pm): - class HookSpec: - @hookspec(firstresult=True) - def he_myhook(self, arg1): - """ add to arg1 """ - - pm.add_hookspecs(HookSpec) - - class Plugin: - @hookimpl() - def he_myhook(self, arg1): - return arg1 + 1 - - pm.register(Plugin()) - results = pm.hook.he_myhook(arg1=17) - assert results == 18 - - def test_load_setuptools_instantiation(self, monkeypatch, pm): - pkg_resources = pytest.importorskip("pkg_resources") - - def my_iter(name): - assert name == "hello" - - class EntryPoint: - name = "myname" - dist = None - - def load(self): - class PseudoPlugin: - x = 42 - return PseudoPlugin() - - return iter([EntryPoint()]) - - monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter) - num = pm.load_setuptools_entrypoints("hello") - assert num == 1 - plugin = pm.get_plugin("myname") - assert plugin.x == 42 - assert pm.list_plugin_distinfo() == [(plugin, None)] - - def test_load_setuptools_not_installed(self, monkeypatch, pm): - monkeypatch.setitem(sys.modules, 'pkg_resources', - types.ModuleType("pkg_resources")) - - with pytest.raises(ImportError): - pm.load_setuptools_entrypoints("qwe") - - def test_add_tracefuncs(self, he_pm): - l = [] - - class api1: - @hookimpl - def he_method1(self): - l.append("he_method1-api1") - - class api2: - @hookimpl - def he_method1(self): - l.append("he_method1-api2") - - he_pm.register(api1()) - he_pm.register(api2()) - - def before(hook_name, hook_impls, kwargs): - l.append((hook_name, list(hook_impls), kwargs)) - - def after(outcome, hook_name, hook_impls, kwargs): - l.append((outcome, hook_name, list(hook_impls), kwargs)) - - undo = he_pm.add_hookcall_monitoring(before, after) - - he_pm.hook.he_method1() - assert len(l) == 4 - assert l[0][0] == "he_method1" - assert len(l[0][1]) == 2 - assert isinstance(l[0][2], dict) - assert l[1] == "he_method1-api2" - assert l[2] == "he_method1-api1" - assert len(l[3]) == 4 - assert l[3][1] == l[0][0] - - undo() - he_pm.hook.he_method1() - assert len(l) == 4 + 2 - - def test_hook_tracing(self, he_pm): - saveindent = [] - - class api1: - @hookimpl - def he_method1(self): - saveindent.append(he_pm.trace.root.indent) - - class api2: - @hookimpl - def he_method1(self): - saveindent.append(he_pm.trace.root.indent) - raise ValueError() - - he_pm.register(api1()) - l = [] - he_pm.trace.root.setwriter(l.append) - undo = he_pm.enable_tracing() - try: - indent = he_pm.trace.root.indent - he_pm.hook.he_method1(arg=1) - assert indent == he_pm.trace.root.indent - assert len(l) == 2 - assert 'he_method1' in l[0] - assert 'finish' in l[1] - - l[:] = [] - he_pm.register(api2()) - - with pytest.raises(ValueError): - he_pm.hook.he_method1(arg=1) - assert he_pm.trace.root.indent == indent - assert saveindent[0] > indent - finally: - undo() - - def test_prefix_hookimpl(self): - pm = PluginManager(hookspec.project_name, "hello_") - - class HookSpec: - @hookspec - def hello_myhook(self, arg1): - """ add to arg1 """ - - pm.add_hookspecs(HookSpec) - - class Plugin: - def hello_myhook(self, arg1): - return arg1 + 1 - - pm.register(Plugin()) - pm.register(Plugin()) - results = pm.hook.hello_myhook(arg1=17) - assert results == [18, 18] - - def test_prefix_hookimpl_dontmatch_module(self): - pm = PluginManager(hookspec.project_name, "hello_") - - class BadPlugin: - hello_module = __import__('email') - - pm.register(BadPlugin()) - pm.check_pending() - - -def test_parse_hookimpl_override(): - class MyPluginManager(PluginManager): - def parse_hookimpl_opts(self, module_or_class, name): - opts = PluginManager.parse_hookimpl_opts(self, module_or_class, name) - if opts is None: - if name.startswith("x1"): - opts = {} - return opts - - class Plugin: - def x1meth(self): - pass - - @hookimpl(hookwrapper=True, tryfirst=True) - def x1meth2(self): - pass - - class Spec: - @hookspec - def x1meth(self): - pass - - @hookspec - def x1meth2(self): - pass - - pm = MyPluginManager(hookspec.project_name) - pm.register(Plugin()) - pm.add_hookspecs(Spec) - assert not pm.hook.x1meth._nonwrappers[0].hookwrapper - assert not pm.hook.x1meth._nonwrappers[0].tryfirst - assert not pm.hook.x1meth._nonwrappers[0].trylast - assert not pm.hook.x1meth._nonwrappers[0].optionalhook - - assert pm.hook.x1meth2._wrappers[0].tryfirst - assert pm.hook.x1meth2._wrappers[0].hookwrapper - - -def test_plugin_getattr_raises_errors(): - """Pluggy must be able to handle plugins which raise weird exceptions - when getattr() gets called (#11). - """ - class DontTouchMe: - def __getattr__(self, x): - raise Exception('cant touch me') - - class Module: - pass - - module = Module() - module.x = DontTouchMe() - - pm = PluginManager(hookspec.project_name) - # register() would raise an error - pm.register(module, 'donttouch') - assert pm.get_plugin('donttouch') is module - - -def test_varnames(): - def f(x): - i = 3 # noqa - - class A: - def f(self, y): - pass - - class B(object): - def __call__(self, z): - pass - - assert varnames(f) == ("x",) - assert varnames(A().f) == ('y',) - assert varnames(B()) == ('z',) - - -def test_varnames_default(): - def f(x, y=3): - pass - - assert varnames(f) == ("x",) - - -def test_varnames_class(): - class C: - def __init__(self, x): - pass - - class D: - pass - - class E(object): - def __init__(self, x): - pass - - class F(object): - pass - - assert varnames(C) == ("x",) - assert varnames(D) == () - assert varnames(E) == ("x",) - assert varnames(F) == () - - -def test_formatdef(): - def function1(): - pass - - assert _formatdef(function1) == 'function1()' - - def function2(arg1): - pass - - assert _formatdef(function2) == "function2(arg1)" - - def function3(arg1, arg2="qwe"): - pass - - assert _formatdef(function3) == "function3(arg1, arg2='qwe')" - - def function4(arg1, *args, **kwargs): - pass - - assert _formatdef(function4) == "function4(arg1, *args, **kwargs)" - - -class Test_MultiCall: - def test_uses_copy_of_methods(self): - l = [lambda: 42] - mc = _MultiCall(l, {}) - repr(mc) - l[:] = [] - res = mc.execute() - return res == 42 - - def MC(self, methods, kwargs, firstresult=False): - hookfuncs = [] - for method in methods: - f = HookImpl(None, "", method, method.example_impl) - hookfuncs.append(f) - return _MultiCall(hookfuncs, kwargs, {"firstresult": firstresult}) - - def test_call_passing(self): - class P1: - @hookimpl - def m(self, __multicall__, x): - assert len(__multicall__.results) == 1 - assert not __multicall__.hook_impls - return 17 - - class P2: - @hookimpl - def m(self, __multicall__, x): - assert __multicall__.results == [] - assert __multicall__.hook_impls - return 23 - - p1 = P1() - p2 = P2() - multicall = self.MC([p1.m, p2.m], {"x": 23}) - assert "23" in repr(multicall) - reslist = multicall.execute() - assert len(reslist) == 2 - # ensure reversed order - assert reslist == [23, 17] - - def test_keyword_args(self): - @hookimpl - def f(x): - return x + 1 - - class A: - @hookimpl - def f(self, x, y): - return x + y - - multicall = self.MC([f, A().f], dict(x=23, y=24)) - assert "'x': 23" in repr(multicall) - assert "'y': 24" in repr(multicall) - reslist = multicall.execute() - assert reslist == [24 + 23, 24] - assert "2 results" in repr(multicall) - - def test_keyword_args_with_defaultargs(self): - @hookimpl - def f(x, z=1): - return x + z - reslist = self.MC([f], dict(x=23, y=24)).execute() - assert reslist == [24] - - def test_tags_call_error(self): - @hookimpl - def f(x): - return x - multicall = self.MC([f], {}) - pytest.raises(HookCallError, multicall.execute) - - def test_call_subexecute(self): - @hookimpl - def m(__multicall__): - subresult = __multicall__.execute() - return subresult + 1 - - @hookimpl - def n(): - return 1 - - call = self.MC([n, m], {}, firstresult=True) - res = call.execute() - assert res == 2 - - def test_call_none_is_no_result(self): - @hookimpl - def m1(): - return 1 - - @hookimpl - def m2(): - return None - - res = self.MC([m1, m2], {}, {"firstresult": True}).execute() - assert res == 1 - res = self.MC([m1, m2], {}, {}).execute() - assert res == [1] - - def test_hookwrapper(self): - l = [] - - @hookimpl(hookwrapper=True) - def m1(): - l.append("m1 init") - yield None - l.append("m1 finish") - - @hookimpl - def m2(): - l.append("m2") - return 2 - - res = self.MC([m2, m1], {}).execute() - assert res == [2] - assert l == ["m1 init", "m2", "m1 finish"] - l[:] = [] - res = self.MC([m2, m1], {}, {"firstresult": True}).execute() - assert res == 2 - assert l == ["m1 init", "m2", "m1 finish"] - - def test_hookwrapper_order(self): - l = [] - - @hookimpl(hookwrapper=True) - def m1(): - l.append("m1 init") - yield 1 - l.append("m1 finish") - - @hookimpl(hookwrapper=True) - def m2(): - l.append("m2 init") - yield 2 - l.append("m2 finish") - - res = self.MC([m2, m1], {}).execute() - assert res == [] - assert l == ["m1 init", "m2 init", "m2 finish", "m1 finish"] - - def test_hookwrapper_not_yield(self): - @hookimpl(hookwrapper=True) - def m1(): - pass - - mc = self.MC([m1], {}) - with pytest.raises(TypeError): - mc.execute() - - def test_hookwrapper_too_many_yield(self): - @hookimpl(hookwrapper=True) - def m1(): - yield 1 - yield 2 - - mc = self.MC([m1], {}) - with pytest.raises(RuntimeError) as ex: - mc.execute() - assert "m1" in str(ex.value) - assert "test_pluggy.py:" in str(ex.value) - - @pytest.mark.parametrize("exc", [ValueError, SystemExit]) - def test_hookwrapper_exception(self, exc): - l = [] - - @hookimpl(hookwrapper=True) - def m1(): - l.append("m1 init") - yield None - l.append("m1 finish") - - @hookimpl - def m2(): - raise exc - - with pytest.raises(exc): - self.MC([m2, m1], {}).execute() - assert l == ["m1 init", "m1 finish"] - - -class TestHookRelay: - def test_happypath(self, pm): - class Api: - @hookspec - def hello(self, arg): - "api hook 1" - - pm.add_hookspecs(Api) - hook = pm.hook - assert hasattr(hook, 'hello') - assert repr(hook.hello).find("hello") != -1 - - class Plugin: - @hookimpl - def hello(self, arg): - return arg + 1 - - plugin = Plugin() - pm.register(plugin) - l = hook.hello(arg=3) - assert l == [4] - assert not hasattr(hook, 'world') - pm.unregister(plugin) - assert hook.hello(arg=3) == [] - - def test_argmismatch(self, pm): - class Api: - @hookspec - def hello(self, arg): - "api hook 1" - - pm.add_hookspecs(Api) - - class Plugin: - @hookimpl - def hello(self, argwrong): - pass - - with pytest.raises(PluginValidationError) as exc: - pm.register(Plugin()) - - assert "argwrong" in str(exc.value) - - def test_only_kwargs(self, pm): - class Api: - @hookspec - def hello(self, arg): - "api hook 1" - - pm.add_hookspecs(Api) - pytest.raises(TypeError, lambda: pm.hook.hello(3)) - - def test_firstresult_definition(self, pm): - class Api: - @hookspec(firstresult=True) - def hello(self, arg): - "api hook 1" - - pm.add_hookspecs(Api) - - class Plugin: - @hookimpl - def hello(self, arg): - return arg + 1 - - pm.register(Plugin()) - res = pm.hook.hello(arg=3) - assert res == 4 - - -class TestTracer: - def test_simple(self): - rootlogger = _TagTracer() - log = rootlogger.get("pytest") - log("hello") - l = [] - rootlogger.setwriter(l.append) - log("world") - assert len(l) == 1 - assert l[0] == "world [pytest]\n" - sublog = log.get("collection") - sublog("hello") - assert l[1] == "hello [pytest:collection]\n" - - def test_indent(self): - rootlogger = _TagTracer() - log = rootlogger.get("1") - l = [] - log.root.setwriter(lambda arg: l.append(arg)) - log("hello") - log.root.indent += 1 - log("line1") - log("line2") - log.root.indent += 1 - log("line3") - log("line4") - log.root.indent -= 1 - log("line5") - log.root.indent -= 1 - log("last") - assert len(l) == 7 - names = [x[:x.rfind(' [')] for x in l] - assert names == ['hello', ' line1', ' line2', - ' line3', ' line4', ' line5', 'last'] - - def test_readable_output_dictargs(self): - rootlogger = _TagTracer() - - out = rootlogger.format_message(['test'], [1]) - assert out == ['1 [test]\n'] - - out2 = rootlogger.format_message(['test'], ['test', {'a': 1}]) - assert out2 == [ - 'test [test]\n', - ' a: 1\n' - ] - - def test_setprocessor(self): - rootlogger = _TagTracer() - log = rootlogger.get("1") - log2 = log.get("2") - assert log2.tags == tuple("12") - l = [] - rootlogger.setprocessor(tuple("12"), lambda *args: l.append(args)) - log("not seen") - log2("seen") - assert len(l) == 1 - tags, args = l[0] - assert "1" in tags - assert "2" in tags - assert args == ("seen",) - l2 = [] - rootlogger.setprocessor("1:2", lambda *args: l2.append(args)) - log2("seen") - tags, args = l2[0] - assert args == ("seen",) - - def test_setmyprocessor(self): - rootlogger = _TagTracer() - log = rootlogger.get("1") - log2 = log.get("2") - l = [] - log2.setmyprocessor(lambda *args: l.append(args)) - log("not seen") - assert not l - log2(42) - assert len(l) == 1 - tags, args = l[0] - assert "1" in tags - assert "2" in tags - assert args == (42,) diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py new file mode 100644 index 00000000..063f8f44 --- /dev/null +++ b/testing/test_pluginmanager.py @@ -0,0 +1,342 @@ +import pytest + +from pluggy import (PluginValidationError, + HookCallError, HookimplMarker, HookspecMarker) + + +hookspec = HookspecMarker("example") +hookimpl = HookimplMarker("example") + + +def test_plugin_double_register(pm): + pm.register(42, name="abc") + with pytest.raises(ValueError): + pm.register(42, name="abc") + with pytest.raises(ValueError): + pm.register(42, name="def") + + +def test_pm(pm): + class A: + pass + + a1, a2 = A(), A() + pm.register(a1) + assert pm.is_registered(a1) + pm.register(a2, "hello") + assert pm.is_registered(a2) + l = pm.get_plugins() + assert a1 in l + assert a2 in l + assert pm.get_plugin('hello') == a2 + assert pm.unregister(a1) == a1 + assert not pm.is_registered(a1) + + l = pm.list_name_plugin() + assert len(l) == 1 + assert l == [("hello", a2)] + + +def test_has_plugin(pm): + class A: + pass + + a1 = A() + pm.register(a1, 'hello') + assert pm.is_registered(a1) + assert pm.has_plugin('hello') + + +def test_register_dynamic_attr(he_pm): + class A: + def __getattr__(self, name): + if name[0] != "_": + return 42 + raise AttributeError() + + a = A() + he_pm.register(a) + assert not he_pm.get_hookcallers(a) + + +def test_pm_name(pm): + class A: + pass + + a1 = A() + name = pm.register(a1, name="hello") + assert name == "hello" + pm.unregister(a1) + assert pm.get_plugin(a1) is None + assert not pm.is_registered(a1) + assert not pm.get_plugins() + name2 = pm.register(a1, name="hello") + assert name2 == name + pm.unregister(name="hello") + assert pm.get_plugin(a1) is None + assert not pm.is_registered(a1) + assert not pm.get_plugins() + + +def test_set_blocked(pm): + class A: + pass + + a1 = A() + name = pm.register(a1) + assert pm.is_registered(a1) + assert not pm.is_blocked(name) + pm.set_blocked(name) + assert pm.is_blocked(name) + assert not pm.is_registered(a1) + + pm.set_blocked("somename") + assert pm.is_blocked("somename") + assert not pm.register(A(), "somename") + pm.unregister(name="somename") + assert pm.is_blocked("somename") + + +def test_register_mismatch_method(he_pm): + class hello: + @hookimpl + def he_method_notexists(self): + pass + + he_pm.register(hello()) + with pytest.raises(PluginValidationError): + he_pm.check_pending() + + +def test_register_mismatch_arg(he_pm): + class hello: + @hookimpl + def he_method1(self, qlwkje): + pass + + with pytest.raises(PluginValidationError): + he_pm.register(hello()) + + +def test_register(pm): + class MyPlugin: + pass + my = MyPlugin() + pm.register(my) + assert my in pm.get_plugins() + my2 = MyPlugin() + pm.register(my2) + assert set([my, my2]).issubset(pm.get_plugins()) + + assert pm.is_registered(my) + assert pm.is_registered(my2) + pm.unregister(my) + assert not pm.is_registered(my) + assert my not in pm.get_plugins() + + +def test_register_unknown_hooks(pm): + class Plugin1: + @hookimpl + def he_method1(self, arg): + return arg + 1 + + pname = pm.register(Plugin1()) + + class Hooks: + @hookspec + def he_method1(self, arg): + pass + + pm.add_hookspecs(Hooks) + # assert not pm._unverified_hooks + assert pm.hook.he_method1(arg=1) == [2] + assert len(pm.get_hookcallers(pm.get_plugin(pname))) == 1 + + +def test_register_historic(pm): + class Hooks: + @hookspec(historic=True) + def he_method1(self, arg): + pass + pm.add_hookspecs(Hooks) + + pm.hook.he_method1.call_historic(kwargs=dict(arg=1)) + l = [] + + class Plugin: + @hookimpl + def he_method1(self, arg): + l.append(arg) + + pm.register(Plugin()) + assert l == [1] + + class Plugin2: + @hookimpl + def he_method1(self, arg): + l.append(arg * 10) + + pm.register(Plugin2()) + assert l == [1, 10] + pm.hook.he_method1.call_historic(kwargs=dict(arg=12)) + assert l == [1, 10, 120, 12] + + +def test_with_result_memorized(pm): + class Hooks: + @hookspec(historic=True) + def he_method1(self, arg): + pass + pm.add_hookspecs(Hooks) + + he_method1 = pm.hook.he_method1 + he_method1.call_historic(lambda res: l.append(res), dict(arg=1)) + l = [] + + class Plugin: + @hookimpl + def he_method1(self, arg): + return arg * 10 + + pm.register(Plugin()) + assert l == [10] + + +def test_with_callbacks_immediately_executed(pm): + class Hooks: + @hookspec(historic=True) + def he_method1(self, arg): + pass + pm.add_hookspecs(Hooks) + + class Plugin1: + @hookimpl + def he_method1(self, arg): + return arg * 10 + + class Plugin2: + @hookimpl + def he_method1(self, arg): + return arg * 20 + + class Plugin3: + @hookimpl + def he_method1(self, arg): + return arg * 30 + + l = [] + pm.register(Plugin1()) + pm.register(Plugin2()) + + he_method1 = pm.hook.he_method1 + he_method1.call_historic(lambda res: l.append(res), dict(arg=1)) + assert l == [20, 10] + pm.register(Plugin3()) + assert l == [20, 10, 30] + + +def test_register_historic_incompat_hookwrapper(pm): + class Hooks: + @hookspec(historic=True) + def he_method1(self, arg): + pass + + pm.add_hookspecs(Hooks) + + l = [] + + class Plugin: + @hookimpl(hookwrapper=True) + def he_method1(self, arg): + l.append(arg) + + with pytest.raises(PluginValidationError): + pm.register(Plugin()) + + +def test_call_extra(pm): + class Hooks: + @hookspec + def he_method1(self, arg): + pass + + pm.add_hookspecs(Hooks) + + def he_method1(arg): + return arg * 10 + + l = pm.hook.he_method1.call_extra([he_method1], dict(arg=1)) + assert l == [10] + + +def test_call_with_too_few_args(pm): + class Hooks: + @hookspec + def he_method1(self, arg): + pass + + pm.add_hookspecs(Hooks) + + class Plugin1: + @hookimpl + def he_method1(self, arg): + 0 / 0 + pm.register(Plugin1()) + with pytest.raises(HookCallError): + pm.hook.he_method1() + + +def test_subset_hook_caller(pm): + class Hooks: + @hookspec + def he_method1(self, arg): + pass + + pm.add_hookspecs(Hooks) + + l = [] + + class Plugin1: + @hookimpl + def he_method1(self, arg): + l.append(arg) + + class Plugin2: + @hookimpl + def he_method1(self, arg): + l.append(arg * 10) + + class PluginNo: + pass + + plugin1, plugin2, plugin3 = Plugin1(), Plugin2(), PluginNo() + pm.register(plugin1) + pm.register(plugin2) + pm.register(plugin3) + pm.hook.he_method1(arg=1) + assert l == [10, 1] + l[:] = [] + + hc = pm.subset_hook_caller("he_method1", [plugin1]) + hc(arg=2) + assert l == [20] + l[:] = [] + + hc = pm.subset_hook_caller("he_method1", [plugin2]) + hc(arg=2) + assert l == [2] + l[:] = [] + + pm.unregister(plugin1) + hc(arg=2) + assert l == [] + l[:] = [] + + pm.hook.he_method1(arg=1) + assert l == [10] + + +def test_add_hookspecs_nohooks(pm): + with pytest.raises(ValueError): + pm.add_hookspecs(10) diff --git a/testing/test_tracer.py b/testing/test_tracer.py new file mode 100644 index 00000000..852e5c19 --- /dev/null +++ b/testing/test_tracer.py @@ -0,0 +1,89 @@ + +from pluggy import _TagTracer + + +def test_simple(): + rootlogger = _TagTracer() + log = rootlogger.get("pytest") + log("hello") + l = [] + rootlogger.setwriter(l.append) + log("world") + assert len(l) == 1 + assert l[0] == "world [pytest]\n" + sublog = log.get("collection") + sublog("hello") + assert l[1] == "hello [pytest:collection]\n" + + +def test_indent(): + rootlogger = _TagTracer() + log = rootlogger.get("1") + l = [] + log.root.setwriter(lambda arg: l.append(arg)) + log("hello") + log.root.indent += 1 + log("line1") + log("line2") + log.root.indent += 1 + log("line3") + log("line4") + log.root.indent -= 1 + log("line5") + log.root.indent -= 1 + log("last") + assert len(l) == 7 + names = [x[:x.rfind(' [')] for x in l] + assert names == [ + 'hello', ' line1', ' line2', + ' line3', ' line4', ' line5', 'last'] + + +def test_readable_output_dictargs(): + rootlogger = _TagTracer() + + out = rootlogger.format_message(['test'], [1]) + assert out == ['1 [test]\n'] + + out2 = rootlogger.format_message(['test'], ['test', {'a': 1}]) + assert out2 == [ + 'test [test]\n', + ' a: 1\n' + ] + + +def test_setprocessor(): + rootlogger = _TagTracer() + log = rootlogger.get("1") + log2 = log.get("2") + assert log2.tags == tuple("12") + l = [] + rootlogger.setprocessor(tuple("12"), lambda *args: l.append(args)) + log("not seen") + log2("seen") + assert len(l) == 1 + tags, args = l[0] + assert "1" in tags + assert "2" in tags + assert args == ("seen",) + l2 = [] + rootlogger.setprocessor("1:2", lambda *args: l2.append(args)) + log2("seen") + tags, args = l2[0] + assert args == ("seen",) + + +def test_setmyprocessor(): + rootlogger = _TagTracer() + log = rootlogger.get("1") + log2 = log.get("2") + l = [] + log2.setmyprocessor(lambda *args: l.append(args)) + log("not seen") + assert not l + log2(42) + assert len(l) == 1 + tags, args = l[0] + assert "1" in tags + assert "2" in tags + assert args == (42,) diff --git a/tox.ini b/tox.ini index 477b2ad1..9c027745 100644 --- a/tox.ini +++ b/tox.ini @@ -1,23 +1,21 @@ [tox] -envlist=check,{py26,py27,py34,py35,pypy}-{pytest27,pytest} +envlist=check,py{26,27,34,35,py}-pytest{28,29,30} [testenv] -commands= py.test {posargs:testing/test_pluggy.py} +commands= py.test {posargs:testing/} deps= - pytest27: pytest>=2.7.0,<2.8.0 - pytest: pytest>=2.8.0 + pytest28: pytest~=2.8.0 + pytest29: pytest~=2.9.0 + pytest30: pytest~=3.0.0 [testenv:check] -usedevelop=True -deps = pytest>=2.7.0,<2.8.0 - pytest-pep8 - pytest-flakes - restructuredtext_lint -commands = - py.test --pep8 - py.test --flakes -m flakes pluggy.py testing/test_pluggy.py - rst-lint CHANGELOG.rst README.rst +deps = + flake8 + restructuredtext_lint +commands = + flake8 pluggy.py setup.py testing + rst-lint CHANGELOG.rst README.rst [testenv:docs] @@ -33,5 +31,7 @@ commands = minversion=2.0 #--pyargs --doctest-modules --ignore=.tox addopts= -rxsX -pep8ignore = E501 E128 E127 norecursedirs = .tox ja .hg .env* + +[flake8] +max-line-length = 99 \ No newline at end of file