simplify internal plugin dispatching code, rename parts of the py._com plugin helpers

--HG--
branch : 1.0.x
This commit is contained in:
holger krekel 2009-08-09 23:51:25 +02:00
parent 5c8df1d4ca
commit a01e4769cc
16 changed files with 137 additions and 173 deletions

View File

@ -1,18 +1,21 @@
Changes between 1.0.0 and 1.0.1 Changes between 1.0.0 and 1.0.1
===================================== =====================================
* various unicode fixes: capturing and prints of unicode strings now * unicode fixes: capturing and unicode writes to sys.stdout
work within tests, they are encoded as "utf8" by default, terminalwriting (through e.g a print statement) now work within tests,
they are encoded as "utf8" by default, also terminalwriting
was adapted and somewhat unified between windows and linux was adapted and somewhat unified between windows and linux
* fix issue #27: better reporting on non-collectable items given on commandline * fix issue #27: better reporting on non-collectable items given on commandline
(e.g. pyc files) (e.g. pyc files)
* "Test" prefixed classes with an __init__ method are *not* collected by default anymore * "Test" prefixed classes are *not* collected by default anymore if they
have an __init__ method
* terser reporting of collection error tracebacks * terser reporting of collection error tracebacks
* renaming of arguments to some special rather internal hooks * streamlined internal plugin arch code, renamed of internal methods
and argnames (related to py/_com.py multicall/plugin)
Changes between 1.0.0b9 and 1.0.0 Changes between 1.0.0b9 and 1.0.0
===================================== =====================================

View File

