diff --git a/CHANGELOG.md b/CHANGELOG.md index ac00eac7..5439b1bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog *[CalVer, YY.month.patch](https://calver.org/)* +## 22.8.2 +- Merged TRIO108 into TRIO107 +- TRIO108 now handles checkpointing in async iterators + ## 22.8.1 - Added TRIO109: Async definitions should not have a `timeout` parameter. Use `trio.[fail/move_on]_[at/after]` - Added TRIO110: `while : await trio.sleep()` should be replaced by a `trio.Event`. diff --git a/README.md b/README.md index 517e5dd1..17c8c986 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,8 @@ pip install flake8-trio - **TRIO104**: `Cancelled` and `BaseException` must be re-raised - when a user tries to `return` or `raise` a different exception. - **TRIO105**: Calling a trio async function without immediately `await`ing it. - **TRIO106**: trio must be imported with `import trio` for the linter to work. -- **TRIO107**: Async functions must have at least one checkpoint on every code path, unless an exception is raised. -- **TRIO108**: Early return from async function must have at least one checkpoint on every code path before it, unless an exception is raised. - Checkpoints are `await`, `async with` `async for`. +- **TRIO107**: exit or `return` from async function with no guaranteed checkpoint or exception since function definition. +- **TRIO108**: exit, yield or return from async iterable with no guaranteed checkpoint since possible function entry (yield or function definition) + Checkpoints are `await`, `async for`, and `async with` (on one of enter/exit). - **TRIO109**: Async function definition with a `timeout` parameter - use `trio.[fail/move_on]_[after/at]` instead - **TRIO110**: `while : await trio.sleep()` should be replaced by a `trio.Event`. diff --git a/flake8_trio.py b/flake8_trio.py index 9a222f75..0a67614a 100644 --- a/flake8_trio.py +++ b/flake8_trio.py @@ -14,7 +14,7 @@ from typing import Any, Dict, Iterable, List, NamedTuple, Optional, Tuple, Type, Union # CalVer: YY.month.patch, e.g. first release of July 2022 == "22.7.1" -__version__ = "22.8.1" +__version__ = "22.8.2" class Statement(NamedTuple): @@ -35,8 +35,8 @@ class Statement(NamedTuple): "TRIO104": "Cancelled (and therefore BaseException) must be re-raised", "TRIO105": "trio async function {} must be immediately awaited", "TRIO106": "trio must be imported with `import trio` for the linter to work", - "TRIO107": "Async functions must have at least one checkpoint on every code path, unless an exception is raised", - "TRIO108": "Early return from async function must have at least one checkpoint on every code path before it.", + "TRIO107": "{0} from async function with no guaranteed checkpoint or exception since function definition on line {1.lineno}", + "TRIO108": "{0} from async iterable with no guaranteed checkpoint since {1.name} on line {1.lineno}", "TRIO109": "Async function definition with a `timeout` parameter - use `trio.[fail/move_on]_[after/at]` instead", "TRIO110": "`while : await trio.sleep()` should be replaced by a `trio.Event`.", } @@ -68,6 +68,7 @@ class Flake8TrioVisitor(ast.NodeVisitor): def __init__(self): super().__init__() self._problems: List[Error] = [] + self.suppress_errors = False @classmethod def run(cls, tree: ast.AST) -> Iterable[Error]: @@ -90,9 +91,10 @@ def visit_nodes( visit(node) def error(self, error: str, node: HasLineInfo, *args: Any, **kwargs: Any): - self._problems.append( - make_error(error, node.lineno, node.col_offset, *args, **kwargs) - ) + if not self.suppress_errors: + self._problems.append( + make_error(error, node.lineno, node.col_offset, *args, **kwargs) + ) def get_state(self, *attrs: str) -> Dict[str, Any]: if not attrs: @@ -103,6 +105,10 @@ def set_state(self, attrs: Dict[str, Any]): for attr, value in attrs.items(): setattr(self, attr, value) + def walk(self, *body: ast.AST) -> Iterable[ast.AST]: + for b in body: + yield from ast.walk(b) + class TrioScope: def __init__(self, node: ast.Call, funcname: str, packagename: str): @@ -561,105 +567,251 @@ def visit_Call(self, node: ast.Call): class Visitor107_108(Flake8TrioVisitor): def __init__(self): super().__init__() - self.all_await = True + self.yield_count = 0 + + self.always_checkpoint: Optional[Statement] = None + self.checkpoint_continue: Optional[Statement] = None + self.checkpoint_break: Optional[Statement] = None + + self.default = self.get_state() def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef): - outer = self.all_await + if has_decorator(node.decorator_list, "overload"): + return + + outer = self.get_state() + self.set_state(self.default) + + self.always_checkpoint = Statement("function definition", node.lineno) - # do not require checkpointing if overloading - self.all_await = has_decorator(node.decorator_list, "overload") self.generic_visit(node) + self.check_function_exit(node) - if not self.all_await: - self.error("TRIO107", node) + self.set_state(outer) - self.all_await = outer + def check_function_exit(self, node: Union[ast.Return, ast.AsyncFunctionDef]): + # error if function exits w/o guaranteed checkpoint since function entry + method = "return" if isinstance(node, ast.Return) else "exit" + + if self.always_checkpoint is not None: + if self.yield_count: + self.error("TRIO108", node, method, self.always_checkpoint) + else: + self.error("TRIO107", node, method, self.always_checkpoint) def visit_Return(self, node: ast.Return): self.generic_visit(node) - if not self.all_await: - self.error("TRIO108", node) + self.check_function_exit(node) + # avoid duplicate error messages - self.all_await = True + self.always_checkpoint = None - # disregard raise's in nested functions + # disregard checkpoints in nested function definitions def visit_FunctionDef(self, node: ast.FunctionDef): - outer = self.all_await + outer = self.get_state() + self.set_state(self.default) self.generic_visit(node) - self.all_await = outer + self.set_state(outer) # checkpoint functions - def visit_Await( - self, node: Union[ast.Await, ast.AsyncFor, ast.AsyncWith, ast.Raise] - ): + def visit_Await(self, node: Union[ast.Await, ast.Raise]): + # the expression being awaited is not checkpointed + # so only set checkpoint after the await node self.generic_visit(node) - self.all_await = True - - visit_AsyncFor = visit_Await - visit_AsyncWith = visit_Await + self.always_checkpoint = None # raising exception means we don't need to checkpoint so we can treat it as one visit_Raise = visit_Await - # valid checkpoint if there's valid checkpoints (or raise) in at least one of: - # (try or else) and all excepts - # finally + # guaranteed to checkpoint on at least one of enter and exit + # if it checkpoints on entry and there's a yield in it, we can't treat it as checkpoint + # but it may not checkpoint on entry, so yields inside need to raise problem + def visit_AsyncWith(self, node: ast.AsyncWith): + self.visit_nodes(node.items) + prebody_yield_count = self.yield_count + + # there's no guarantee of checkpoint before entry + self.visit_nodes(node.body) + + # no yield in body, treat as checkpoint + if prebody_yield_count == self.yield_count: + self.always_checkpoint = None + + # error if no checkpoint since earlier yield or function entry + def visit_Yield(self, node: ast.Yield): + self.generic_visit(node) + self.yield_count += 1 + if self.always_checkpoint is not None: + self.error("TRIO108", node, "yield", self.always_checkpoint) + + # mark as requiring checkpoint after + self.always_checkpoint = Statement("yield", node.lineno) + + # valid checkpoint if there's valid checkpoints (or raise) in: + # (try or else) and all excepts, or in finally + # + # try can jump into any except or into the finally* at any point during it's + # execution so we need to make sure except & finally can handle worst-case + # * unless there's a bare except / except BaseException - not implemented. def visit_Try(self, node: ast.Try): - if self.all_await: - self.generic_visit(node) - return + # except & finally guaranteed to enter with checkpoint if checkpointed + # before try and no yield in try body. + body_always_checkpoint = self.always_checkpoint + for inner_node in self.walk(*node.body): + if isinstance(inner_node, ast.Yield): + body_always_checkpoint = Statement("yield", inner_node.lineno) + break # check try body self.visit_nodes(node.body) - body_await = self.all_await - self.all_await = False + + # save state at end of try for entering else + try_checkpoint = self.always_checkpoint # check that all except handlers checkpoint (await or most likely raise) - all_except_await = True + all_except_checkpoint: Optional[Statement] = None for handler in node.handlers: + # enter with worst case of try + self.always_checkpoint = body_always_checkpoint + self.visit_nodes(handler) - all_except_await &= self.all_await - self.all_await = False + + if self.always_checkpoint is not None: + all_except_checkpoint = self.always_checkpoint # check else + # if else runs it's after all of try, so restore state to back then + self.always_checkpoint = try_checkpoint self.visit_nodes(node.orelse) - # (try or else) and all excepts - self.all_await = (body_await or self.all_await) and all_except_await + # checkpoint if else checkpoints, and all excepts checkpoint + if all_except_checkpoint is not None: + self.always_checkpoint = all_except_checkpoint - # finally can check on it's own - self.visit_nodes(node.finalbody) + # if there's no finally, don't restore state from try + if node.finalbody: + # can enter from try, else, or any except + if body_always_checkpoint is not None: + self.always_checkpoint = body_always_checkpoint + self.visit_nodes(node.finalbody) - # valid checkpoint if both body and orelse have checkpoints + # valid checkpoint if both body and orelse checkpoint def visit_If(self, node: Union[ast.If, ast.IfExp]): - if self.all_await: - self.generic_visit(node) - return - - # ignore checkpoints in condition + # visit condition self.visit_nodes(node.test) - self.all_await = False + outer = self.get_state("always_checkpoint") - # check body + # visit body self.visit_nodes(node.body) - body_await = self.all_await - self.all_await = False + body_outer = self.get_state("always_checkpoint") + # reset to after condition and visit orelse + self.set_state(outer) self.visit_nodes(node.orelse) - # checkpoint if both body and else - self.all_await = body_await and self.all_await + # if body failed, reset to that state + if body_outer["always_checkpoint"] is not None: + self.set_state(body_outer) + + # otherwise keep state (fail or not) as it was after orelse # inline if visit_IfExp = visit_If - # ignore checkpoints in loops due to continue/break shenanigans - def visit_While(self, node: Union[ast.While, ast.For]): - outer = self.all_await - self.generic_visit(node) - self.all_await = outer + # Check for yields w/o checkpoint inbetween due to entering loop body the first time, + # after completing all of loop body, and after any continues. + # yield in else have same requirement + # state after the loop same as above, and in addition the state at any break + def visit_loop(self, node: Union[ast.While, ast.For, ast.AsyncFor]): + # save state in case of nested loops + outer = self.get_state( + "checkpoint_continue", "checkpoint_break", "suppress_errors" + ) + + # visit condition + if isinstance(node, ast.While): + self.visit_nodes(node.test) + else: + self.visit_nodes(node.target) + self.visit_nodes(node.iter) + + self.checkpoint_continue = None + pre_body_always_checkpoint = self.always_checkpoint + + # AsyncFor guaranteed checkpoint at every iteration + if isinstance(node, ast.AsyncFor): + pre_body_always_checkpoint = None + self.always_checkpoint = None + + # if we normally enter loop with checkpoint, check for worst-case start of loop + # due to `continue` or multiple iterations + elif self.always_checkpoint is None: + # silently check if body unsets yield + # so we later can check if body errors out on worst case of entering + self.suppress_errors = True + + # self.checkpoint_continue is set to False if loop body ever does + # continue with self.always_checkpoint == False + self.visit_nodes(node.body) + + self.suppress_errors = outer["suppress_errors"] + + if self.checkpoint_continue is not None: + self.always_checkpoint = self.checkpoint_continue + + self.checkpoint_break = None + self.visit_nodes(node.body) + + # AsyncFor guarantees checkpoint on running out of iterable + # so reset checkpoint state at end of loop. (but not state at break) + if isinstance(node, ast.AsyncFor): + self.always_checkpoint = None + else: + # enter orelse with worst case: + # loop body might execute fully before entering orelse + # (current state of self.always_checkpoint) + # or not at all + if pre_body_always_checkpoint is not None: + self.always_checkpoint = pre_body_always_checkpoint + # or at a continue + elif self.checkpoint_continue is not None: + self.always_checkpoint = self.checkpoint_continue + + # visit orelse + self.visit_nodes(node.orelse) + + # We may exit from: + # orelse (which covers no body, body until continue, and all body) + # break + if self.checkpoint_break is not None: + self.always_checkpoint = self.checkpoint_break + + # reset state in case of nested loops + self.set_state(outer) - visit_For = visit_While + visit_While = visit_loop + visit_For = visit_loop + visit_AsyncFor = visit_loop + + # save state in case of continue/break at a point not guaranteed to checkpoint + def visit_Continue(self, node: ast.Continue): + if self.always_checkpoint is not None: + self.checkpoint_continue = self.always_checkpoint + + def visit_Break(self, node: ast.Break): + if self.always_checkpoint is not None: + self.checkpoint_break = self.always_checkpoint + + # first node in a condition is guaranteed to run, but may shortcut so checkpoints + # in remaining nodes are not guaranteed + # Not fully implemented: worst case shortcut with yields in condition + def visit_BoolOp(self, node: ast.BoolOp): + self.visit(node.op) + self.visit_nodes(node.values[:1]) + outer = self.always_checkpoint + self.visit_nodes(node.values[1:]) + + self.always_checkpoint = outer class Plugin: diff --git a/tests/trio107.py b/tests/trio107.py index 6046e65b..4db91b84 100644 --- a/tests/trio107.py +++ b/tests/trio107.py @@ -1,19 +1,23 @@ import typing -from typing import Union, overload +from typing import Any, Union, overload + +import trio _ = "" +# INCLUDE TRIO108 + -async def foo(): +async def foo() -> Any: await foo() -async def foo2(): # error: 0 +async def foo2(): # error: 0, "exit", Statement("function", lineno) ... # If -async def foo_if_1(): # error: 0 +async def foo_if_1(): # error: 0, "exit", Statement("function", lineno) if _: await foo() @@ -31,8 +35,11 @@ async def foo_if_3(): ... -async def foo_if_4(): # error: 0 - if await foo(): +async def foo_if_4(): # safe + await foo() + if ...: + ... + else: ... @@ -41,18 +48,96 @@ async def foo_ifexp_1(): # safe print(await foo() if _ else await foo()) -async def foo_ifexp_2(): # error: 0 - print(_ if await foo() else await foo()) +async def foo_ifexp_2(): # error: 0, "exit", Statement("function", lineno) + print(_ if False and await foo() else await foo()) + + +# nested function definition +async def foo_func_1(): + await foo() + + async def foo_func_2(): # error: 4, "exit", Statement("function", lineno) + ... + + +async def foo_func_3(): # error: 0, "exit", Statement("function", lineno) + async def foo_func_4(): + await foo() + + +async def foo_func_5(): # error: 0, "exit", Statement("function", lineno) + def foo_func_6(): # safe + async def foo_func_7(): # error: 8, "exit", Statement("function", lineno) + ... + + +async def foo_func_8(): # error: 0, "exit", Statement("function", lineno) + def foo_func_9(): + raise + + +# normal function +def foo_normal_func_1(): + return + + +def foo_normal_func_2(): + ... + + +# overload decorator +@overload +async def foo_overload_1(_: bytes): + ... + + +@typing.overload +async def foo_overload_1(_: str): + ... + + +async def foo_overload_1(_: Union[bytes, str]): + await foo() + + +# conditions +async def foo_condition_1(): # safe + if await foo(): + ... + + +async def foo_condition_2(): # error: 0, "exit", Statement("function", lineno) + if False and await foo(): + ... + + +async def foo_condition_3(): # error: 0, "exit", Statement("function", lineno) + if ... and await foo(): + ... + + +async def foo_condition_4(): # safe + while await foo(): + ... + + +async def foo_condition_5(): # safe + for i in await foo(): + ... + + +async def foo_condition_6(): # in theory error, but not worth parsing + for i in (None, await foo()): + break # loops -async def foo_while_1(): # error: 0 +async def foo_while_1(): # error: 0, "exit", Statement("function", lineno) while _: await foo() -# due to not wanting to handle continue/break semantics -async def foo_while_2(): # error: 0 +async def foo_while_2(): # now safe while _: await foo() else: @@ -65,23 +150,107 @@ async def foo_while_3(): # safe ... -async def foo_for_1(): # error: 0 - for __ in _: +# for +async def foo_for_1(): # error: 0, "exit", Statement("function", lineno) + for _ in "": + await foo() + + +async def foo_for_2(): # now safe + for _ in "": + await foo() + else: + await foo() + + +async def foo_while_break_1(): # safe + while ...: + await foo() + break + else: + await foo() + + +async def foo_while_break_2(): # error: 0, "exit", Statement("function", lineno) + while ...: + break + else: + await foo() + + +async def foo_while_break_3(): # error: 0, "exit", Statement("function", lineno) + while ...: + await foo() + break + else: + ... + + +async def foo_while_break_4(): # error: 0, "exit", Statement("function", lineno) + while ...: + break + else: + ... + + +async def foo_while_continue_1(): # safe + while ...: + await foo() + continue + else: + await foo() + + +async def foo_while_continue_2(): # safe + while ...: + continue + else: await foo() -# due to not wanting to handle continue/break semantics -async def foo_for_2(): # error: 0 - for __ in _: +async def foo_while_continue_3(): # error: 0, "exit", Statement("function", lineno) + while ...: await foo() + continue + else: + ... + + +async def foo_while_continue_4(): # error: 0, "exit", Statement("function", lineno) + while ...: + continue else: + ... + + +async def foo_async_for_1(): + async for _ in trio.trick_pyright: + ... + + +# async with +# async with guarantees checkpoint on at least one of entry or exit +async def foo_async_with(): + async with trio.trick_pyright: + ... + + +# raise +async def foo_raise_1(): # safe + raise ValueError() + + +async def foo_raise_2(): # safe + if _: await foo() + else: + raise ValueError() # try # safe only if (try or else) and all except bodies either await or raise # if foo() raises a ValueError it's not checkpointed -async def foo_try_1(): # error: 0 +async def foo_try_1(): # error: 0, "exit", Statement("function", lineno) try: await foo() except ValueError: @@ -112,18 +281,6 @@ async def foo_try_3(): # safe raise -# raise -async def foo_raise_1(): # safe - raise ValueError() - - -async def foo_raise_2(): # safe - if _: - await foo() - else: - raise ValueError() - - async def foo_try_4(): # safe try: ... @@ -135,66 +292,46 @@ async def foo_try_4(): # safe await foo() -# early return -async def foo_return_1(): # silent to avoid duplicate errors - return # TRIO108 - - -async def foo_return_2(): # safe - if _: - return # TRIO108 +async def foo_try_5(): # safe await foo() + try: + pass + except: + pass + else: + pass -async def foo_return_3(): # error: 0 - if _: - await foo() - return # safe - - -# nested function definition -async def foo_func_1(): - await foo() - - async def foo_func_2(): # error: 4 - ... +async def foo_try_6(): # error: 0, "exit", Statement("function", lineno) + try: + pass + except: + pass + else: + pass -async def foo_func_3(): # error: 0 - async def foo_func_4(): +async def foo_try_7(): # safe + try: await foo() + except: + await foo() + else: + pass -async def foo_func_5(): # error: 0 - def foo_func_6(): # safe - async def foo_func_7(): # error: 8 - ... - - -async def foo_func_8(): # error: 0 - def foo_func_9(): - raise - - -# normal function -def foo_normal_func_1(): - return - - -def foo_normal_func_2(): - ... - - -# overload decorator -@overload -async def foo_overload_1(_: bytes): - ... +# early return +async def foo_return_1(): + return # error: 4, "return", Statement("function definition", lineno-1) -@typing.overload -async def foo_overload_1(_: str): - ... +async def foo_return_2(): # safe + if _: + return # error: 8, "return", Statement("function definition", lineno-2) + await foo() -async def foo_overload_1(_: Union[bytes, str]): - await foo() +async def foo_return_3(): # error: 0, "exit", Statement("function", lineno) + if _: + await foo() + return # safe diff --git a/tests/trio108.py b/tests/trio108.py index c7333f40..38478380 100644 --- a/tests/trio108.py +++ b/tests/trio108.py @@ -1,22 +1,579 @@ +import contextlib +import contextlib as anything +from contextlib import asynccontextmanager, contextmanager +from typing import Any + +import trio + _ = "" +# INCLUDE TRIO107 + + +async def foo() -> Any: + await foo() + + +async def foo_yield_1(): + await foo() + yield 5 + await foo() + + +async def foo_yield_2(): + yield # error: 4, "yield", Statement("function definition", lineno-1) + yield # error: 4, "yield", Statement("yield", lineno-1) + await foo() + + +async def foo_yield_3(): # error: 0, "exit", Statement("yield", lineno+2) + await foo() + yield + + +async def foo_yield_4(): # error: 0, "exit", Statement("yield", lineno+3) + yield # error: 4, "yield", Statement("function definition", lineno-1) + await (yield) # error: 11, "yield", Statement("yield", lineno-1) + yield # safe + + +async def foo_yield_return_1(): + yield # error: 4, "yield", Statement("function definition", lineno-1) + return # error: 4, "return", Statement("yield", lineno-1) + + +async def foo_yield_return_2(): + await foo() + yield + return # error: 4, "return", Statement("yield", lineno-1) + + +async def foo_yield_return_3(): + await foo() + yield + await foo() + return + + +# async with +# async with guarantees checkpoint on at least one of entry or exit +async def foo_async_with(): # error: 0, "exit", Statement("yield", lineno+2) + async with trio.fail_after(5): + yield # error: 8, "yield", Statement("function definition", lineno-2) + + +# fmt: off +async def foo_async_with_2(): # error: 0, "exit", Statement("yield", lineno+4) + # with'd expression evaluated before checkpoint + async with (yield): # error: 16, "yield", Statement("function definition", lineno-2) + # not guaranteed that async with checkpoints on entry (or is that only for trio?) + yield # error: 8, "yield", Statement("yield", lineno-2) +# fmt: on + + +async def foo_async_with_3(): # error: 0, "exit", Statement("yield", lineno+3) + async with trio.fail_after(5): + ... + yield # safe + + +async def foo_async_with_4(): # error: 0, "exit", Statement("yield", lineno+4) + async with trio.fail_after(5): + yield # error: 8, "yield", Statement("function definition", lineno-2) + await foo() + yield + + +async def foo_async_with_5(): # error: 0, "exit", Statement("yield", lineno+3) + async with trio.fail_after(5): + yield # error: 8, "yield", Statement("function definition", lineno-2) + yield # error: 4, "yield", Statement("yield", lineno-1) + + +# async for +async def foo_async_for(): # error: 0, "exit", Statement("yield", lineno+6) + async for i in ( + yield # error: 8, "yield", Statement("function definition", lineno-2) + ): + yield # safe + else: + yield # safe + + +# await anext(iter) is not called on break +async def foo_async_for_2(): # error: 0, "exit", Statement("yield", lineno+2) + async for i in trio.trick_pyright: + yield + if ...: + break + + +async def foo_async_for_3(): # safe + async for i in trio.trick_pyright: + yield + + +async def foo_async_for_4(): # safe + async for i in trio.trick_pyright: + yield + continue + + +# for +async def foo_for(): # error: 0, "exit", Statement("yield", lineno+3) + await foo() + for i in "": + yield # error: 8, "yield", Statement("yield", lineno) + + +async def foo_for_1(): # error: 0, "exit", Statement("function definition", lineno) + for _ in "": + await foo() + yield + + +# while + +# safe if checkpoint in else +async def foo_while_1(): # error: 0, "exit", Statement("yield", lineno+5) + while ...: + ... + else: + await foo() # will always run + yield # safe + + +# simple yield-in-loop case +async def foo_while_2(): # error: 0, "exit", Statement("yield", lineno+3) + await foo() + while ...: + yield # error: 8, "yield", Statement("yield", lineno) + + +# multiple errors: 8, "yield", Statement("yield", lineno-2) +# no checkpoint after yield if else is entered +async def foo_while_3(): # error: 0, "exit", Statement("yield", lineno+5) + while ...: + await foo() + yield + else: + yield # error: 8, "yield", Statement("function definition", lineno-5) + + +# should raise multiple errors +# check that errors are suppressed in visit_While +async def foo_while_4(): # error: 0, "exit", Statement("yield", lineno+3) + await foo() + while ...: + yield # error: 8, "yield", Statement("yield", lineno) + while ...: + yield # error: 12, "yield", Statement("yield", lineno-2) + while ...: + yield # error: 16, "yield", Statement("yield", lineno-2) + + +# check error suppression is reset +async def foo_while_5(): + await foo() + while ...: + yield # error: 8, "yield", Statement("yield", lineno) + + async def foo_nested_error(): # error: 8, "exit", Statement("yield", lineno+1)# error: 8, "exit", Statement("yield", lineno+1) + yield # error: 12, "yield", Statement("function definition", lineno-1)# error: 12, "yield", Statement("function definition", lineno-1) + + await foo() + + +# --- while + continue --- +# no checkpoint on continue +async def foo_while_continue_1(): # error: 0, "exit", Statement("yield", lineno+3) + await foo() + while ...: + yield # error: 8, "yield", Statement("yield", lineno) + if ...: + continue + await foo() + + +# multiple continues +async def foo_while_continue_2(): # error: 0, "exit", Statement("yield", lineno+3) + await foo() + while ...: + yield # error: 8, "yield", Statement("yield", lineno) + if ...: + continue + await foo() + if ...: + continue + while ...: + yield # safe + await foo() + + +# --- while + break --- +# else might not run +async def foo_while_break_1(): # error: 0, "exit", Statement("yield", lineno+6) + while ...: + if ...: + break + else: + await foo() + yield # error: 4, "yield", Statement("function definition", lineno-6) + + +# no checkpoint on break +async def foo_while_break_2(): # error: 0, "exit", Statement("yield", lineno+3) + await foo() + while ...: + yield # safe + if ...: + break + await foo() + + +# guaranteed if else and break +async def foo_while_break_3(): # error: 0, "exit", Statement("yield", lineno+7) + while ...: + await foo() + if ...: + break # if it breaks, have checkpointed + else: + await foo() # runs if 0-iter + yield # safe + + +# break at non-guaranteed checkpoint +async def foo_while_break_4(): # error: 0, "exit", Statement("yield", lineno+7) + while ...: + if ...: + break + await foo() # might not run + else: + await foo() # might not run + yield # error: 4, "yield", Statement("function definition", lineno-7) + + +# check break is reset on nested +async def foo_while_break_5(): # error: 0, "exit", Statement("yield", lineno+12) + await foo() + while ...: + yield + if ...: + break + await foo() + while ...: + yield # safe + await foo() + yield # safe + await foo() + yield # error: 4, "yield", Statement("yield", lineno-9) + + +# check multiple breaks +async def foo_while_break_6(): # error: 0, "exit", Statement("yield", lineno+11) + await foo() + while ...: + yield + if ...: + break + await foo() + yield + await foo() + if ...: + break + yield # error: 4, "yield", Statement("yield", lineno-8) + -async def foo(): +async def foo_while_break_7(): # error: 0, "exit", Statement("yield", lineno+5) + while ...: + await foo() + if ...: + break + yield + break + + +# try +async def foo_try_1(): # error: 0, "exit", Statement("yield", lineno+2) + try: + yield # error: 8, "yield", Statement("function definition", lineno-2) + except: + pass + + +# no checkpoint after yield in ValueError +async def foo_try_2(): # error: 0, "exit", Statement("yield", lineno+5) + try: + await foo() + except ValueError: + # try might not have checkpointed + yield # error: 8, "yield", Statement("function definition", lineno-5) + except: + await foo() + else: + pass + + +async def foo_try_3(): # error: 0, "exit", Statement("yield", lineno+6) + try: + ... + except: + await foo() + else: + yield # error: 8, "yield", Statement("function definition", lineno-6) + + +async def foo_try_4(): # safe + try: + ... + except: + yield # error: 8, "yield", Statement("function definition", lineno-4) + finally: + await foo() + + +async def foo_try_5(): + try: + await foo() + finally: + # try might crash before checkpoint + yield # error: 8, "yield", Statement("function definition", lineno-5) + await foo() + + +async def foo_try_6(): # error: 0, "exit", Statement("yield", lineno+5) + try: + await foo() + except ValueError: + pass + yield # error: 4, "yield", Statement("function definition", lineno-5) + + +async def foo_try_7(): # error: 0, "exit", Statement("yield", lineno+16) + await foo() + try: + yield + await foo() + except ValueError: + await foo() + yield + await foo() + except SyntaxError: + yield # error: 8, "yield", Statement("yield", lineno-7) + await foo() + finally: + pass + # If the try raises an exception without checkpointing, and it's not caught + # by any of the excepts, jumping straight to the finally. + yield # error: 4, "yield", Statement("yield", lineno-13) + + +## safe only if (try or else) and all except bodies either await or raise +## if foo() raises a ValueError it's not checkpointed +# Should raise multiple errors +async def foo_try_8(): # error: 0, "exit", Statement("yield", lineno+3) + try: + await foo() + yield + await foo() + except ValueError: + ... + except: + raise + else: + await foo() + + +# no checkpoint after yield in else +async def foo_try_9(): # error: 0, "exit", Statement("yield", lineno+6) + try: + await foo() + except: + await foo() + else: + yield + + +# in theory safe, but not currently handled +async def foo_try_10(): + try: + await foo() + except: + await foo() + finally: + yield # error: 8, "yield", Statement("function definition", lineno-6) + await foo() + + +# if +async def foo_if_1(): + if ...: + yield # error: 8, "yield", Statement("function definition", lineno-2) + await foo() + else: + yield # error: 8, "yield", Statement("function definition", lineno-5) + await foo() + + +async def foo_if_2(): # error: 0, "exit", Statement("yield", lineno+6) + await foo() + if ...: + ... + else: + yield + yield # error: 4, "yield", Statement("yield", lineno-1) + + +async def foo_if_3(): # error: 0, "exit", Statement("yield", lineno+6) await foo() + if ...: + yield + else: + ... + yield # error: 4, "yield", Statement("yield", lineno-3) -# early return -async def foo_return_1(): # silent to avoid duplicate errors - return # error: 4 +async def foo_if_4(): # error: 0, "exit", Statement("yield", lineno+7) + await foo() + yield + if ...: + await foo() + else: + ... + yield # error: 4, "yield", Statement("yield", lineno-5) + + +async def foo_if_5(): # error: 0, "exit", Statement("yield", lineno+8) + await foo() + if ...: + yield + await foo() + else: + yield + ... + yield # error: 4, "yield", Statement("yield", lineno-2) + + +async def foo_if_6(): # error: 0, "exit", Statement("yield", lineno+8) + await foo() + if ...: + yield + else: + yield + await foo() + ... + yield # error: 4, "yield", Statement("yield", lineno-5) + + +async def foo_if_14(): # error: 0, "exit", Statement("function definition", lineno) + if ...: + await foo() + yield + await foo() + + +async def foo_if_15(): # error: 0, "exit", Statement("function definition", lineno) + if ...: + ... + else: + await foo() + yield + await foo() + + +# IfExp +# should raise multiple +async def foo_ifexp_1(): # error: 0, "exit", Statement("yield", lineno+1) + print((yield) if await foo() else (yield)) + + +# should raise multiple +async def foo_ifexp_2(): # error: 0, "exit", Statement("yield", lineno+2) + print( + (yield) # error: 9, "yield", Statement("function definition", lineno-2) + if False and await foo() + else await foo() + ) + + +# normal function +def foo_normal_func_1(): + return + +def foo_normal_func_2(): + ... -async def foo_return_2(): # safe - if _: - return # error: 8 + +def foo_normal_func_3(): + yield + + +# nested function definition +async def foo_func_1(): await foo() + async def foo_func_2(): # error: 4, "exit", Statement("yield", lineno+1) + yield # error: 8, "yield", Statement("function definition", lineno-1) + -async def foo_return_3(): # TRIO103 - if _: +async def foo_func_3(): # error: 0, "exit", Statement("yield", lineno+2) + await foo() + yield + + async def foo_func_4(): await foo() - return # safe + + +async def foo_func_5(): # error: 0, "exit", Statement("yield", lineno+2) + await foo() + yield + + def foo_func_6(): # safe + yield + + async def foo_func_7(): + await foo() + ... + + +# async def foo_multiple_1(): # error: 0, "exit", Statement("yield", lineno+2) # error: 0, "exit", Statement("function definition", lineno) +# if ...: +# yield # error: 8, "yield", Statement("function definition", lineno-2) + + +# TODO: in theory there's a guaranteed checkpoint in case `if` isn't entered +@asynccontextmanager +async def foo_cm_1(): # error: 0, "exit", Statement("yield", lineno+2) + if ...: + yield # error: 8, "yield", Statement("function definition", lineno-2) + + +@contextlib.asynccontextmanager +async def foo_cm_2(): # error: 0, "exit", Statement("yield", lineno+2) + if ...: + yield # error: 8, "yield", Statement("function definition", lineno-2) + + +@anything.asynccontextmanager +async def foo_cm_3(): # error: 0, "exit", Statement("yield", lineno+2) + if ...: + yield # error: 8, "yield", Statement("function definition", lineno-2) + + +@contextmanager +def foo_cm_4(): + if ...: + yield + + +@contextlib.contextmanager +def foo_cm_5(): + if ...: + yield + + +@anything.contextmanager +def foo_cm_6(): + if ...: + yield