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] Add python2 compatible coroutine support (bug 660426)
Date: Thu, 05 Jul 2018 05:27:59
Message-Id: 20180705052426.12135-1-zmedico@gentoo.org
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

Replies