Skip to content
34 changes: 17 additions & 17 deletions Lib/contextlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,27 +158,26 @@ def __exit__(self, type, value, traceback):
# Don't re-raise the passed in exception. (issue27122)
if exc is value:
return False
# Likewise, avoid suppressing if a StopIteration exception
# Avoid suppressing if a StopIteration exception
# was passed to throw() and later wrapped into a RuntimeError
# (see PEP 479).
if type is StopIteration and exc.__cause__ is value:
return False
# (see PEP 479 for sync generators; async generators also
# have this behavior). But do this only if the exception wrapped
# by the RuntimeError is actually Stop(Async)Iteration (see
# issue29692).
if isinstance(value, (StopIteration, StopAsyncIteration)):
if exc.__cause__ is value:
return False
raise
except:
except BaseException as exc:
# only re-raise if it's *not* the exception that was
# passed to throw(), because __exit__() must not raise
# an exception unless __exit__() itself failed. But throw()
# has to raise the exception to signal propagation, so this
# fixes the impedance mismatch between the throw() protocol
# and the __exit__() protocol.
#
# This cannot use 'except BaseException as exc' (as in the
# async implementation) to maintain compatibility with
# Python 2, where old-style class exceptions are not caught
# by 'except BaseException'.
if sys.exc_info()[1] is value:
return False
raise
if exc is not value:
raise
return False
raise RuntimeError("generator didn't stop after throw()")


Expand All @@ -195,16 +194,16 @@ def _recreate_cm(self):

async def __aenter__(self):
try:
return await self.gen.__anext__()
return await anext(self.gen)
except StopAsyncIteration:
raise RuntimeError("generator didn't yield") from None

async def __aexit__(self, typ, value, traceback):
if typ is None:
try:
await self.gen.__anext__()
await anext(self.gen)
except StopAsyncIteration:
return
return False
else:
raise RuntimeError("generator didn't stop")
else:
Expand All @@ -214,7 +213,6 @@ async def __aexit__(self, typ, value, traceback):
# in this implementation
try:
await self.gen.athrow(typ, value, traceback)
raise RuntimeError("generator didn't stop after athrow()")
except StopAsyncIteration as exc:
return exc is not value
except RuntimeError as exc:
Expand All @@ -233,6 +231,8 @@ async def __aexit__(self, typ, value, traceback):
except BaseException as exc:
if exc is not value:
raise
return False
raise RuntimeError("generator didn't stop after athrow()")


def contextmanager(func):
Expand Down
23 changes: 13 additions & 10 deletions Lib/test/test_contextlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,19 +126,22 @@ def woohoo():
self.assertEqual(state, [1, 42, 999])

def test_contextmanager_except_stopiter(self):
stop_exc = StopIteration('spam')
@contextmanager
def woohoo():
yield
try:
with self.assertWarnsRegex(DeprecationWarning,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this warning is from py2?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah the exception means this warning is never checked for

"StopIteration"):
with woohoo():
raise stop_exc
except Exception as ex:
self.assertIs(ex, stop_exc)
else:
self.fail('StopIteration was suppressed')

class StopIterationSubclass(StopIteration):
pass

for stop_exc in (StopIteration('spam'), StopIterationSubclass('spam')):
with self.subTest(type=type(stop_exc)):
try:
with woohoo():
raise stop_exc
except Exception as ex:
self.assertIs(ex, stop_exc)
else:
self.fail(f'{stop_exc} was suppressed')

def test_contextmanager_except_pep479(self):
code = """\
Expand Down
13 changes: 12 additions & 1 deletion Lib/test/test_contextlib_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,18 @@ async def test_contextmanager_except_stopiter(self):
async def woohoo():
yield

for stop_exc in (StopIteration('spam'), StopAsyncIteration('ham')):
class StopIterationSubclass(StopIteration):
pass

class StopAsyncIterationSubclass(StopAsyncIteration):
pass

for stop_exc in (
StopIteration('spam'),
StopAsyncIteration('ham'),
StopIterationSubclass('spam'),
StopAsyncIterationSubclass('spam')
):
with self.subTest(type=type(stop_exc)):
try:
async with woohoo():
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
handle StopIteration subclass raised from @contextlib.contextmanager generator