diff --git a/Doc/library/asyncio-exceptions.rst b/Doc/library/asyncio-exceptions.rst index 7ad9103ca3fdfc..9c9d6df5ab0d49 100644 --- a/Doc/library/asyncio-exceptions.rst +++ b/Doc/library/asyncio-exceptions.rst @@ -20,6 +20,10 @@ Exceptions This class was made an alias of :exc:`TimeoutError`. + .. versionchanged:: 3.14 + + This class was made a unique subclass of :exc:`TimeoutError`. + .. exception:: CancelledError diff --git a/Doc/library/concurrent.futures.rst b/Doc/library/concurrent.futures.rst index e3b24451188cc4..593d511abd4f7d 100644 --- a/Doc/library/concurrent.futures.rst +++ b/Doc/library/concurrent.futures.rst @@ -542,13 +542,17 @@ Exception classes .. exception:: TimeoutError - A deprecated alias of :exc:`TimeoutError`, + A near-alias of :exc:`TimeoutError`, raised when a future operation exceeds the given timeout. .. versionchanged:: 3.11 This class was made an alias of :exc:`TimeoutError`. + .. versionchanged:: 3.14 + + This class was made a unique subclass of :exc:`TimeoutError`. + .. exception:: BrokenExecutor diff --git a/Doc/library/multiprocessing.rst b/Doc/library/multiprocessing.rst index 9fa76c4ce59d00..6ac2ef70ffed53 100644 --- a/Doc/library/multiprocessing.rst +++ b/Doc/library/multiprocessing.rst @@ -726,6 +726,10 @@ The :mod:`multiprocessing` package mostly replicates the API of the Raised by methods with a timeout when the timeout expires. + .. versionchanged:: 3.14 + + This class now subclasses :exc:`TimeoutError` + Pipes and Queues ^^^^^^^^^^^^^^^^ diff --git a/Lib/asyncio/exceptions.py b/Lib/asyncio/exceptions.py index 5ece595aad6475..c10ace6aa5be06 100644 --- a/Lib/asyncio/exceptions.py +++ b/Lib/asyncio/exceptions.py @@ -11,7 +11,10 @@ class CancelledError(BaseException): """The Future or Task was cancelled.""" -TimeoutError = TimeoutError # make local alias for the standard exception +# GH-124308, BPO-32413: Catching TimeoutError should catch asyncio.TimeoutError, but +# not vice versa. +class TimeoutError(TimeoutError): + """Operation timed out.""" class InvalidStateError(Exception): diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index 2112dd4b99d17f..cf3dfb5a606dce 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -486,7 +486,7 @@ async def wait_for(fut, timeout): try: return fut.result() except exceptions.CancelledError as exc: - raise TimeoutError from exc + raise exceptions.TimeoutError from exc async with timeouts.timeout(timeout): return await fut diff --git a/Lib/asyncio/timeouts.py b/Lib/asyncio/timeouts.py index e6f5100691d362..863854449e8fbc 100644 --- a/Lib/asyncio/timeouts.py +++ b/Lib/asyncio/timeouts.py @@ -113,7 +113,7 @@ async def __aexit__( # Since there are no new cancel requests, we're # handling this. if issubclass(exc_type, exceptions.CancelledError): - raise TimeoutError from exc_val + raise exceptions.TimeoutError from exc_val elif exc_val is not None: self._insert_timeout_error(exc_val) if isinstance(exc_val, ExceptionGroup): @@ -135,7 +135,7 @@ def _on_timeout(self) -> None: def _insert_timeout_error(exc_val: BaseException) -> None: while exc_val.__context__ is not None: if isinstance(exc_val.__context__, exceptions.CancelledError): - te = TimeoutError() + te = exceptions.TimeoutError() te.__context__ = te.__cause__ = exc_val.__context__ exc_val.__context__ = te break diff --git a/Lib/concurrent/futures/_base.py b/Lib/concurrent/futures/_base.py index 707fcdfde79acd..c733ed6c4f1a82 100644 --- a/Lib/concurrent/futures/_base.py +++ b/Lib/concurrent/futures/_base.py @@ -42,7 +42,10 @@ class CancelledError(Error): """The Future was cancelled.""" pass -TimeoutError = TimeoutError # make local alias for the standard exception +# GH-124308, BPO-42413: Catching TimeoutError should catch futures.TimeoutError, but +# not vice versa. +class TimeoutError(TimeoutError): + pass class InvalidStateError(Error): """The operation is not allowed in this state.""" diff --git a/Lib/multiprocessing/context.py b/Lib/multiprocessing/context.py index ddcc7e7900999e..31d8727ee859b7 100644 --- a/Lib/multiprocessing/context.py +++ b/Lib/multiprocessing/context.py @@ -17,7 +17,7 @@ class ProcessError(Exception): class BufferTooShort(ProcessError): pass -class TimeoutError(ProcessError): +class TimeoutError(ProcessError, TimeoutError): pass class AuthenticationError(ProcessError): diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index 4b3a0645cfc84a..a30eac4bc253cf 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -25,6 +25,7 @@ import pickle import weakref import warnings +import contextlib import test.support import test.support.script_helper from test import support @@ -2605,6 +2606,15 @@ def test_async_timeout(self): get = TimingWrapper(res.get) self.assertRaises(multiprocessing.TimeoutError, get, timeout=TIMEOUT2) self.assertTimingAlmostEqual(get.elapsed, TIMEOUT2) + + # BPO-42413: Catching TimeoutError should catch multiprocessing.TimeoutError + with self.assertRaises(TimeoutError): + raise multiprocessing.TimeoutError + + # GH-124308: Catching multiprocessing.TimeoutError should not catch TimeoutError + with self.assertRaises(TimeoutError): + with contextlib.suppress(multiprocessing.TimeoutError): + raise TimeoutError finally: if event is not None: event.set() diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index a1013ab803348d..b33bcf6fc8d27f 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -3502,7 +3502,7 @@ def test_run_coroutine_threadsafe_with_timeout(self): when a timeout is raised.""" callback = lambda: self.target(timeout=0) future = self.loop.run_in_executor(None, callback) - with self.assertRaises(asyncio.TimeoutError): + with self.assertRaises(TimeoutError): self.loop.run_until_complete(future) test_utils.run_briefly(self.loop) # Check that there's no pending task (add has been cancelled) diff --git a/Lib/test/test_asyncio/test_timeouts.py b/Lib/test/test_asyncio/test_timeouts.py index f5543e191d07ff..da62ba4dc75183 100644 --- a/Lib/test/test_asyncio/test_timeouts.py +++ b/Lib/test/test_asyncio/test_timeouts.py @@ -5,6 +5,7 @@ import asyncio +from contextlib import suppress from test.test_asyncio.utils import await_without_task @@ -406,6 +407,17 @@ async def task(): self.assertIsNone(e3.__cause__) self.assertIs(e2.__context__, e3) + async def test_timeouterror_is_unique(self): + # BPO-42413: Catching TimeoutError should include asyncio.TimeoutError + with self.assertRaises(TimeoutError): + async with asyncio.timeout(0.01): + await asyncio.sleep(1) + + with self.assertRaises(TimeoutError): + # GH-124308: Catching asyncio.TimeoutError should not include TimeoutError + with suppress(asyncio.TimeoutError): + raise TimeoutError + if __name__ == '__main__': unittest.main() diff --git a/Lib/test/test_concurrent_futures/test_future.py b/Lib/test/test_concurrent_futures/test_future.py index 4066ea1ee4b367..179cc2d8ac235a 100644 --- a/Lib/test/test_concurrent_futures/test_future.py +++ b/Lib/test/test_concurrent_futures/test_future.py @@ -4,10 +4,11 @@ from concurrent import futures from concurrent.futures._base import ( PENDING, RUNNING, CANCELLED, CANCELLED_AND_NOTIFIED, FINISHED, Future) +from contextlib import suppress from test import support -from .util import ( +from test.test_concurrent_futures.util import ( PENDING_FUTURE, RUNNING_FUTURE, CANCELLED_FUTURE, CANCELLED_AND_NOTIFIED_FUTURE, EXCEPTION_FUTURE, SUCCESSFUL_FUTURE, BaseTestCase, create_future, setup_module) @@ -196,6 +197,15 @@ def test_result_with_timeout(self): self.assertRaises(OSError, EXCEPTION_FUTURE.result, timeout=0) self.assertEqual(SUCCESSFUL_FUTURE.result(timeout=0), 42) + # BPO-42413: Catching TimeoutError should catch futures.TimeoutError + with self.assertRaises(TimeoutError): + raise futures.TimeoutError + + with self.assertRaises(TimeoutError): + # GH-124308: Catching futures.TimeoutError should not catch TimeoutError + with suppress(futures.TimeoutError): + raise TimeoutError + def test_result_with_success(self): # TODO(brian@sweetapp.com): This test is timing dependent. def notification(): diff --git a/Misc/NEWS.d/next/Library/2024-09-22-16-05-21.gh-issue-124308.EPJqAB.rst b/Misc/NEWS.d/next/Library/2024-09-22-16-05-21.gh-issue-124308.EPJqAB.rst new file mode 100644 index 00000000000000..66e4b91accdfa3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-09-22-16-05-21.gh-issue-124308.EPJqAB.rst @@ -0,0 +1,2 @@ +Prevent :exc:`TimeoutError` from being caught when catching +:exc:`asyncio.TimeoutError` or :exc:`concurrent.futures.TimeoutError`.