diff --git a/py/impl/test/pluginmanager.py b/py/impl/test/pluginmanager.py index 7f070896c..fdbbecb03 100644 --- a/py/impl/test/pluginmanager.py +++ b/py/impl/test/pluginmanager.py @@ -44,8 +44,7 @@ class PluginManager(object): if name in self._name2plugin: return False self._name2plugin[name] = plugin - self.hook.pytest_plugin_registered(plugin=plugin) - self._checkplugin(plugin) + self.hook.pytest_plugin_registered(manager=self, plugin=plugin) self.comregistry.register(plugin) return True @@ -138,46 +137,6 @@ class PluginManager(object): def _warn(self, msg): print ("===WARNING=== %s" % (msg,)) - def _checkplugin(self, plugin): - # ===================================================== - # check plugin hooks - # ===================================================== - methods = collectattr(plugin) - hooks = collectattr(hookspec) - stringio = py.io.TextIO() - def Print(*args): - if args: - stringio.write(" ".join(map(str, args))) - stringio.write("\n") - - fail = False - while methods: - name, method = methods.popitem() - #print "checking", name - if isgenerichook(name): - continue - if name not in hooks: - Print("found unknown hook:", name) - fail = True - else: - method_args = getargs(method) - if '__multicall__' in method_args: - method_args.remove('__multicall__') - hook = hooks[name] - hookargs = getargs(hook) - for arg in method_args: - if arg not in hookargs: - Print("argument %r not available" %(arg, )) - Print("actual definition: %s" %(formatdef(method))) - Print("available hook arguments: %s" % - ", ".join(hookargs)) - fail = True - break - #if not fail: - # print "matching hook:", formatdef(method) - if fail: - name = getattr(plugin, '__name__', plugin) - raise self.Error("%s:\n%s" %(name, stringio.getvalue())) # # # API for interacting with registered and instantiated plugin objects @@ -224,9 +183,6 @@ class PluginManager(object): config.hook.pytest_unconfigure(config=config) config.pluginmanager.unregister(self) -# -# XXX old code to automatically load classes -# def canonical_importname(name): name = name.lower() modprefix = "pytest_" @@ -254,59 +210,3 @@ def importplugin(importspec): -def isgenerichook(name): - return name == "pytest_plugins" or \ - name.startswith("pytest_funcarg__") - -def getargs(func): - args = py.std.inspect.getargs(py.code.getrawcode(func))[0] - startindex = py.std.inspect.ismethod(func) and 1 or 0 - return args[startindex:] - -def collectattr(obj, prefixes=("pytest_",)): - methods = {} - for apiname in dir(obj): - for prefix in prefixes: - if apiname.startswith(prefix): - methods[apiname] = getattr(obj, apiname) - return methods - -def formatdef(func): - return "%s%s" %( - func.__name__, - py.std.inspect.formatargspec(*py.std.inspect.getargspec(func)) - ) - -if __name__ == "__main__": - import py.plugin - basedir = py._dir.join('_plugin') - name2text = {} - for p in basedir.listdir("pytest_*"): - if p.ext == ".py" or ( - p.check(dir=1) and p.join("__init__.py").check()): - impname = p.purebasename - if impname.find("__") != -1: - continue - try: - plugin = importplugin(impname) - except (ImportError, py.impl.test.outcome.Skipped): - name2text[impname] = "IMPORT ERROR" - else: - doc = plugin.__doc__ or "" - doc = doc.strip() - name2text[impname] = doc - - for name in sorted(name2text.keys()): - text = name2text[name] - if name[0] == "_": - continue - print ("%-20s %s" % (name, text.split("\n")[0])) - - #text = py.std.textwrap.wrap(name2text[name], - # width = 80, - # initial_indent="%s: " % name, - # replace_whitespace = False) - #for line in text: - # print line - - diff --git a/py/plugin/hookspec.py b/py/plugin/hookspec.py index 22e65e2b4..bb3352f7d 100644 --- a/py/plugin/hookspec.py +++ b/py/plugin/hookspec.py @@ -159,7 +159,7 @@ def pytest_looponfailinfo(failreports, rootdirs): # error handling and internal debugging hooks # ------------------------------------------------------------------------- -def pytest_plugin_registered(plugin): +def pytest_plugin_registered(plugin, manager): """ a new py lib plugin got registered. """ def pytest_plugin_unregistered(plugin): diff --git a/py/plugin/pytest_helpconfig.py b/py/plugin/pytest_helpconfig.py index d87372b7c..7ca2125f2 100644 --- a/py/plugin/pytest_helpconfig.py +++ b/py/plugin/pytest_helpconfig.py @@ -1,7 +1,7 @@ """ provide version info, conftest/environment config names. """ import py -import sys +import inspect, sys def pytest_addoption(parser): group = parser.getgroup('debugconfig') @@ -61,3 +61,73 @@ conftest_options = ( ('collect_ignore', '(relative) paths ignored during collection'), ('rsyncdirs', 'to-be-rsynced directories for dist-testing'), ) + +# ===================================================== +# validate plugin syntax and hooks +# ===================================================== + +def pytest_plugin_registered(manager, plugin): + hookspec = manager.hook._hookspecs + methods = collectattr(plugin) + hooks = collectattr(hookspec) + stringio = py.io.TextIO() + def Print(*args): + if args: + stringio.write(" ".join(map(str, args))) + stringio.write("\n") + + fail = False + while methods: + name, method = methods.popitem() + #print "checking", name + if isgenerichook(name): + continue + if name not in hooks: + Print("found unknown hook:", name) + fail = True + else: + method_args = getargs(method) + if '__multicall__' in method_args: + method_args.remove('__multicall__') + hook = hooks[name] + hookargs = getargs(hook) + for arg in method_args: + if arg not in hookargs: + Print("argument %r not available" %(arg, )) + Print("actual definition: %s" %(formatdef(method))) + Print("available hook arguments: %s" % + ", ".join(hookargs)) + fail = True + break + #if not fail: + # print "matching hook:", formatdef(method) + if fail: + name = getattr(plugin, '__name__', plugin) + raise PluginValidationError("%s:\n%s" %(name, stringio.getvalue())) + +class PluginValidationError(Exception): + """ plugin failed validation. """ + +def isgenerichook(name): + return name == "pytest_plugins" or \ + name.startswith("pytest_funcarg__") + +def getargs(func): + args = inspect.getargs(py.code.getrawcode(func))[0] + startindex = inspect.ismethod(func) and 1 or 0 + return args[startindex:] + +def collectattr(obj, prefixes=("pytest_",)): + methods = {} + for apiname in dir(obj): + for prefix in prefixes: + if apiname.startswith(prefix): + methods[apiname] = getattr(obj, apiname) + return methods + +def formatdef(func): + return "%s%s" %( + func.__name__, + inspect.formatargspec(*inspect.getargspec(func)) + ) + diff --git a/testing/plugin/test_pytest_helpconfig.py b/testing/plugin/test_pytest_helpconfig.py index 0ddbe53ea..6ec2c43aa 100644 --- a/testing/plugin/test_pytest_helpconfig.py +++ b/testing/plugin/test_pytest_helpconfig.py @@ -1,4 +1,5 @@ import py, os +from py.plugin.pytest_helpconfig import collectattr def test_version(testdir): assert py.version == py.__version__ @@ -16,3 +17,15 @@ def test_helpconfig(testdir): "*cmdline*conftest*ENV*", ]) +def test_collectattr(): + class A: + def pytest_hello(self): + pass + class B(A): + def pytest_world(self): + pass + methods = py.builtin.sorted(collectattr(B)) + assert list(methods) == ['pytest_hello', 'pytest_world'] + methods = py.builtin.sorted(collectattr(B())) + assert list(methods) == ['pytest_hello', 'pytest_world'] + diff --git a/testing/pytest/test_pluginmanager.py b/testing/pytest/test_pluginmanager.py index 3620ec92b..e6aed0152 100644 --- a/testing/pytest/test_pluginmanager.py +++ b/testing/pytest/test_pluginmanager.py @@ -1,5 +1,5 @@ import py, os -from py.impl.test.pluginmanager import PluginManager, canonical_importname, collectattr +from py.impl.test.pluginmanager import PluginManager, canonical_importname class TestBootstrapping: def test_consider_env_fails_to_import(self, monkeypatch): @@ -185,14 +185,14 @@ class TestBootstrapping: class hello: def pytest_gurgel(self): pass - py.test.raises(pp.Error, "pp.register(hello())") + py.test.raises(Exception, "pp.register(hello())") def test_register_mismatch_arg(self): pp = PluginManager() class hello: def pytest_configure(self, asd): pass - excinfo = py.test.raises(pp.Error, "pp.register(hello())") + excinfo = py.test.raises(Exception, "pp.register(hello())") def test_canonical_importname(self): for name in 'xyz', 'pytest_xyz', 'pytest_Xyz', 'Xyz': @@ -270,18 +270,6 @@ class TestPytestPluginInteractions: assert not pluginmanager.listattr("hello") assert pluginmanager.listattr("x") == [42] -def test_collectattr(): - class A: - def pytest_hello(self): - pass - class B(A): - def pytest_world(self): - pass - methods = py.builtin.sorted(collectattr(B)) - assert list(methods) == ['pytest_hello', 'pytest_world'] - methods = py.builtin.sorted(collectattr(B())) - assert list(methods) == ['pytest_hello', 'pytest_world'] - @py.test.mark.xfail def test_namespace_has_default_and_env_plugins(testdir): p = testdir.makepyfile("""