Skip to content

Commit cc73475

Browse files
committed
Handle the async close and throw protocol from codegen.
1 parent 2a44069 commit cc73475

6 files changed

Lines changed: 417 additions & 69 deletions

File tree

‎Include/internal/pycore_opcode_utils.h‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,9 @@ extern "C" {
8282
#define CONSTANT_MINUS_ONE 11
8383
#define CONSTANT_BUILTIN_FROZENSET 12
8484
#define CONSTANT_EMPTY_TUPLE 13
85-
#define NUM_COMMON_CONSTANTS 14
85+
#define CONSTANT_GENERATOREXIT 14
86+
#define CONSTANT_ATTRIBUTEERROR 15
87+
#define NUM_COMMON_CONSTANTS 16
8688

8789
/* Values used in the oparg for RESUME */
8890
#define RESUME_AT_FUNC_START 0

‎Lib/opcode.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@
4444
builtins.set,
4545
# Append-only — must match CONSTANT_* in
4646
# Include/internal/pycore_opcode_utils.h.
47-
None, "", True, False, -1, builtins.frozenset, ()]
47+
None, "", True, False, -1, builtins.frozenset, (),
48+
builtins.GeneratorExit, builtins.AttributeError]
4849
_nb_ops = _opcode.get_nb_ops()
4950

5051
hascompare = [opmap["COMPARE_OP"]]

‎Lib/test/test_yield_from_async.py‎

Lines changed: 301 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@
66
the 1:1 mapping. Tests with no PEP 380 analogue go in `TestPEP828Extras`.
77
"""
88

9+
import asyncio
910
import unittest
1011
import inspect
1112
from functools import partial
1213

1314
lazy from test import test_yield_from
14-
from test.support import captured_stderr, disable_gc, gc_collect, run_no_yield_async_fn, catch_unraisable_exception
15+
from test.support import (
16+
captured_stderr, disable_gc, gc_collect, requires_working_socket,
17+
run_no_yield_async_fn,
18+
)
1519

1620
_async_test = partial(partial, run_no_yield_async_fn)
1721

@@ -599,13 +603,12 @@ async def g():
599603
self.assertEqual(await anext(gi), 1)
600604
await gi.athrow(AttributeError)
601605

602-
with catch_unraisable_exception() as cm:
606+
# PEP 828 only ignores AttributeError when looking up aclose().
607+
with self.assertRaises(ZeroDivisionError):
603608
gi = g()
604609
self.assertEqual(await anext(gi), 1)
605610
await gi.aclose()
606611

607-
self.assertEqual(ZeroDivisionError, cm.unraisable.exc_type)
608-
609612
@_async_test
610613
async def test_exception_in_initial_next_call_ayf(self):
611614
"""
@@ -1004,7 +1007,7 @@ def __aiter__(self):
10041007
async def __anext__(self):
10051008
return 42
10061009
async def aclose(self_):
1007-
self.assertTrue(g1.gi_running)
1010+
self.assertTrue(g1.ag_running)
10081011
with self.assertRaises(RuntimeError):
10091012
await anext(g1)
10101013
async def one():
@@ -1744,5 +1747,298 @@ async def outer():
17441747
self.assertIs(raised, error.exception)
17451748

17461749

