From 32f804b266e4c34c4149c5b75f91dc882e115060 Mon Sep 17 00:00:00 2001 From: Jason Haydaman Date: Tue, 22 May 2018 09:35:35 -0500 Subject: [PATCH 1/3] bpo-33238: Add InvalidStateError to concurrent.futures. Future.set_result and Future.set_exception now raise InvalidStateError if the futures are not pending or running. This mirrors the behavior of asyncio.Future, and prevents AssertionErrors in asyncio.wrap_future when set_result is called multiple times. --- Lib/asyncio/base_futures.py | 8 ++------ Lib/concurrent/futures/__init__.py | 1 + Lib/concurrent/futures/_base.py | 8 ++++++++ Lib/test/test_concurrent_futures.py | 20 ++++++++++++++++++++ 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/Lib/asyncio/base_futures.py b/Lib/asyncio/base_futures.py index 5182884e16d64b..bd65beec553cbc 100644 --- a/Lib/asyncio/base_futures.py +++ b/Lib/asyncio/base_futures.py @@ -1,17 +1,13 @@ __all__ = () -import concurrent.futures._base +import concurrent.futures import reprlib from . import format_helpers -Error = concurrent.futures._base.Error CancelledError = concurrent.futures.CancelledError TimeoutError = concurrent.futures.TimeoutError - - -class InvalidStateError(Error): - """The operation is not allowed in this state.""" +InvalidStateError = concurrent.futures.InvalidStateError # States for Future. diff --git a/Lib/concurrent/futures/__init__.py b/Lib/concurrent/futures/__init__.py index 8434fcf4b5ead4..d746aeac50a997 100644 --- a/Lib/concurrent/futures/__init__.py +++ b/Lib/concurrent/futures/__init__.py @@ -10,6 +10,7 @@ ALL_COMPLETED, CancelledError, TimeoutError, + InvalidStateError, BrokenExecutor, Future, Executor, diff --git a/Lib/concurrent/futures/_base.py b/Lib/concurrent/futures/_base.py index 4f22f7ee0e6d0a..f72d0560571abb 100644 --- a/Lib/concurrent/futures/_base.py +++ b/Lib/concurrent/futures/_base.py @@ -53,6 +53,10 @@ class TimeoutError(Error): """The operation exceeded the given deadline.""" pass +class InvalidStateError(Error): + """The operation is not allowed in this state.""" + pass + class _Waiter(object): """Provides the event that wait() and as_completed() block on.""" def __init__(self): @@ -513,6 +517,8 @@ def set_result(self, result): Should only be used by Executor implementations and unit tests. """ with self._condition: + if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED, FINISHED]: + raise InvalidStateError('{}: {!r}'.format(self._state, self)) self._result = result self._state = FINISHED for waiter in self._waiters: @@ -526,6 +532,8 @@ def set_exception(self, exception): Should only be used by Executor implementations and unit tests. """ with self._condition: + if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED, FINISHED]: + raise InvalidStateError('{}: {!r}'.format(self._state, self)) self._exception = exception self._state = FINISHED for waiter in self._waiters: diff --git a/Lib/test/test_concurrent_futures.py b/Lib/test/test_concurrent_futures.py index b258a0eafde6d4..375f5310edbb60 100644 --- a/Lib/test/test_concurrent_futures.py +++ b/Lib/test/test_concurrent_futures.py @@ -1206,6 +1206,26 @@ def notification(): self.assertTrue(isinstance(f1.exception(timeout=5), OSError)) t.join() + def test_multiple_set_result(self): + f = create_future(state=PENDING) + f.set_result(1) + + with self.assertRaises(futures.InvalidStateError): + f.set_result(2) + + self.assertTrue(f.done()) + self.assertEqual(f.result(), 1) + + def test_multiple_set_exception(self): + f = create_future(state=PENDING) + e = ValueError() + f.set_exception(e) + + with self.assertRaises(futures.InvalidStateError): + f.set_exception(Exception()) + + self.assertEqual(f.exception(), e) + @test.support.reap_threads def test_main(): From c3c7169277dc3f790f05a90490ac36cd14a6b6c3 Mon Sep 17 00:00:00 2001 From: Jason Haydaman Date: Thu, 24 May 2018 08:29:54 -0500 Subject: [PATCH 2/3] Use assertRaisesRegex in tests, and sets instead of lists. --- Lib/concurrent/futures/_base.py | 4 ++-- Lib/test/test_concurrent_futures.py | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Lib/concurrent/futures/_base.py b/Lib/concurrent/futures/_base.py index f72d0560571abb..d4416c62450e81 100644 --- a/Lib/concurrent/futures/_base.py +++ b/Lib/concurrent/futures/_base.py @@ -517,7 +517,7 @@ def set_result(self, result): Should only be used by Executor implementations and unit tests. """ with self._condition: - if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED, FINISHED]: + if self._state in {CANCELLED, CANCELLED_AND_NOTIFIED, FINISHED}: raise InvalidStateError('{}: {!r}'.format(self._state, self)) self._result = result self._state = FINISHED @@ -532,7 +532,7 @@ def set_exception(self, exception): Should only be used by Executor implementations and unit tests. """ with self._condition: - if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED, FINISHED]: + if self._state in {CANCELLED, CANCELLED_AND_NOTIFIED, FINISHED}: raise InvalidStateError('{}: {!r}'.format(self._state, self)) self._exception = exception self._state = FINISHED diff --git a/Lib/test/test_concurrent_futures.py b/Lib/test/test_concurrent_futures.py index 375f5310edbb60..f2c28ac12b1d09 100644 --- a/Lib/test/test_concurrent_futures.py +++ b/Lib/test/test_concurrent_futures.py @@ -1210,7 +1210,11 @@ def test_multiple_set_result(self): f = create_future(state=PENDING) f.set_result(1) - with self.assertRaises(futures.InvalidStateError): + with self.assertRaisesRegex( + futures.InvalidStateError, + 'FINISHED: ' + ): f.set_result(2) self.assertTrue(f.done()) @@ -1221,7 +1225,11 @@ def test_multiple_set_exception(self): e = ValueError() f.set_exception(e) - with self.assertRaises(futures.InvalidStateError): + with self.assertRaisesRegex( + futures.InvalidStateError, + 'FINISHED: ' + ): f.set_exception(Exception()) self.assertEqual(f.exception(), e) From 38f34ac755364b4f4185c73f4496b92222074ec0 Mon Sep 17 00:00:00 2001 From: Jason Haydaman Date: Thu, 24 May 2018 09:41:52 -0500 Subject: [PATCH 3/3] Add NEWS and update concurrent.futures Documentation. --- Doc/library/concurrent.futures.rst | 16 ++++++++++++++++ .../2018-05-24-09-15-52.bpo-33238.ooDfoo.rst | 4 ++++ 2 files changed, 20 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2018-05-24-09-15-52.bpo-33238.ooDfoo.rst diff --git a/Doc/library/concurrent.futures.rst b/Doc/library/concurrent.futures.rst index 707d24dc2529cc..6934acc7f88e27 100644 --- a/Doc/library/concurrent.futures.rst +++ b/Doc/library/concurrent.futures.rst @@ -380,6 +380,11 @@ The :class:`Future` class encapsulates the asynchronous execution of a callable. This method should only be used by :class:`Executor` implementations and unit tests. + .. versionchanged:: 3.8 + This method raises + :exc:`concurrent.futures.InvalidStateError` if the :class:`Future` is + already done. + .. method:: set_exception(exception) Sets the result of the work associated with the :class:`Future` to the @@ -388,6 +393,10 @@ The :class:`Future` class encapsulates the asynchronous execution of a callable. This method should only be used by :class:`Executor` implementations and unit tests. + .. versionchanged:: 3.8 + This method raises + :exc:`concurrent.futures.InvalidStateError` if the :class:`Future` is + already done. Module Functions ---------------- @@ -466,6 +475,13 @@ Exception classes .. versionadded:: 3.7 +.. exception:: InvalidStateError + + Raised when an operation is performed on a future that is not allowed + in the current state. + + .. versionadded:: 3.8 + .. currentmodule:: concurrent.futures.thread .. exception:: BrokenThreadPool diff --git a/Misc/NEWS.d/next/Library/2018-05-24-09-15-52.bpo-33238.ooDfoo.rst b/Misc/NEWS.d/next/Library/2018-05-24-09-15-52.bpo-33238.ooDfoo.rst new file mode 100644 index 00000000000000..b03ab106888961 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-05-24-09-15-52.bpo-33238.ooDfoo.rst @@ -0,0 +1,4 @@ +Add ``InvalidStateError`` to :mod:`concurrent.futures`. +``Future.set_result`` and ``Future.set_exception`` now raise +``InvalidStateError`` if the futures are not pending or running. Patch by +Jason Haydaman.