Align coroutine await-result diagnostics with Python 3.15 (#9284)

Report the qualified awaitable and returned type when __await__ does not
produce an iterator, preserving the existing coroutine/iterator checks.
Migrate test_coroutines.py from pinned CPython v3.15.0 through update_lib.

Assisted-by: OpenAI Codex:model-version-unavailable
This commit is contained in:
youknowdot authored and GitHub committed 2026-10-22 10:13:11 +09:00
1 parent b976859e8d
commit 4bd4320932
2 files changed
+136 -35

No files matched your search

+129 -32
View File
@@ -1,3 +1,4 @@
import ast
import contextlib
import copy
import inspect
@@ -407,6 +408,118 @@ class AsyncBadSyntaxTest(unittest.TestCase):
with self.subTest(code=code), self.assertRaises(SyntaxError):
compile(code, "<test>", "exec")
def test_async_comprehension_scope(self):
# List/set/dict comprehensions with await or async for are allowed
# only in async functions, or at module level with top-level await.
allowed = [
"async def f():\n [await x for x in y]",
"async def f():\n {await x for x in y}",
"async def f():\n {k: await x for k, x in y}",
"async def f():\n [x async for x in y]",
"async def f():\n {x async for x in y}",
"async def f():\n {k: x async for k, x in y}",
"async def f():\n [[await x for x in y] for y in z]",
# Defaults, bases, and genexp iterables are evaluated in the
# enclosing scope.
"async def outer():\n async def f(x=[await y for y in z]): pass",
"async def f():\n class C([await x for x in y]): pass",
"async def f():\n (x for x in [await y for y in z])",
"async def f():\n (x for x in [y async for y in z])",
]
for code in allowed:
with self.subTest(code=code):
compile(code, "<test>", "exec")
# Generator expressions with await are async genexps and may appear
# outside async functions. A listcomp nested in a genexp body is also
# allowed (the genexp becomes an async generator).
for code in [
"(await x for x in y)",
"def f():\n (await x for x in y)",
"class C:\n (await x for x in y)",
"lambda: (await x for x in y)",
"([await x for x in y] for y in z)",
"def f():\n ([await x for x in y] for y in z)",
"class C:\n ([await x for x in y] for y in z)",
"async def f():\n ([await x for x in y] for y in z)",
]:
with self.subTest(code=code):
compile(code, "<test>", "exec")
err = "asynchronous comprehension outside of an asynchronous function"
invalid = [
"[await x for x in y]",
"{await x for x in y}",
"{k: await x for k, x in y}",
"[x async for x in y]",
"{x async for x in y}",
"{k: x async for k, x in y}",
"[[await x for x in y] for y in z]",
"[[x async for x in y] for y in z]",
"def f():\n [await x for x in y]",
"def f():\n [x async for x in y]",
"async def f():\n def g():\n [await x for x in y]",
"class C:\n [await x for x in y]",
"class C:\n {await x for x in y}",
"class C:\n {k: await x for k, x in y}",
"class C:\n [x async for x in y]",
"class C:\n [[await x for x in y] for y in z]",
"async def f():\n class C:\n x = [await y for y in z]",
"async def f():\n class C:\n x = [y async for y in z]",
# Lambdas are never async, even inside async def.
"lambda: [await x for x in y]",
"async def f():\n lambda: [await x for x in y]",
"class C:\n f = lambda: [await x for x in y]",
# Defaults, bases, and genexp iterables run in the enclosing scope.
"(x for x in [await y for y in z])",
"(x for x in [y async for y in z])",
"def f():\n (x for x in [await y for y in z])",
"async def f(x=[await y for y in z]): pass",
"def f(x=[await y for y in z]): pass",
"class C:\n def f(self, x=[await y for y in z]): pass",
"class C([await x for x in y]): pass",
# Type aliases and type-parameter scopes.
"type T = [await x for x in y]",
"type T = [x async for x in y]",
"def f[T=[await x for x in y]](): pass",
"def f[T: [await x for x in y]](): pass",
"async def f[T=[await x for x in y]](): pass",
"async def f(x: [await y for y in z]): pass",
]
for code in invalid:
with self.subTest(code=code):
support.check_syntax_error(self, code, err)
support.check_syntax_error(
self, "await x", "'await' outside function")
support.check_syntax_error(
self, "class C:\n await x", "'await' outside function")
support.check_syntax_error(
self, "def f():\n await x", "'await' outside async function")
flags = ast.PyCF_ALLOW_TOP_LEVEL_AWAIT
for code in [
"[await x for x in y]",
"async def f(x=[await y for y in z]): pass",
"class C([await x for x in y]): pass",
"f'{[await x for x in y]}'",
"(x for x in [await y for y in z])",
"(x for x in [y async for y in z])",
]:
with self.subTest(code=code, tla=True):
compile(code, "<test>", "exec", flags=flags)
still_invalid = [
"lambda: [await x for x in y]",
"def f():\n (x for x in [await y for y in z])",
"class C:\n def f(self, x=[await y for y in z]): pass",
"type T = [await x for x in y]",
"async def f[T=[await x for x in y]](): pass",
]
for code in still_invalid:
with self.subTest(code=code, tla=True):
with self.assertRaisesRegex(SyntaxError, err):
compile(code, "<test>", "exec", flags=flags)
def test_badsyntax_2(self):
samples = [
"""def foo():
@@ -1008,7 +1121,7 @@ class CoroutineTest(unittest.TestCase):
return (await Awaitable())
with self.assertRaisesRegex(
TypeError, "__await__.*returned non-iterator of type"):
TypeError, "__await__.*must return an iterator, not"):
run_async(foo())
@@ -1106,7 +1219,7 @@ class CoroutineTest(unittest.TestCase):
return await Awaitable()
with self.assertRaisesRegex(
TypeError, r"__await__\(\) returned a coroutine"):
TypeError, r"__await__\(\) must return an iterator, not coroutine"):
run_async(foo())
c.close()
@@ -1120,7 +1233,7 @@ class CoroutineTest(unittest.TestCase):
return await Awaitable()
with self.assertRaisesRegex(
TypeError, "__await__.*returned non-iterator of type"):
TypeError, "__await__.*must return an iterator, not"):
run_async(foo())
@@ -2265,36 +2378,20 @@ class CoroutineTest(unittest.TestCase):
# before fixing, visible stack from throw would be shorter than from send.
self.assertEqual(len_send, len_throw)
def test_call_aiter_once_in_comprehension(self):
def test_call_generator_in_frame_clear(self):
# gh-143939: Running a generator while clearing the coroutine's frame
# should not be misinterpreted as a yield.
class CallGeneratorOnDealloc:
def __del__(self):
next(x for x in [1])
class AsyncIterator:
def __init__(self):
self.val = 0
async def __anext__(self):
if self.val == 2:
raise StopAsyncIteration
self.val += 1
return self.val
# No __aiter__ method
class C:
def __aiter__(self):
return AsyncIterator()
async def run_listcomp():
return [i async for i in C()]
async def run_asyncgen():
ag = (i async for i in C())
return [i async for i in ag]
self.assertEqual(run_async(run_listcomp()), ([], [1, 2]))
self.assertEqual(run_async(run_asyncgen()), ([], [1, 2]))
async def coro():
obj = CallGeneratorOnDealloc()
return 42
yielded, result = run_async(coro())
self.assertEqual(yielded, [])
self.assertEqual(result, 42)
@unittest.skipIf(
support.is_emscripten or support.is_wasi,
@@ -2520,7 +2617,7 @@ class CAPITest(unittest.TestCase):
return (await future)
with self.assertRaisesRegex(
TypeError, "__await__.*returned non-iterator of type 'int'"):
TypeError, "__await__.*must return an iterator, not int"):
self.assertEqual(foo().send(None), 1)
+7 -3
View File
@@ -676,12 +676,16 @@ pub(crate) fn get_awaitable_iter(obj: PyObjectRef, vm: &VirtualMachine) -> PyRes
.contains(crate::bytecode::CodeFlags::ITERABLE_COROUTINE)
})
{
return Err(vm.new_type_error("__await__() returned a coroutine"));
return Err(vm.new_type_error(format!(
"{}.__await__() must return an iterator, not coroutine",
obj.class().fully_qualified_name(vm)?
)));
}
if !PyIter::check(&result) {
return Err(vm.new_type_error(format!(
"__await__() returned non-iterator of type '{}'",
result.class().name()
"{}.__await__() must return an iterator, not {}",
obj.class().fully_qualified_name(vm)?,
result.class().fully_qualified_name(vm)?
)));
}
return Ok(result);