Gentoo Archives: gentoo-portage-dev

From: Brian Dolbec <dolsen@g.o>
To: gentoo-portage-dev@l.g.o
Subject: Re: [gentoo-portage-dev] [PATCH] Future: implement add_done_callback for asyncio compat (bug 591760)
Date: Sun, 26 Mar 2017 19:08:05
Message-Id: 20170326120758.66fbc67f.dolsen@gentoo.org
In Reply to: [gentoo-portage-dev] [PATCH] Future: implement add_done_callback for asyncio compat (bug 591760) by Zac Medico
1 On Sun, 26 Mar 2017 03:13:11 -0700
2 Zac Medico <zmedico@g.o> wrote:
3
4 > Implement the add_done_callback and remove_done_callback methods,
5 > since they are required in order to make further progress toward
6 > asyncio compatibility.
7 >
8 > Also implement the AbstractEventLoop create_future method for the
9 > EventLoop class, so that it returns an instance of _EventLoopFuture.
10 > EventLoop currently does not implement some of the
11 > asyncio.AbstractEventLoop methods that asyncio.Future requires for
12 > its add_done_callback implementation, and the create_future method
13 > conveniently solves this problem.
14 >
15 > X-Gentoo-bug: 591760
16 > X-Gentoo-bug-url: https://bugs.gentoo.org/show_bug.cgi?id=591760
17 > ---
18 > pym/portage/tests/ebuild/test_ipc_daemon.py | 3 +-
19 > .../tests/util/eventloop/test_call_soon_fifo.py | 6 +-
20 > pym/portage/tests/util/futures/__init__.py | 0
21 > pym/portage/tests/util/futures/__test__.py | 0
22 > .../tests/util/futures/test_done_callback.py | 35 +++++++++
23 > pym/portage/util/_async/SchedulerInterface.py | 3 +-
24 > pym/portage/util/_eventloop/EventLoop.py | 14 ++++
25 > pym/portage/util/futures/futures.py | 82
26 > ++++++++++++++++++++-- 8 files changed, 132 insertions(+), 11
27 > deletions(-) create mode 100644
28 > pym/portage/tests/util/futures/__init__.py create mode 100644
29 > pym/portage/tests/util/futures/__test__.py create mode 100644
30 > pym/portage/tests/util/futures/test_done_callback.py
31 >
32 > diff --git a/pym/portage/tests/ebuild/test_ipc_daemon.py
33 > b/pym/portage/tests/ebuild/test_ipc_daemon.py index 68f139a..fc79165
34 > 100644 --- a/pym/portage/tests/ebuild/test_ipc_daemon.py
35 > +++ b/pym/portage/tests/ebuild/test_ipc_daemon.py
36 > @@ -16,7 +16,6 @@ from portage.util import ensure_dirs
37 > from portage.util._async.ForkProcess import ForkProcess
38 > from portage.util._async.TaskScheduler import TaskScheduler
39 > from portage.util._eventloop.global_event_loop import
40 > global_event_loop -from portage.util.futures.futures import Future
41 > from _emerge.SpawnProcess import SpawnProcess
42 > from _emerge.EbuildBuildDir import EbuildBuildDir
43 > from _emerge.EbuildIpcDaemon import EbuildIpcDaemon
44 > @@ -150,7 +149,7 @@ class IpcDaemonTestCase(TestCase):
45 > self._run_done.set_result(True)
46 >
47 > def _run(self, event_loop, task_scheduler, timeout):
48 > - self._run_done = Future()
49 > + self._run_done = event_loop.create_future()
50 > timeout_id = event_loop.timeout_add(timeout,
51 > self._timeout_callback, task_scheduler)
52 > task_scheduler.addExitListener(self._exit_callback)
53 > diff --git a/pym/portage/tests/util/eventloop/test_call_soon_fifo.py
54 > b/pym/portage/tests/util/eventloop/test_call_soon_fifo.py index
55 > 5ecc13f..f970c67 100644 ---
56 > a/pym/portage/tests/util/eventloop/test_call_soon_fifo.py +++
57 > b/pym/portage/tests/util/eventloop/test_call_soon_fifo.py @@ -7,22
58 > +7,22 @@ import random from portage import os
59 > from portage.tests import TestCase
60 > from portage.util._eventloop.global_event_loop import
61 > global_event_loop -from portage.util.futures.futures import Future
62 > +
63 >
64 > class CallSoonFifoTestCase(TestCase):
65 >
66 > def testCallSoonFifo(self):
67 >
68 > + event_loop = global_event_loop()
69 > inputs = [random.random() for index in range(10)]
70 > outputs = []
71 > - finished = Future()
72 > + finished = event_loop.create_future()
73 >
74 > def add_output(value):
75 > outputs.append(value)
76 > if len(outputs) == len(inputs):
77 > finished.set_result(True)
78 >
79 > - event_loop = global_event_loop()
80 > for value in inputs:
81 > event_loop.call_soon(functools.partial(add_output,
82 > value))
83 > diff --git a/pym/portage/tests/util/futures/__init__.py
84 > b/pym/portage/tests/util/futures/__init__.py new file mode 100644
85 > index 0000000..e69de29
86 > diff --git a/pym/portage/tests/util/futures/__test__.py
87 > b/pym/portage/tests/util/futures/__test__.py new file mode 100644
88 > index 0000000..e69de29
89 > diff --git a/pym/portage/tests/util/futures/test_done_callback.py
90 > b/pym/portage/tests/util/futures/test_done_callback.py new file mode
91 > 100644 index 0000000..76b727b
92 > --- /dev/null
93 > +++ b/pym/portage/tests/util/futures/test_done_callback.py
94 > @@ -0,0 +1,35 @@
95 > +# Copyright 2017 Gentoo Foundation
96 > +# Distributed under the terms of the GNU General Public License v2
97 > +
98 > +from portage.tests import TestCase
99 > +from portage.util._eventloop.global_event_loop import
100 > global_event_loop +
101 > +
102 > +class FutureDoneCallbackTestCase(TestCase):
103 > +
104 > + def testFutureDoneCallback(self):
105 > +
106 > + event_loop = global_event_loop()
107 > +
108 > + def done_callback(finished):
109 > + done_callback_called.set_result(True)
110 > +
111 > + done_callback_called = event_loop.create_future()
112 > + finished = event_loop.create_future()
113 > + finished.add_done_callback(done_callback)
114 > + event_loop.call_soon(finished.set_result, True)
115 > + event_loop.run_until_complete(done_callback_called)
116 > +
117 > + def done_callback2(finished):
118 > + done_callback2_called.set_result(True)
119 > +
120 > + done_callback_called = event_loop.create_future()
121 > + done_callback2_called = event_loop.create_future()
122 > + finished = event_loop.create_future()
123 > + finished.add_done_callback(done_callback)
124 > + finished.add_done_callback(done_callback2)
125 > + finished.remove_done_callback(done_callback)
126 > + event_loop.call_soon(finished.set_result, True)
127 > + event_loop.run_until_complete(done_callback2_called)
128 > +
129 > + self.assertFalse(done_callback_called.done())
130 > diff --git a/pym/portage/util/_async/SchedulerInterface.py
131 > b/pym/portage/util/_async/SchedulerInterface.py index
132 > 6028fd9..21420ae 100644 ---
133 > a/pym/portage/util/_async/SchedulerInterface.py +++
134 > b/pym/portage/util/_async/SchedulerInterface.py @@ -13,7 +13,8 @@
135 > class SchedulerInterface(SlotObject):
136 > _event_loop_attrs = ("IO_ERR", "IO_HUP", "IO_IN",
137 > "IO_NVAL", "IO_OUT", "IO_PRI",
138 > - "call_soon", "call_soon_threadsafe",
139 > "child_watch_add",
140 > + "call_soon", "call_soon_threadsafe",
141 > + "child_watch_add", "create_future",
142 > "idle_add", "io_add_watch", "iteration",
143 > "run_until_complete", "source_remove", "timeout_add")
144 >
145 > diff --git a/pym/portage/util/_eventloop/EventLoop.py
146 > b/pym/portage/util/_eventloop/EventLoop.py index 308157b..712838e
147 > 100644 --- a/pym/portage/util/_eventloop/EventLoop.py
148 > +++ b/pym/portage/util/_eventloop/EventLoop.py
149 > @@ -22,6 +22,11 @@ try:
150 > except ImportError:
151 > import dummy_threading as threading
152 >
153 > +import portage
154 > +portage.proxy.lazyimport.lazyimport(globals(),
155 > + 'portage.util.futures.futures:_EventLoopFuture',
156 > +)
157 > +
158 > from portage import OrderedDict
159 > from portage.util import writemsg_level
160 > from ..SlotObject import SlotObject
161 > @@ -157,6 +162,15 @@ class EventLoop(object):
162 > self._sigchld_src_id = None
163 > self._pid = os.getpid()
164 >
165 > + def create_future(self):
166 > + """
167 > + Create a Future object attached to the loop. This
168 > returns
169 > + an instance of _EventLoopFuture, because EventLoop
170 > is currently
171 > + missing some of the asyncio.AbstractEventLoop
172 > methods that
173 > + asyncio.Future requires.
174 > + """
175 > + return _EventLoopFuture(loop=self)
176 > +
177 > def _new_source_id(self):
178 > """
179 > Generate a new source id. This method is thread-safe.
180 > diff --git a/pym/portage/util/futures/futures.py
181 > b/pym/portage/util/futures/futures.py index c648f10..dd913a1 100644
182 > --- a/pym/portage/util/futures/futures.py
183 > +++ b/pym/portage/util/futures/futures.py
184 > @@ -23,10 +23,6 @@ except ImportError:
185 >
186 > from portage.exception import PortageException
187 >
188 > - _PENDING = 'PENDING'
189 > - _CANCELLED = 'CANCELLED'
190 > - _FINISHED = 'FINISHED'
191 > -
192 > class Error(PortageException):
193 > pass
194 >
195 > @@ -37,12 +33,40 @@ except ImportError:
196 > class InvalidStateError(Error):
197 > pass
198 >
199 > - class Future(object):
200 > + Future = None
201 > +
202 > +from portage.util._eventloop.global_event_loop import
203 > global_event_loop +
204 > +_PENDING = 'PENDING'
205 > +_CANCELLED = 'CANCELLED'
206 > +_FINISHED = 'FINISHED'
207 > +
208 > +class _EventLoopFuture(object):
209 > + """
210 > + This class provides (a subset of) the asyncio.Future
211 > interface, for
212 > + use with the EventLoop class, because EventLoop is
213 > currently
214 > + missing some of the asyncio.AbstractEventLoop
215 > methods that
216 > + asyncio.Future requires.
217 > + """
218 >
219 > # Class variables serving as defaults for instance
220 > variables. _state = _PENDING
221 > _result = None
222 > _exception = None
223 > + _loop = None
224 > +
225 > + def __init__(self, loop=None):
226 > + """Initialize the future.
227 > +
228 > + The optional loop argument allows explicitly
229 > setting the event
230 > + loop object used by the future. If it's not
231 > provided, the future uses
232 > + the default event loop.
233 > + """
234 > + if loop is None:
235 > + self._loop = global_event_loop()
236 > + else:
237 > + self._loop = loop
238 > + self._callbacks = []
239 >
240 > def cancel(self):
241 > """Cancel the future and schedule callbacks.
242 > @@ -54,8 +78,27 @@ except ImportError:
243 > if self._state != _PENDING:
244 > return False
245 > self._state = _CANCELLED
246 > + self._schedule_callbacks()
247 > return True
248 >
249 > + def _schedule_callbacks(self):
250 > + """Internal: Ask the event loop to call all
251 > callbacks. +
252 > + The callbacks are scheduled to be called as
253 > soon as possible. Also
254 > + clears the callback list.
255 > + """
256 > + callbacks = self._callbacks[:]
257 > + if not callbacks:
258 > + return
259 > +
260 > + self._callbacks[:] = []
261 > + for callback in callbacks:
262 > + self._loop.call_soon(callback, self)
263 > +
264 > + def cancelled(self):
265 > + """Return True if the future was
266 > cancelled."""
267 > + return self._state == _CANCELLED
268 > +
269 > def done(self):
270 > """Return True if the future is done.
271 >
272 > @@ -93,6 +136,29 @@ except ImportError:
273 > raise InvalidStateError('Exception
274 > is not set.') return self._exception
275 >
276 > + def add_done_callback(self, fn):
277 > + """Add a callback to be run when the future
278 > becomes done. +
279 > + The callback is called with a single
280 > argument - the future object. If
281 > + the future is already done when this is
282 > called, the callback is
283 > + scheduled with call_soon.
284 > + """
285 > + if self._state != _PENDING:
286 > + self._loop.call_soon(fn, self)
287 > + else:
288 > + self._callbacks.append(fn)
289 > +
290 > + def remove_done_callback(self, fn):
291 > + """Remove all instances of a callback from
292 > the "call when done" list. +
293 > + Returns the number of callbacks removed.
294 > + """
295 > + filtered_callbacks = [f for f in
296 > self._callbacks if f != fn]
297 > + removed_count = len(self._callbacks) -
298 > len(filtered_callbacks)
299 > + if removed_count:
300 > + self._callbacks[:] =
301 > filtered_callbacks
302 > + return removed_count
303 > +
304 > def set_result(self, result):
305 > """Mark the future done and set its result.
306 >
307 > @@ -103,6 +169,7 @@ except ImportError:
308 > raise InvalidStateError('{}:
309 > {!r}'.format(self._state, self)) self._result = result
310 > self._state = _FINISHED
311 > + self._schedule_callbacks()
312 >
313 > def set_exception(self, exception):
314 > """Mark the future done and set an exception.
315 > @@ -116,3 +183,8 @@ except ImportError:
316 > exception = exception()
317 > self._exception = exception
318 > self._state = _FINISHED
319 > + self._schedule_callbacks()
320 > +
321 > +
322 > +if Future is None:
323 > + Future = _EventLoopFuture
324
325 looks fine... /me ignoring the lack of parameters descriptions in the
326 docstrings
327
328 --
329 Brian Dolbec <dolsen>

Replies