Skip to content

Commit 28ea359

Browse files
committed
utils/concurrent: Handle an empty futures list better (FutureSetWatcher)
That is, ensure a `doneAll` signal is always emitted
1 parent 1e90ad0 commit 28ea359

File tree

2 files changed

+34
-1
lines changed

2 files changed

+34
-1
lines changed

Orange/widgets/utils/concurrent.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -570,10 +570,11 @@ class FutureSetWatcher(QObject):
570570
doneAll = Signal()
571571

572572
def __init__(self, futures=None, *args, **kwargs):
573+
# type: (List[Future], ...) -> None
573574
super().__init__(*args, **kwargs)
574575
self.__futures = None
575576
self.__countdone = 0
576-
if futures:
577+
if futures is not None:
577578
self.setFutures(futures)
578579

579580
def setFutures(self, futures):
@@ -608,6 +609,9 @@ def on_done(index, f):
608609
pass
609610

610611
future.add_done_callback(partial(on_done, i))
612+
if not self.__futures:
613+
# `futures` was an empty sequence.
614+
methodinvoke(self, "doneAll", ())()
611615

612616
@Slot(int, Future)
613617
def __emitpending(self, index, future):
@@ -644,6 +648,10 @@ def flush(self):
644648
assert QThread.currentThread() is self.thread()
645649
QCoreApplication.sendPostedEvents(self, QEvent.MetaCall)
646650

651+
def wait(self):
652+
assert self.__futures is not None
653+
concurrent.futures.wait(self.__futures)
654+
647655

648656
class methodinvoke(object):
649657
"""

Orange/widgets/utils/tests/test_concurrent.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,31 @@ def as_set(seq):
201201
self.assertSetEqual(as_set(spy.resultAt), {(0, True)})
202202
self.assertSetEqual(as_set(spy.exceptionAt), set())
203203

204+
# doneAll must always be emitted after the doneAt signal.
205+
executor = ThreadPoolExecutor(max_workers=2)
206+
futures = [executor.submit(pow, 1000, 1000) for _ in range(100)]
207+
watcher = FutureSetWatcher(futures)
208+
emithistory = []
209+
watcher.doneAt.connect(lambda i, f: emithistory.append(("doneAt", i, f)))
210+
watcher.doneAll.connect(lambda: emithistory.append(("doneAll", )))
211+
212+
spy = spies(watcher)
213+
watcher.wait()
214+
self.assertEqual(len(spy.doneAll), 0)
215+
self.assertEqual(len(spy.doneAt), 0)
216+
watcher.flush()
217+
self.assertEqual(len(spy.doneAt), 100)
218+
self.assertEqual(list(spy.doneAll), [[]])
219+
self.assertSetEqual(set(emithistory[:-1]),
220+
{("doneAt", i, f) for i, f in enumerate(futures)})
221+
self.assertEqual(emithistory[-1], ("doneAll",))
222+
223+
# doneAll must be emitted even when on an empty futures list
224+
watcher = FutureSetWatcher()
225+
watcher.setFutures([])
226+
spy = spies(watcher)
227+
self.assertTrue(spy.doneAll.wait())
228+
204229

205230
class TestTask(CoreAppTestCase):
206231

0 commit comments

Comments
 (0)