1750+
@requires_working_socket()
1751+
class TestAsyncDelegation(unittest.IsolatedAsyncioTestCase):
1752+
async def test_cleanup_suspends(self):
1753+
async def inner():
1754+
try:
1755+
yield 1
1756+
finally:
1757+
trace.append("cleanup started")
1758+
await asyncio.sleep(0)
1759+
trace.append("cleanup completed")
1760+
1761+
async def outer(sub):
1762+
yield from sub
1763+
1764+
for depth in (1, 3):
1765+
for close in (True, False):
1766+
with self.subTest(depth=depth, close=close):
1767+
trace = []
1768+
gen = inner()
1769+
for _ in range(depth):
1770+
gen = outer(gen)
1771+
self.assertEqual(await anext(gen), 1)
1772+
if close:
1773+
await gen.aclose()
1774+
else:
1775+
original = GeneratorExit()
1776+
with self.assertRaises(GeneratorExit) as caught:
1777+
await gen.athrow(original)
1778+
self.assertIs(caught.exception, original)
1779+
self.assertEqual(trace,
1780+
["cleanup started", "cleanup completed"])
1781+
1782+
async def test_generator_exit_after_suspension(self):
1783+
async def inner():
1784+
try:
1785+
yield 1
1786+
except GeneratorExit:
1787+
await asyncio.sleep(0)
1788+
if action == "raise":
1789+
raise GeneratorExit()
1790+
if action == "yield":
1791+
yield 2
1792+
return 42
1793+
1794+
async def outer(sub):
1795+
yield from sub
1796+
self.fail("delegation continued after GeneratorExit")
1797+
1798+
for action in ("return", "raise", "yield"):
1799+
for close in (True, False):
1800+
with self.subTest(action=action, close=close):
1801+
sub = inner()
1802+
try:
1803+
gen = outer(sub)
1804+
self.assertEqual(await anext(gen), 1)
1805+
original = GeneratorExit()
1806+
operation = gen.aclose() if close else gen.athrow(original)
1807+
if action == "yield":
1808+
with self.assertRaisesRegex(
1809+
RuntimeError, "async generator ignored GeneratorExit"):
1810+
await operation
1811+
elif close:
1812+
await operation
1813+
else:
1814+
with self.assertRaises(GeneratorExit) as caught:
1815+
await operation
1816+
self.assertIs(caught.exception, original)
1817+
finally:
1818+
if action == "yield":
1819+
# Finish the delegate after its illegal yield
1820+
# during closing.
1821+
await anext(sub, None)
1822+
else:
1823+
await sub.aclose()
1824+
1825+
async def test_custom_iterator_methods_suspend(self):
1826+
class Iterator:
1827+
def __aiter__(self):
1828+
return self
1829+
1830+
async def __anext__(self):
1831+
return 1
1832+
1833+
async def athrow(self, exc):
1834+
trace.append(exc)
1835+
await asyncio.sleep(0)
1836+
trace.append("athrow completed")
1837+
return 2
1838+
1839+
async def aclose(self):
1840+
await asyncio.sleep(0)
1841+
trace.append("aclose completed")
1842+
1843+
def throw(self, *args):
1844+
raise AssertionError("synchronous throw called")
1845+
1846+
def close(self):
1847+
raise AssertionError("synchronous close called")
1848+
1849+
async def outer():
1850+
yield from Iterator()
1851+
1852+
trace = []
1853+
gen = outer()
1854+
self.assertEqual(await anext(gen), 1)
1855+
original = ValueError()
1856+
self.assertEqual(await gen.athrow(original), 2)
1857+
self.assertEqual(trace, [original, "athrow completed"])
1858+
await gen.aclose()
1859+
self.assertEqual(trace,
1860+
[original, "athrow completed", "aclose completed"])
1861+
1862+
async def test_custom_iterator_throw_return_value(self):
1863+
class Iterator:
1864+
def __aiter__(self):
1865+
return self
1866+
1867+
async def __anext__(self):
1868+
return 1
1869+
1870+
async def athrow(self, exc):
1871+
await asyncio.sleep(0)
1872+
raise StopAsyncIteration(result)
1873+
1874+
async def outer():
1875+
return (yield from Iterator())
1876+
1877+
for result in (None, 42, (1, 2)):
1878+
with self.subTest(result=result):
1879+
gen = outer()
1880+
self.assertEqual(await anext(gen), 1)
1881+
with self.assertRaises(StopAsyncIteration) as caught:
1882+
await gen.athrow(ValueError())
1883+
self.assertIs(caught.exception.value, result)
1884+
1885+
async def test_missing_throw_does_not_swallow_stop_async_iteration(self):
1886+
class Iterator:
1887+
def __aiter__(self):
1888+
return self
1889+
1890+
async def __anext__(self):
1891+
return 1
1892+
1893+
async def outer():
1894+
yield from Iterator()
1895+
1896+
gen = outer()
1897+
self.assertEqual(await anext(gen), 1)
1898+
original = StopAsyncIteration(42)
1899+
with self.assertRaisesRegex(
1900+
RuntimeError, "async generator raised StopAsyncIteration") as caught:
1901+
await gen.athrow(original)
1902+
self.assertIs(caught.exception.__cause__, original)
1903+
1904+
async def test_close_does_not_swallow_stop_async_iteration(self):
1905+
class Iterator:
1906+
def __aiter__(self):
1907+
return self
1908+
1909+
async def __anext__(self):
1910+
return 1
1911+
1912+
async def aclose(self):
1913+
await asyncio.sleep(0)
1914+
raise original
1915+
1916+
async def outer():
1917+
yield from Iterator()
1918+
1919+
for close in (True, False):
1920+
with self.subTest(close=close):
1921+
gen = outer()
1922+
self.assertEqual(await anext(gen), 1)
1923+
original = StopAsyncIteration(42)
1924+
with self.assertRaisesRegex(
1925+
RuntimeError, "async generator raised StopAsyncIteration") as caught:
1926+
if close:
1927+
await gen.aclose()
1928+
else:
1929+
await gen.athrow(GeneratorExit())
1930+
self.assertIs(caught.exception.__cause__, original)
1931+
1932+
async def test_methods_return_general_awaitables(self):
1933+
class Awaitable:
1934+
def __init__(self, result):
1935+
self.result = result
1936+
1937+
def __await__(self):
1938+
yield
1939+
return self.result
1940+
1941+
class Iterator:
1942+
def __aiter__(self):
1943+
return self
1944+
1945+
def __anext__(self):
1946+
return Awaitable(1)
1947+
1948+
def asend(self, value):
1949+
return Awaitable(value)
1950+
1951+
def athrow(self, exc):
1952+
return Awaitable(exc)
1953+
1954+
def aclose(self):
1955+
return Awaitable(None)
1956+
1957+
async def outer():
1958+
yield from Iterator()
1959+
1960+
gen = outer()
1961+
self.assertEqual(await anext(gen), 1)
1962+
self.assertEqual(await gen.asend(2), 2)
1963+
original = ValueError()
1964+
self.assertIs(await gen.athrow(original), original)
1965+
await gen.aclose()
1966+
1967+
async def test_optional_method_lookup_errors(self):
1968+
class Iterator:
1969+
def __aiter__(self):
1970+
return self
1971+
1972+
async def __anext__(self):
1973+
return 1
1974+
1975+
def __getattr__(self, name):
1976+
if name in ("aclose", "athrow"):
1977+
raise lookup_error
1978+
raise AttributeError(name)
1979+
1980+
async def outer():
1981+
yield from Iterator()
1982+
1983+
for exc_type in (AttributeError, LookupError, StopAsyncIteration):
1984+
for mode in ("close", "exit", "throw"):
1985+
with self.subTest(exc_type=exc_type, mode=mode):
1986+
lookup_error = exc_type("method lookup failed")
1987+
gen = outer()
1988+
self.assertEqual(await anext(gen), 1)
1989+
original = GeneratorExit() if mode == "exit" else ValueError()
1990+
operation = (gen.aclose() if mode == "close"
1991+
else gen.athrow(original))
1992+
if exc_type is AttributeError:
1993+
if mode == "close":
1994+
await operation
1995+
else:
1996+
with self.assertRaises(type(original)) as caught:
1997+
await operation
1998+
self.assertIs(caught.exception, original)
1999+
elif exc_type is StopAsyncIteration:
2000+
with self.assertRaisesRegex(
2001+
RuntimeError,
2002+
"async generator raised StopAsyncIteration") as caught:
2003+
await operation
2004+
self.assertIs(caught.exception.__cause__, lookup_error)
2005+
else:
2006+
with self.assertRaises(exc_type) as caught:
2007+
await operation
2008+
self.assertIs(caught.exception, lookup_error)
2009+
2010+
async def test_attribute_error_from_async_methods(self):
2011+
class Iterator:
2012+
def __aiter__(self):
2013+
return self
2014+
2015+
async def __anext__(self):
2016+
return 1
2017+
2018+
async def aclose(self):
2019+
await asyncio.sleep(0)
2020+
raise method_error
2021+
2022+
async def athrow(self, exc):
2023+
await asyncio.sleep(0)
2024+
raise method_error
2025+
2026+
async def outer():
2027+
yield from Iterator()
2028+
2029+
for mode in ("close", "exit", "throw"):
2030+
with self.subTest(mode=mode):
2031+
method_error = AttributeError("method failed")
2032+
gen = outer()
2033+
self.assertEqual(await anext(gen), 1)
2034+
original = GeneratorExit() if mode == "exit" else ValueError()
2035+
with self.assertRaises(AttributeError) as caught:
2036+
if mode == "close":
2037+
await gen.aclose()
2038+
else:
2039+
await gen.athrow(original)
2040+
self.assertIs(caught.exception, method_error)
2041+
2042+
17472043
if __name__ == '__main__':
17482044
unittest.main()

0 commit comments

Comments
 (0)