1 |
For readability, it's desirable to make asynchronous code use |
2 |
coroutines to avoid callbacks when possible. For python2 compatibility, |
3 |
generators that yield Futures can be used to implement coroutines. |
4 |
|
5 |
Add a compat_coroutine module which provides a @coroutine decorator |
6 |
and a coroutine_return function that can be used to return a value |
7 |
from a generator. The decorated function returns a Future which is |
8 |
done when the generator is exhausted. Usage is very similar to asyncio |
9 |
coroutine usage in python3.4 (see unit tests). |
10 |
|
11 |
Bug: https://bugs.gentoo.org/660426 |
12 |
--- |
13 |
[PATCH v2] fixed to support decoration of object methods, and added |
14 |
a unit test using this support to demonstrate interaction between |
15 |
multiple coroutines |
16 |
|
17 |
.../tests/util/futures/test_compat_coroutine.py | 122 +++++++++++++++++++++ |
18 |
pym/portage/util/futures/compat_coroutine.py | 96 ++++++++++++++++ |
19 |
2 files changed, 218 insertions(+) |
20 |
create mode 100644 pym/portage/tests/util/futures/test_compat_coroutine.py |
21 |
create mode 100644 pym/portage/util/futures/compat_coroutine.py |
22 |
|
23 |
diff --git a/pym/portage/tests/util/futures/test_compat_coroutine.py b/pym/portage/tests/util/futures/test_compat_coroutine.py |
24 |
new file mode 100644 |
25 |
index 0000000000..f9de409ae4 |
26 |
--- /dev/null |
27 |
+++ b/pym/portage/tests/util/futures/test_compat_coroutine.py |
28 |
@@ -0,0 +1,122 @@ |
29 |
+# Copyright 2018 Gentoo Foundation |
30 |
+# Distributed under the terms of the GNU General Public License v2 |
31 |
+ |
32 |
+from portage.util.futures import asyncio |
33 |
+from portage.util.futures.compat_coroutine import ( |
34 |
+ coroutine, |
35 |
+ coroutine_return, |
36 |
+) |
37 |
+from portage.tests import TestCase |
38 |
+ |
39 |
+ |
40 |
+class CompatCoroutineTestCase(TestCase): |
41 |
+ |
42 |
+ def test_returning_coroutine(self): |
43 |
+ @coroutine |
44 |
+ def returning_coroutine(): |
45 |
+ coroutine_return('success') |
46 |
+ yield None |
47 |
+ |
48 |
+ self.assertEqual('success', |
49 |
+ asyncio.get_event_loop().run_until_complete(returning_coroutine())) |
50 |
+ |
51 |
+ def test_raising_coroutine(self): |
52 |
+ |
53 |
+ class TestException(Exception): |
54 |
+ pass |
55 |
+ |
56 |
+ @coroutine |
57 |
+ def raising_coroutine(): |
58 |
+ raise TestException('exception') |
59 |
+ yield None |
60 |
+ |
61 |
+ self.assertRaises(TestException, |
62 |
+ asyncio.get_event_loop().run_until_complete, raising_coroutine()) |
63 |
+ |
64 |
+ def test_cancelled_coroutine(self): |
65 |
+ |
66 |
+ @coroutine |
67 |
+ def endlessly_sleeping_coroutine(loop=None): |
68 |
+ loop = asyncio._wrap_loop(loop) |
69 |
+ yield loop.create_future() |
70 |
+ |
71 |
+ loop = asyncio.get_event_loop() |
72 |
+ future = endlessly_sleeping_coroutine(loop=loop) |
73 |
+ loop.call_soon(future.cancel) |
74 |
+ |
75 |
+ self.assertRaises(asyncio.CancelledError, |
76 |
+ loop.run_until_complete, future) |
77 |
+ |
78 |
+ def test_sleeping_coroutine(self): |
79 |
+ @coroutine |
80 |
+ def sleeping_coroutine(): |
81 |
+ for i in range(3): |
82 |
+ x = yield asyncio.sleep(0, result=i) |
83 |
+ self.assertEqual(x, i) |
84 |
+ |
85 |
+ asyncio.get_event_loop().run_until_complete(sleeping_coroutine()) |
86 |
+ |
87 |
+ def test_method_coroutine(self): |
88 |
+ |
89 |
+ class Cubby(object): |
90 |
+ |
91 |
+ _empty = object() |
92 |
+ |
93 |
+ def __init__(self, loop): |
94 |
+ self._loop = loop |
95 |
+ self._value = self._empty |
96 |
+ self._waiters = [] |
97 |
+ |
98 |
+ def _notify(self): |
99 |
+ waiters = self._waiters |
100 |
+ self._waiters = [] |
101 |
+ for waiter in waiters: |
102 |
+ waiter.set_result(None) |
103 |
+ |
104 |
+ def _wait(self): |
105 |
+ waiter = self._loop.create_future() |
106 |
+ self._waiters.append(waiter) |
107 |
+ return waiter |
108 |
+ |
109 |
+ @coroutine |
110 |
+ def read(self): |
111 |
+ while self._value is self._empty: |
112 |
+ yield self._wait() |
113 |
+ |
114 |
+ value = self._value |
115 |
+ self._value = self._empty |
116 |
+ self._notify() |
117 |
+ coroutine_return(value) |
118 |
+ |
119 |
+ @coroutine |
120 |
+ def write(self, value): |
121 |
+ while self._value is not self._empty: |
122 |
+ yield self._wait() |
123 |
+ |
124 |
+ self._value = value |
125 |
+ self._notify() |
126 |
+ |
127 |
+ @coroutine |
128 |
+ def writer_coroutine(cubby, values, sentinel): |
129 |
+ for value in values: |
130 |
+ yield cubby.write(value) |
131 |
+ yield cubby.write(sentinel) |
132 |
+ |
133 |
+ @coroutine |
134 |
+ def reader_coroutine(cubby, sentinel): |
135 |
+ results = [] |
136 |
+ while True: |
137 |
+ result = yield cubby.read() |
138 |
+ if result == sentinel: |
139 |
+ break |
140 |
+ results.append(result) |
141 |
+ coroutine_return(results) |
142 |
+ |
143 |
+ loop = asyncio.get_event_loop() |
144 |
+ cubby = Cubby(loop) |
145 |
+ values = list(range(3)) |
146 |
+ writer = asyncio.ensure_future(writer_coroutine(cubby, values, None), loop=loop) |
147 |
+ reader = asyncio.ensure_future(reader_coroutine(cubby, None), loop=loop) |
148 |
+ loop.run_until_complete(asyncio.wait([writer, reader])) |
149 |
+ |
150 |
+ self.assertEqual(reader.result(), values) |
151 |
diff --git a/pym/portage/util/futures/compat_coroutine.py b/pym/portage/util/futures/compat_coroutine.py |
152 |
new file mode 100644 |
153 |
index 0000000000..32909f4b4c |
154 |
--- /dev/null |
155 |
+++ b/pym/portage/util/futures/compat_coroutine.py |
156 |
@@ -0,0 +1,96 @@ |
157 |
+# Copyright 2018 Gentoo Foundation |
158 |
+# Distributed under the terms of the GNU General Public License v2 |
159 |
+ |
160 |
+from portage.util.futures import asyncio |
161 |
+import functools |
162 |
+ |
163 |
+ |
164 |
+def coroutine(generator_func): |
165 |
+ """ |
166 |
+ A decorator for a generator function that behaves as coroutine function. |
167 |
+ The generator should yield a Future instance in order to wait for it, |
168 |
+ and the result becomes the result of the current yield-expression, |
169 |
+ via the PEP 342 generator send() method. |
170 |
+ |
171 |
+ The decorated function returns a Future which is done when the generator |
172 |
+ is exhausted. The generator can return a value via the coroutine_return |
173 |
+ function. |
174 |
+ """ |
175 |
+ # Note that functools.partial does not work for decoration of |
176 |
+ # methods, since it doesn't implement the descriptor protocol. |
177 |
+ # This problem is solve by defining a wrapper function. |
178 |
+ @functools.wraps(generator_func) |
179 |
+ def wrapped(*args, **kwargs): |
180 |
+ return _generator_future(generator_func, *args, **kwargs) |
181 |
+ return wrapped |
182 |
+ |
183 |
+ |
184 |
+def coroutine_return(result=None): |
185 |
+ """ |
186 |
+ Return a result from the current coroutine. |
187 |
+ """ |
188 |
+ raise _CoroutineReturnValue(result) |
189 |
+ |
190 |
+ |
191 |
+def _generator_future(generator_func, *args, **kwargs): |
192 |
+ """ |
193 |
+ Call generator_func with the given arguments, and return a Future |
194 |
+ that is done when the resulting generation is exhausted. If is a |
195 |
+ keyword argument named 'loop' is given, then it is used instead of |
196 |
+ the default event loop. |
197 |
+ """ |
198 |
+ loop = asyncio._wrap_loop(kwargs.get('loop')) |
199 |
+ result = loop.create_future() |
200 |
+ _GeneratorTask(generator_func(*args, **kwargs), result, loop=loop) |
201 |
+ return result |
202 |
+ |
203 |
+ |
204 |
+class _CoroutineReturnValue(Exception): |
205 |
+ def __init__(self, result): |
206 |
+ self.result = result |
207 |
+ |
208 |
+ |
209 |
+class _GeneratorTask(object): |
210 |
+ """ |
211 |
+ Asynchronously executes the generator to completion, waiting for |
212 |
+ the result of each Future that it yields, and sending the result |
213 |
+ to the generator. |
214 |
+ """ |
215 |
+ def __init__(self, generator, result, loop): |
216 |
+ self._generator = generator |
217 |
+ self._result = result |
218 |
+ self._loop = loop |
219 |
+ result.add_done_callback(self._cancel_callback) |
220 |
+ loop.call_soon(self._next) |
221 |
+ |
222 |
+ def _cancel_callback(self, result): |
223 |
+ if result.cancelled(): |
224 |
+ self._generator.close() |
225 |
+ |
226 |
+ def _next(self, previous=None): |
227 |
+ if self._result.cancelled(): |
228 |
+ return |
229 |
+ try: |
230 |
+ if previous is None: |
231 |
+ future = next(self._generator) |
232 |
+ elif previous.cancelled(): |
233 |
+ self._generator.throw(asyncio.CancelledError()) |
234 |
+ future = next(self._generator) |
235 |
+ elif previous.exception() is None: |
236 |
+ future = self._generator.send(previous.result()) |
237 |
+ else: |
238 |
+ self._generator.throw(previous.exception()) |
239 |
+ future = next(self._generator) |
240 |
+ |
241 |
+ except _CoroutineReturnValue as e: |
242 |
+ if not self._result.cancelled(): |
243 |
+ self._result.set_result(e.result) |
244 |
+ except StopIteration: |
245 |
+ if not self._result.cancelled(): |
246 |
+ self._result.set_result(None) |
247 |
+ except Exception as e: |
248 |
+ if not self._result.cancelled(): |
249 |
+ self._result.set_exception(e) |
250 |
+ else: |
251 |
+ future = asyncio.ensure_future(future, loop=self._loop) |
252 |
+ future.add_done_callback(self._next) |
253 |
-- |
254 |
2.13.6 |