Gentoo Archives: gentoo-portage-dev

From: Zac Medico <zmedico@g.o>
To: gentoo-portage-dev@l.g.o
Cc: Zac Medico <zmedico@g.o>
Subject: [gentoo-portage-dev] [PATCH v2] Add python2 compatible coroutine support (bug 660426)
Date: Sun, 08 Jul 2018 04:33:45
Message-Id: 20180708042903.27951-1-zmedico@gentoo.org
In Reply to: [gentoo-portage-dev] [PATCH] Add python2 compatible coroutine support (bug 660426) by Zac Medico
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