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 |
.../tests/util/futures/test_compat_coroutine.py | 57 ++++++++++++++ |
14 |
pym/portage/util/futures/compat_coroutine.py | 90 ++++++++++++++++++++++ |
15 |
2 files changed, 147 insertions(+) |
16 |
create mode 100644 pym/portage/tests/util/futures/test_compat_coroutine.py |
17 |
create mode 100644 pym/portage/util/futures/compat_coroutine.py |
18 |
|
19 |
diff --git a/pym/portage/tests/util/futures/test_compat_coroutine.py b/pym/portage/tests/util/futures/test_compat_coroutine.py |
20 |
new file mode 100644 |
21 |
index 0000000000..4a1d931b6b |
22 |
--- /dev/null |
23 |
+++ b/pym/portage/tests/util/futures/test_compat_coroutine.py |
24 |
@@ -0,0 +1,57 @@ |
25 |
+# Copyright 2018 Gentoo Foundation |
26 |
+# Distributed under the terms of the GNU General Public License v2 |
27 |
+ |
28 |
+from portage.util.futures import asyncio |
29 |
+from portage.util.futures.compat_coroutine import ( |
30 |
+ coroutine, |
31 |
+ coroutine_return, |
32 |
+) |
33 |
+from portage.tests import TestCase |
34 |
+ |
35 |
+ |
36 |
+class CompatCoroutineTestCase(TestCase): |
37 |
+ |
38 |
+ def test_returning_coroutine(self): |
39 |
+ @coroutine |
40 |
+ def returning_coroutine(): |
41 |
+ coroutine_return('success') |
42 |
+ yield None |
43 |
+ |
44 |
+ self.assertEqual('success', |
45 |
+ asyncio.get_event_loop().run_until_complete(returning_coroutine())) |
46 |
+ |
47 |
+ def test_raising_coroutine(self): |
48 |
+ |
49 |
+ class TestException(Exception): |
50 |
+ pass |
51 |
+ |
52 |
+ @coroutine |
53 |
+ def raising_coroutine(): |
54 |
+ raise TestException('exception') |
55 |
+ yield None |
56 |
+ |
57 |
+ self.assertRaises(TestException, |
58 |
+ asyncio.get_event_loop().run_until_complete, raising_coroutine()) |
59 |
+ |
60 |
+ def test_cancelled_coroutine(self): |
61 |
+ |
62 |
+ @coroutine |
63 |
+ def endlessly_sleeping_coroutine(loop=None): |
64 |
+ loop = asyncio._wrap_loop(loop) |
65 |
+ yield loop.create_future() |
66 |
+ |
67 |
+ loop = asyncio.get_event_loop() |
68 |
+ future = endlessly_sleeping_coroutine(loop=loop) |
69 |
+ loop.call_soon(future.cancel) |
70 |
+ |
71 |
+ self.assertRaises(asyncio.CancelledError, |
72 |
+ loop.run_until_complete, future) |
73 |
+ |
74 |
+ def test_sleeping_coroutine(self): |
75 |
+ @coroutine |
76 |
+ def sleeping_coroutine(): |
77 |
+ for i in range(3): |
78 |
+ x = yield asyncio.sleep(0, result=i) |
79 |
+ self.assertEqual(x, i) |
80 |
+ |
81 |
+ asyncio.get_event_loop().run_until_complete(sleeping_coroutine()) |
82 |
diff --git a/pym/portage/util/futures/compat_coroutine.py b/pym/portage/util/futures/compat_coroutine.py |
83 |
new file mode 100644 |
84 |
index 0000000000..eea0b2883e |
85 |
--- /dev/null |
86 |
+++ b/pym/portage/util/futures/compat_coroutine.py |
87 |
@@ -0,0 +1,90 @@ |
88 |
+# Copyright 2018 Gentoo Foundation |
89 |
+# Distributed under the terms of the GNU General Public License v2 |
90 |
+ |
91 |
+from portage.util.futures import asyncio |
92 |
+import functools |
93 |
+ |
94 |
+ |
95 |
+def coroutine(generator_func): |
96 |
+ """ |
97 |
+ A decorator for a generator function that behaves as coroutine function. |
98 |
+ The generator should yield a Future instance in order to wait for it, |
99 |
+ and the result becomes the result of the current yield-expression, |
100 |
+ via the PEP 342 generator send() method. |
101 |
+ |
102 |
+ The decorated function returns a Future which is done when the generator |
103 |
+ is exhausted. The generator can return a value via the coroutine_return |
104 |
+ function. |
105 |
+ """ |
106 |
+ return functools.partial(_generator_future, generator_func) |
107 |
+ |
108 |
+ |
109 |
+def coroutine_return(result=None): |
110 |
+ """ |
111 |
+ Return a result from the current coroutine. |
112 |
+ """ |
113 |
+ raise _CoroutineReturnValue(result) |
114 |
+ |
115 |
+ |
116 |
+def _generator_future(generator_func, *args, **kwargs): |
117 |
+ """ |
118 |
+ Call generator_func with the given arguments, and return a Future |
119 |
+ that is done when the resulting generation is exhausted. If is a |
120 |
+ keyword argument named 'loop' is given, then it is used instead of |
121 |
+ the default event loop. |
122 |
+ """ |
123 |
+ loop = asyncio._wrap_loop(kwargs.get('loop')) |
124 |
+ result = loop.create_future() |
125 |
+ _GeneratorTask(generator_func(*args, **kwargs), result, loop=loop) |
126 |
+ return result |
127 |
+ |
128 |
+ |
129 |
+class _CoroutineReturnValue(Exception): |
130 |
+ def __init__(self, result): |
131 |
+ self.result = result |
132 |
+ |
133 |
+ |
134 |
+class _GeneratorTask(object): |
135 |
+ """ |
136 |
+ Asynchronously executes the generator to completion, waiting for |
137 |
+ the result of each Future that it yields, and sending the result |
138 |
+ to the generator. |
139 |
+ """ |
140 |
+ def __init__(self, generator, result, loop): |
141 |
+ self._generator = generator |
142 |
+ self._result = result |
143 |
+ self._loop = loop |
144 |
+ result.add_done_callback(self._cancel_callback) |
145 |
+ self._next() |
146 |
+ |
147 |
+ def _cancel_callback(self, result): |
148 |
+ if result.cancelled(): |
149 |
+ self._generator.close() |
150 |
+ |
151 |
+ def _next(self, previous=None): |
152 |
+ if self._result.cancelled(): |
153 |
+ return |
154 |
+ try: |
155 |
+ if previous is None: |
156 |
+ future = next(self._generator) |
157 |
+ elif previous.cancelled(): |
158 |
+ self._generator.throw(asyncio.CancelledError()) |
159 |
+ future = next(self._generator) |
160 |
+ elif previous.exception() is None: |
161 |
+ future = self._generator.send(previous.result()) |
162 |
+ else: |
163 |
+ self._generator.throw(previous.exception()) |
164 |
+ future = next(self._generator) |
165 |
+ |
166 |
+ except _CoroutineReturnValue as e: |
167 |
+ if not self._result.cancelled(): |
168 |
+ self._result.set_result(e.result) |
169 |
+ except StopIteration: |
170 |
+ if not self._result.cancelled(): |
171 |
+ self._result.set_result(None) |
172 |
+ except Exception as e: |
173 |
+ if not self._result.cancelled(): |
174 |
+ self._result.set_exception(e) |
175 |
+ else: |
176 |
+ future = asyncio.ensure_future(future, loop=self._loop) |
177 |
+ future.add_done_callback(self._next) |
178 |
-- |
179 |
2.13.6 |