From 220c40570c4dd613c6d608da592ff28f07e811e8 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Wed, 15 Mar 2023 12:45:51 +0100 Subject: [PATCH] implement autofix for 91x --- CONTRIBUTING.md | 3 + flake8_trio/runner.py | 16 +- flake8_trio/visitors/__init__.py | 11 +- flake8_trio/visitors/flake8triovisitor.py | 10 + flake8_trio/visitors/helpers.py | 8 + flake8_trio/visitors/visitor91x.py | 287 ++++- flake8_trio/visitors/visitor_utility.py | 26 +- tests/autofix_files/trio100.py.diff | 86 ++ .../trio100_simple_autofix.py.diff | 86 ++ tests/autofix_files/trio910.py | 584 +++++++++ tests/autofix_files/trio910.py.diff | 207 +++ tests/autofix_files/trio911.py | 1147 +++++++++++++++++ tests/autofix_files/trio911.py.diff | 782 +++++++++++ tests/autofix_files/trio91x_autofix.py | 131 ++ tests/autofix_files/trio91x_autofix.py.diff | 80 ++ tests/eval_files/trio910.py | 4 + tests/eval_files/trio911.py | 31 + tests/eval_files/trio91x_autofix.py | 116 ++ tests/test_flake8_trio.py | 108 +- 19 files changed, 3656 insertions(+), 67 deletions(-) create mode 100644 tests/autofix_files/trio100.py.diff create mode 100644 tests/autofix_files/trio100_simple_autofix.py.diff create mode 100644 tests/autofix_files/trio910.py create mode 100644 tests/autofix_files/trio910.py.diff create mode 100644 tests/autofix_files/trio911.py create mode 100644 tests/autofix_files/trio911.py.diff create mode 100644 tests/autofix_files/trio91x_autofix.py create mode 100644 tests/autofix_files/trio91x_autofix.py.diff create mode 100644 tests/eval_files/trio91x_autofix.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eb0b4f1b..8abd2cc1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,6 +32,9 @@ To check that all codes are tested and documented there's a test that error code ## Test generator Tests are automatically generated for files in the `tests/eval_files/` directory, with the code that it's testing interpreted from the file name. The file extension is split off, if there's a match for for `_py\d*` it strips that off and uses it to determine if there's a minimum python version for which the test should only run. +### autofix files +Checks that have autofixing can have a file in the `tests/autofix_files` directory matching the filename in `tests/eval_files`. The result of running the checker on the eval file with autofix enabled will then be compared to the content of the autofix file and will print a diff (if `-s` is on) and assert that the content is the same. `--generate-autofix` is added as a pytest flag to ease development, which will print a diff (with `-s`) and overwrite the content of the autofix file. Also see the magic line marker `pass # AUTOFIX_LINE ` below + ### `error:` Lines containing `error:` are parsed as expecting an error of the code matching the file name, with everything on the line after the colon `eval`'d and passed as arguments to `flake8_trio.Error_codes[].str_format`. The `globals` argument to `eval` contains a `lineno` variable assigned the current line number, and the `flake8_trio.Statement` namedtuple. The first element after `error:` *must* be an integer containing the column where the error on that line originates. #### `TRIOxxx:` diff --git a/flake8_trio/runner.py b/flake8_trio/runner.py index 945281ec..b39eb454 100644 --- a/flake8_trio/runner.py +++ b/flake8_trio/runner.py @@ -13,7 +13,12 @@ import libcst as cst -from .visitors import ERROR_CLASSES, ERROR_CLASSES_CST, utility_visitors +from .visitors import ( + ERROR_CLASSES, + ERROR_CLASSES_CST, + utility_visitors, + utility_visitors_cst, +) if TYPE_CHECKING: from argparse import Namespace @@ -104,6 +109,13 @@ def __init__(self, options: Namespace, module: Module): super().__init__() self.state = SharedState(options) self.options = options + + # Could possibly enable/disable utility visitors here, if visitors declared + # dependencies + self.utility_visitors: tuple[Flake8TrioVisitor_cst, ...] = tuple( + v(self.state) for v in utility_visitors_cst + ) + self.visitors: tuple[Flake8TrioVisitor_cst, ...] = tuple( v(self.state) for v in ERROR_CLASSES_CST if self.selected(v.error_codes) ) @@ -112,7 +124,7 @@ def __init__(self, options: Namespace, module: Module): def run(self) -> Iterable[Error]: if not self.visitors: return - for v in self.visitors: + for v in (*self.utility_visitors, *self.visitors): self.module = cst.MetadataWrapper(self.module).visit(v) yield from self.state.problems diff --git a/flake8_trio/visitors/__init__.py b/flake8_trio/visitors/__init__.py index 133ea177..6aa2fe77 100644 --- a/flake8_trio/visitors/__init__.py +++ b/flake8_trio/visitors/__init__.py @@ -12,11 +12,18 @@ if TYPE_CHECKING: from .flake8triovisitor import Flake8TrioVisitor, Flake8TrioVisitor_cst -__all__ = ["ERROR_CLASSES", "default_disabled_error_codes", "utility_visitors"] +__all__ = [ + "ERROR_CLASSES", + "ERROR_CLASSES_CST", + "default_disabled_error_codes", + "utility_visitors", + "utility_visitors_cst", +] ERROR_CLASSES: set[type[Flake8TrioVisitor]] = set() ERROR_CLASSES_CST: set[type[Flake8TrioVisitor_cst]] = set() -utility_visitors: set[type[Flake8TrioVisitor]] = set() default_disabled_error_codes: list[str] = [] +utility_visitors: set[type[Flake8TrioVisitor]] = set() +utility_visitors_cst: set[type[Flake8TrioVisitor_cst]] = set() # Import all visitors so their decorators run, filling the above containers # This has to be done at the end to avoid circular imports diff --git a/flake8_trio/visitors/flake8triovisitor.py b/flake8_trio/visitors/flake8triovisitor.py index c664cf5c..4e51fedf 100644 --- a/flake8_trio/visitors/flake8triovisitor.py +++ b/flake8_trio/visitors/flake8triovisitor.py @@ -227,3 +227,13 @@ def error( *args, ) ) + + @property + def library(self) -> tuple[str, ...]: + return self.__state.library if self.__state.library else ("trio",) + + # library_str not used in cst yet + + def add_library(self, name: str) -> None: + if name not in self.__state.library: + self.__state.library = self.__state.library + (name,) diff --git a/flake8_trio/visitors/helpers.py b/flake8_trio/visitors/helpers.py index 70c9cf2a..b309d4ed 100644 --- a/flake8_trio/visitors/helpers.py +++ b/flake8_trio/visitors/helpers.py @@ -19,6 +19,7 @@ ERROR_CLASSES_CST, default_disabled_error_codes, utility_visitors, + utility_visitors_cst, ) if TYPE_CHECKING: @@ -58,6 +59,13 @@ def utility_visitor(c: type[T]) -> type[T]: return c +def utility_visitor_cst(c: type[T_CST]) -> type[T_CST]: + assert not hasattr(c, "error_codes") + c.error_codes = {} + utility_visitors_cst.add(c) + return c + + def _get_identifier(node: ast.expr) -> str: if isinstance(node, ast.Name): return node.id diff --git a/flake8_trio/visitors/visitor91x.py b/flake8_trio/visitors/visitor91x.py index 7f8a81d2..64d24d15 100644 --- a/flake8_trio/visitors/visitor91x.py +++ b/flake8_trio/visitors/visitor91x.py @@ -8,7 +8,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Any +from typing import TYPE_CHECKING, Any import libcst as cst import libcst.matchers as m @@ -24,6 +24,10 @@ iter_guaranteed_once_cst, ) +if TYPE_CHECKING: + from collections.abc import Sequence + + ARTIFICIAL_STATEMENT = Statement("artificial", -1) @@ -46,8 +50,10 @@ class LoopState: uncheckpointed_before_continue: set[Statement] = field(default_factory=set) uncheckpointed_before_break: set[Statement] = field(default_factory=set) - artificial_errors: set[cst.Return | cst.FunctionDef | cst.Yield] = field( - default_factory=set + + artificial_errors: set[cst.Return | cst.Yield] = field(default_factory=set) + nodes_needing_checkpoints: list[cst.Return | cst.Yield] = field( + default_factory=list ) def copy(self): @@ -58,6 +64,7 @@ def copy(self): uncheckpointed_before_continue=self.uncheckpointed_before_continue.copy(), uncheckpointed_before_break=self.uncheckpointed_before_break.copy(), artificial_errors=self.artificial_errors.copy(), + nodes_needing_checkpoints=self.nodes_needing_checkpoints.copy(), ) @@ -77,6 +84,12 @@ def copy(self): ) +def checkpoint_statement(library: str) -> cst.SimpleStatementLine: + return cst.SimpleStatementLine( + [cst.Expr(cst.parse_expression(f"await {library}.lowlevel.checkpoint()"))] + ) + + @error_class_cst @disabled_by_default class Visitor91X(Flake8TrioVisitor_cst): @@ -99,6 +112,18 @@ def __init__(self, *args: Any, **kwargs: Any): self.uncheckpointed_statements: set[Statement] = set() self.comp_unknown = False + # these are file-wide, so intentionally not save-stated upon entry/exit + # of functions/loops/etc + self.explicitly_imported_library: dict[str, bool] = { + "trio": False, + "anyio": False, + } + self.add_import: set[str] = set() + + # this one is not save-stated, but I fail to come up with any scenario + # where that matters + self.add_statement: cst.SimpleStatementLine | None = None + self.loop_state = LoopState() self.try_state = TryState() @@ -142,13 +167,9 @@ def uncheckpointed_before_break(self) -> set[Statement]: def uncheckpointed_before_break(self, value: set[Statement]): self.loop_state.uncheckpointed_before_break = value - @property - def artificial_errors(self) -> set[cst.Return | cst.FunctionDef | cst.Yield]: - return self.loop_state.artificial_errors - - @artificial_errors.setter - def artificial_errors(self, value: set[cst.Return | cst.FunctionDef | cst.Yield]): - self.loop_state.artificial_errors = value # pragma: no cover + def checkpoint_statement(self) -> cst.SimpleStatementLine: + self.ensure_imported_library() + return checkpoint_statement(self.library[0]) def visit_FunctionDef(self, node: cst.FunctionDef) -> None: # don't lint functions whose bodies solely consist of pass or ellipsis @@ -186,38 +207,112 @@ def visit_FunctionDef(self, node: cst.FunctionDef) -> None: def leave_FunctionDef( self, original_node: cst.FunctionDef, updated_node: cst.FunctionDef ) -> cst.FunctionDef: - if self.async_function: - # updated_node does not have a Position, so we must send original_node - self.check_function_exit(original_node) + if ( + self.async_function + # updated_node does not have a position, so we must send original_node + and self.check_function_exit(original_node) + and self.options.autofix + and isinstance(updated_node.body, cst.IndentedBlock) + ): + # insert checkpoint at the end of body + new_body = list(updated_node.body.body) + new_body.append(self.checkpoint_statement()) + indentedblock = updated_node.body.with_changes(body=new_body) + updated_node = updated_node.with_changes(body=indentedblock) + self.restore_state(original_node) - return updated_node + return updated_node # noqa: R504 + + # error if function exit/return/yields with uncheckpointed statements + # returns a bool indicating if any real (i.e. not artificial) errors were raised + # so caller can insert checkpoint before statement (if yield/return) or at end + # of body (functiondef) + def check_function_exit( + self, + original_node: cst.FunctionDef | cst.Return | cst.Yield, + ) -> bool: + if not self.uncheckpointed_statements: + return False - # error if function exits or returns with uncheckpointed statements - def check_function_exit(self, node: cst.Return | cst.FunctionDef): + # Artificial statement is injected in visit_While_body to make sure errors + # are raised on multiple loops, if e.g. the end of a loop is uncheckpointed. + if ARTIFICIAL_STATEMENT in self.uncheckpointed_statements: + # function can't end in the middle of a loop body, where artificial + # statements are injected + assert not isinstance(original_node, cst.FunctionDef) + + # Add it to artificial errors, so loop logic can later turn it into + # a real error if needed. + self.loop_state.artificial_errors.add(original_node) + + # Add this as a node potentially needing checkpoints only if it + # missing checkpoints solely depends on whether the artificial statement is + # "real" + if len(self.uncheckpointed_statements) == 1: + self.loop_state.nodes_needing_checkpoints.append(original_node) + return False + + # raise the actual errors for statement in self.uncheckpointed_statements: - self.error_91x(node, statement) + if statement == ARTIFICIAL_STATEMENT: + continue + self.error_91x(original_node, statement) + + return True def leave_Return( self, original_node: cst.Return, updated_node: cst.Return ) -> cst.Return: if not self.async_function: return updated_node - self.check_function_exit(original_node) + if self.check_function_exit(original_node): + self.add_statement = self.checkpoint_statement() # avoid duplicate error messages self.uncheckpointed_statements = set() + # return original node to avoid problems with identity equality + assert original_node.deep_equals(updated_node) + return original_node + + # TODO: generate an error in these two if transforming+visiting is done in a single + # pass and emit-error-on-transform can be enabled/disabled. The error can't be + # generated in the yield/return since it doesn't know if it will be autofixed. + + # SimpleStatementSuite and multi-statement lines can probably be autofixed, but + # for now just don't insert checkpoints in the wrong place. + def leave_SimpleStatementSuite( + self, + original_node: cst.SimpleStatementSuite, + updated_node: cst.SimpleStatementSuite, + ) -> cst.SimpleStatementSuite: + self.add_statement = None return updated_node + # insert checkpoint before yield with a flattensentinel, if indicated + def leave_SimpleStatementLine( + self, + original_node: cst.SimpleStatementLine, + updated_node: cst.SimpleStatementLine, + ) -> cst.SimpleStatementLine | cst.FlattenSentinel[cst.SimpleStatementLine]: + if self.add_statement is None: + return updated_node + + # multiple statements on a single line is not handled + if len(updated_node.body) > 1: + self.add_statement = None + return updated_node + + res = cst.FlattenSentinel([self.add_statement, updated_node]) + self.add_statement = None + return res # noqa: R504 + def error_91x( - self, node: cst.Return | cst.FunctionDef | cst.Yield, statement: Statement + self, + node: cst.Return | cst.FunctionDef | cst.Yield, + statement: Statement, ): - # artificial statement is injected in visit_While_body to make sure errors - # are raised on multiple loops, if e.g. the end of a loop is uncheckpointed. - # Here we add it to artificial errors, so loop logic can later turn it into - # a real error if needed. - if statement == ARTIFICIAL_STATEMENT: - self.loop_state.artificial_errors.add(node) - return + assert statement != ARTIFICIAL_STATEMENT + if isinstance(node, cst.FunctionDef): msg = "exit" else: @@ -260,13 +355,16 @@ def leave_Yield( if not self.async_function: return updated_node self.has_yield = True - for statement in self.uncheckpointed_statements: - self.error_91x(original_node, statement) + + if self.check_function_exit(original_node): + self.add_statement = self.checkpoint_statement() # mark as requiring checkpoint after pos = self.get_metadata(PositionProvider, original_node).start self.uncheckpointed_statements = {Statement("yield", pos.line, pos.column)} - return updated_node + # return original to avoid problems with identity equality + assert original_node.deep_equals(updated_node) + return original_node # valid checkpoint if there's valid checkpoints (or raise) in: # (try or else) and all excepts, or in finally @@ -283,6 +381,7 @@ def visit_Try(self, node: cst.Try): self.try_state.body_uncheckpointed_statements = ( self.uncheckpointed_statements.copy() ) + # yields inside `try` can always be uncheckpointed for inner_node in m.findall(node.body, m.Yield()): pos = self.get_metadata(PositionProvider, inner_node).start self.try_state.body_uncheckpointed_statements.add( @@ -394,6 +493,8 @@ def visit_While(self, node: cst.While | cst.For): self.loop_state = LoopState() self.infinite_loop = self.body_guaranteed_once = False + visit_For = visit_While + # Check for yields w/o checkpoint in between due to entering loop body the first time, # after completing all of loop body, and after any continues. # yield in else have same requirement @@ -409,7 +510,7 @@ def visit_While_test(self, node: cst.While): def visit_For_iter(self, node: cst.For): self.body_guaranteed_once = iter_guaranteed_once_cst(node.iter) - def visit_While_body(self, node: cst.While | cst.For): + def visit_While_body(self, node: cst.For | cst.While): if not self.async_function: return @@ -432,23 +533,32 @@ def visit_While_body(self, node: cst.While | cst.For): visit_For_body = visit_While_body - def leave_While_body(self, node: cst.While | cst.For): + def leave_While_body(self, node: cst.For | cst.While): if not self.async_function: return + # if there's errors due to the artificial statement # raise a real error for each statement in outer[uncheckpointed_statements], - # uncheckpointed_before_continue, and uncheckpointed_before_break - new_uncheckpointed_statements = ( - self.outer[node]["uncheckpointed_statements"] - | self.uncheckpointed_statements - ) - for err_node in self.artificial_errors: + # uncheckpointed_before_continue, and checkpoints at the end of the loop + any_error = False + for err_node in self.loop_state.artificial_errors: for stmt in ( - new_uncheckpointed_statements | self.uncheckpointed_before_continue + self.outer[node]["uncheckpointed_statements"] + | self.uncheckpointed_statements + | self.uncheckpointed_before_continue ): + if stmt == ARTIFICIAL_STATEMENT: + continue + any_error = True self.error_91x(err_node, stmt) - # replace artificial in break with prebody uncheckpointed statements + # if there's no errors from artificial statements, we don't need to insert + # the potential checkpoints + if not any_error: + self.loop_state.nodes_needing_checkpoints = [] + + # replace artificial statements in else with prebody uncheckpointed statements + # non-artificial stmts before continue/break/at body end will already be in them for stmts in ( self.uncheckpointed_before_continue, self.uncheckpointed_before_break, @@ -479,7 +589,7 @@ def leave_While_body(self, node: cst.While | cst.For): leave_For_body = leave_While_body - def leave_While_orelse(self, node: cst.While | cst.For): + def leave_While_orelse(self, node: cst.For | cst.While): if not self.async_function: return # if this is an infinite loop, with no break in it, don't raise @@ -494,10 +604,31 @@ def leave_While_orelse(self, node: cst.While | cst.For): # reset break & continue in case of nested loops self.outer[node]["uncheckpointed_statements"] = self.uncheckpointed_statements - self.restore_state(node) leave_For_orelse = leave_While_orelse + def leave_While( + self, original_node: cst.For | cst.While, updated_node: cst.For | cst.While + ) -> ( + cst.While + | cst.For + | cst.FlattenSentinel[cst.For | cst.While] + | cst.RemovalSentinel + ): + if self.loop_state.nodes_needing_checkpoints: + self.ensure_imported_library() + transformer = InsertCheckpointsInLoopBody( + self.loop_state.nodes_needing_checkpoints, self.library[0] + ) + # type of updated_node expanded to the return type + updated_node = updated_node.visit(transformer) # type: ignore + + self.restore_state(original_node) + # https://github.com/afonasev/flake8-return/issues/133 + return updated_node # noqa: R504 + + leave_For = leave_While + # save state in case of continue/break at a point not guaranteed to checkpoint def visit_Continue(self, node: cst.Continue): if not self.async_function: @@ -590,3 +721,77 @@ def visit_CompFor(self, node: cst.CompFor): # ignore their content def visit_GeneratorExp(self, node: cst.GeneratorExp): return False + + def visit_Import(self, node: cst.Import): + """Register explicitly imported library. + + This is *slightly* different enough from what the utility visitor does + that it's added here, but the functionality is maybe better placed in there. + """ + for alias in node.names: + if m.matches( + alias, m.ImportAlias(name=m.Name("trio") | m.Name("anyio"), asname=None) + ): + assert isinstance(alias.name.value, str) + self.explicitly_imported_library[alias.name.value] = True + + def ensure_imported_library(self) -> None: + """Mark library for import. + + Check that the library we'd use to insert checkpoints + is imported - if not, mark it to be inserted later. + """ + if not self.explicitly_imported_library[self.library[0]]: + self.add_import.add(self.library[0]) + + def leave_Module(self, original_node: cst.Module, updated_node: cst.Module): + """Add needed library import, if any, to the module.""" + if not self.add_import: + return updated_node + + # make sure we insert the import after docstring[s], comments at the beginning of + # the file, and after other imports (especially important to be after __future__) + new_body = list(updated_node.body) + index = 0 + while m.matches( + new_body[index], + m.SimpleStatementLine( + [m.ImportFrom() | m.Import() | m.Expr(m.SimpleString())] + ), + ): + index += 1 + # trivial to insert multiple imports - but it should not happen with current + # implementation + assert len(self.add_import) == 1 + new_body.insert(index, cst.parse_statement(f"import {self.library[0]}")) + return updated_node.with_changes(body=new_body) + + +# necessary as we don't know whether to insert checkpoints on the first pass of a loop +# so we transform the loop body afterwards +class InsertCheckpointsInLoopBody(cst.CSTTransformer): + def __init__( + self, nodes_needing_checkpoint: Sequence[cst.Yield | cst.Return], library: str + ): + super().__init__() + self.nodes_needing_checkpoint = nodes_needing_checkpoint + self.add_statement: cst.SimpleStatementLine | None = None + self.library = library + + # insert checkpoint before yield with a flattensentinel, if indicated + # type checkers don't like going across classes, esp as the method accesses + # and modifies self.add_statement, but #YOLO + leave_SimpleStatementLine = Visitor91X.leave_SimpleStatementLine # type: ignore + + def leave_Yield( + self, + original_node: cst.Yield, + updated_node: cst.Yield, + ) -> cst.Yield: + # we need to check *original* node here, since updated node is a copy + # which loses identity equality + if original_node in self.nodes_needing_checkpoint: + self.add_statement = checkpoint_statement(self.library) + return updated_node + + leave_Return = leave_Yield # type: ignore diff --git a/flake8_trio/visitors/visitor_utility.py b/flake8_trio/visitors/visitor_utility.py index f195b6bc..d18cad9b 100644 --- a/flake8_trio/visitors/visitor_utility.py +++ b/flake8_trio/visitors/visitor_utility.py @@ -5,8 +5,11 @@ import ast from typing import Any -from .flake8triovisitor import Flake8TrioVisitor -from .helpers import utility_visitor +import libcst as cst +import libcst.matchers as m + +from .flake8triovisitor import Flake8TrioVisitor, Flake8TrioVisitor_cst +from .helpers import utility_visitor, utility_visitor_cst @utility_visitor @@ -116,3 +119,22 @@ def visit_Import(self, node: ast.Import): name = alias.name if name in ("trio", "anyio") and alias.asname is None: self.add_library(name) + + +@utility_visitor_cst +class VisitorLibraryHandler_cst(Flake8TrioVisitor_cst): + def __init__(self, *args: Any, **kwargs: Any): + super().__init__(*args, **kwargs) + # check whether library we're working towards has been explicitly + # specified with --anyio, otherwise assume Trio - but we update if we + # see imports + if self.options.anyio: + self.add_library("anyio") + + def visit_Import(self, node: cst.Import): + for alias in node.names: + if m.matches( + alias, m.ImportAlias(name=m.Name("trio") | m.Name("anyio"), asname=None) + ): + assert isinstance(alias.name.value, str) + self.add_library(alias.name.value) diff --git a/tests/autofix_files/trio100.py.diff b/tests/autofix_files/trio100.py.diff new file mode 100644 index 00000000..ea3f2a9a --- /dev/null +++ b/tests/autofix_files/trio100.py.diff @@ -0,0 +1,86 @@ +--- ++++ +@@ -2,24 +2,24 @@ + + import trio + +-with trio.move_on_after(10): # error: 5, "trio", "move_on_after" +- ... ++# error: 5, "trio", "move_on_after" ++... + + + async def function_name(): + # fmt: off +- async with trio.fail_after(10): ...; ...; ... # error: 15, "trio", "fail_after" ++ ...; ...; ... + # fmt: on +- async with trio.fail_after(10): # error: 15, "trio", "fail_after" +- ... +- async with trio.fail_at(10): # error: 15, "trio", "fail_at" +- ... +- async with trio.move_on_after(10): # error: 15, "trio", "move_on_after" +- ... +- async with trio.move_on_at(10): # error: 15, "trio", "move_on_at" +- ... +- async with trio.CancelScope(...): # error: 15, "trio", "CancelScope" +- ... ++ # error: 15, "trio", "fail_after" ++ ... ++ # error: 15, "trio", "fail_at" ++ ... ++ # error: 15, "trio", "move_on_after" ++ ... ++ # error: 15, "trio", "move_on_at" ++ ... ++ # error: 15, "trio", "CancelScope" ++ ... + + with trio.move_on_after(10): + await trio.sleep(1) +@@ -36,8 +36,8 @@ + with open("filename") as _: + ... + +- with trio.fail_after(10): # error: 9, "trio", "fail_after" +- ... ++ # error: 9, "trio", "fail_after" ++ ... + + send_channel, receive_channel = trio.open_memory_channel(0) + async with trio.fail_after(10): +@@ -48,22 +48,22 @@ + async for _ in receive_channel: + ... + +- async with trio.fail_after(10): # error: 15, "trio", "fail_after" +- for _ in receive_channel: +- ... ++ # error: 15, "trio", "fail_after" ++ for _ in receive_channel: ++ ... + + # fix missed alarm when function is defined inside the with scope +- with trio.move_on_after(10): # error: 9, "trio", "move_on_after" ++ # error: 9, "trio", "move_on_after" ++ ++ async def foo(): ++ await trio.sleep(1) ++ ++ # error: 9, "trio", "move_on_after" ++ if ...: + + async def foo(): +- await trio.sleep(1) +- +- with trio.move_on_after(10): # error: 9, "trio", "move_on_after" +- if ...: +- +- async def foo(): +- if ...: +- await trio.sleep(1) ++ if ...: ++ await trio.sleep(1) + + async with random_ignored_library.fail_after(10): + ... diff --git a/tests/autofix_files/trio100_simple_autofix.py.diff b/tests/autofix_files/trio100_simple_autofix.py.diff new file mode 100644 index 00000000..a8096ba0 --- /dev/null +++ b/tests/autofix_files/trio100_simple_autofix.py.diff @@ -0,0 +1,86 @@ +--- ++++ +@@ -2,28 +2,29 @@ + + # a + # b +-with trio.move_on_after(10): # error: 5, "trio", "move_on_after" +- # c +- # d +- print(1) # e +- # f +- # g +- print(2) # h +- # i +- # j +- print(3) # k +- # l +- # m ++# error: 5, "trio", "move_on_after" ++# c ++# d ++print(1) # e ++# f ++# g ++print(2) # h ++# i ++# j ++print(3) # k ++# l ++# m ++pass + # n + +-with trio.move_on_after(10): # error: 5, "trio", "move_on_after" +- ... ++# error: 5, "trio", "move_on_after" ++... + + + # a + # b + # fmt: off +-with trio.move_on_after(10): ...;...;... # error: 5, "trio", "move_on_after" ++...;...;... + # fmt: on + # c + # d +@@ -37,22 +38,22 @@ + + + # multiline with, despite only being one statement +-with ( # a +- # b +- # c +- trio.move_on_after( # error: 4, "trio", "move_on_after" +- # d +- 9999999999999999999999999999999999999999999999999999999 # e +- # f +- ) # g +- # h +-): # this comment is kept +- ... ++# a ++# b ++# c ++# error: 4, "trio", "move_on_after" ++# d ++# e ++# f ++# g ++# h ++# this comment is kept ++... + + # fmt: off +-with ( # a +- # b +- trio.move_on_after(10) # error: 4, "trio", "move_on_after" +- # c +-): ...; ...; ... ++# a ++# b ++# error: 4, "trio", "move_on_after" ++# c ++...; ...; ... + # fmt: on diff --git a/tests/autofix_files/trio910.py b/tests/autofix_files/trio910.py new file mode 100644 index 00000000..9a122423 --- /dev/null +++ b/tests/autofix_files/trio910.py @@ -0,0 +1,584 @@ +# mypy: disable-error-code="unreachable" +import typing +from typing import Any, overload + +import pytest +import trio + +_ = "" + +custom_disabled_decorator: Any = ... + + +async def foo() -> Any: + await foo() + + +def bar() -> Any: + ... + + +# ARG --enable-visitor-codes-regex=(TRIO910)|(TRIO911) +# ARG --no-checkpoint-warning-decorator=custom_disabled_decorator + + +# function whose body solely consists of pass, ellipsis, or string constants is safe +async def foo_empty_1(): + ... + + +async def foo_empty_2(): + pass + + +async def foo_empty_3(): + """comment""" + + +async def foo_empty_4(): + ... + """comment""" + ... + """comment2""" + + +async def foo1(): # error: 0, "exit", Statement("function definition", lineno) + bar() + await trio.lowlevel.checkpoint() + + +# If +async def foo_if_1(): # error: 0, "exit", Statement("function definition", lineno) + if _: + await foo() + await trio.lowlevel.checkpoint() + + +async def foo_if_2(): + if _: + await foo() + else: + await foo() + + +async def foo_if_3(): + await foo() + if _: + ... + + +async def foo_if_4(): # safe + await foo() + if ...: + ... + else: + ... + + +# IfExp +async def foo_ifexp_1(): # safe + print(await foo() if _ else await foo()) + + +async def foo_ifexp_2(): # error: 0, "exit", Statement("function definition", lineno) + print(_ if False and await foo() else await foo()) + await trio.lowlevel.checkpoint() + + +# nested function definition +async def foo_func_1(): + await foo() + + async def foo_func_2(): # error: 4, "exit", Statement("function definition", lineno) + bar() + await trio.lowlevel.checkpoint() + + +# we don't get a newline after the nested function definition before the checkpoint +# when autofixing +# fmt: off +async def foo_func_3(): # error: 0, "exit", Statement("function definition", lineno) + async def foo_func_4(): + await foo() + await trio.lowlevel.checkpoint() + + +async def foo_func_5(): # error: 0, "exit", Statement("function definition", lineno) + def foo_func_6(): # safe + async def foo_func_7(): # error: 8, "exit", Statement("function definition", lineno) + bar() + await trio.lowlevel.checkpoint() + await trio.lowlevel.checkpoint() + + +async def foo_func_8(): # error: 0, "exit", Statement("function definition", lineno) + def foo_func_9(): + raise + await trio.lowlevel.checkpoint() +# fmt: on + + +# 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(_: bytes | str): + await foo() + + +# conditions +async def foo_condition_1(): # safe + if await foo(): + ... + + +async def foo_condition_2(): # error: 0, "exit", Statement("function definition", lineno) + if False and await foo(): + ... + await trio.lowlevel.checkpoint() + + +async def foo_condition_3(): # error: 0, "exit", Statement("function definition", lineno) + if ... and await foo(): + ... + await trio.lowlevel.checkpoint() + + +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, "exit", Statement("function definition", lineno) + while _: + await foo() + await trio.lowlevel.checkpoint() + + +async def foo_while_2(): # now safe + while _: + await foo() + else: + await foo() + + +async def foo_while_3(): # safe + await foo() + while _: + ... + + +async def foo_while_4(): # error: 0, "exit", Statement("function definition", lineno) + while False: + await foo() + await trio.lowlevel.checkpoint() + + +# for +async def foo_for_1(): # error: 0, "exit", Statement("function definition", lineno) + for _ in "": + await foo() + await trio.lowlevel.checkpoint() + + +async def foo_for_2(): # now safe + for _ in "": + await foo() + else: + await foo() + + +async def foo_while_break_1(): # safe + while bar(): + await foo() + break + else: + await foo() + + +async def foo_while_break_2(): # error: 0, "exit", Statement("function definition", lineno) + while bar(): + break + else: + await foo() + await trio.lowlevel.checkpoint() + + +async def foo_while_break_3(): # error: 0, "exit", Statement("function definition", lineno) + while bar(): + await foo() + break + else: + ... + await trio.lowlevel.checkpoint() + + +async def foo_while_break_4(): # error: 0, "exit", Statement("function definition", lineno) + while bar(): + break + else: + ... + await trio.lowlevel.checkpoint() + + +async def foo_while_continue_1(): # safe + while bar(): + await foo() + continue + else: + await foo() + + +async def foo_while_continue_2(): # safe + while bar(): + continue + else: + await foo() + + +async def foo_while_continue_3(): # error: 0, "exit", Statement("function definition", lineno) + while bar(): + await foo() + continue + else: + ... + await trio.lowlevel.checkpoint() + + +async def foo_while_continue_4(): # error: 0, "exit", Statement("function definition", lineno) + while bar(): + continue + else: + ... + await trio.lowlevel.checkpoint() + + +async def foo_async_for_1(): + async for _ in bar(): + ... + + +# async with +# async with guarantees checkpoint on at least one of entry or exit +async def foo_async_with(): + async with bar(): + ... + + +# 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, "exit", Statement("function definition", lineno) + try: + await foo() + except ValueError: + ... + except: + raise + else: + await foo() + await trio.lowlevel.checkpoint() + + +async def foo_try_2(): # safe + try: + ... + except ValueError: + ... + except: + raise + finally: + await foo() + + +async def foo_try_3(): # safe + try: + await foo() + except ValueError: + await foo() + except: + raise + + +async def foo_try_4(): # safe + try: + ... + except ValueError: + raise + except: + raise + else: + await foo() + + +async def foo_try_5(): # safe + await foo() + try: + pass + except: + pass + else: + pass + + +async def foo_try_6(): # error: 0, "exit", Statement("function definition", lineno) + try: + pass + except: + pass + else: + pass + await trio.lowlevel.checkpoint() + + +async def foo_try_7(): # safe + try: + await foo() + except: + await foo() + else: + pass + + +# https://github.com/Zac-HD/flake8-trio/issues/45 +async def to_queue(iter_func, queue): + async with iter_func() as it: + async for x in it: + queue.put(x) + + +async def to_queue2(iter_func, queue): + try: + async with iter_func() as it: + async for x in it: + queue.put(x) + finally: + queue.put(None) + + +# safe, exception is propagated out +async def try_checkpoint_empty_finally(): + try: + await trio.sleep(0) + finally: + ... + + +# unsafe, exception is suppressed +async def try_exception_suppressed(): # error: 0, 'exit', Statement('function definition', lineno) + try: + await trio.sleep(0) + except: + ... + await trio.lowlevel.checkpoint() + + +# safe +async def try_bare_except_reraises(): + try: + await trio.sleep(0) + except: + raise + finally: + ... + + +async def return_in_finally_bare_except_checkpoint(): + try: + await trio.sleep(0) + except: + await trio.sleep(0) + finally: + return + + +async def return_in_finally_bare_except_empty(): + try: + await trio.sleep(0) + except: + ... + finally: + await trio.lowlevel.checkpoint() + return # error: 8, 'return', Statement('function definition', lineno-6) + + +# early return +async def foo_return_1(): + await trio.lowlevel.checkpoint() + return # error: 4, "return", Statement("function definition", lineno-1) + + +async def foo_return_2(): # safe + if _: + await trio.lowlevel.checkpoint() + return # error: 8, "return", Statement("function definition", lineno-2) + await foo() + + +async def foo_return_3(): # error: 0, "exit", Statement("function definition", lineno) + if _: + await foo() + return # safe + await trio.lowlevel.checkpoint() + + +# loop over non-empty static collection +async def foo_loop_static(): + for i in [1, 2, 3]: + await foo() + + +# also handle range with constants +async def foo_range_1(): + for i in range(5): + await foo() + + +async def foo_range_2(): + for i in range(5, 10): + await foo() + + +async def foo_range_3(): + for i in range(10, 5, -1): + await foo() + + +async def foo_range_4(): # error: 0, "exit", Statement("function definition", lineno) + for i in range(10, 5): + await foo() + await trio.lowlevel.checkpoint() + + +# error on complex parameters +async def foo_range_5(): # error: 0, "exit", Statement("function definition", lineno) + for i in range(3 - 2): + await foo() + await trio.lowlevel.checkpoint() + + +# https://github.com/Zac-HD/flake8-trio/issues/47 +async def f(): + while True: + if ...: + await trio.sleep(0) + return + # If you delete this loop, no warning. + while bar(): + await foo() + + +async def f1(): + while True: + if ...: + await trio.sleep(0) + return + + +async def f2(): + while True: + await trio.sleep(0) + return + + +# code coverage +def foo_sync(): + # try in sync function + try: + pass + except: + pass + + # continue/break in sync function + while True: + if ...: + continue + if ...: + break + + # boolop in sync function + True and True # type: ignore + + +# don't warn on pytest.fixture +@pytest.fixture +async def foo_test(): + print("...") + + +@pytest.fixture() +async def foo_test2(): + print("...") + + +@custom_disabled_decorator +async def foo_ARG_test(): + print("...") + + +@custom_disabled_decorator() +async def foo_ARG_test2(): + print("...") + + +@pytest.fixture +async def foo_test_return(): + return + + +@pytest.fixture() +async def foo_test_return2(): + return + + +async def foo_comprehension_1(): + [... for x in range(10) if await foo()] + + +# should error +async def foo_comprehension_2(): # error: 0, "exit", Statement("function definition", lineno) + [await foo() for x in range(10) if bar()] + await trio.lowlevel.checkpoint() + + +async def foo_comprehension_3(): + [... async for x in bar()] diff --git a/tests/autofix_files/trio910.py.diff b/tests/autofix_files/trio910.py.diff new file mode 100644 index 00000000..167684f1 --- /dev/null +++ b/tests/autofix_files/trio910.py.diff @@ -0,0 +1,207 @@ +--- ++++ +@@ -44,12 +44,14 @@ + + async def foo1(): # error: 0, "exit", Statement("function definition", lineno) + bar() ++ await trio.lowlevel.checkpoint() + + + # If + async def foo_if_1(): # error: 0, "exit", Statement("function definition", lineno) + if _: + await foo() ++ await trio.lowlevel.checkpoint() + + + async def foo_if_2(): +@@ -80,6 +82,7 @@ + + async def foo_ifexp_2(): # error: 0, "exit", Statement("function definition", lineno) + print(_ if False and await foo() else await foo()) ++ await trio.lowlevel.checkpoint() + + + # nested function definition +@@ -88,6 +91,7 @@ + + async def foo_func_2(): # error: 4, "exit", Statement("function definition", lineno) + bar() ++ await trio.lowlevel.checkpoint() + + + # we don't get a newline after the nested function definition before the checkpoint +@@ -96,17 +100,21 @@ + async def foo_func_3(): # error: 0, "exit", Statement("function definition", lineno) + async def foo_func_4(): + await foo() ++ await trio.lowlevel.checkpoint() + + + async def foo_func_5(): # error: 0, "exit", Statement("function definition", lineno) + def foo_func_6(): # safe + async def foo_func_7(): # error: 8, "exit", Statement("function definition", lineno) + bar() ++ await trio.lowlevel.checkpoint() ++ await trio.lowlevel.checkpoint() + + + async def foo_func_8(): # error: 0, "exit", Statement("function definition", lineno) + def foo_func_9(): + raise ++ await trio.lowlevel.checkpoint() + # fmt: on + + +@@ -143,11 +151,13 @@ + async def foo_condition_2(): # error: 0, "exit", Statement("function definition", lineno) + if False and await foo(): + ... ++ await trio.lowlevel.checkpoint() + + + async def foo_condition_3(): # error: 0, "exit", Statement("function definition", lineno) + if ... and await foo(): + ... ++ await trio.lowlevel.checkpoint() + + + async def foo_condition_4(): # safe +@@ -169,6 +179,7 @@ + async def foo_while_1(): # error: 0, "exit", Statement("function definition", lineno) + while _: + await foo() ++ await trio.lowlevel.checkpoint() + + + async def foo_while_2(): # now safe +@@ -187,12 +198,14 @@ + async def foo_while_4(): # error: 0, "exit", Statement("function definition", lineno) + while False: + await foo() ++ await trio.lowlevel.checkpoint() + + + # for + async def foo_for_1(): # error: 0, "exit", Statement("function definition", lineno) + for _ in "": + await foo() ++ await trio.lowlevel.checkpoint() + + + async def foo_for_2(): # now safe +@@ -215,6 +228,7 @@ + break + else: + await foo() ++ await trio.lowlevel.checkpoint() + + + async def foo_while_break_3(): # error: 0, "exit", Statement("function definition", lineno) +@@ -223,6 +237,7 @@ + break + else: + ... ++ await trio.lowlevel.checkpoint() + + + async def foo_while_break_4(): # error: 0, "exit", Statement("function definition", lineno) +@@ -230,6 +245,7 @@ + break + else: + ... ++ await trio.lowlevel.checkpoint() + + + async def foo_while_continue_1(): # safe +@@ -253,6 +269,7 @@ + continue + else: + ... ++ await trio.lowlevel.checkpoint() + + + async def foo_while_continue_4(): # error: 0, "exit", Statement("function definition", lineno) +@@ -260,6 +277,7 @@ + continue + else: + ... ++ await trio.lowlevel.checkpoint() + + + async def foo_async_for_1(): +@@ -298,6 +316,7 @@ + raise + else: + await foo() ++ await trio.lowlevel.checkpoint() + + + async def foo_try_2(): # safe +@@ -348,6 +367,7 @@ + pass + else: + pass ++ await trio.lowlevel.checkpoint() + + + async def foo_try_7(): # safe +@@ -389,6 +409,7 @@ + await trio.sleep(0) + except: + ... ++ await trio.lowlevel.checkpoint() + + + # safe +@@ -416,16 +437,19 @@ + except: + ... + finally: ++ await trio.lowlevel.checkpoint() + return # error: 8, 'return', Statement('function definition', lineno-6) + + + # early return + async def foo_return_1(): ++ await trio.lowlevel.checkpoint() + return # error: 4, "return", Statement("function definition", lineno-1) + + + async def foo_return_2(): # safe + if _: ++ await trio.lowlevel.checkpoint() + return # error: 8, "return", Statement("function definition", lineno-2) + await foo() + +@@ -434,6 +458,7 @@ + if _: + await foo() + return # safe ++ await trio.lowlevel.checkpoint() + + + # loop over non-empty static collection +@@ -461,12 +486,14 @@ + async def foo_range_4(): # error: 0, "exit", Statement("function definition", lineno) + for i in range(10, 5): + await foo() ++ await trio.lowlevel.checkpoint() + + + # error on complex parameters + async def foo_range_5(): # error: 0, "exit", Statement("function definition", lineno) + for i in range(3 - 2): + await foo() ++ await trio.lowlevel.checkpoint() + + + # https://github.com/Zac-HD/flake8-trio/issues/47 +@@ -550,6 +577,7 @@ + # should error + async def foo_comprehension_2(): # error: 0, "exit", Statement("function definition", lineno) + [await foo() for x in range(10) if bar()] ++ await trio.lowlevel.checkpoint() + + + async def foo_comprehension_3(): diff --git a/tests/autofix_files/trio911.py b/tests/autofix_files/trio911.py new file mode 100644 index 00000000..e167b67f --- /dev/null +++ b/tests/autofix_files/trio911.py @@ -0,0 +1,1147 @@ +from typing import Any + +import pytest +import trio + +_: Any = "" + +# ARG --enable-visitor-codes-regex=(TRIO910)|(TRIO911) + + +async def foo() -> Any: + await foo() + + +def bar(*args) -> Any: + ... + + +async def foo_yield_1(): + await foo() + yield 5 + await foo() + + +async def foo_yield_2(): + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Statement("function definition", lineno-1) + await trio.lowlevel.checkpoint() + 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 + await trio.lowlevel.checkpoint() + + +async def foo_yield_4(): # error: 0, "exit", Statement("yield", lineno+3) + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Statement("function definition", lineno-1) + await trio.lowlevel.checkpoint() + await (yield) # error: 11, "yield", Statement("yield", lineno-1) + yield # safe + await trio.lowlevel.checkpoint() + + +async def foo_yield_return_1(): + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Statement("function definition", lineno-1) + await trio.lowlevel.checkpoint() + return # error: 4, "return", Statement("yield", lineno-1) + + +async def foo_yield_return_2(): + await foo() + yield + await trio.lowlevel.checkpoint() + return # error: 4, "return", Statement("yield", lineno-1) + + +async def foo_yield_return_3(): + await foo() + yield + await foo() + return + + +# async with +# checkpoint on both entry and exit +async def foo_async_with(): + async with trio.fail_after(5): + yield + + +# fmt: off +async def foo_async_with_2(): + # with'd expression evaluated before checkpoint + async with (yield): # error: 16, "yield", Statement("function definition", lineno-2) + await trio.lowlevel.checkpoint() + yield +# fmt: on + + +async def foo_async_with_3(): + async with trio.fail_after(5): + yield + await trio.lowlevel.checkpoint() + yield # error: 8, "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) + ): + await trio.lowlevel.checkpoint() + yield # safe + else: + yield # safe + await trio.lowlevel.checkpoint() + + +# 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 + await trio.lowlevel.checkpoint() + + +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 "": + await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("yield", lineno) + await trio.lowlevel.checkpoint() + + +async def foo_for_1(): # error: 0, "exit", Statement("function definition", lineno) # error: 0, "exit", Statement("yield", lineno+3) + for _ in "": + await foo() + yield + await trio.lowlevel.checkpoint() + + +# while + + +# safe if checkpoint in else +async def foo_while_1(): # error: 0, "exit", Statement("yield", lineno+5) + while foo(): + ... + else: + await foo() # will always run + yield # safe + await trio.lowlevel.checkpoint() + + +# simple yield-in-loop case +async def foo_while_2(): # error: 0, "exit", Statement("yield", lineno+3) + await foo() + while foo(): + await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("yield", lineno) + await trio.lowlevel.checkpoint() + + +# no checkpoint after yield if else is entered +async def foo_while_3(): # error: 0, "exit", Statement("yield", lineno+5) + while foo(): + await foo() + yield + else: + await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("yield", lineno-2) # error: 8, "yield", Statement("function definition", lineno-5) + await trio.lowlevel.checkpoint() + + +# check that errors are suppressed in visit_While +async def foo_while_4(): # error: 0, "exit", Statement("yield", lineno+3) # error: 0, "exit", Statement("yield", lineno+5) # error: 0, "exit", Statement("yield", lineno+7) + await foo() + while foo(): + await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("yield", lineno) # error: 8, "yield", Statement("yield", lineno+2) # error: 8, "yield", Statement("yield", lineno+4) + while foo(): + await trio.lowlevel.checkpoint() + yield # error: 12, "yield", Statement("yield", lineno)# error: 12, "yield", Statement("yield", lineno-2)# error: 12, "yield", Statement("yield", lineno+2) + while foo(): + await trio.lowlevel.checkpoint() + yield # error: 16, "yield", Statement("yield", lineno-2)# error: 16, "yield", Statement("yield", lineno) + await trio.lowlevel.checkpoint() + + +# check that state management is handled in for loops as well +async def foo_while_4_for(): # error: 0, "exit", Statement("yield", lineno+3) # error: 0, "exit", Statement("yield", lineno+5) # error: 0, "exit", Statement("yield", lineno+7) + await foo() + for i in bar(): + await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("yield", lineno) # error: 8, "yield", Statement("yield", lineno+2) # error: 8, "yield", Statement("yield", lineno+4) + for i in bar(): + await trio.lowlevel.checkpoint() + yield # error: 12, "yield", Statement("yield", lineno)# error: 12, "yield", Statement("yield", lineno-2)# error: 12, "yield", Statement("yield", lineno+2) + for i in bar(): + await trio.lowlevel.checkpoint() + yield # error: 16, "yield", Statement("yield", lineno-2)# error: 16, "yield", Statement("yield", lineno) + await trio.lowlevel.checkpoint() + + +# check error suppression is reset +async def foo_while_5(): + await foo() + while foo(): + await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("yield", lineno) + + async def foo_nested_error(): # error: 8, "exit", Statement("yield", lineno+1) + await trio.lowlevel.checkpoint() + yield # error: 12, "yield", Statement("function definition", lineno-1) + await trio.lowlevel.checkpoint() + + await foo() + + +# --- while + continue --- +# no checkpoint on continue +async def foo_while_continue_1(): # error: 0, "exit", Statement("yield", lineno+3) + await foo() + while foo(): + await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("yield", lineno) + if ...: + continue + await foo() + await trio.lowlevel.checkpoint() + + +# multiple continues +async def foo_while_continue_2(): # error: 0, "exit", Statement("yield", lineno+3) + await foo() + while foo(): + await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("yield", lineno) + if foo(): + continue + await foo() + if ...: + continue + while foo(): + yield # safe + await foo() + await trio.lowlevel.checkpoint() + + +# --- while + break --- +# else might not run +async def foo_while_break_1(): # error: 0, "exit", Statement("yield", lineno+6) + while foo(): + if ...: + break + else: + await foo() + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Statement("function definition", lineno-6) + await trio.lowlevel.checkpoint() + + +# no checkpoint on break +async def foo_while_break_2(): # error: 0, "exit", Statement("yield", lineno+3) + await foo() + while foo(): + yield # safe + if ...: + break + await foo() + await trio.lowlevel.checkpoint() + + +# guaranteed if else and break +async def foo_while_break_3(): # error: 0, "exit", Statement("yield", lineno+7) + while foo(): + await foo() + if ...: + break # if it breaks, have checkpointed + else: + await foo() # runs if 0-iter + yield # safe + await trio.lowlevel.checkpoint() + + +# break at non-guaranteed checkpoint +async def foo_while_break_4(): # error: 0, "exit", Statement("yield", lineno+7) + while foo(): + if ...: + break + await foo() # might not run + else: + await foo() # might not run + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Statement("function definition", lineno-7) + await trio.lowlevel.checkpoint() + + +# check break is reset on nested +async def foo_while_break_5(): # error: 0, "exit", Statement("yield", lineno+12) + await foo() + while foo(): + yield + if ...: + break + await foo() + while foo(): + yield # safe + await foo() + yield # safe + await foo() + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Statement("yield", lineno-9) + await trio.lowlevel.checkpoint() + + +# check multiple breaks +async def foo_while_break_6(): # error: 0, "exit", Statement("yield", lineno+11) + await foo() + while foo(): + yield + if ...: + break + await foo() + yield + await foo() + if ...: + break + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Statement("yield", lineno-8) + await trio.lowlevel.checkpoint() + + +async def foo_while_break_7(): # error: 0, "exit", Statement("function definition", lineno)# error: 0, "exit", Statement("yield", lineno+5) + while foo(): + await foo() + if ...: + break + yield + break + await trio.lowlevel.checkpoint() + + +async def foo_while_endless_1(): + while True: + await foo() + yield + + +async def foo_while_endless_2(): # error: 0, "exit", Statement("function definition", lineno)# error: 0, "exit", Statement("yield", lineno+3) + while foo(): + await foo() + yield + await trio.lowlevel.checkpoint() + + +async def foo_while_endless_3(): + while True: + ... + yield # type: ignore[unreachable] + await foo() + + +async def foo_while_endless_4(): + await foo() + while True: + yield + while True: + await foo() + yield + + +# try +async def foo_try_1(): # error: 0, "exit", Statement("function definition", lineno) # error: 0, "exit", Statement("yield", lineno+2) + try: + await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("function definition", lineno-2) + except: + pass + await trio.lowlevel.checkpoint() + + +# no checkpoint after yield in ValueError +async def foo_try_2(): # error: 0, "exit", Statement("yield", lineno+5) + try: + await foo() + except ValueError: + await trio.lowlevel.checkpoint() + # try might not have checkpointed + yield # error: 8, "yield", Statement("function definition", lineno-5) + except: + await foo() + else: + pass + await trio.lowlevel.checkpoint() + + +async def foo_try_3(): # error: 0, "exit", Statement("yield", lineno+6) + try: + ... + except: + await foo() + else: + await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("function definition", lineno-6) + await trio.lowlevel.checkpoint() + + +async def foo_try_4(): # safe + try: + ... + except: + await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("function definition", lineno-4) + finally: + await foo() + + +async def foo_try_5(): + try: + await foo() + finally: + await trio.lowlevel.checkpoint() + # 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 + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Statement("function definition", lineno-5) + await trio.lowlevel.checkpoint() + + +async def foo_try_7(): # error: 0, "exit", Statement("yield", lineno+17) + await foo() + try: + yield + await foo() + except ValueError: + await foo() + yield + await foo() + except SyntaxError: + await trio.lowlevel.checkpoint() + 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. + # Then the error will be propagated upwards + yield # safe + await trio.lowlevel.checkpoint() + + +## safe only if (try or else) and all except bodies either await or raise +## if foo() raises a ValueError it's not checkpointed, and may or may not yield +async def foo_try_8(): # error: 0, "exit", Statement("function definition", lineno) # error: 0, "exit", Statement("yield", lineno+3) + try: + await foo() + yield + await foo() + except ValueError: + ... + except: + raise + else: + await foo() + await trio.lowlevel.checkpoint() + + +# 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 + await trio.lowlevel.checkpoint() + + +# bare except means we'll jump to finally after full execution of either try or the except +async def foo_try_10(): + try: + await foo() + except: + await foo() + finally: + yield + await foo() + + +async def foo_try_10_BaseException(): + try: + await foo() + except BaseException: + await foo() + finally: + yield + await foo() + + +# not fully covering excepts +async def foo_try_10_exception(): + try: + await foo() + except ValueError: + await foo() + finally: + await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("function definition", lineno-6) + await foo() + + +async def foo_try_10_no_except(): + try: + await foo() + finally: + await trio.lowlevel.checkpoint() + # try might crash before checkpoint + yield # error: 8, "yield", Statement("function definition", lineno-5) + await foo() + + +# if +async def foo_if_1(): + if ...: + await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("function definition", lineno-2) + await foo() + else: + await trio.lowlevel.checkpoint() + 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 + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Statement("yield", lineno-1) + await trio.lowlevel.checkpoint() + + +async def foo_if_3(): # error: 0, "exit", Statement("yield", lineno+6) + await foo() + if ...: + yield + else: + ... + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Statement("yield", lineno-3) + await trio.lowlevel.checkpoint() + + +async def foo_if_4(): # error: 0, "exit", Statement("yield", lineno+7) + await foo() + yield + if ...: + await foo() + else: + ... + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Statement("yield", lineno-5) + await trio.lowlevel.checkpoint() + + +async def foo_if_5(): # error: 0, "exit", Statement("yield", lineno+8) + await foo() + if ...: + yield + await foo() + else: + yield + ... + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Statement("yield", lineno-2) + await trio.lowlevel.checkpoint() + + +async def foo_if_6(): # error: 0, "exit", Statement("yield", lineno+8) + await foo() + if ...: + yield + else: + yield + await foo() + ... + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Statement("yield", lineno-5) + await trio.lowlevel.checkpoint() + + +async def foo_if_7(): # error: 0, "exit", Statement("function definition", lineno) + if ...: + await foo() + yield + await foo() + await trio.lowlevel.checkpoint() + + +async def foo_if_8(): # error: 0, "exit", Statement("function definition", lineno) + if ...: + ... + else: + await foo() + yield + await foo() + await trio.lowlevel.checkpoint() + + +# IfExp +async def foo_ifexp_1(): # error: 0, "exit", Statement("yield", lineno+1) # error: 0, "exit", Statement("yield", lineno+1) + print((yield) if await foo() else (yield)) + await trio.lowlevel.checkpoint() + + +# Will either enter else, and it's a guaranteed checkpoint - or enter if, in which +# case the problem is the yield. +async def foo_ifexp_2(): # error: 0, "exit", Statement("yield", lineno+2) + await trio.lowlevel.checkpoint() + print( + (yield) # error: 9, "yield", Statement("function definition", lineno-2) + if ... and await foo() + else await foo() + ) + await trio.lowlevel.checkpoint() + + +# normal function +def foo_sync_1(): + return + + +def foo_sync_2(): + ... + + +def foo_sync_3(): + yield + + +def foo_sync_4(): + if ...: + return + yield + + +def foo_sync_5(): + if ...: + return + yield + + +def foo_sync_6(): + while foo(): + yield + + +def foo_sync_7(): + while foo(): + if ...: + return + yield + + +# nested function definition +async def foo_func_1(): + await foo() + + async def foo_func_2(): # error: 4, "exit", Statement("yield", lineno+1) + await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("function definition", lineno-1) + await trio.lowlevel.checkpoint() + + +# autofix doesn't insert newline after nested function def and before checkpoint +# so we need to disable black +# fmt: off +async def foo_func_3(): # error: 0, "exit", Statement("yield", lineno+2) + await foo() + yield + + async def foo_func_4(): + await foo() + await trio.lowlevel.checkpoint() + + +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() + ... + await trio.lowlevel.checkpoint() +# fmt: on + + +# No error from function definition, but may shortcut after yield +async def foo_boolops_1(): # error: 0, "exit", Stmt("yield", line+1) + _ = await foo() and (yield) and await foo() + await trio.lowlevel.checkpoint() + + +# may shortcut after any of the yields +async def foo_boolops_2(): # error: 0, "exit", Stmt("yield", line+4) # error: 0, "exit", Stmt("yield", line+6) + await trio.lowlevel.checkpoint() + # known false positive - but chained yields in bool should be rare + _ = ( + await foo() + and (yield) + and await foo() + and (yield) # error: 13, "yield", Stmt("yield", line-2, 13) + ) + await trio.lowlevel.checkpoint() + + +# fmt: off +async def foo_boolops_3(): # error: 0, "exit", Stmt("yield", line+1) # error: 0, "exit", Stmt("yield", line+4) # error: 0, "exit", Stmt("yield", line+5) + await trio.lowlevel.checkpoint() + _ = (await foo() or (yield) or await foo()) or ( + ... + or ( + (yield) # error: 13, "yield", Stmt("yield", line-3) + and (yield)) # error: 17, "yield", Stmt("yield", line-1) + ) + await trio.lowlevel.checkpoint() +# fmt: on + + +# loop over non-empty static collection +async def foo_loop_static(): + # break/else behaviour on guaranteed body execution + for _ in [1, 2, 3]: + await foo() + else: + yield + await foo() + yield + + for _ in [1, 2, 3]: + await foo() + if ...: + break + else: + yield + await foo() + yield + + # continue + for _ in [1, 2, 3]: + if ...: + continue + await foo() + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-7) + + # continue/else + for _ in [1, 2, 3]: + if ...: + continue + await foo() + else: + await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Stmt("yield", line-8) + await foo() + yield + + for _ in [1, 2, 3]: + await foo() + if ...: + break + else: + yield + await foo() + yield + + # test different containers + for _ in (1, 2, 3): + await foo() + yield + + for _ in (foo(), foo()): + await foo() + yield + + for _ in ((),): + await foo() + yield + + for _ in "hello": + await foo() + yield + + for _ in b"hello": + await foo() + yield + + for _ in r"hello": + await foo() + yield + + for _ in {1, 2, 3}: + await foo() + yield + + for _ in (): + await foo() + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + for _ in {1: 2, 3: 4}: + await foo() + yield + + for _ in " ".strip(): + await foo() + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + for _ in range(0): + await foo() + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + for _ in (*range(0),): + await foo() + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + for _ in (*(1, 2),): + await foo() + yield + + for _ in {**{}}: + await foo() + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + for _ in {**{}, **{}}: + await foo() + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + for _ in {**{1: 2}}: + await foo() + yield + + for _ in (*range(0), *[1, 2, 3]): + await foo() + yield + + for _ in {**{}, **{1: 2}}: # type: ignore[arg-type] + await foo() + yield + + x: Any = ... + for _ in (*x, *[1, 2, 3]): + await foo() + yield + + for _ in {**x, **{1: 2}}: + await foo() + yield + + for _ in {}: + await foo() + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + for _ in "": + await foo() + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + for _ in """""": + await foo() + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + for _ in [[], []][0]: + await foo() + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + for _ in [[], []].__getitem__(0): + await foo() + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + # not handled + for _ in list((1, 2)): + await foo() + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-5) + + for _ in list(): + await foo() + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + # while + while True: + await foo() + if ...: + break + yield + + while True: + if ...: + break + await foo() + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-6) + + while True: + if ...: + continue + await foo() + if ...: + break + yield + + while False: + await foo() # type: ignore[unreachable] + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + while "hello": + await foo() + yield + + # false positive on containers + while [1, 2]: + await foo() + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-5) + + # will get caught by any number of linters, but trio911 will also complain + for _ in 5: # type: ignore + await foo() + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-5) + + # range with constant arguments also handled, see more extensive tests in 910 + for i in range(5): + await foo() + yield + + for i in range(0x23): + await foo() + yield + + for i in range(0b01): + await foo() + yield + + for i in range(1 + 1): # not handled + await foo() + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + for i in range(None): # type: ignore + await foo() + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + for i in range(+3): + await foo() + yield + + for i in range(-3.5): # type: ignore + await foo() + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + # duplicated from 910 to have all range tests in one place + for i in range(5, 10): + await foo() + yield + + for i in range(10, 5, -1): + await foo() + yield + + # ~0 == -1 + for i in range(10, 5, ~0): + await foo() + yield + + # length > sys.maxsize + for i in range(27670116110564327421): + await foo() + yield + + for i in range(10, 5): + await foo() + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + # binary operations are not handled + for i in range(3 - 2): + await foo() + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-5) + + for i in range(10**3): + await foo() + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + # nor nested unary operations + for i in range(--3): + await foo() + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-5) + + await foo() + + +# don't warn on pytest.fixture +@pytest.fixture +async def foo_test(): + yield + + +@pytest.fixture() +async def foo_test2(): + yield + + +async def comprehensions(): + # guaranteed iteration with await in test + [... for x in range(10) if await foo()] + yield # safe + + # guaranteed iteration and await in value, but test is not guaranteed + [await foo() for x in range(10) if bar()] + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + # guaranteed iteration and await in value + [await foo() for x in range(10)] + yield # safe + + # not guaranteed to iter + [await foo() for x in bar()] + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + # await statement in loop expression + [... for x in bar(await foo())] + yield + + # set comprehensions use same logic as list + {await foo() for x in range(10)} + yield # safe + + {await foo() for x in bar()} + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-3) + + # dict comprehensions use same logic as list + {await foo(): 5 for x in bar()} + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + # other than `await` can be in both key&val + {await foo(): 5 for x in range(10)} + yield + + {5: await foo() for x in range(10)} + yield + + # generator expressions are never treated as safe + (await foo() for x in range(10)) + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + (await foo() for x in bar() if await foo()) + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-3) + + # async for always safe + [... async for x in bar()] + yield # safe + {... async for x in bar()} + yield # safe + {...: ... async for x in bar()} + yield # safe + + # other than in generator expression + (... async for x in bar()) + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + # multiple loops + [... for x in range(10) for y in range(10) if await foo()] + yield + [... for x in range(10) for y in bar() if await foo()] + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-2) + [... for x in bar() for y in range(10) if await foo()] + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-2) + + [await foo() for x in range(10) for y in range(10)] + yield + [await foo() for x in range(10) for y in bar()] + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-2) + [await foo() for x in bar() for y in range(10)] + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-2) + + # trip loops! + [... for x in range(10) for y in range(10) async for z in bar()] + yield + [... for x in range(10) for y in range(10) for z in range(10)] + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-2) + + # multiple ifs + [... for x in range(10) for y in range(10) if await foo() if await foo()] + yield + + [... for x in range(10) for y in bar() if await foo() if await foo()] + await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-3) + + # nested comprehensions + [[await foo() for x in range(10)] for y in range(10)] + yield + + # code coverage: inner comprehension with no checkpointed statements + [... for x in [await foo()] for y in x] diff --git a/tests/autofix_files/trio911.py.diff b/tests/autofix_files/trio911.py.diff new file mode 100644 index 00000000..c9b54841 --- /dev/null +++ b/tests/autofix_files/trio911.py.diff @@ -0,0 +1,782 @@ +--- ++++ +@@ -23,7 +23,9 @@ + + + async def foo_yield_2(): ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Statement("function definition", lineno-1) ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Statement("yield", lineno-1) + await foo() + +@@ -31,22 +33,29 @@ + async def foo_yield_3(): # error: 0, "exit", Statement("yield", lineno+2) + await foo() + yield ++ await trio.lowlevel.checkpoint() + + + async def foo_yield_4(): # error: 0, "exit", Statement("yield", lineno+3) ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Statement("function definition", lineno-1) ++ await trio.lowlevel.checkpoint() + await (yield) # error: 11, "yield", Statement("yield", lineno-1) + yield # safe ++ await trio.lowlevel.checkpoint() + + + async def foo_yield_return_1(): ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Statement("function definition", lineno-1) ++ await trio.lowlevel.checkpoint() + return # error: 4, "return", Statement("yield", lineno-1) + + + async def foo_yield_return_2(): + await foo() + yield ++ await trio.lowlevel.checkpoint() + return # error: 4, "return", Statement("yield", lineno-1) + + +@@ -68,6 +77,7 @@ + async def foo_async_with_2(): + # with'd expression evaluated before checkpoint + async with (yield): # error: 16, "yield", Statement("function definition", lineno-2) ++ await trio.lowlevel.checkpoint() + yield + # fmt: on + +@@ -75,6 +85,7 @@ + async def foo_async_with_3(): + async with trio.fail_after(5): + yield ++ await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("yield", lineno-1) + + +@@ -83,9 +94,11 @@ + async for i in ( + yield # error: 8, "yield", Statement("function definition", lineno-2) + ): ++ await trio.lowlevel.checkpoint() + yield # safe + else: + yield # safe ++ await trio.lowlevel.checkpoint() + + + # await anext(iter) is not called on break +@@ -94,6 +107,7 @@ + yield + if ...: + break ++ await trio.lowlevel.checkpoint() + + + async def foo_async_for_3(): # safe +@@ -111,13 +125,16 @@ + async def foo_for(): # error: 0, "exit", Statement("yield", lineno+3) + await foo() + for i in "": ++ await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("yield", lineno) ++ await trio.lowlevel.checkpoint() + + + async def foo_for_1(): # error: 0, "exit", Statement("function definition", lineno) # error: 0, "exit", Statement("yield", lineno+3) + for _ in "": + await foo() + yield ++ await trio.lowlevel.checkpoint() + + + # while +@@ -130,13 +147,16 @@ + else: + await foo() # will always run + yield # safe ++ await trio.lowlevel.checkpoint() + + + # simple yield-in-loop case + async def foo_while_2(): # error: 0, "exit", Statement("yield", lineno+3) + await foo() + while foo(): ++ await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("yield", lineno) ++ await trio.lowlevel.checkpoint() + + + # no checkpoint after yield if else is entered +@@ -145,39 +165,52 @@ + await foo() + yield + else: ++ await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("yield", lineno-2) # error: 8, "yield", Statement("function definition", lineno-5) ++ await trio.lowlevel.checkpoint() + + + # check that errors are suppressed in visit_While + async def foo_while_4(): # error: 0, "exit", Statement("yield", lineno+3) # error: 0, "exit", Statement("yield", lineno+5) # error: 0, "exit", Statement("yield", lineno+7) + await foo() + while foo(): ++ await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("yield", lineno) # error: 8, "yield", Statement("yield", lineno+2) # error: 8, "yield", Statement("yield", lineno+4) + while foo(): ++ await trio.lowlevel.checkpoint() + yield # error: 12, "yield", Statement("yield", lineno)# error: 12, "yield", Statement("yield", lineno-2)# error: 12, "yield", Statement("yield", lineno+2) + while foo(): ++ await trio.lowlevel.checkpoint() + yield # error: 16, "yield", Statement("yield", lineno-2)# error: 16, "yield", Statement("yield", lineno) ++ await trio.lowlevel.checkpoint() + + + # check that state management is handled in for loops as well + async def foo_while_4_for(): # error: 0, "exit", Statement("yield", lineno+3) # error: 0, "exit", Statement("yield", lineno+5) # error: 0, "exit", Statement("yield", lineno+7) + await foo() + for i in bar(): ++ await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("yield", lineno) # error: 8, "yield", Statement("yield", lineno+2) # error: 8, "yield", Statement("yield", lineno+4) + for i in bar(): ++ await trio.lowlevel.checkpoint() + yield # error: 12, "yield", Statement("yield", lineno)# error: 12, "yield", Statement("yield", lineno-2)# error: 12, "yield", Statement("yield", lineno+2) + for i in bar(): ++ await trio.lowlevel.checkpoint() + yield # error: 16, "yield", Statement("yield", lineno-2)# error: 16, "yield", Statement("yield", lineno) ++ await trio.lowlevel.checkpoint() + + + # check error suppression is reset + async def foo_while_5(): + await foo() + while foo(): ++ await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("yield", lineno) + + async def foo_nested_error(): # error: 8, "exit", Statement("yield", lineno+1) ++ await trio.lowlevel.checkpoint() + yield # error: 12, "yield", Statement("function definition", lineno-1) ++ await trio.lowlevel.checkpoint() + + await foo() + +@@ -187,16 +220,19 @@ + async def foo_while_continue_1(): # error: 0, "exit", Statement("yield", lineno+3) + await foo() + while foo(): ++ await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("yield", lineno) + if ...: + continue + await foo() ++ await trio.lowlevel.checkpoint() + + + # multiple continues + async def foo_while_continue_2(): # error: 0, "exit", Statement("yield", lineno+3) + await foo() + while foo(): ++ await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("yield", lineno) + if foo(): + continue +@@ -206,6 +242,7 @@ + while foo(): + yield # safe + await foo() ++ await trio.lowlevel.checkpoint() + + + # --- while + break --- +@@ -216,7 +253,9 @@ + break + else: + await foo() ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Statement("function definition", lineno-6) ++ await trio.lowlevel.checkpoint() + + + # no checkpoint on break +@@ -227,6 +266,7 @@ + if ...: + break + await foo() ++ await trio.lowlevel.checkpoint() + + + # guaranteed if else and break +@@ -238,6 +278,7 @@ + else: + await foo() # runs if 0-iter + yield # safe ++ await trio.lowlevel.checkpoint() + + + # break at non-guaranteed checkpoint +@@ -248,7 +289,9 @@ + await foo() # might not run + else: + await foo() # might not run ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Statement("function definition", lineno-7) ++ await trio.lowlevel.checkpoint() + + + # check break is reset on nested +@@ -264,7 +307,9 @@ + await foo() + yield # safe + await foo() ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Statement("yield", lineno-9) ++ await trio.lowlevel.checkpoint() + + + # check multiple breaks +@@ -279,7 +324,9 @@ + await foo() + if ...: + break ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Statement("yield", lineno-8) ++ await trio.lowlevel.checkpoint() + + + async def foo_while_break_7(): # error: 0, "exit", Statement("function definition", lineno)# error: 0, "exit", Statement("yield", lineno+5) +@@ -289,6 +336,7 @@ + break + yield + break ++ await trio.lowlevel.checkpoint() + + + async def foo_while_endless_1(): +@@ -301,6 +349,7 @@ + while foo(): + await foo() + yield ++ await trio.lowlevel.checkpoint() + + + async def foo_while_endless_3(): +@@ -322,9 +371,11 @@ + # try + async def foo_try_1(): # error: 0, "exit", Statement("function definition", lineno) # error: 0, "exit", Statement("yield", lineno+2) + try: ++ await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("function definition", lineno-2) + except: + pass ++ await trio.lowlevel.checkpoint() + + + # no checkpoint after yield in ValueError +@@ -332,12 +383,14 @@ + try: + await foo() + except ValueError: ++ await trio.lowlevel.checkpoint() + # try might not have checkpointed + yield # error: 8, "yield", Statement("function definition", lineno-5) + except: + await foo() + else: + pass ++ await trio.lowlevel.checkpoint() + + + async def foo_try_3(): # error: 0, "exit", Statement("yield", lineno+6) +@@ -346,13 +399,16 @@ + except: + await foo() + else: ++ await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("function definition", lineno-6) ++ await trio.lowlevel.checkpoint() + + + async def foo_try_4(): # safe + try: + ... + except: ++ await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("function definition", lineno-4) + finally: + await foo() +@@ -362,6 +418,7 @@ + try: + await foo() + finally: ++ await trio.lowlevel.checkpoint() + # try might crash before checkpoint + yield # error: 8, "yield", Statement("function definition", lineno-5) + await foo() +@@ -372,7 +429,9 @@ + await foo() + except ValueError: + pass ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Statement("function definition", lineno-5) ++ await trio.lowlevel.checkpoint() + + + async def foo_try_7(): # error: 0, "exit", Statement("yield", lineno+17) +@@ -385,6 +444,7 @@ + yield + await foo() + except SyntaxError: ++ await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("yield", lineno-7) + await foo() + finally: +@@ -393,6 +453,7 @@ + # by any of the excepts, jumping straight to the finally. + # Then the error will be propagated upwards + yield # safe ++ await trio.lowlevel.checkpoint() + + + ## safe only if (try or else) and all except bodies either await or raise +@@ -408,6 +469,7 @@ + raise + else: + await foo() ++ await trio.lowlevel.checkpoint() + + + # no checkpoint after yield in else +@@ -418,6 +480,7 @@ + await foo() + else: + yield ++ await trio.lowlevel.checkpoint() + + + # bare except means we'll jump to finally after full execution of either try or the except +@@ -448,6 +511,7 @@ + except ValueError: + await foo() + finally: ++ await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("function definition", lineno-6) + await foo() + +@@ -456,6 +520,7 @@ + try: + await foo() + finally: ++ await trio.lowlevel.checkpoint() + # try might crash before checkpoint + yield # error: 8, "yield", Statement("function definition", lineno-5) + await foo() +@@ -464,9 +529,11 @@ + # if + async def foo_if_1(): + if ...: ++ await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("function definition", lineno-2) + await foo() + else: ++ await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("function definition", lineno-5) + await foo() + +@@ -477,7 +544,9 @@ + ... + else: + yield ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Statement("yield", lineno-1) ++ await trio.lowlevel.checkpoint() + + + async def foo_if_3(): # error: 0, "exit", Statement("yield", lineno+6) +@@ -486,7 +555,9 @@ + yield + else: + ... ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Statement("yield", lineno-3) ++ await trio.lowlevel.checkpoint() + + + async def foo_if_4(): # error: 0, "exit", Statement("yield", lineno+7) +@@ -496,7 +567,9 @@ + await foo() + else: + ... ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Statement("yield", lineno-5) ++ await trio.lowlevel.checkpoint() + + + async def foo_if_5(): # error: 0, "exit", Statement("yield", lineno+8) +@@ -507,7 +580,9 @@ + else: + yield + ... ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Statement("yield", lineno-2) ++ await trio.lowlevel.checkpoint() + + + async def foo_if_6(): # error: 0, "exit", Statement("yield", lineno+8) +@@ -518,7 +593,9 @@ + yield + await foo() + ... ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Statement("yield", lineno-5) ++ await trio.lowlevel.checkpoint() + + + async def foo_if_7(): # error: 0, "exit", Statement("function definition", lineno) +@@ -526,6 +603,7 @@ + await foo() + yield + await foo() ++ await trio.lowlevel.checkpoint() + + + async def foo_if_8(): # error: 0, "exit", Statement("function definition", lineno) +@@ -535,21 +613,25 @@ + await foo() + yield + await foo() ++ await trio.lowlevel.checkpoint() + + + # IfExp + async def foo_ifexp_1(): # error: 0, "exit", Statement("yield", lineno+1) # error: 0, "exit", Statement("yield", lineno+1) + print((yield) if await foo() else (yield)) ++ await trio.lowlevel.checkpoint() + + + # Will either enter else, and it's a guaranteed checkpoint - or enter if, in which + # case the problem is the yield. + async def foo_ifexp_2(): # error: 0, "exit", Statement("yield", lineno+2) ++ await trio.lowlevel.checkpoint() + print( + (yield) # error: 9, "yield", Statement("function definition", lineno-2) + if ... and await foo() + else await foo() + ) ++ await trio.lowlevel.checkpoint() + + + # normal function +@@ -594,7 +676,9 @@ + await foo() + + async def foo_func_2(): # error: 4, "exit", Statement("yield", lineno+1) ++ await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Statement("function definition", lineno-1) ++ await trio.lowlevel.checkpoint() + + + # autofix doesn't insert newline after nested function def and before checkpoint +@@ -606,6 +690,7 @@ + + async def foo_func_4(): + await foo() ++ await trio.lowlevel.checkpoint() + + + async def foo_func_5(): # error: 0, "exit", Statement("yield", lineno+2) +@@ -618,16 +703,19 @@ + async def foo_func_7(): + await foo() + ... ++ await trio.lowlevel.checkpoint() + # fmt: on + + + # No error from function definition, but may shortcut after yield + async def foo_boolops_1(): # error: 0, "exit", Stmt("yield", line+1) + _ = await foo() and (yield) and await foo() ++ await trio.lowlevel.checkpoint() + + + # may shortcut after any of the yields + async def foo_boolops_2(): # error: 0, "exit", Stmt("yield", line+4) # error: 0, "exit", Stmt("yield", line+6) ++ await trio.lowlevel.checkpoint() + # known false positive - but chained yields in bool should be rare + _ = ( + await foo() +@@ -635,16 +723,19 @@ + and await foo() + and (yield) # error: 13, "yield", Stmt("yield", line-2, 13) + ) ++ await trio.lowlevel.checkpoint() + + + # fmt: off + async def foo_boolops_3(): # error: 0, "exit", Stmt("yield", line+1) # error: 0, "exit", Stmt("yield", line+4) # error: 0, "exit", Stmt("yield", line+5) ++ await trio.lowlevel.checkpoint() + _ = (await foo() or (yield) or await foo()) or ( + ... + or ( + (yield) # error: 13, "yield", Stmt("yield", line-3) + and (yield)) # error: 17, "yield", Stmt("yield", line-1) + ) ++ await trio.lowlevel.checkpoint() + # fmt: on + + +@@ -672,6 +763,7 @@ + if ...: + continue + await foo() ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-7) + + # continue/else +@@ -680,6 +772,7 @@ + continue + await foo() + else: ++ await trio.lowlevel.checkpoint() + yield # error: 8, "yield", Stmt("yield", line-8) + await foo() + yield +@@ -724,6 +817,7 @@ + + for _ in (): + await foo() ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + for _ in {1: 2, 3: 4}: +@@ -732,14 +826,17 @@ + + for _ in " ".strip(): + await foo() ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + for _ in range(0): + await foo() ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + for _ in (*range(0),): + await foo() ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + for _ in (*(1, 2),): +@@ -748,10 +845,12 @@ + + for _ in {**{}}: + await foo() ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + for _ in {**{}, **{}}: + await foo() ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + for _ in {**{1: 2}}: +@@ -777,31 +876,38 @@ + + for _ in {}: + await foo() ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + for _ in "": + await foo() ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + for _ in """""": + await foo() ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + for _ in [[], []][0]: + await foo() ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + for _ in [[], []].__getitem__(0): + await foo() ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + # not handled + for _ in list((1, 2)): + await foo() ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-5) + + for _ in list(): + await foo() ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + # while +@@ -815,6 +921,7 @@ + if ...: + break + await foo() ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-6) + + while True: +@@ -827,6 +934,7 @@ + + while False: + await foo() # type: ignore[unreachable] ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + while "hello": +@@ -836,11 +944,13 @@ + # false positive on containers + while [1, 2]: + await foo() ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-5) + + # will get caught by any number of linters, but trio911 will also complain + for _ in 5: # type: ignore + await foo() ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-5) + + # range with constant arguments also handled, see more extensive tests in 910 +@@ -858,10 +968,12 @@ + + for i in range(1 + 1): # not handled + await foo() ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + for i in range(None): # type: ignore + await foo() ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + for i in range(+3): +@@ -870,6 +982,7 @@ + + for i in range(-3.5): # type: ignore + await foo() ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + # duplicated from 910 to have all range tests in one place +@@ -893,20 +1006,24 @@ + + for i in range(10, 5): + await foo() ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + # binary operations are not handled + for i in range(3 - 2): + await foo() ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-5) + + for i in range(10**3): + await foo() ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + # nor nested unary operations + for i in range(--3): + await foo() ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-5) + + await foo() +@@ -930,6 +1047,7 @@ + + # guaranteed iteration and await in value, but test is not guaranteed + [await foo() for x in range(10) if bar()] ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + # guaranteed iteration and await in value +@@ -938,6 +1056,7 @@ + + # not guaranteed to iter + [await foo() for x in bar()] ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + # await statement in loop expression +@@ -949,10 +1068,12 @@ + yield # safe + + {await foo() for x in bar()} ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-3) + + # dict comprehensions use same logic as list + {await foo(): 5 for x in bar()} ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + # other than `await` can be in both key&val +@@ -964,9 +1085,11 @@ + + # generator expressions are never treated as safe + (await foo() for x in range(10)) ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + (await foo() for x in bar() if await foo()) ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-3) + + # async for always safe +@@ -979,27 +1102,33 @@ + + # other than in generator expression + (... async for x in bar()) ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-4) + + # multiple loops + [... for x in range(10) for y in range(10) if await foo()] + yield + [... for x in range(10) for y in bar() if await foo()] ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-2) + [... for x in bar() for y in range(10) if await foo()] ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-2) + + [await foo() for x in range(10) for y in range(10)] + yield + [await foo() for x in range(10) for y in bar()] ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-2) + [await foo() for x in bar() for y in range(10)] ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-2) + + # trip loops! + [... for x in range(10) for y in range(10) async for z in bar()] + yield + [... for x in range(10) for y in range(10) for z in range(10)] ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-2) + + # multiple ifs +@@ -1007,6 +1136,7 @@ + yield + + [... for x in range(10) for y in bar() if await foo() if await foo()] ++ await trio.lowlevel.checkpoint() + yield # error: 4, "yield", Stmt("yield", line-3) + + # nested comprehensions diff --git a/tests/autofix_files/trio91x_autofix.py b/tests/autofix_files/trio91x_autofix.py new file mode 100644 index 00000000..cb4868ba --- /dev/null +++ b/tests/autofix_files/trio91x_autofix.py @@ -0,0 +1,131 @@ +from __future__ import annotations + +"""Docstring for file + +So we make sure that import is added after it. +""" +# isort: skip_file +# ARG --enable-visitor-codes-regex=(TRIO910)|(TRIO911) + +from typing import Any +import trio + + +def bar() -> Any: + ... + + +async def foo() -> Any: + await foo() + + +async def foo1(): # TRIO910: 0, "exit", Statement("function definition", lineno) + bar() + await trio.lowlevel.checkpoint() + + +async def foo_return(): + bar() + await trio.lowlevel.checkpoint() + return # TRIO910: 4, "return", Statement("function definition", lineno-2) + + +async def foo_yield(): # TRIO911: 0, "exit", Statement("yield", lineno+2) + bar() + await trio.lowlevel.checkpoint() + yield # TRIO911: 4, "yield", Statement("function definition", lineno-2) + await trio.lowlevel.checkpoint() + + +async def foo_if(): + if ...: + await trio.lowlevel.checkpoint() + return # TRIO910: 8, "return", Statement("function definition", lineno-2) + elif ...: + await trio.lowlevel.checkpoint() + return # TRIO910: 8, "return", Statement("function definition", lineno-4) + else: + await trio.lowlevel.checkpoint() + return # TRIO910: 8, "return", Statement("function definition", lineno-6) + + +async def foo_while(): + await foo() + while True: + await trio.lowlevel.checkpoint() + yield # TRIO911: 8, "yield", Statement("yield", lineno) + + +async def foo_while2(): + await foo() + while True: + yield + await foo() + + +async def foo_while3(): + await foo() + while True: + if ...: + return + await foo() + + +# check that multiple checkpoints don't get inserted +async def foo_while4(): + while True: + if ...: + await trio.lowlevel.checkpoint() + yield # TRIO911: 12, "yield", Statement("yield", lineno) # TRIO911: 12, "yield", Statement("yield", lineno+2) # TRIO911: 12, "yield", Statement("function definition", lineno-3) + if ...: + await trio.lowlevel.checkpoint() + yield # TRIO911: 12, "yield", Statement("yield", lineno) # TRIO911: 12, "yield", Statement("yield", lineno-2) # TRIO911: 12, "yield", Statement("function definition", lineno-5) # TRIO911: 12, "yield", Statement("yield", lineno-2) + # this warns about the yield on lineno-2 twice, since it can arrive here from it in two different ways + + +# check state management of nested loops +async def foo_nested_while(): + while True: + await trio.lowlevel.checkpoint() + yield # TRIO911: 8, "yield", Statement("function definition", lineno-2) + while True: + await trio.lowlevel.checkpoint() + yield # TRIO911: 12, "yield", Statement("yield", lineno-2) + while True: + await trio.lowlevel.checkpoint() + yield # TRIO911: 16, "yield", Statement("yield", lineno-2) # TRIO911: 16, "yield", Statement("yield", lineno) + + +async def foo_while_nested_func(): + while True: + await trio.lowlevel.checkpoint() + yield # TRIO911: 8, "yield", Statement("function definition", lineno-2) # TRIO911: 8, "yield", Statement("yield", lineno) + + async def bar(): + while ...: + ... + await foo() + + +# not handled, but at least doesn't insert an unnecessary checkpoint +async def foo_singleline(): + await foo() + # fmt: off + yield; yield # TRIO911: 11, "yield", Statement("yield", lineno, 4) + # fmt: on + await foo() + + +# not autofixed +async def foo_singleline2(): + # fmt: off + yield; await foo() # TRIO911: 4, "yield", Statement("function definition", lineno-2) + # fmt: on + + +# not autofixed +async def foo_singleline3(): + # fmt: off + if ...: yield # TRIO911: 12, "yield", Statement("function definition", lineno-2) + # fmt: on + await foo() diff --git a/tests/autofix_files/trio91x_autofix.py.diff b/tests/autofix_files/trio91x_autofix.py.diff new file mode 100644 index 00000000..65d90259 --- /dev/null +++ b/tests/autofix_files/trio91x_autofix.py.diff @@ -0,0 +1,80 @@ +--- ++++ +@@ -8,6 +8,7 @@ + # ARG --enable-visitor-codes-regex=(TRIO910)|(TRIO911) + + from typing import Any ++import trio + + + def bar() -> Any: +@@ -20,30 +21,38 @@ + + async def foo1(): # TRIO910: 0, "exit", Statement("function definition", lineno) + bar() ++ await trio.lowlevel.checkpoint() + + + async def foo_return(): + bar() ++ await trio.lowlevel.checkpoint() + return # TRIO910: 4, "return", Statement("function definition", lineno-2) + + + async def foo_yield(): # TRIO911: 0, "exit", Statement("yield", lineno+2) + bar() ++ await trio.lowlevel.checkpoint() + yield # TRIO911: 4, "yield", Statement("function definition", lineno-2) ++ await trio.lowlevel.checkpoint() + + + async def foo_if(): + if ...: ++ await trio.lowlevel.checkpoint() + return # TRIO910: 8, "return", Statement("function definition", lineno-2) + elif ...: ++ await trio.lowlevel.checkpoint() + return # TRIO910: 8, "return", Statement("function definition", lineno-4) + else: ++ await trio.lowlevel.checkpoint() + return # TRIO910: 8, "return", Statement("function definition", lineno-6) + + + async def foo_while(): + await foo() + while True: ++ await trio.lowlevel.checkpoint() + yield # TRIO911: 8, "yield", Statement("yield", lineno) + + +@@ -66,8 +75,10 @@ + async def foo_while4(): + while True: + if ...: ++ await trio.lowlevel.checkpoint() + yield # TRIO911: 12, "yield", Statement("yield", lineno) # TRIO911: 12, "yield", Statement("yield", lineno+2) # TRIO911: 12, "yield", Statement("function definition", lineno-3) + if ...: ++ await trio.lowlevel.checkpoint() + yield # TRIO911: 12, "yield", Statement("yield", lineno) # TRIO911: 12, "yield", Statement("yield", lineno-2) # TRIO911: 12, "yield", Statement("function definition", lineno-5) # TRIO911: 12, "yield", Statement("yield", lineno-2) + # this warns about the yield on lineno-2 twice, since it can arrive here from it in two different ways + +@@ -75,15 +86,19 @@ + # check state management of nested loops + async def foo_nested_while(): + while True: ++ await trio.lowlevel.checkpoint() + yield # TRIO911: 8, "yield", Statement("function definition", lineno-2) + while True: ++ await trio.lowlevel.checkpoint() + yield # TRIO911: 12, "yield", Statement("yield", lineno-2) + while True: ++ await trio.lowlevel.checkpoint() + yield # TRIO911: 16, "yield", Statement("yield", lineno-2) # TRIO911: 16, "yield", Statement("yield", lineno) + + + async def foo_while_nested_func(): + while True: ++ await trio.lowlevel.checkpoint() + yield # TRIO911: 8, "yield", Statement("function definition", lineno-2) # TRIO911: 8, "yield", Statement("yield", lineno) + + async def bar(): diff --git a/tests/eval_files/trio910.py b/tests/eval_files/trio910.py index 5273ac18..10c8126d 100644 --- a/tests/eval_files/trio910.py +++ b/tests/eval_files/trio910.py @@ -90,6 +90,9 @@ async def foo_func_2(): # error: 4, "exit", Statement("function definition", li bar() +# we don't get a newline after the nested function definition before the checkpoint +# when autofixing +# fmt: off async def foo_func_3(): # error: 0, "exit", Statement("function definition", lineno) async def foo_func_4(): await foo() @@ -104,6 +107,7 @@ async def foo_func_7(): # error: 8, "exit", Statement("function definition", li async def foo_func_8(): # error: 0, "exit", Statement("function definition", lineno) def foo_func_9(): raise +# fmt: on # normal function diff --git a/tests/eval_files/trio911.py b/tests/eval_files/trio911.py index d878dba1..56a33ccd 100644 --- a/tests/eval_files/trio911.py +++ b/tests/eval_files/trio911.py @@ -159,6 +159,17 @@ async def foo_while_4(): # error: 0, "exit", Statement("yield", lineno+3) # err yield # error: 16, "yield", Statement("yield", lineno-2)# error: 16, "yield", Statement("yield", lineno) +# check that state management is handled in for loops as well +async def foo_while_4_for(): # error: 0, "exit", Statement("yield", lineno+3) # error: 0, "exit", Statement("yield", lineno+5) # error: 0, "exit", Statement("yield", lineno+7) + await foo() + for i in bar(): + yield # error: 8, "yield", Statement("yield", lineno) # error: 8, "yield", Statement("yield", lineno+2) # error: 8, "yield", Statement("yield", lineno+4) + for i in bar(): + yield # error: 12, "yield", Statement("yield", lineno)# error: 12, "yield", Statement("yield", lineno-2)# error: 12, "yield", Statement("yield", lineno+2) + for i in bar(): + yield # error: 16, "yield", Statement("yield", lineno-2)# error: 16, "yield", Statement("yield", lineno) + + # check error suppression is reset async def foo_while_5(): await foo() @@ -292,6 +303,22 @@ async def foo_while_endless_2(): # error: 0, "exit", Statement("function defini yield +async def foo_while_endless_3(): + while True: + ... + yield # type: ignore[unreachable] + await foo() + + +async def foo_while_endless_4(): + await foo() + while True: + yield + while True: + await foo() + yield + + # try async def foo_try_1(): # error: 0, "exit", Statement("function definition", lineno) # error: 0, "exit", Statement("yield", lineno+2) try: @@ -570,6 +597,9 @@ async def foo_func_2(): # error: 4, "exit", Statement("yield", lineno+1) yield # error: 8, "yield", Statement("function definition", lineno-1) +# autofix doesn't insert newline after nested function def and before checkpoint +# so we need to disable black +# fmt: off async def foo_func_3(): # error: 0, "exit", Statement("yield", lineno+2) await foo() yield @@ -588,6 +618,7 @@ def foo_func_6(): # safe async def foo_func_7(): await foo() ... +# fmt: on # No error from function definition, but may shortcut after yield diff --git a/tests/eval_files/trio91x_autofix.py b/tests/eval_files/trio91x_autofix.py new file mode 100644 index 00000000..c1f09607 --- /dev/null +++ b/tests/eval_files/trio91x_autofix.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +"""Docstring for file + +So we make sure that import is added after it. +""" +# isort: skip_file +# ARG --enable-visitor-codes-regex=(TRIO910)|(TRIO911) + +from typing import Any + + +def bar() -> Any: + ... + + +async def foo() -> Any: + await foo() + + +async def foo1(): # TRIO910: 0, "exit", Statement("function definition", lineno) + bar() + + +async def foo_return(): + bar() + return # TRIO910: 4, "return", Statement("function definition", lineno-2) + + +async def foo_yield(): # TRIO911: 0, "exit", Statement("yield", lineno+2) + bar() + yield # TRIO911: 4, "yield", Statement("function definition", lineno-2) + + +async def foo_if(): + if ...: + return # TRIO910: 8, "return", Statement("function definition", lineno-2) + elif ...: + return # TRIO910: 8, "return", Statement("function definition", lineno-4) + else: + return # TRIO910: 8, "return", Statement("function definition", lineno-6) + + +async def foo_while(): + await foo() + while True: + yield # TRIO911: 8, "yield", Statement("yield", lineno) + + +async def foo_while2(): + await foo() + while True: + yield + await foo() + + +async def foo_while3(): + await foo() + while True: + if ...: + return + await foo() + + +# check that multiple checkpoints don't get inserted +async def foo_while4(): + while True: + if ...: + yield # TRIO911: 12, "yield", Statement("yield", lineno) # TRIO911: 12, "yield", Statement("yield", lineno+2) # TRIO911: 12, "yield", Statement("function definition", lineno-3) + if ...: + yield # TRIO911: 12, "yield", Statement("yield", lineno) # TRIO911: 12, "yield", Statement("yield", lineno-2) # TRIO911: 12, "yield", Statement("function definition", lineno-5) # TRIO911: 12, "yield", Statement("yield", lineno-2) + # this warns about the yield on lineno-2 twice, since it can arrive here from it in two different ways + + +# check state management of nested loops +async def foo_nested_while(): + while True: + yield # TRIO911: 8, "yield", Statement("function definition", lineno-2) + while True: + yield # TRIO911: 12, "yield", Statement("yield", lineno-2) + while True: + yield # TRIO911: 16, "yield", Statement("yield", lineno-2) # TRIO911: 16, "yield", Statement("yield", lineno) + + +async def foo_while_nested_func(): + while True: + yield # TRIO911: 8, "yield", Statement("function definition", lineno-2) # TRIO911: 8, "yield", Statement("yield", lineno) + + async def bar(): + while ...: + ... + await foo() + + +# not handled, but at least doesn't insert an unnecessary checkpoint +async def foo_singleline(): + await foo() + # fmt: off + yield; yield # TRIO911: 11, "yield", Statement("yield", lineno, 4) + # fmt: on + await foo() + + +# not autofixed +async def foo_singleline2(): + # fmt: off + yield; await foo() # TRIO911: 4, "yield", Statement("function definition", lineno-2) + # fmt: on + + +# not autofixed +async def foo_singleline3(): + # fmt: off + if ...: yield # TRIO911: 12, "yield", Statement("function definition", lineno-2) + # fmt: on + await foo() diff --git a/tests/test_flake8_trio.py b/tests/test_flake8_trio.py index 195f4a9d..94aca2ff 100644 --- a/tests/test_flake8_trio.py +++ b/tests/test_flake8_trio.py @@ -37,7 +37,9 @@ (f.stem.upper(), f) for f in (Path(__file__).parent / "eval_files").iterdir() ) autofix_files: dict[str, Path] = { - f.stem.upper(): f for f in (Path(__file__).parent / "autofix_files").iterdir() + f.stem.upper(): f + for f in (Path(__file__).parent / "autofix_files").iterdir() + if f.suffix == ".py" } # check that there's an eval file for each autofix file assert set(autofix_files.keys()) - {f[0] for f in test_files} == set() @@ -74,23 +76,88 @@ def check_version(test: str): } -def check_autofix(test: str, plugin: Plugin, generate_autofix: bool): +# difflib generates lots of lines with one trailing space, which is an eyesore +# and trips up pre-commit, git diffs, etc. If there actually was diff trailing +# space in the content it's picked up elsewhere and by pre-commit. +def strip_difflib_space(s: str) -> str: + if s[-2:] == " \n": + return s[:-2] + "\n" + return s + + +def diff_strings(first: str, second: str, /) -> str: + return "".join( + map( + strip_difflib_space, + difflib.unified_diff( + first.splitlines(keepends=True), + second.splitlines(keepends=True), + ), + ) + ) + + +# replaces all instances of `original` with `new` in string +# unless it's preceded by a `-`, which indicates it's part of a command-line flag +def replace_library(string: str, original: str = "trio", new: str = "anyio") -> str: + return re.sub(rf"(?anyio, + # meaning it's replaced in visited_code, we also replace it in previous generated code + # and in the previous diff + if anyio: + previous_autofixed = replace_library(previous_autofixed) + autofix_diff_content = replace_library(autofix_diff_content) + + # save any difference in the autofixed code + diff = diff_strings(previous_autofixed, visited_code) + + # generate diff between unfixed and visited code, i.e. what's added/removed + added_autofix_diff = diff_strings(unfixed_code, visited_code) + + # print diff, mainly helpful during development + if diff: + print("\n", diff) + + # if --generate-autofix is specified, which it may be during development, + # just silently overwrite the content. + if generate_autofix and not anyio: autofix_files[test].write_text(visited_code) + autofix_diff_file.write_text(added_autofix_diff) return - assert visited_code == current_generated + + # assert that there's no difference in the autofixed code from before + assert visited_code == previous_autofixed + # and assert that the diff is the same, which it should be if the above passes + assert added_autofix_diff == autofix_diff_content @pytest.mark.parametrize(("test", "path"), test_files) @@ -107,11 +174,11 @@ def test_eval(test: str, path: Path, generate_autofix: bool): plugin = Plugin.from_source(content) _ = assert_expected_errors(plugin, *expected, args=parsed_args) - check_autofix(test, plugin, generate_autofix) + check_autofix(test, plugin, content, generate_autofix) @pytest.mark.parametrize(("test", "path"), test_files) -def test_eval_anyio(test: str, path: Path): +def test_eval_anyio(test: str, path: Path, generate_autofix: bool): # read content, replace instances of trio with anyio, and write to tmp_file content = path.read_text() @@ -120,9 +187,7 @@ def test_eval_anyio(test: str, path: Path): # if test is marked NOTRIO, it's not written to require substitution if "# NOTRIO" not in content: - # replace instances of trio with anyio, unless it's part of a flag - # (which all conveniently start with --trio... atm) - content = re.sub(r"(? len("trio") @@ -150,6 +216,8 @@ def test_eval_anyio(test: str, path: Path): message = error.message.format(*error.args) assert "anyio" in message or "trio" not in message + check_autofix(test, plugin, content, generate_autofix, anyio=True) + def _parse_eval_file(test: str, content: str) -> tuple[list[Error], list[str]]: # version check @@ -341,9 +409,9 @@ def print_first_diff(errors: Sequence[Error], expected: Sequence[Error]): for err, exp in zip(errors, expected): if err == exp: continue - if not first_error_line or err.line == first_error_line[0]: + if not first_error_line or err.line == first_error_line[0].line: first_error_line.append(err) - if not first_expected_line or exp.line == first_expected_line[0]: + if not first_expected_line or exp.line == first_expected_line[0].line: first_expected_line.append(exp) if first_expected_line != first_error_line: