From 6b945224ec581d4627e8545cda12d0adfee784c9 Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Mon, 12 Jul 2021 09:17:07 +0900 Subject: [PATCH 1/4] bpo-44594: fix (Async)ExitStack handling of __context__ Make enter_context(foo()) / enter_async_context(foo()) equivalent to `[async] with foo()` regarding __context__ when an exception is raised. Previously exceptions would be caught and re-raised with the wrong context when explicitly overriding __context__ with None. TODO: unit tests --- Lib/contextlib.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/contextlib.py b/Lib/contextlib.py index 004d1037b78a476..63dd1dedcf7f584 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -531,10 +531,10 @@ def _fix_exception_context(new_exc, old_exc): # Context may not be correct, so find the end of the chain while 1: exc_context = new_exc.__context__ - if exc_context is old_exc: + if exc_context is None or exc_context is old_exc: # Context is already set correctly (see issue 20317) return - if exc_context is None or exc_context is frame_exc: + if exc_context is frame_exc: break new_exc = exc_context # Change the end of the chain to point to the exception @@ -671,10 +671,10 @@ def _fix_exception_context(new_exc, old_exc): # Context may not be correct, so find the end of the chain while 1: exc_context = new_exc.__context__ - if exc_context is old_exc: + if exc_context is None or exc_context is old_exc: # Context is already set correctly (see issue 20317) return - if exc_context is None or exc_context is frame_exc: + if exc_context is frame_exc: break new_exc = exc_context # Change the end of the chain to point to the exception From db2f55b6827a320b515a9d0d7097bb93f5fa58af Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Mon, 12 Jul 2021 18:47:33 +0900 Subject: [PATCH 2/4] unit tests --- Lib/test/test_contextlib.py | 34 ++++++++++++++++++++++++++++++ Lib/test/test_contextlib_async.py | 35 +++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/Lib/test/test_contextlib.py b/Lib/test/test_contextlib.py index 9c27866cd661cb1..825197f26824b0d 100644 --- a/Lib/test/test_contextlib.py +++ b/Lib/test/test_contextlib.py @@ -794,6 +794,40 @@ def suppress_exc(*exc_details): self.assertIsInstance(inner_exc, ValueError) self.assertIsInstance(inner_exc.__context__, ZeroDivisionError) + def test_exit_exception_explicit_none_context(self): + # Ensure ExitStack chaining matches actual nested `with` statements + # regarding explicit __context__ = None. + + class MyException(Exception): + pass + + @contextmanager + def my_cm(): + try: + yield + except BaseException: + exc = MyException() + try: + raise exc + finally: + exc.__context__ = None + + @contextmanager + def my_cm_with_exit_stack(): + with self.exit_stack() as stack: + stack.enter_context(my_cm()) + yield stack + + for cm in (my_cm, my_cm_with_exit_stack): + with self.subTest(): + try: + with cm(): + raise IndexError() + except MyException as exc: + self.assertIsNone(exc.__context__) + else: + self.fail("Expected IndexError, but no exception was raised") + def test_exit_exception_non_suppressing(self): # http://bugs.python.org/issue19092 def raise_exc(exc): diff --git a/Lib/test/test_contextlib_async.py b/Lib/test/test_contextlib_async.py index 7904abff7d1aa57..004c6deef4bf4fd 100644 --- a/Lib/test/test_contextlib_async.py +++ b/Lib/test/test_contextlib_async.py @@ -556,6 +556,41 @@ async def suppress_exc(*exc_details): self.assertIsInstance(inner_exc, ValueError) self.assertIsInstance(inner_exc.__context__, ZeroDivisionError) + @_async_test + async def test_async_exit_exception_explicit_none_context(self): + # Ensure AsyncExitStack chaining matches actual nested `with` statements + # regarding explicit __context__ = None. + + class MyException(Exception): + pass + + @asynccontextmanager + async def my_cm(): + try: + yield + except BaseException: + exc = MyException() + try: + raise exc + finally: + exc.__context__ = None + + @asynccontextmanager + async def my_cm_with_exit_stack(): + async with self.exit_stack() as stack: + await stack.enter_async_context(my_cm()) + yield stack + + for cm in (my_cm, my_cm_with_exit_stack): + with self.subTest(): + try: + async with cm(): + raise IndexError() + except MyException as exc: + self.assertIsNone(exc.__context__) + else: + self.fail("Expected IndexError, but no exception was raised") + @_async_test async def test_instance_bypass_async(self): class Example(object): pass From d5f7cbe77b47adb589431a04ed7a975f87188aa1 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Mon, 12 Jul 2021 10:32:50 +0000 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NEWS.d/next/Library/2021-07-12-10-32-48.bpo-44594.eEa5zi.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2021-07-12-10-32-48.bpo-44594.eEa5zi.rst diff --git a/Misc/NEWS.d/next/Library/2021-07-12-10-32-48.bpo-44594.eEa5zi.rst b/Misc/NEWS.d/next/Library/2021-07-12-10-32-48.bpo-44594.eEa5zi.rst new file mode 100644 index 000000000000000..c4364ef6f0fac16 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-07-12-10-32-48.bpo-44594.eEa5zi.rst @@ -0,0 +1 @@ +Fix an edge case of :class:`ExitStack` and :class:`AsyncExitStack` exception chaining. They will now match `with` block behavior when `__context__` is explicitly set to `None` when the exception is in flight. \ No newline at end of file From c70f7e256a5648c55598362ce51f05deca339c48 Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Mon, 12 Jul 2021 19:56:37 +0900 Subject: [PATCH 4/4] fix rst warning --- .../next/Library/2021-07-12-10-32-48.bpo-44594.eEa5zi.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2021-07-12-10-32-48.bpo-44594.eEa5zi.rst b/Misc/NEWS.d/next/Library/2021-07-12-10-32-48.bpo-44594.eEa5zi.rst index c4364ef6f0fac16..a2bfd8ff5b51bc0 100644 --- a/Misc/NEWS.d/next/Library/2021-07-12-10-32-48.bpo-44594.eEa5zi.rst +++ b/Misc/NEWS.d/next/Library/2021-07-12-10-32-48.bpo-44594.eEa5zi.rst @@ -1 +1,3 @@ -Fix an edge case of :class:`ExitStack` and :class:`AsyncExitStack` exception chaining. They will now match `with` block behavior when `__context__` is explicitly set to `None` when the exception is in flight. \ No newline at end of file +Fix an edge case of :class:`ExitStack` and :class:`AsyncExitStack` exception +chaining. They will now match ``with`` block behavior when ``__context__`` is +explicitly set to ``None`` when the exception is in flight.