From 3a5d28f3fe9ea2d02accd6435cdc265f667c3f7f Mon Sep 17 00:00:00 2001 From: holger krekel Date: Wed, 13 Oct 2010 11:12:27 +0200 Subject: [PATCH] removed unnccessary indirections in the PluginManager, also fixed a bug in _core.varnames(), which probably considerably speeds up hook calls. --HG-- branch : trunk --- pytest/_core.py | 197 +++++++++++++------------------- pytest/plugin/config.py | 2 +- pytest/plugin/pytester.py | 18 +-- testing/plugin/test_pytester.py | 12 +- testing/test_pluginmanager.py | 103 +++++++++-------- 5 files changed, 147 insertions(+), 185 deletions(-) diff --git a/pytest/_core.py b/pytest/_core.py index 977fdb8df..2ce7ea9f6 100644 --- a/pytest/_core.py +++ b/pytest/_core.py @@ -1,25 +1,28 @@ +import sys, os +import inspect import py +from pytest import hookspec assert py.__version__.split(".")[:2] >= ['2', '0'], ("installation problem: " "%s is too old, remove or upgrade 'py'" % (py.__version__)) -import sys, os - default_plugins = ( "config session terminal python runner pdb capture mark skipping tmpdir " "monkeypatch recwarn pastebin unittest helpconfig nose assertion genscript " "junitxml doctest").split() +IMPORTPREFIX = "pytest_" + class PluginManager(object): - def __init__(self): - from pytest import hookspec - self.registry = Registry() + def __init__(self, load=False): self._name2plugin = {} + self._plugins = [] self._hints = [] - self.hook = HookRelay([hookspec], registry=self.registry) + self.hook = HookRelay([hookspec], pm=self) self.register(self) - for spec in default_plugins: - self.import_plugin(spec) + if load: + for spec in default_plugins: + self.import_plugin(spec) def _getpluginname(self, plugin, name): if name is None: @@ -31,19 +34,22 @@ class PluginManager(object): def register(self, plugin, name=None, prepend=False): assert not self.isregistered(plugin), plugin - assert not self.registry.isregistered(plugin), plugin + assert not self.isregistered(plugin), plugin name = self._getpluginname(plugin, name) if name in self._name2plugin: return False self._name2plugin[name] = plugin self.call_plugin(plugin, "pytest_addhooks", {'pluginmanager': self}) self.hook.pytest_plugin_registered(manager=self, plugin=plugin) - self.registry.register(plugin, prepend=prepend) + if not prepend: + self._plugins.append(plugin) + else: + self._plugins.insert(0, plugin) return True def unregister(self, plugin): self.hook.pytest_plugin_unregistered(plugin=plugin) - self.registry.unregister(plugin) + self._plugins.remove(plugin) for name, value in list(self._name2plugin.items()): if value == plugin: del self._name2plugin[name] @@ -59,7 +65,7 @@ class PluginManager(object): self.hook._addhooks(spec, prefix="pytest_") def getplugins(self): - return list(self.registry) + return list(self._plugins) def skipifmissing(self, name): if not self.hasplugin(name): @@ -68,10 +74,9 @@ class PluginManager(object): def hasplugin(self, name): try: self.getplugin(name) + return True except KeyError: return False - else: - return True def getplugin(self, name): try: @@ -134,7 +139,7 @@ class PluginManager(object): mod = importplugin(modname) except KeyboardInterrupt: raise - except: + except: e = py.std.sys.exc_info()[1] if not hasattr(py.test, 'skip'): raise @@ -145,35 +150,15 @@ class PluginManager(object): self.register(mod, modname) self.consider_module(mod) - def pytest_terminal_summary(self, terminalreporter): - tw = terminalreporter._tw - if terminalreporter.config.option.traceconfig: - for hint in self._hints: - tw.line("hint: %s" % hint) - - # - # - # API for interacting with registered and instantiated plugin objects - # - # - def listattr(self, attrname, plugins=None): - return self.registry.listattr(attrname, plugins=plugins) - - def notify_exception(self, excinfo=None): - if excinfo is None: - excinfo = py.code.ExceptionInfo() - excrepr = excinfo.getrepr(funcargs=True, showlocals=True) - res = self.hook.pytest_internalerror(excrepr=excrepr) - if not py.builtin.any(res): - for line in str(excrepr).split("\n"): - sys.stderr.write("INTERNALERROR> %s\n" %line) - sys.stderr.flush() - - def do_addoption(self, parser): - mname = "pytest_addoption" - methods = self.registry.listattr(mname, reverse=True) - mc = MultiCall(methods, {'parser': parser}) - mc.execute() + def pytest_plugin_registered(self, plugin): + dic = self.call_plugin(plugin, "pytest_namespace", {}) or {} + if dic: + self._setns(py.test, dic) + if hasattr(self, '_config'): + self.call_plugin(plugin, "pytest_addoption", + {'parser': self._config._parser}) + self.call_plugin(plugin, "pytest_configure", + {'config': self._config}) def _setns(self, obj, dic): for name, value in dic.items(): @@ -191,20 +176,16 @@ class PluginManager(object): setattr(obj, name, value) obj.__all__.append(name) - def pytest_plugin_registered(self, plugin): - dic = self.call_plugin(plugin, "pytest_namespace", {}) or {} - if dic: - self._setns(py.test, dic) - if hasattr(self, '_config'): - self.call_plugin(plugin, "pytest_addoption", - {'parser': self._config._parser}) - self.call_plugin(plugin, "pytest_configure", - {'config': self._config}) + def pytest_terminal_summary(self, terminalreporter): + tw = terminalreporter._tw + if terminalreporter.config.option.traceconfig: + for hint in self._hints: + tw.line("hint: %s" % hint) - def call_plugin(self, plugin, methname, kwargs): - return MultiCall( - methods=self.listattr(methname, plugins=[plugin]), - kwargs=kwargs, firstresult=True).execute() + def do_addoption(self, parser): + mname = "pytest_addoption" + methods = reversed(self.listattr(mname)) + MultiCall(methods, {'parser': parser}).execute() def do_configure(self, config): assert not hasattr(self, '_config') @@ -217,11 +198,33 @@ class PluginManager(object): config.hook.pytest_unconfigure(config=config) config.pluginmanager.unregister(self) + def notify_exception(self, excinfo): + excrepr = excinfo.getrepr(funcargs=True, showlocals=True) + res = self.hook.pytest_internalerror(excrepr=excrepr) + if not py.builtin.any(res): + for line in str(excrepr).split("\n"): + sys.stderr.write("INTERNALERROR> %s\n" %line) + sys.stderr.flush() + + def listattr(self, attrname, plugins=None): + if plugins is None: + plugins = self._plugins + l = [] + for plugin in plugins: + try: + l.append(getattr(plugin, attrname)) + except AttributeError: + continue + return l + + def call_plugin(self, plugin, methname, kwargs): + return MultiCall(methods=self.listattr(methname, plugins=[plugin]), + kwargs=kwargs, firstresult=True).execute() + def canonical_importname(name): name = name.lower() - modprefix = "pytest_" - if not name.startswith(modprefix): - name = modprefix + name + if not name.startswith(IMPORTPREFIX): + name = IMPORTPREFIX + name return name def importplugin(importspec): @@ -246,12 +249,10 @@ def importplugin(importspec): class MultiCall: - """ execute a call into multiple python functions/methods. """ - + """ execute a call into multiple python functions/methods. """ def __init__(self, methods, kwargs, firstresult=False): - self.methods = methods[:] - self.kwargs = kwargs.copy() - self.kwargs['__multicall__'] = self + self.methods = list(methods) + self.kwargs = kwargs self.results = [] self.firstresult = firstresult @@ -277,65 +278,26 @@ class MultiCall: try: kwargs[argname] = self.kwargs[argname] except KeyError: - pass # might be optional param + if argname == "__multicall__": + kwargs[argname] = self return kwargs -def varnames(func): - import inspect +def varnames(func, cache={}): if not inspect.isfunction(func) and not inspect.ismethod(func): func = getattr(func, '__call__', func) ismethod = inspect.ismethod(func) rawcode = py.code.getrawcode(func) try: - return rawcode.co_varnames[ismethod:] + return rawcode.co_varnames[ismethod:rawcode.co_argcount] except AttributeError: return () -class Registry: - """ - Manage Plugins: register/unregister call calls to plugins. - """ - def __init__(self, plugins=None): - if plugins is None: - plugins = [] - self._plugins = plugins - - def register(self, plugin, prepend=False): - assert not isinstance(plugin, str) - assert not plugin in self._plugins - if not prepend: - self._plugins.append(plugin) - else: - self._plugins.insert(0, plugin) - - def unregister(self, plugin): - self._plugins.remove(plugin) - - def isregistered(self, plugin): - return plugin in self._plugins - - def __iter__(self): - return iter(self._plugins) - - def listattr(self, attrname, plugins=None, reverse=False): - l = [] - if plugins is None: - plugins = self._plugins - for plugin in plugins: - try: - l.append(getattr(plugin, attrname)) - except AttributeError: - continue - if reverse: - l.reverse() - return l - class HookRelay: - def __init__(self, hookspecs, registry, prefix="pytest_"): + def __init__(self, hookspecs, pm, prefix="pytest_"): if not isinstance(hookspecs, list): hookspecs = [hookspecs] self._hookspecs = [] - self._registry = registry + self._pm = pm for hookspec in hookspecs: self._addhooks(hookspec, prefix) @@ -357,9 +319,6 @@ class HookRelay: prefix, hookspecs,)) - def _performcall(self, name, multicall): - return multicall.execute() - class HookCaller: def __init__(self, hookrelay, name, firstresult): self.hookrelay = hookrelay @@ -370,16 +329,16 @@ class HookCaller: return "" %(self.name,) def __call__(self, **kwargs): - methods = self.hookrelay._registry.listattr(self.name) + methods = self.hookrelay._pm.listattr(self.name) mc = MultiCall(methods, kwargs, firstresult=self.firstresult) - return self.hookrelay._performcall(self.name, mc) + return mc.execute() def pcall(self, plugins, **kwargs): - methods = self.hookrelay._registry.listattr(self.name, plugins=plugins) + methods = self.hookrelay._pm.listattr(self.name, plugins=plugins) mc = MultiCall(methods, kwargs, firstresult=self.firstresult) - return self.hookrelay._performcall(self.name, mc) + return mc.execute() -pluginmanager = PluginManager() # will trigger default plugin loading +pluginmanager = PluginManager(load=True) # will trigger default plugin importing def main(args=None): global pluginmanager @@ -393,6 +352,6 @@ def main(args=None): e = sys.exc_info()[1] sys.stderr.write("ERROR: %s\n" %(e.args[0],)) exitstatus = 3 - pluginmanager = PluginManager() + pluginmanager = PluginManager(load=True) return exitstatus diff --git a/pytest/plugin/config.py b/pytest/plugin/config.py index 58501948a..89dd49c58 100644 --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -246,7 +246,7 @@ class Config(object): processopt=self._processopt, ) #: a pluginmanager instance - self.pluginmanager = pluginmanager or PluginManager() + self.pluginmanager = pluginmanager or PluginManager(load=True) self._conftest = Conftest(onimport=self._onimportconftest) self.hook = self.pluginmanager.hook diff --git a/pytest/plugin/pytester.py b/pytest/plugin/pytester.py index d646f66c8..39fa18a45 100644 --- a/pytest/plugin/pytester.py +++ b/pytest/plugin/pytester.py @@ -27,7 +27,7 @@ class PytestArg: self.request = request def gethookrecorder(self, hook): - hookrecorder = HookRecorder(hook._registry) + hookrecorder = HookRecorder(hook._pm) hookrecorder.start_recording(hook._hookspecs) self.request.addfinalizer(hookrecorder.finish_recording) return hookrecorder @@ -45,8 +45,8 @@ class ParsedCall: return "" %(self._name, d) class HookRecorder: - def __init__(self, registry): - self._registry = registry + def __init__(self, pluginmanager): + self._pluginmanager = pluginmanager self.calls = [] self._recorders = {} @@ -62,13 +62,13 @@ class HookRecorder: setattr(RecordCalls, name, self._makecallparser(method)) recorder = RecordCalls() self._recorders[hookspec] = recorder - self._registry.register(recorder) - self.hook = HookRelay(hookspecs, registry=self._registry, + self._pluginmanager.register(recorder) + self.hook = HookRelay(hookspecs, pm=self._pluginmanager, prefix="pytest_") def finish_recording(self): for recorder in self._recorders.values(): - self._registry.unregister(recorder) + self._pluginmanager.unregister(recorder) self._recorders.clear() def _makecallparser(self, method): @@ -493,8 +493,8 @@ class PseudoPlugin: class ReportRecorder(object): def __init__(self, hook): self.hook = hook - self.registry = hook._registry - self.registry.register(self) + self.pluginmanager = hook._pm + self.pluginmanager.register(self) def getcall(self, name): return self.hookrecorder.getcall(name) @@ -558,7 +558,7 @@ class ReportRecorder(object): self.hookrecorder.calls[:] = [] def unregister(self): - self.registry.unregister(self) + self.pluginmanager.unregister(self) self.hookrecorder.finish_recording() class LineComp: diff --git a/testing/plugin/test_pytester.py b/testing/plugin/test_pytester.py index cd6f8ea70..17701221e 100644 --- a/testing/plugin/test_pytester.py +++ b/testing/plugin/test_pytester.py @@ -1,7 +1,7 @@ import py import os, sys from pytest.plugin.pytester import LineMatcher, LineComp, HookRecorder -from pytest._core import Registry +from pytest._core import PluginManager def test_reportrecorder(testdir): item = testdir.getitem("def test_func(): pass") @@ -72,7 +72,7 @@ def test_testdir_runs_with_plugin(testdir): ]) def test_hookrecorder_basic(): - rec = HookRecorder(Registry()) + rec = HookRecorder(PluginManager()) class ApiClass: def pytest_xyz(self, arg): "x" @@ -84,7 +84,7 @@ def test_hookrecorder_basic(): py.test.raises(ValueError, "rec.popcall('abc')") def test_hookrecorder_basic_no_args_hook(): - rec = HookRecorder(Registry()) + rec = HookRecorder(PluginManager()) apimod = type(os)('api') def pytest_xyz(): "x" @@ -97,17 +97,17 @@ def test_hookrecorder_basic_no_args_hook(): def test_functional(testdir, linecomp): reprec = testdir.inline_runsource(""" import py - from pytest._core import HookRelay, Registry + from pytest._core import HookRelay, PluginManager pytest_plugins="pytester" def test_func(_pytest): class ApiClass: def pytest_xyz(self, arg): "x" - hook = HookRelay([ApiClass], Registry()) + hook = HookRelay([ApiClass], PluginManager(load=False)) rec = _pytest.gethookrecorder(hook) class Plugin: def pytest_xyz(self, arg): return arg + 1 - rec._registry.register(Plugin()) + rec._pluginmanager.register(Plugin()) res = rec.hook.pytest_xyz(arg=41) assert res == [42] """) diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 28d2fe1aa..26d682dde 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -1,6 +1,6 @@ import py, os from pytest._core import PluginManager, canonical_importname -from pytest._core import Registry, MultiCall, HookRelay, varnames +from pytest._core import MultiCall, HookRelay, varnames class TestBootstrapping: @@ -143,7 +143,7 @@ class TestBootstrapping: pp = PluginManager() py.test.raises(ImportError, "pp.consider_conftest(mod)") - def test_registry(self): + def test_pm(self): pp = PluginManager() class A: pass a1, a2 = A(), A() @@ -160,7 +160,7 @@ class TestBootstrapping: pp.unregister(a2) assert not pp.isregistered(a2) - def test_registry_ordering(self): + def test_pm_ordering(self): pp = PluginManager() class A: pass a1, a2 = A(), A() @@ -182,7 +182,7 @@ class TestBootstrapping: assert mod in l py.test.raises(AssertionError, "pp.register(mod)") mod2 = py.std.types.ModuleType("pytest_hello") - #pp.register(mod2) # double registry + #pp.register(mod2) # double pm py.test.raises(AssertionError, "pp.register(mod)") #assert not pp.isregistered(mod2) assert pp.getplugins() == l @@ -197,14 +197,14 @@ class TestBootstrapping: assert pp.isregistered(mod) def test_register_mismatch_method(self): - pp = PluginManager() + pp = PluginManager(load=True) class hello: def pytest_gurgel(self): pass py.test.raises(Exception, "pp.register(hello())") def test_register_mismatch_arg(self): - pp = PluginManager() + pp = PluginManager(load=True) class hello: def pytest_configure(self, asd): pass @@ -228,6 +228,36 @@ class TestBootstrapping: out, err = capfd.readouterr() assert not err + def test_register(self): + pm = PluginManager(load=False) + class MyPlugin: + pass + my = MyPlugin() + pm.register(my) + assert pm.getplugins() + my2 = MyPlugin() + pm.register(my2) + assert pm.getplugins()[1:] == [my, my2] + + assert pm.isregistered(my) + assert pm.isregistered(my2) + pm.unregister(my) + assert not pm.isregistered(my) + assert pm.getplugins()[1:] == [my2] + + def test_listattr(self): + plugins = PluginManager() + class api1: + x = 41 + class api2: + x = 42 + class api3: + x = 43 + plugins.register(api1()) + plugins.register(api2()) + plugins.register(api3()) + l = list(plugins.listattr('x')) + assert l == [41, 42, 43] class TestPytestPluginInteractions: @@ -370,7 +400,7 @@ def test_namespace_has_default_and_env_plugins(testdir): def test_varnames(): def f(x): - pass + i = 3 class A: def f(self, y): pass @@ -425,6 +455,14 @@ class TestMultiCall: assert reslist == [24+23, 24] assert "2 results" in repr(multicall) + def test_keyword_args_with_defaultargs(self): + def f(x, z=1): + return x + z + reslist = MultiCall([f], dict(x=23, y=24)).execute() + assert reslist == [24] + reslist = MultiCall([f], dict(x=23, z=2)).execute() + assert reslist == [25] + def test_keywords_call_error(self): multicall = MultiCall([lambda x: x], {}) py.test.raises(TypeError, "multicall.execute()") @@ -451,79 +489,44 @@ class TestMultiCall: res = MultiCall([m1, m2], {}).execute() assert res == [1] -class TestRegistry: - - def test_register(self): - registry = Registry() - class MyPlugin: - pass - my = MyPlugin() - registry.register(my) - assert list(registry) == [my] - my2 = MyPlugin() - registry.register(my2) - assert list(registry) == [my, my2] - - assert registry.isregistered(my) - assert registry.isregistered(my2) - registry.unregister(my) - assert not registry.isregistered(my) - assert list(registry) == [my2] - - def test_listattr(self): - plugins = Registry() - class api1: - x = 41 - class api2: - x = 42 - class api3: - x = 43 - plugins.register(api1()) - plugins.register(api2()) - plugins.register(api3()) - l = list(plugins.listattr('x')) - assert l == [41, 42, 43] - l = list(plugins.listattr('x', reverse=True)) - assert l == [43, 42, 41] - class TestHookRelay: def test_happypath(self): - registry = Registry() + pm = PluginManager() class Api: def hello(self, arg): "api hook 1" - mcm = HookRelay(hookspecs=Api, registry=registry, prefix="he") + mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") assert hasattr(mcm, 'hello') assert repr(mcm.hello).find("hello") != -1 class Plugin: def hello(self, arg): return arg + 1 - registry.register(Plugin()) + pm.register(Plugin()) l = mcm.hello(arg=3) assert l == [4] assert not hasattr(mcm, 'world') def test_only_kwargs(self): - registry = Registry() + pm = PluginManager() class Api: def hello(self, arg): "api hook 1" - mcm = HookRelay(hookspecs=Api, registry=registry, prefix="he") + mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") py.test.raises(TypeError, "mcm.hello(3)") def test_firstresult_definition(self): - registry = Registry() + pm = PluginManager() class Api: def hello(self, arg): "api hook 1" hello.firstresult = True - mcm = HookRelay(hookspecs=Api, registry=registry, prefix="he") + mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") class Plugin: def hello(self, arg): return arg + 1 - registry.register(Plugin()) + pm.register(Plugin()) res = mcm.hello(arg=3) assert res == 4