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

Skip to content

Commit d58831e

Browse files
committed
Merge #20317 from 3.3
2 parents 4a2dbeb + 09761e7 commit d58831e

3 files changed

Lines changed: 38 additions & 1 deletion

File tree

Lib/contextlib.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,11 +298,19 @@ def __exit__(self, *exc_details):
298298
# we were actually nesting multiple with statements
299299
frame_exc = sys.exc_info()[1]
300300
def _fix_exception_context(new_exc, old_exc):
301+
# Context isn't what we want, so find the end of the chain
301302
while 1:
302303
exc_context = new_exc.__context__
303-
if exc_context in (None, frame_exc):
304+
if exc_context is old_exc:
305+
# Context is already set correctly (see issue 20317)
306+
return
307+
if exc_context is None or exc_context is frame_exc:
304308
break
309+
details = id(new_exc), id(old_exc), id(exc_context)
310+
raise Exception(str(details))
305311
new_exc = exc_context
312+
# Change the end of the chain to point to the exception
313+
# we expect it to reference
306314
new_exc.__context__ = old_exc
307315

308316
# Callbacks are invoked in LIFO order to match the behaviour of

Lib/test/test_contextlib.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,29 @@ def suppress_exc(*exc_details):
626626
else:
627627
self.fail("Expected KeyError, but no exception was raised")
628628

629+
def test_exit_exception_with_correct_context(self):
630+
# http://bugs.python.org/issue20317
631+
@contextmanager
632+
def gets_the_context_right():
633+
try:
634+
yield 6
635+
finally:
636+
1 / 0
637+
638+
# The contextmanager already fixes the context, so prior to the
639+
# fix, ExitStack would try to fix it *again* and get into an
640+
# infinite self-referential loop
641+
try:
642+
with ExitStack() as stack:
643+
stack.enter_context(gets_the_context_right())
644+
stack.enter_context(gets_the_context_right())
645+
stack.enter_context(gets_the_context_right())
646+
except ZeroDivisionError as exc:
647+
self.assertIsInstance(exc.__context__, ZeroDivisionError)
648+
self.assertIsInstance(exc.__context__.__context__, ZeroDivisionError)
649+
self.assertIsNone(exc.__context__.__context__.__context__)
650+
651+
629652
def test_body_exception_suppress(self):
630653
def suppress_exc(*exc_details):
631654
return True

Misc/NEWS

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ Library
3737
microsecond is now rounded to one millisecond, instead of being rounded to
3838
zero.
3939

40+
- Issue #20317: ExitStack.__exit__ could create a self-referential loop if an
41+
exception raised by a cleanup operation already had its context set
42+
correctly (for example, by the @contextmanager decorator). The infinite
43+
loop this caused is now avoided by checking if the expected context is
44+
already set before trying to fix it.
45+
4046
- Issue #20311: select.epoll.poll() now rounds the timeout away from zero,
4147
instead of rounding towards zero. For example, a timeout of one microsecond
4248
is now rounded to one millisecond, instead of being rounded to zero.

0 commit comments

Comments
 (0)