implement a new hook type: hook wrappers using a "yield" to distinguish
between working at the front and at the end of a hook call chain. The idea is to make it easier for a plugin to "wrap" a certain hook call and use context managers, in particular allow a major cleanup of capturing.
This commit is contained in:
parent
b47fdbe0a7
commit
f43cda9681
|
@ -240,18 +240,22 @@ class PluginManager(object):
|
||||||
pass
|
pass
|
||||||
l = []
|
l = []
|
||||||
last = []
|
last = []
|
||||||
|
wrappers = []
|
||||||
for plugin in plugins:
|
for plugin in plugins:
|
||||||
try:
|
try:
|
||||||
meth = getattr(plugin, attrname)
|
meth = getattr(plugin, attrname)
|
||||||
if hasattr(meth, 'tryfirst'):
|
|
||||||
last.append(meth)
|
|
||||||
elif hasattr(meth, 'trylast'):
|
|
||||||
l.insert(0, meth)
|
|
||||||
else:
|
|
||||||
l.append(meth)
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
continue
|
continue
|
||||||
|
if hasattr(meth, 'hookwrapper'):
|
||||||
|
wrappers.append(meth)
|
||||||
|
elif hasattr(meth, 'tryfirst'):
|
||||||
|
last.append(meth)
|
||||||
|
elif hasattr(meth, 'trylast'):
|
||||||
|
l.insert(0, meth)
|
||||||
|
else:
|
||||||
|
l.append(meth)
|
||||||
l.extend(last)
|
l.extend(last)
|
||||||
|
l.extend(wrappers)
|
||||||
self._listattrcache[key] = list(l)
|
self._listattrcache[key] = list(l)
|
||||||
return l
|
return l
|
||||||
|
|
||||||
|
@ -272,6 +276,14 @@ def importplugin(importspec):
|
||||||
|
|
||||||
class MultiCall:
|
class MultiCall:
|
||||||
""" execute a call into multiple python functions/methods. """
|
""" execute a call into multiple python functions/methods. """
|
||||||
|
|
||||||
|
class WrongHookWrapper(Exception):
|
||||||
|
""" a hook wrapper does not behave correctly. """
|
||||||
|
def __init__(self, func, message):
|
||||||
|
Exception.__init__(self, func, message)
|
||||||
|
self.func = func
|
||||||
|
self.message = message
|
||||||
|
|
||||||
def __init__(self, methods, kwargs, firstresult=False):
|
def __init__(self, methods, kwargs, firstresult=False):
|
||||||
self.methods = list(methods)
|
self.methods = list(methods)
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
|
@ -283,16 +295,39 @@ class MultiCall:
|
||||||
return "<MultiCall %s, kwargs=%r>" %(status, self.kwargs)
|
return "<MultiCall %s, kwargs=%r>" %(status, self.kwargs)
|
||||||
|
|
||||||
def execute(self):
|
def execute(self):
|
||||||
while self.methods:
|
next_finalizers = []
|
||||||
method = self.methods.pop()
|
try:
|
||||||
kwargs = self.getkwargs(method)
|
while self.methods:
|
||||||
res = method(**kwargs)
|
method = self.methods.pop()
|
||||||
if res is not None:
|
kwargs = self.getkwargs(method)
|
||||||
self.results.append(res)
|
if hasattr(method, "hookwrapper"):
|
||||||
if self.firstresult:
|
it = method(**kwargs)
|
||||||
return res
|
next = getattr(it, "next", None)
|
||||||
if not self.firstresult:
|
if next is None:
|
||||||
return self.results
|
next = getattr(it, "__next__", None)
|
||||||
|
if next is None:
|
||||||
|
raise self.WrongHookWrapper(method,
|
||||||
|
"wrapper does not contain a yield")
|
||||||
|
res = next()
|
||||||
|
next_finalizers.append((method, next))
|
||||||
|
else:
|
||||||
|
res = method(**kwargs)
|
||||||
|
if res is not None:
|
||||||
|
self.results.append(res)
|
||||||
|
if self.firstresult:
|
||||||
|
return res
|
||||||
|
if not self.firstresult:
|
||||||
|
return self.results
|
||||||
|
finally:
|
||||||
|
for method, fin in reversed(next_finalizers):
|
||||||
|
try:
|
||||||
|
fin()
|
||||||
|
except StopIteration:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise self.WrongHookWrapper(method,
|
||||||
|
"wrapper contain more than one yield")
|
||||||
|
|
||||||
|
|
||||||
def getkwargs(self, method):
|
def getkwargs(self, method):
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
|
|
|
@ -523,6 +523,95 @@ class TestMultiCall:
|
||||||
res = MultiCall([m1, m2], {}).execute()
|
res = MultiCall([m1, m2], {}).execute()
|
||||||
assert res == [1]
|
assert res == [1]
|
||||||
|
|
||||||
|
def test_hookwrapper(self):
|
||||||
|
l = []
|
||||||
|
def m1():
|
||||||
|
l.append("m1 init")
|
||||||
|
yield None
|
||||||
|
l.append("m1 finish")
|
||||||
|
m1.hookwrapper = True
|
||||||
|
|
||||||
|
def m2():
|
||||||
|
l.append("m2")
|
||||||
|
return 2
|
||||||
|
res = MultiCall([m2, m1], {}).execute()
|
||||||
|
assert res == [2]
|
||||||
|
assert l == ["m1 init", "m2", "m1 finish"]
|
||||||
|
l[:] = []
|
||||||
|
res = MultiCall([m2, m1], {}, firstresult=True).execute()
|
||||||
|
assert res == 2
|
||||||
|
assert l == ["m1 init", "m2", "m1 finish"]
|
||||||
|
|
||||||
|
def test_hookwrapper_order(self):
|
||||||
|
l = []
|
||||||
|
def m1():
|
||||||
|
l.append("m1 init")
|
||||||
|
yield 1
|
||||||
|
l.append("m1 finish")
|
||||||
|
m1.hookwrapper = True
|
||||||
|
|
||||||
|
def m2():
|
||||||
|
l.append("m2 init")
|
||||||
|
yield 2
|
||||||
|
l.append("m2 finish")
|
||||||
|
m2.hookwrapper = True
|
||||||
|
res = MultiCall([m2, m1], {}).execute()
|
||||||
|
assert res == [1, 2]
|
||||||
|
assert l == ["m1 init", "m2 init", "m2 finish", "m1 finish"]
|
||||||
|
|
||||||
|
def test_listattr_hookwrapper_ordering(self):
|
||||||
|
class P1:
|
||||||
|
@pytest.mark.hookwrapper
|
||||||
|
def m(self):
|
||||||
|
return 17
|
||||||
|
|
||||||
|
class P2:
|
||||||
|
def m(self):
|
||||||
|
return 23
|
||||||
|
|
||||||
|
class P3:
|
||||||
|
@pytest.mark.tryfirst
|
||||||
|
def m(self):
|
||||||
|
return 19
|
||||||
|
|
||||||
|
pluginmanager = PluginManager()
|
||||||
|
p1 = P1()
|
||||||
|
p2 = P2()
|
||||||
|
p3 = P3()
|
||||||
|
pluginmanager.register(p1)
|
||||||
|
pluginmanager.register(p2)
|
||||||
|
pluginmanager.register(p3)
|
||||||
|
methods = pluginmanager.listattr('m')
|
||||||
|
assert methods == [p2.m, p3.m, p1.m]
|
||||||
|
## listattr keeps a cache and deleting
|
||||||
|
## a function attribute requires clearing it
|
||||||
|
#pluginmanager._listattrcache.clear()
|
||||||
|
#del P1.m.__dict__['tryfirst']
|
||||||
|
|
||||||
|
def test_hookwrapper_not_yield(self):
|
||||||
|
def m1():
|
||||||
|
pass
|
||||||
|
m1.hookwrapper = True
|
||||||
|
|
||||||
|
mc = MultiCall([m1], {})
|
||||||
|
with pytest.raises(mc.WrongHookWrapper) as ex:
|
||||||
|
mc.execute()
|
||||||
|
assert ex.value.func == m1
|
||||||
|
assert ex.value.message
|
||||||
|
|
||||||
|
def test_hookwrapper_too_many_yield(self):
|
||||||
|
def m1():
|
||||||
|
yield 1
|
||||||
|
yield 2
|
||||||
|
m1.hookwrapper = True
|
||||||
|
|
||||||
|
mc = MultiCall([m1], {})
|
||||||
|
with pytest.raises(mc.WrongHookWrapper) as ex:
|
||||||
|
mc.execute()
|
||||||
|
assert ex.value.func == m1
|
||||||
|
assert ex.value.message
|
||||||
|
|
||||||
|
|
||||||
class TestHookRelay:
|
class TestHookRelay:
|
||||||
def test_happypath(self):
|
def test_happypath(self):
|
||||||
pm = PluginManager()
|
pm = PluginManager()
|
||||||
|
|
Loading…
Reference in New Issue