diff --git a/Lib/asyncio/futures.py b/Lib/asyncio/futures.py index c95fce035cd548..5f6fa2348726cf 100644 --- a/Lib/asyncio/futures.py +++ b/Lib/asyncio/futures.py @@ -190,7 +190,8 @@ def result(self): the future is done and has an exception set, this exception is raised. """ if self._state == _CANCELLED: - raise self._make_cancelled_error() + exc = self._make_cancelled_error() + raise exc if self._state != _FINISHED: raise exceptions.InvalidStateError('Result is not ready.') self.__log_traceback = False @@ -207,7 +208,8 @@ def exception(self): InvalidStateError. """ if self._state == _CANCELLED: - raise self._make_cancelled_error() + exc = self._make_cancelled_error() + raise exc if self._state != _FINISHED: raise exceptions.InvalidStateError('Exception is not set.') self.__log_traceback = False diff --git a/Lib/asyncio/taskgroups.py b/Lib/asyncio/taskgroups.py index 9fa772ca9d02cc..f2ee9648c43876 100644 --- a/Lib/asyncio/taskgroups.py +++ b/Lib/asyncio/taskgroups.py @@ -66,20 +66,6 @@ async def __aenter__(self): return self async def __aexit__(self, et, exc, tb): - tb = None - try: - return await self._aexit(et, exc) - finally: - # Exceptions are heavy objects that can have object - # cycles (bad for GC); let's not keep a reference to - # a bunch of them. It would be nicer to use a try/finally - # in __aexit__ directly but that introduced some diff noise - self._parent_task = None - self._errors = None - self._base_error = None - exc = None - - async def _aexit(self, et, exc): self._exiting = True if (exc is not None and @@ -136,10 +122,7 @@ async def _aexit(self, et, exc): assert not self._tasks if self._base_error is not None: - try: - raise self._base_error - finally: - exc = None + raise self._base_error if self._parent_cancel_requested: # If this flag is set we *must* call uncancel(). @@ -150,14 +133,8 @@ async def _aexit(self, et, exc): # Propagate CancelledError if there is one, except if there # are other errors -- those have priority. - try: - if propagate_cancellation_error is not None and not self._errors: - try: - raise propagate_cancellation_error - finally: - exc = None - finally: - propagate_cancellation_error = None + if propagate_cancellation_error is not None and not self._errors: + raise propagate_cancellation_error if et is not None and not issubclass(et, exceptions.CancelledError): self._errors.append(exc) @@ -169,14 +146,14 @@ async def _aexit(self, et, exc): if self._parent_task.cancelling(): self._parent_task.uncancel() self._parent_task.cancel() + # Exceptions are heavy objects that can have object + # cycles (bad for GC); let's not keep a reference to + # a bunch of them. try: - raise BaseExceptionGroup( - 'unhandled errors in a TaskGroup', - self._errors, - ) from None + me = BaseExceptionGroup('unhandled errors in a TaskGroup', self._errors) + raise me from None finally: - exc = None - + self._errors = None def create_task(self, coro, *, name=None, context=None): """Create a new task in this group and return it. diff --git a/Lib/test/test_asyncio/test_futures.py b/Lib/test/test_asyncio/test_futures.py index c566b28adb2408..458b70451a306a 100644 --- a/Lib/test/test_asyncio/test_futures.py +++ b/Lib/test/test_asyncio/test_futures.py @@ -659,28 +659,6 @@ def __del__(self): fut = self._new_future(loop=self.loop) fut.set_result(Evil()) - def test_future_cancelled_result_refcycles(self): - f = self._new_future(loop=self.loop) - f.cancel() - exc = None - try: - f.result() - except asyncio.CancelledError as e: - exc = e - self.assertIsNotNone(exc) - self.assertListEqual(gc.get_referrers(exc), []) - - def test_future_cancelled_exception_refcycles(self): - f = self._new_future(loop=self.loop) - f.cancel() - exc = None - try: - f.exception() - except asyncio.CancelledError as e: - exc = e - self.assertIsNotNone(exc) - self.assertListEqual(gc.get_referrers(exc), []) - @unittest.skipUnless(hasattr(futures, '_CFuture'), 'requires the C _asyncio module') diff --git a/Lib/test/test_asyncio/test_taskgroups.py b/Lib/test/test_asyncio/test_taskgroups.py index 138f59ebf57ef7..4852536defc93d 100644 --- a/Lib/test/test_asyncio/test_taskgroups.py +++ b/Lib/test/test_asyncio/test_taskgroups.py @@ -1,7 +1,7 @@ # Adapted with permission from the EdgeDB project; # license: PSFL. -import gc + import asyncio import contextvars import contextlib @@ -11,6 +11,7 @@ from test.test_asyncio.utils import await_without_task + # To prevent a warning "test altered the execution environment" def tearDownModule(): asyncio.set_event_loop_policy(None) @@ -898,95 +899,6 @@ async def outer(): await outer() - async def test_exception_refcycles_direct(self): - """Test that TaskGroup doesn't keep a reference to the raised ExceptionGroup""" - tg = asyncio.TaskGroup() - exc = None - - class _Done(Exception): - pass - - try: - async with tg: - raise _Done - except ExceptionGroup as e: - exc = e - - self.assertIsNotNone(exc) - self.assertListEqual(gc.get_referrers(exc), []) - - - async def test_exception_refcycles_errors(self): - """Test that TaskGroup deletes self._errors, and __aexit__ args""" - tg = asyncio.TaskGroup() - exc = None - - class _Done(Exception): - pass - - try: - async with tg: - raise _Done - except* _Done as excs: - exc = excs.exceptions[0] - - self.assertIsInstance(exc, _Done) - self.assertListEqual(gc.get_referrers(exc), []) - - - async def test_exception_refcycles_parent_task(self): - """Test that TaskGroup deletes self._parent_task""" - tg = asyncio.TaskGroup() - exc = None - - class _Done(Exception): - pass - - async def coro_fn(): - async with tg: - raise _Done - - try: - async with asyncio.TaskGroup() as tg2: - tg2.create_task(coro_fn()) - except* _Done as excs: - exc = excs.exceptions[0].exceptions[0] - - self.assertIsInstance(exc, _Done) - self.assertListEqual(gc.get_referrers(exc), []) - - async def test_exception_refcycles_propagate_cancellation_error(self): - """Test that TaskGroup deletes propagate_cancellation_error""" - tg = asyncio.TaskGroup() - exc = None - - try: - async with asyncio.timeout(-1): - async with tg: - await asyncio.sleep(0) - except TimeoutError as e: - exc = e.__cause__ - - self.assertIsInstance(exc, asyncio.CancelledError) - self.assertListEqual(gc.get_referrers(exc), []) - - async def test_exception_refcycles_base_error(self): - """Test that TaskGroup deletes self._base_error""" - class MyKeyboardInterrupt(KeyboardInterrupt): - pass - - tg = asyncio.TaskGroup() - exc = None - - try: - async with tg: - raise MyKeyboardInterrupt - except MyKeyboardInterrupt as e: - exc = e - - self.assertIsNotNone(exc) - self.assertListEqual(gc.get_referrers(exc), []) - if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Library/2024-10-04-08-46-00.gh-issue-124958.rea9-x.rst b/Misc/NEWS.d/next/Library/2024-10-04-08-46-00.gh-issue-124958.rea9-x.rst deleted file mode 100644 index 534d5bb8c898da..00000000000000 --- a/Misc/NEWS.d/next/Library/2024-10-04-08-46-00.gh-issue-124958.rea9-x.rst +++ /dev/null @@ -1 +0,0 @@ -Fix refcycles in exceptions raised from :class:`asyncio.TaskGroup` and the python implementation of :class:`asyncio.Future`
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: