Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e6d1aa1

Browse files
authored
bpo-44594: fix (Async)ExitStack handling of __context__ (gh-27089)
* 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.
1 parent a25dcae commit e6d1aa1

4 files changed

Lines changed: 76 additions & 4 deletions

File tree

Lib/contextlib.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -553,10 +553,10 @@ def _fix_exception_context(new_exc, old_exc):
553553
# Context may not be correct, so find the end of the chain
554554
while 1:
555555
exc_context = new_exc.__context__
556-
if exc_context is old_exc:
556+
if exc_context is None or exc_context is old_exc:
557557
# Context is already set correctly (see issue 20317)
558558
return
559-
if exc_context is None or exc_context is frame_exc:
559+
if exc_context is frame_exc:
560560
break
561561
new_exc = exc_context
562562
# Change the end of the chain to point to the exception
@@ -693,10 +693,10 @@ def _fix_exception_context(new_exc, old_exc):
693693
# Context may not be correct, so find the end of the chain
694694
while 1:
695695
exc_context = new_exc.__context__
696-
if exc_context is old_exc:
696+
if exc_context is None or exc_context is old_exc:
697697
# Context is already set correctly (see issue 20317)
698698
return
699-
if exc_context is None or exc_context is frame_exc:
699+
if exc_context is frame_exc:
700700
break
701701
new_exc = exc_context
702702
# Change the end of the chain to point to the exception

Lib/test/test_contextlib.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,40 @@ def suppress_exc(*exc_details):
799799
self.assertIsInstance(inner_exc, ValueError)
800800
self.assertIsInstance(inner_exc.__context__, ZeroDivisionError)
801801

802+
def test_exit_exception_explicit_none_context(self):
803+
# Ensure ExitStack chaining matches actual nested `with` statements
804+
# regarding explicit __context__ = None.
805+
806+
class MyException(Exception):
807+
pass
808+
809+
@contextmanager
810+
def my_cm():
811+
try:
812+
yield
813+
except BaseException:
814+
exc = MyException()
815+
try:
816+
raise exc
817+
finally:
818+
exc.__context__ = None
819+
820+
@contextmanager
821+
def my_cm_with_exit_stack():
822+
with self.exit_stack() as stack:
823+
stack.enter_context(my_cm())
824+
yield stack
825+
826+
for cm in (my_cm, my_cm_with_exit_stack):
827+
with self.subTest():
828+
try:
829+
with cm():
830+
raise IndexError()
831+
except MyException as exc:
832+
self.assertIsNone(exc.__context__)
833+
else:
834+
self.fail("Expected IndexError, but no exception was raised")
835+
802836
def test_exit_exception_non_suppressing(self):
803837
# http://bugs.python.org/issue19092
804838
def raise_exc(exc):

Lib/test/test_contextlib_async.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,41 @@ async def suppress_exc(*exc_details):
646646
self.assertIsInstance(inner_exc, ValueError)
647647
self.assertIsInstance(inner_exc.__context__, ZeroDivisionError)
648648

649+
@_async_test
650+
async def test_async_exit_exception_explicit_none_context(self):
651+
# Ensure AsyncExitStack chaining matches actual nested `with` statements
652+
# regarding explicit __context__ = None.
653+
654+
class MyException(Exception):
655+
pass
656+
657+
@asynccontextmanager
658+
async def my_cm():
659+
try:
660+
yield
661+
except BaseException:
662+
exc = MyException()
663+
try:
664+
raise exc
665+
finally:
666+
exc.__context__ = None
667+
668+
@asynccontextmanager
669+
async def my_cm_with_exit_stack():
670+
async with self.exit_stack() as stack:
671+
await stack.enter_async_context(my_cm())
672+
yield stack
673+
674+
for cm in (my_cm, my_cm_with_exit_stack):
675+
with self.subTest():
676+
try:
677+
async with cm():
678+
raise IndexError()
679+
except MyException as exc:
680+
self.assertIsNone(exc.__context__)
681+
else:
682+
self.fail("Expected IndexError, but no exception was raised")
683+
649684
@_async_test
650685
async def test_instance_bypass_async(self):
651686
class Example(object): pass
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix an edge case of :class:`ExitStack` and :class:`AsyncExitStack` exception
2+
chaining. They will now match ``with`` block behavior when ``__context__`` is
3+
explicitly set to ``None`` when the exception is in flight.

0 commit comments

Comments
 (0)