@ -52,7 +52,7 @@ initpkg(__name__,
'_com.Registry' : ('./_com.py', 'Registry'), '_com.Registry' : ('./_com.py', 'Registry'),
'_com.MultiCall' : ('./_com.py', 'MultiCall'), '_com.MultiCall' : ('./_com.py', 'MultiCall'),
'_com.comregistry' : ('./_com.py', 'comregistry'), '_com.comregistry' : ('./_com.py', 'comregistry'),
'_com.Hooks' : ('./_com.py', 'Hooks'), '_com.HookRelay' : ('./_com.py', 'HookRelay'),
# py lib cmdline tools # py lib cmdline tools
'cmdline.pytest' : ('./cmdline/pytest.py', 'main',), 'cmdline.pytest' : ('./cmdline/pytest.py', 'main',),

View File

@ -5,77 +5,50 @@ py lib plugins and plugin call management
import py import py
class MultiCall: class MultiCall:
""" Manage a specific call into many python functions/methods. """ execute a call into multiple python functions/methods. """
Simple example: def __init__(self, methods, kwargs, firstresult=False):
MultiCall([list1.append, list2.append], 42).execute()
"""
def __init__(self, methods, *args, **kwargs):
self.methods = methods[:] self.methods = methods[:]
self.args = args
self.kwargs = kwargs self.kwargs = kwargs
self.results = [] self.results = []
self.firstresult = firstresult
def __repr__(self): def __repr__(self):
args = [] status = "%d results, %d meths" % (len(self.results), len(self.methods))
if self.args: return "<MultiCall %s, kwargs=%r>" %(status, self.kwargs)
args.append("posargs=%r" %(self.args,))
kw = self.kwargs
args.append(", ".join(["%s=%r" % x for x in self.kwargs.items()]))
args = " ".join(args)
status = "results: %r, rmethods: %r" % (self.results, self.methods)
return "<MultiCall %s %s>" %(args, status)
def execute(self, firstresult=False): def execute(self):
while self.methods: while self.methods:
currentmethod = self.methods.pop() method = self.methods.pop()
res = self.execute_method(currentmethod) res = self._call1(method)
if hasattr(self, '_ex1'):
self.results = [res]
break
if res is not None: if res is not None:
self.results.append(res) self.results.append(res)
if firstresult: if self.firstresult:
break break
if not firstresult: if not self.firstresult:
return self.results return self.results
if self.results: if self.results:
return self.results[-1] return self.results[-1]
def execute_method(self, currentmethod): def _call1(self, method):
self.currentmethod = currentmethod kwargs = self.kwargs
# provide call introspection if "__call__" is the first positional argument if '__call__' in varnames(method):
if hasattr(currentmethod, 'im_self'): kwargs = kwargs.copy()
varnames = currentmethod.im_func.func_code.co_varnames kwargs['__call__'] = self
needscall = varnames[1:2] == ('__call__',) return method(**kwargs)
else:
try:
varnames = currentmethod.func_code.co_varnames
except AttributeError:
# builtin function
varnames = ()
needscall = varnames[:1] == ('__call__',)
if needscall:
return currentmethod(self, *self.args, **self.kwargs)
else:
#try:
return currentmethod(*self.args, **self.kwargs)
#except TypeError:
# print currentmethod.__module__, currentmethod.__name__, self.args, self.kwargs
# raise
def exclude_other_results(self):
self._ex1 = True
def varnames(rawcode):
rawcode = getattr(rawcode, 'im_func', rawcode)
rawcode = getattr(rawcode, 'func_code', rawcode)
try:
return rawcode.co_varnames
except AttributeError:
return ()
class Registry: class Registry:
""" """
Manage Plugins: Load plugins and manage calls to plugins. Manage Plugins: register/unregister call calls to plugins.
""" """
logfile = None
MultiCall = MultiCall
def __init__(self, plugins=None): def __init__(self, plugins=None):
if plugins is None: if plugins is None:
plugins = [] plugins = []
@ -83,6 +56,7 @@ class Registry:
def register(self, plugin): def register(self, plugin):
assert not isinstance(plugin, str) assert not isinstance(plugin, str)
assert not plugin in self._plugins
self._plugins.append(plugin) self._plugins.append(plugin)
def unregister(self, plugin): def unregister(self, plugin):
@ -107,45 +81,44 @@ class Registry:
l.reverse() l.reverse()
return l return l
class Hooks: class HookRelay:
def __init__(self, hookspecs, registry=None): def __init__(self, hookspecs, registry):
self._hookspecs = hookspecs self._hookspecs = hookspecs
if registry is None: self._registry = registry
registry = py._com.comregistry
self.registry = registry
for name, method in vars(hookspecs).items(): for name, method in vars(hookspecs).items():
if name[:1] != "_": if name[:1] != "_":
firstresult = getattr(method, 'firstresult', False) setattr(self, name, self._makecall(name))
mm = HookCall(registry, name, firstresult=firstresult)
setattr(self, name, mm)
def __repr__(self):
return "<Hooks %r %r>" %(self._hookspecs, self.registry)
class HookCall: def _makecall(self, name, extralookup=None):
def __init__(self, registry, name, firstresult, extralookup=None): hookspecmethod = getattr(self._hookspecs, name)
self.registry = registry firstresult = getattr(hookspecmethod, 'firstresult', False)
return HookCaller(self, name, firstresult=firstresult,
extralookup=extralookup)
def _getmethods(self, name, extralookup=()):
return self._registry.listattr(name, extra=extralookup)
def _performcall(self, name, multicall):
return multicall.execute()
def __repr__(self):
return "<HookRelay %r %r>" %(self._hookspecs, self._registry)
class HookCaller:
def __init__(self, hookrelay, name, firstresult, extralookup=()):
self.hookrelay = hookrelay
self.name = name self.name = name
self.firstresult = firstresult self.firstresult = firstresult
self.extralookup = extralookup and [extralookup] or () self.extralookup = extralookup and [extralookup] or ()
def clone(self, extralookup):
return HookCall(self.registry, self.name, self.firstresult, extralookup)
def __repr__(self): def __repr__(self):
mode = self.firstresult and "firstresult" or "each" return "<HookCaller %r firstresult=%s %s>" %(
return "<HookCall %r mode=%s %s>" %(self.name, mode, self.registry) self.name, self.firstresult, self.hookrelay)
def __call__(self, *args, **kwargs): def __call__(self, **kwargs):
if args: methods = self.hookrelay._getmethods(self.name,
raise TypeError("only keyword arguments allowed " extralookup=self.extralookup)
"for api call to %r" % self.name) mc = MultiCall(methods, kwargs, firstresult=self.firstresult)
attr = self.registry.listattr(self.name, extra=self.extralookup) return self.hookrelay._performcall(self.name, mc)
mc = MultiCall(attr, **kwargs)
# XXX this should be doable from a hook impl: comregistry = Registry([])
if self.registry.logfile:
self.registry.logfile.write("%s(**%s) # firstresult=%s\n" %
(self.name, kwargs, self.firstresult))
self.registry.logfile.flush()
return mc.execute(firstresult=self.firstresult)
comregistry = Registry()

View File

@ -88,8 +88,8 @@ class Gateway(object):
self._channelfactory = ChannelFactory(self, _startcount) self._channelfactory = ChannelFactory(self, _startcount)
self._cleanup.register(self) self._cleanup.register(self)
if _startcount == 1: # only import 'py' on the "client" side if _startcount == 1: # only import 'py' on the "client" side
from py._com import Hooks import py
self.hook = Hooks(ExecnetAPI) self.hook = py._com.HookRelay(ExecnetAPI, py._com.comregistry)
else: else:
self.hook = ExecnetAPI() self.hook = ExecnetAPI()

View File

@ -21,7 +21,8 @@ class GatewayManager:
if not spec.chdir and not spec.popen: if not spec.chdir and not spec.popen:
spec.chdir = defaultchdir spec.chdir = defaultchdir
self.specs.append(spec) self.specs.append(spec)
self.hook = py._com.Hooks(py.execnet._HookSpecs) self.hook = py._com.HookRelay(
py.execnet._HookSpecs, py._com.comregistry)
def makegateways(self): def makegateways(self):
assert not self.gateways assert not self.gateways

View File

@ -1,15 +1,12 @@
import py import py
import os import os
from py._com import Registry, MultiCall from py._com import Registry, MultiCall, HookRelay
from py._com import Hooks
pytest_plugins = "xfail"
class TestMultiCall: class TestMultiCall:
def test_uses_copy_of_methods(self): def test_uses_copy_of_methods(self):
l = [lambda: 42] l = [lambda: 42]
mc = MultiCall(l) mc = MultiCall(l, {})
repr(mc) repr(mc)
l[:] = [] l[:] = []
res = mc.execute() res = mc.execute()
@ -18,22 +15,19 @@ class TestMultiCall:
def test_call_passing(self): def test_call_passing(self):
class P1: class P1:
def m(self, __call__, x): def m(self, __call__, x):
assert __call__.currentmethod == self.m
assert len(__call__.results) == 1 assert len(__call__.results) == 1
assert not __call__.methods assert not __call__.methods
return 17 return 17
class P2: class P2:
def m(self, __call__, x): def m(self, __call__, x):
assert __call__.currentmethod == self.m
assert __call__.args
assert __call__.results == [] assert __call__.results == []
assert __call__.methods assert __call__.methods
return 23 return 23
p1 = P1() p1 = P1()
p2 = P2() p2 = P2()
multicall = MultiCall([p1.m, p2.m], 23) multicall = MultiCall([p1.m, p2.m], {'x': 23})
assert "23" in repr(multicall) assert "23" in repr(multicall)
reslist = multicall.execute() reslist = multicall.execute()
assert len(reslist) == 2 assert len(reslist) == 2
@ -43,62 +37,44 @@ class TestMultiCall:
def test_keyword_args(self): def test_keyword_args(self):
def f(x): def f(x):
return x + 1 return x + 1
multicall = MultiCall([f], x=23) multicall = MultiCall([f], dict(x=23))
assert "x=23" in repr(multicall) assert "'x': 23" in repr(multicall)
reslist = multicall.execute() reslist = multicall.execute()
assert reslist == [24] assert reslist == [24]
assert "24" in repr(multicall) assert "1 results" in repr(multicall)
def test_optionalcallarg(self): def test_optionalcallarg(self):
class P1: class P1:
def m(self, x): def m(self, x):
return x return x
call = MultiCall([P1().m], 23) call = MultiCall([P1().m], dict(x=23))
assert "23" in repr(call) assert "23" in repr(call)
assert call.execute() == [23] assert call.execute() == [23]
assert call.execute(firstresult=True) == 23 call = MultiCall([P1().m], dict(x=23), firstresult=True)
def test_call_subexecute(self): def test_call_subexecute(self):
def m(__call__): def m(__call__):
subresult = __call__.execute(firstresult=True) subresult = __call__.execute()
return subresult + 1 return subresult + 1
def n(): def n():
return 1 return 1
call = MultiCall([n, m]) call = MultiCall([n, m], {}, firstresult=True)
res = call.execute(firstresult=True)
assert res == 2
def test_call_exclude_other_results(self):
def m(__call__):
__call__.exclude_other_results()
return 10
def n():
return 1
call = MultiCall([n, n, m, n])
res = call.execute() res = call.execute()
assert res == [10] assert res == 2
# doesn't really make sense for firstresult-mode - because
# we might not have had a chance to run at all.
#res = call.execute(firstresult=True)
#assert res == 10
def test_call_none_is_no_result(self): def test_call_none_is_no_result(self):
def m1(): def m1():
return 1 return 1
def m2(): def m2():
return None return None
mc = MultiCall([m1, m2]) res = MultiCall([m1, m2], {}, firstresult=True).execute()
res = mc.execute(firstresult=True)
assert res == 1 assert res == 1
res = MultiCall([m1, m2], {}).execute()
assert res == [1]
class TestRegistry: class TestRegistry:
def test_MultiCall(self):
plugins = Registry()
assert hasattr(plugins, "MultiCall")
def test_register(self): def test_register(self):
registry = Registry() registry = Registry()
@ -142,14 +118,14 @@ class TestRegistry:
def test_api_and_defaults(): def test_api_and_defaults():
assert isinstance(py._com.comregistry, Registry) assert isinstance(py._com.comregistry, Registry)
class TestHooks: class TestHookRelay:
def test_happypath(self): def test_happypath(self):
registry = Registry() registry = Registry()
class Api: class Api:
def hello(self, arg): def hello(self, arg):
pass pass
mcm = Hooks(hookspecs=Api, registry=registry) mcm = HookRelay(hookspecs=Api, registry=registry)
assert hasattr(mcm, 'hello') assert hasattr(mcm, 'hello')
assert repr(mcm.hello).find("hello") != -1 assert repr(mcm.hello).find("hello") != -1
class Plugin: class Plugin:
@ -160,23 +136,21 @@ class TestHooks:
assert l == [4] assert l == [4]
assert not hasattr(mcm, 'world') assert not hasattr(mcm, 'world')
def test_needskeywordargs(self): def test_only_kwargs(self):
registry = Registry() registry = Registry()
class Api: class Api:
def hello(self, arg): def hello(self, arg):
pass pass
mcm = Hooks(hookspecs=Api, registry=registry) mcm = HookRelay(hookspecs=Api, registry=registry)
excinfo = py.test.raises(TypeError, "mcm.hello(3)") py.test.raises(TypeError, "mcm.hello(3)")
assert str(excinfo.value).find("only keyword arguments") != -1
assert str(excinfo.value).find("hello(self, arg)")
def test_firstresult(self): def test_firstresult_definition(self):
registry = Registry() registry = Registry()
class Api: class Api:
def hello(self, arg): pass def hello(self, arg): pass
hello.firstresult = True hello.firstresult = True
mcm = Hooks(hookspecs=Api, registry=registry) mcm = HookRelay(hookspecs=Api, registry=registry)
class Plugin: class Plugin:
def hello(self, arg): def hello(self, arg):
return arg + 1 return arg + 1
@ -186,15 +160,16 @@ class TestHooks:
def test_default_plugins(self): def test_default_plugins(self):
class Api: pass class Api: pass
mcm = Hooks(hookspecs=Api) mcm = HookRelay(hookspecs=Api, registry=py._com.comregistry)
assert mcm.registry == py._com.comregistry assert mcm._registry == py._com.comregistry
def test_hooks_extra_plugins(self): def test_hooks_extra_plugins(self):
registry = Registry() registry = Registry()
class Api: class Api:
def hello(self, arg): def hello(self, arg):
pass pass
hook_hello = Hooks(hookspecs=Api, registry=registry).hello hookrelay = HookRelay(hookspecs=Api, registry=registry)
hook_hello = hookrelay.hello
class Plugin: class Plugin:
def hello(self, arg): def hello(self, arg):
return arg + 1 return arg + 1
@ -202,7 +177,7 @@ class TestHooks:
class Plugin2: class Plugin2:
def hello(self, arg): def hello(self, arg):
return arg + 2 return arg + 2
newhook = hook_hello.clone(extralookup=Plugin2()) newhook = hookrelay._makecall("hello", extralookup=Plugin2())
l = newhook(arg=3) l = newhook(arg=3)
assert l == [5, 4] assert l == [5, 4]
l2 = hook_hello(arg=3) l2 = hook_hello(arg=3)

View File

@ -47,7 +47,7 @@ class HookRecorder:
recorder = RecordCalls() recorder = RecordCalls()
self._recorders[hookspecs] = recorder self._recorders[hookspecs] = recorder
self._comregistry.register(recorder) self._comregistry.register(recorder)
self.hook = py._com.Hooks(hookspecs, registry=self._comregistry) self.hook = py._com.HookRelay(hookspecs, registry=self._comregistry)
def finish_recording(self): def finish_recording(self):
for recorder in self._recorders.values(): for recorder in self._recorders.values():

View File

@ -185,7 +185,7 @@ class CaptureManager:
method = self._getmethod(collector.config, collector.fspath) method = self._getmethod(collector.config, collector.fspath)
self.resumecapture(method) self.resumecapture(method)
try: try:
rep = __call__.execute(firstresult=True) rep = __call__.execute()
finally: finally:
outerr = self.suspendcapture() outerr = self.suspendcapture()
addouterr(rep, outerr) addouterr(rep, outerr)
@ -208,7 +208,7 @@ class CaptureManager:
method = self._getmethod(session.config, None) method = self._getmethod(session.config, None)
self.resumecapture(method) self.resumecapture(method)
try: try:
rep = __call__.execute(firstresult=True) rep = __call__.execute()
finally: finally:
outerr = self.suspendcapture() outerr = self.suspendcapture()
if rep: if rep:
@ -221,7 +221,7 @@ class CaptureManager:
def pytest_runtest_makereport(self, __call__, item, call): def pytest_runtest_makereport(self, __call__, item, call):
self.deactivate_funcargs() self.deactivate_funcargs()
rep = __call__.execute(firstresult=True) rep = __call__.execute()
outerr = self.suspendcapture() outerr = self.suspendcapture()
outerr = (item.outerr[0] + outerr[0], item.outerr[1] + outerr[1]) outerr = (item.outerr[0] + outerr[0], item.outerr[1] + outerr[1])
if not rep.passed: if not rep.passed:

View File

@ -3,7 +3,7 @@
import py import py
def pytest_pyfunc_call(__call__, pyfuncitem): def pytest_pyfunc_call(__call__, pyfuncitem):
if not __call__.execute(firstresult=True): if not __call__.execute():
testfunction = pyfuncitem.obj testfunction = pyfuncitem.obj
if pyfuncitem._isyieldedfunction(): if pyfuncitem._isyieldedfunction():
testfunction(*pyfuncitem._args) testfunction(*pyfuncitem._args)

View File

@ -35,7 +35,7 @@ class Execnetcleanup:
def pytest_pyfunc_call(self, __call__, pyfuncitem): def pytest_pyfunc_call(self, __call__, pyfuncitem):
if self._gateways is not None: if self._gateways is not None:
gateways = self._gateways[:] gateways = self._gateways[:]
res = __call__.execute(firstresult=True) res = __call__.execute()
while len(self._gateways) > len(gateways): while len(self._gateways) > len(gateways):
self._gateways[-1].exit() self._gateways[-1].exit()
return res return res

View File

@ -8,14 +8,27 @@ def pytest_addoption(parser):
def pytest_configure(config): def pytest_configure(config):
hooklog = config.getvalue("hooklog") hooklog = config.getvalue("hooklog")
if hooklog: if hooklog:
assert not config.pluginmanager.comregistry.logfile config._hooklogfile = open(hooklog, 'w', 0)
config.pluginmanager.comregistry.logfile = open(hooklog, 'w') config._hooklog_oldperformcall = config.hook._performcall
config.hook._performcall = (lambda name, multicall:
logged_call(name=name, multicall=multicall, config=config))
def logged_call(name, multicall, config):
f = config._hooklogfile
f.write("%s(**%s)\n" % (name, multicall.kwargs))
try:
res = config._hooklog_oldperformcall(name=name, multicall=multicall)
except:
f.write("-> exception")
raise
f.write("-> %r" % (res,))
return res
def pytest_unconfigure(config): def pytest_unconfigure(config):
f = config.pluginmanager.comregistry.logfile try:
if f: del config.hook.__dict__['_performcall']
f.close() except KeyError:
config.pluginmanager.comregistry.logfile = None pass
# =============================================================================== # ===============================================================================
# plugin tests # plugin tests

View File

@ -24,7 +24,7 @@ def pytest_runtest_makereport(__call__, item, call):
return return
if hasattr(item, 'obj') and hasattr(item.obj, 'func_dict'): if hasattr(item, 'obj') and hasattr(item.obj, 'func_dict'):
if 'xfail' in item.obj.func_dict: if 'xfail' in item.obj.func_dict:
res = __call__.execute(firstresult=True) res = __call__.execute()
if call.excinfo: if call.excinfo:
res.skipped = True res.skipped = True
res.failed = res.passed = False res.failed = res.passed = False

View File

@ -16,10 +16,9 @@ class PluginManager(object):
if comregistry is None: if comregistry is None:
comregistry = py._com.Registry() comregistry = py._com.Registry()
self.comregistry = comregistry self.comregistry = comregistry
self.MultiCall = self.comregistry.MultiCall
self.impname2plugin = {} self.impname2plugin = {}
self.hook = py._com.Hooks( self.hook = py._com.HookRelay(
hookspecs=hookspec, hookspecs=hookspec,
registry=self.comregistry) registry=self.comregistry)
@ -166,20 +165,24 @@ class PluginManager(object):
return self.hook.pytest_internalerror(excrepr=excrepr) return self.hook.pytest_internalerror(excrepr=excrepr)
def do_addoption(self, parser): def do_addoption(self, parser):
methods = self.comregistry.listattr("pytest_addoption", reverse=True) mname = "pytest_addoption"
mc = py._com.MultiCall(methods, parser=parser) methods = self.comregistry.listattr(mname, reverse=True)
mc = py._com.MultiCall(methods, {'parser': parser})
mc.execute() mc.execute()
def pytest_plugin_registered(self, plugin): def pytest_plugin_registered(self, plugin):
if hasattr(self, '_config'): if hasattr(self, '_config'):
self.call_plugin(plugin, "pytest_addoption", parser=self._config._parser) self.call_plugin(plugin, "pytest_addoption",
self.call_plugin(plugin, "pytest_configure", config=self._config) {'parser': self._config._parser})
self.call_plugin(plugin, "pytest_configure",
{'config': self._config})
#dic = self.call_plugin(plugin, "pytest_namespace") #dic = self.call_plugin(plugin, "pytest_namespace")
#self._updateext(dic) #self._updateext(dic)
def call_plugin(self, plugin, methname, **kwargs): def call_plugin(self, plugin, methname, kwargs):
return self.MultiCall(self.listattr(methname, plugins=[plugin]), return py._com.MultiCall(
**kwargs).execute(firstresult=True) methods=self.listattr(methname, plugins=[plugin]),
kwargs=kwargs, firstresult=True).execute()
def _updateext(self, dic): def _updateext(self, dic):
if dic: if dic:

View File

@ -155,8 +155,8 @@ class PyCollectorMixin(PyobjMixin, py.test.collect.Collector):
cls = clscol and clscol.obj or None cls = clscol and clscol.obj or None
metafunc = funcargs.Metafunc(funcobj, config=self.config, metafunc = funcargs.Metafunc(funcobj, config=self.config,
cls=cls, module=module) cls=cls, module=module)
gentesthook = self.config.hook.pytest_generate_tests.clone( gentesthook = self.config.hook._makecall(
extralookup=module) "pytest_generate_tests", extralookup=module)
gentesthook(metafunc=metafunc) gentesthook(metafunc=metafunc)
if not metafunc._calls: if not metafunc._calls:
return self.Function(name, parent=self) return self.Function(name, parent=self)

View File

@ -145,7 +145,7 @@ class TestCollectFS:
names = [x.name for x in col.collect()] names = [x.name for x in col.collect()]
assert names == ["dir1", "dir2", "test_one.py", "test_two.py", "x"] assert names == ["dir1", "dir2", "test_one.py", "test_two.py", "x"]
class TestCollectPluginHooks: class TestCollectPluginHookRelay:
def test_pytest_collect_file(self, testdir): def test_pytest_collect_file(self, testdir):
tmpdir = testdir.tmpdir tmpdir = testdir.tmpdir
wascalled = [] wascalled = []

View File

@ -222,10 +222,6 @@ class TestPytestPluginInteractions:
config.pluginmanager.register(A()) config.pluginmanager.register(A())
assert len(l) == 2 assert len(l) == 2
def test_MultiCall(self):
pp = PluginManager()
assert hasattr(pp, 'MultiCall')
# lower level API # lower level API
def test_listattr(self): def test_listattr(self):