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 | ||||
|         l = [] | ||||
|         last = [] | ||||
|         wrappers = [] | ||||
|         for plugin in plugins: | ||||
|             try: | ||||
|                 meth = getattr(plugin, attrname) | ||||
|                 if hasattr(meth, 'tryfirst'): | ||||
|             except AttributeError: | ||||
|                 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) | ||||
|             except AttributeError: | ||||
|                 continue | ||||
|         l.extend(last) | ||||
|         l.extend(wrappers) | ||||
|         self._listattrcache[key] = list(l) | ||||
|         return l | ||||
| 
 | ||||
|  | @ -272,6 +276,14 @@ def importplugin(importspec): | |||
| 
 | ||||
| class MultiCall: | ||||
|     """ 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): | ||||
|         self.methods = list(methods) | ||||
|         self.kwargs = kwargs | ||||
|  | @ -283,9 +295,22 @@ class MultiCall: | |||
|         return "<MultiCall %s, kwargs=%r>" %(status, self.kwargs) | ||||
| 
 | ||||
|     def execute(self): | ||||
|         next_finalizers = [] | ||||
|         try: | ||||
|             while self.methods: | ||||
|                 method = self.methods.pop() | ||||
|                 kwargs = self.getkwargs(method) | ||||
|                 if hasattr(method, "hookwrapper"): | ||||
|                     it = method(**kwargs) | ||||
|                     next = getattr(it, "next", None) | ||||
|                     if next is None: | ||||
|                         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) | ||||
|  | @ -293,6 +318,16 @@ class MultiCall: | |||
|                         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): | ||||
|         kwargs = {} | ||||
|  |  | |||
|  | @ -523,6 +523,95 @@ class TestMultiCall: | |||
|         res = MultiCall([m1, m2], {}).execute() | ||||
|         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: | ||||
|     def test_happypath(self): | ||||
|         pm = PluginManager() | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue