removed unnccessary indirections in the PluginManager,

also fixed a bug in _core.varnames(), which probably considerably
speeds up hook calls.

--HG--
branch : trunk
This commit is contained in:
holger krekel 2010-10-13 11:12:27 +02:00
parent 270deb015e
commit 3a5d28f3fe
5 changed files with 147 additions and 185 deletions

View File

@ -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 "<HookCaller %r>" %(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

View File

@ -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

View File

@ -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 "<ParsedCall %r(**%r)>" %(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:

View File

@ -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]
""")

View File

@ -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