diff --git a/Lib/test/test_coroutines.py b/Lib/test/test_coroutines.py index a515e0f5c..674273e6c 100644 --- a/Lib/test/test_coroutines.py +++ b/Lib/test/test_coroutines.py @@ -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, "", "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, "", "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, "", "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, "", "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, "", "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) diff --git a/crates/vm/src/coroutine.rs b/crates/vm/src/coroutine.rs index b1ea72b13..637e5b977 100644 --- a/crates/vm/src/coroutine.rs +++ b/crates/vm/src/coroutine.rs @@ -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);