mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 10:44:36 +08:00
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:
1 parent
b976859e8d
commit
4bd4320932
2 files changed
+136
-35
No files matched your search
Vendored
+129
-32
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user