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: