Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions mypy/message_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ def with_additional_msg(self, info: str) -> ErrorMessage:
ASYNC_FOR_OUTSIDE_COROUTINE: Final = '"async for" outside async function'
ASYNC_WITH_OUTSIDE_COROUTINE: Final = '"async with" outside async function'

AWAIT_WITH_OUTSIDE_COROUTINE: Final = '"await" outside coroutine ("async def")'

INCOMPATIBLE_TYPES_IN_YIELD: Final = ErrorMessage('Incompatible types in "yield"')
INCOMPATIBLE_TYPES_IN_YIELD_FROM: Final = ErrorMessage('Incompatible types in "yield from"')
INCOMPATIBLE_TYPES_IN_STR_INTERPOLATION: Final = "Incompatible types in string interpolation"
Expand Down
30 changes: 26 additions & 4 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@
set_callable_name as set_callable_name,
)
from mypy.semanal_typeddict import TypedDictAnalyzer
from mypy.traverser import has_await_expression
from mypy.tvar_scope import TypeVarLikeScope
from mypy.typeanal import (
SELF_TYPE_NAMES,
Expand Down Expand Up @@ -6076,7 +6077,20 @@ def visit_type_application(self, expr: TypeApplication) -> None:
def visit_list_comprehension(self, expr: ListComprehension) -> None:
if any(expr.generator.is_async):
if not self.is_func_scope() or not self.function_stack[-1].is_coroutine:
self.fail(message_registry.ASYNC_FOR_OUTSIDE_COROUTINE, expr, code=codes.SYNTAX)
self.fail(
message_registry.ASYNC_FOR_OUTSIDE_COROUTINE,
expr,
code=codes.SYNTAX,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't really like the use of codes.SYNTAX vs codes.AWAIT_NOT_ASYNC here. Prior to this PR AWAIT_NOT_ASYNC was only used once.

As far as I understand, the purpose of using a special code was to support "specific" environments: e.g. IPython allows top-level awaits, so it should be easy to disable such a check when checking IPython snippets.

I see two possible resolutions here:

  1. Declare that we only type-check true python code. Then codes.AWAIT_NOT_ASYNC can be safely removed altogether and replaced with codes.SYNTAX
  2. Declare that we want to support such environments. Then all these checks become more complicated: when at module scope (note it isn't same as not self.is_func_scope()), produce codes.AWAIT_NOT_ASYNC, otherwise produce codes.SYNTAX. This can also be extracted into a helper like def require_async_scope(self, message). This logic is close to what visit_await_expr was doing before.

I somewhat prefer 2 as that isn't that much work to do, but can live with 1 either.

Disclaimer: please note I'm not a mypy maintainer and just try to help sometimes. You'd better ask for a maintainer's opinion here: @brianschubert do you have any preference?

Copy link
Collaborator

@sterliakov sterliakov Nov 14, 2024

Choose a reason for hiding this comment

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

OTOH 2 can also be misleading and deserves an explicit mention in the docs.

Copy link
Author

Choose a reason for hiding this comment

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

Thank you for the review and explanation, I understand your point so thank you for clarifying it. Will then wait for any guidance on how to proceed on any of these two options.

Copy link

@jakkdl jakkdl Nov 27, 2024

Choose a reason for hiding this comment

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

I don't think this is an important enough problem to change our stance on allowing top-level awaits, so you're probably safer to go ahead with option 2 for now. A bare await outside async has always been a syntax error in python, so whatever reason the original PR had for not making that use codes.SYNTAX should apply here as well.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think 2 is better, and there's precedent for switching around error codes with missing return statements. I also don't think anyone with authority is going to provide an opinion...

def f() -> str:  # E: Missing return statement  [empty-body]
    pass

def g() -> str:  # E: Missing return statement  [return]
    if True:
        pass

serious=True,
)
elif has_await_expression(expr):
if not self.is_func_scope() or not self.function_stack[-1].is_coroutine:
self.fail(
message_registry.AWAIT_WITH_OUTSIDE_COROUTINE,
expr,
code=codes.AWAIT_NOT_ASYNC,
serious=True,
)

expr.generator.accept(self)

Expand All @@ -6090,7 +6104,12 @@ def visit_set_comprehension(self, expr: SetComprehension) -> None:
def visit_dictionary_comprehension(self, expr: DictionaryComprehension) -> None:
if any(expr.is_async):
if not self.is_func_scope() or not self.function_stack[-1].is_coroutine:
self.fail(message_registry.ASYNC_FOR_OUTSIDE_COROUTINE, expr, code=codes.SYNTAX)
self.fail(
message_registry.ASYNC_FOR_OUTSIDE_COROUTINE,
expr,
code=codes.SYNTAX,
serious=True,
)

with self.enter(expr):
self.analyze_comp_for(expr)
Expand Down Expand Up @@ -6167,9 +6186,12 @@ def visit_await_expr(self, expr: AwaitExpr) -> None:
# This is not a blocker, because some enviroments (like ipython)
# support top level awaits.
self.fail('"await" outside function', expr, serious=True, code=codes.TOP_LEVEL_AWAIT)
elif not self.function_stack[-1].is_coroutine:
elif (
not self.function_stack[-1].is_coroutine
and self.scope_stack[-1] != SCOPE_COMPREHENSION
):
self.fail(
'"await" outside coroutine ("async def")',
message_registry.AWAIT_WITH_OUTSIDE_COROUTINE,
expr,
serious=True,
code=codes.AWAIT_NOT_ASYNC,
Expand Down
16 changes: 15 additions & 1 deletion test-data/unit/check-async-await.test
Original file line number Diff line number Diff line change
Expand Up @@ -1013,13 +1013,17 @@ async def foo(x: int) -> int: ...

# These are allowed in some cases:
top_level = await foo(1) # E: "await" outside function [top-level-await]
crasher = [await foo(x) for x in [1, 2, 3]] # E: "await" outside function [top-level-await]
crasher = [await foo(x) for x in [1, 2, 3]] # E: "await" outside coroutine ("async def") [await-not-async] \
Copy link
Collaborator

Choose a reason for hiding this comment

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

More explicit tests would be really helpful here. Consider at least checking all of the following in different scopes (module level, non-async function body and async function body at least; the former two should produce the same errors and async function should allow all of those):

(await x for x in xs)  # OK
[await x for x in xs]  # E
{await x for x in xs}  # E
{0: await x for x in xs}  # E
{await x: 0 for x in xs}  # E

(x async for x in xs)  # OK
[x async for x in xs]  # E
{x async for x in xs}  # E
{x: 0 async for x in xs}  # E

(x for x in await xs)  # E
[x for x in await xs]  # E
{x for x in await xs}  # E
{x: 0 for x in await xs}  # E

There's already testAsyncForOutsideCoroutine for the 2nd block, the 3rd one is probably not interesting (follows regular rules), but the 1st one isn't exercised yet.

# E: "await" outside function [top-level-await]

def bad() -> None:
# These are always critical / syntax issues:
y = [await foo(x) for x in [1, 2, 3]] # E: "await" outside coroutine ("async def") [await-not-async]
async def good() -> None:
y = [await foo(x) for x in [1, 2, 3]] # OK



[builtins fixtures/async_await.pyi]
[typing fixtures/typing-async.pyi]

Expand Down Expand Up @@ -1080,3 +1084,13 @@ class Launcher(P):

[builtins fixtures/async_await.pyi]
[typing fixtures/typing-async.pyi]

[case testAwaitInsideGeneratorExpr]
def foo():
yield 0

def bar():
(await x for x in foo())

[builtins fixtures/async_await.pyi]
[typing fixtures/typing-async.pyi]
Loading