From 3feea1a9fa76a301bd2e95566219035e19b3d83a Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Sat, 18 Feb 2023 14:32:25 +0100
Subject: [PATCH] reimplement trio100 & trio101 with libcst
---
flake8_trio/__init__.py | 40 +++++++++--
flake8_trio/runner.py | 32 ++++++++-
flake8_trio/visitors/__init__.py | 5 +-
flake8_trio/visitors/flake8triovisitor.py | 73 +++++++++++++++++++
flake8_trio/visitors/helpers.py | 86 +++++++++++++++++++++--
flake8_trio/visitors/visitor100.py | 67 ++++++++++++++++++
flake8_trio/visitors/visitor101.py | 64 +++++++++++++++++
flake8_trio/visitors/visitors.py | 79 +--------------------
tests/eval_files/trio100.py | 31 +++++++-
tests/test_flake8_trio.py | 22 +++---
10 files changed, 397 insertions(+), 102 deletions(-)
create mode 100644 flake8_trio/visitors/visitor100.py
create mode 100644 flake8_trio/visitors/visitor101.py
diff --git a/flake8_trio/__init__.py b/flake8_trio/__init__.py
index 17fede08..0d3277f0 100644
--- a/flake8_trio/__init__.py
+++ b/flake8_trio/__init__.py
@@ -13,16 +13,19 @@
import ast
import keyword
+import os
import re
import tokenize
from argparse import ArgumentTypeError, Namespace
from typing import TYPE_CHECKING
-from .runner import Flake8TrioRunner
+import libcst as cst
+
+from .runner import Flake8TrioRunner, Flake8TrioRunner_cst
from .visitors import default_disabled_error_codes
if TYPE_CHECKING:
- from collections.abc import Iterable
+ from collections.abc import Iterable, Sequence
from os import PathLike
from flake8.options.manager import OptionManager
@@ -34,24 +37,53 @@
__version__ = "23.2.5"
+# Enable support in libcst for new grammar
+# See e.g. https://github.com/Instagram/LibCST/issues/862
+# wrapping the call and restoring old values in case there's other libcst parsers
+# in the same environment, which we don't wanna mess up.
+def cst_parse_module_native(source: str) -> cst.Module:
+ var = os.environ.get("LIBCST_PARSER_TYPE")
+ try:
+ os.environ["LIBCST_PARSER_TYPE"] = "native"
+ mod = cst.parse_module(source)
+ finally:
+ del os.environ["LIBCST_PARSER_TYPE"]
+ if var is not None: # pragma: no cover
+ os.environ["LIBCST_PARSER_TYPE"] = var
+ return mod
+
+
class Plugin:
name = __name__
version = __version__
options: Namespace = Namespace()
- def __init__(self, tree: ast.AST):
+ def __init__(self, tree: ast.AST, lines: Sequence[str]):
super().__init__()
self._tree = tree
+ source = "".join(lines)
+
+ self._module: cst.Module = cst_parse_module_native(source)
@classmethod
def from_filename(cls, filename: str | PathLike[str]) -> Plugin: # pragma: no cover
# only used with --runslow
with tokenize.open(filename) as f:
source = f.read()
- return cls(ast.parse(source))
+ return cls.from_source(source)
+
+ # alternative `__init__` to avoid re-splitting and/or re-joining lines
+ @classmethod
+ def from_source(cls, source: str) -> Plugin:
+ plugin = Plugin.__new__(cls)
+ super(Plugin, plugin).__init__()
+ plugin._tree = ast.parse(source)
+ plugin._module = cst_parse_module_native(source)
+ return plugin
def run(self) -> Iterable[Error]:
yield from Flake8TrioRunner.run(self._tree, self.options)
+ yield from Flake8TrioRunner_cst(self.options).run(self._module)
@staticmethod
def add_options(option_manager: OptionManager):
diff --git a/flake8_trio/runner.py b/flake8_trio/runner.py
index 7b408e09..ddc907ed 100644
--- a/flake8_trio/runner.py
+++ b/flake8_trio/runner.py
@@ -11,14 +11,18 @@
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
-from .visitors import ERROR_CLASSES, utility_visitors
+import libcst as cst
+
+from .visitors import ERROR_CLASSES, ERROR_CLASSES_CST, utility_visitors
if TYPE_CHECKING:
from argparse import Namespace
from collections.abc import Iterable
+ from libcst import Module
+
from .base import Error
- from .visitors.flake8triovisitor import Flake8TrioVisitor
+ from .visitors.flake8triovisitor import Flake8TrioVisitor, Flake8TrioVisitor_cst
@dataclass
@@ -93,3 +97,27 @@ def visit(self, node: ast.AST):
# restore any outer state that was saved in the visitor method
for subclass in *self.utility_visitors, *self.visitors:
subclass.set_state(subclass.outer.pop(node, {}))
+
+
+class Flake8TrioRunner_cst:
+ def __init__(self, options: Namespace):
+ super().__init__()
+ self.state = SharedState(options)
+ self.options = options
+ self.visitors: tuple[Flake8TrioVisitor_cst, ...] = tuple(
+ v(self.state) for v in ERROR_CLASSES_CST if self.selected(v.error_codes)
+ )
+
+ def run(self, module: Module) -> Iterable[Error]:
+ if not self.visitors:
+ return
+ wrapper = cst.MetadataWrapper(module)
+ for v in self.visitors:
+ _ = wrapper.visit(v)
+ yield from self.state.problems
+
+ def selected(self, error_codes: dict[str, str]) -> bool:
+ return any(
+ re.match(self.state.options.enable_visitor_codes_regex, code)
+ for code in error_codes
+ )
diff --git a/flake8_trio/visitors/__init__.py b/flake8_trio/visitors/__init__.py
index c4ab9933..c22fc40b 100644
--- a/flake8_trio/visitors/__init__.py
+++ b/flake8_trio/visitors/__init__.py
@@ -10,16 +10,19 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
- from .flake8triovisitor import Flake8TrioVisitor
+ from .flake8triovisitor import Flake8TrioVisitor, Flake8TrioVisitor_cst
__all__ = ["ERROR_CLASSES", "default_disabled_error_codes", "utility_visitors"]
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] = []
# Import all visitors so their decorators run, filling the above containers
# This has to be done at the end to avoid circular imports
from . import visitor_utility # isort: skip
+from . import visitor100 # isort: skip
+from . import visitor101 # isort: skip
from . import visitor102 # isort: skip
from . import visitor103_104 # isort: skip
from . import visitor105 # isort: skip
diff --git a/flake8_trio/visitors/flake8triovisitor.py b/flake8_trio/visitors/flake8triovisitor.py
index 8065dfc6..45808fcd 100644
--- a/flake8_trio/visitors/flake8triovisitor.py
+++ b/flake8_trio/visitors/flake8triovisitor.py
@@ -7,6 +7,10 @@
from abc import ABC
from typing import TYPE_CHECKING, Any, Union
+import libcst as cst
+import libcst.matchers as m
+from libcst.metadata import PositionProvider
+
from ..base import Error, Statement
if TYPE_CHECKING:
@@ -157,3 +161,72 @@ def library_str(self) -> str:
def add_library(self, name: str) -> None:
if name not in self.__state.library:
self.__state.library = self.__state.library + (name,)
+
+
+class Flake8TrioVisitor_cst(m.MatcherDecoratableTransformer, ABC):
+ # abstract attribute by not providing a value
+ error_codes: dict[str, str] # pyright: reportUninitializedInstanceVariable=false
+ METADATA_DEPENDENCIES = (PositionProvider,)
+
+ def __init__(self, shared_state: SharedState):
+ super().__init__()
+ self.outer: dict[cst.BaseStatement, dict[str, Any]] = {}
+ self.__state = shared_state
+
+ self.options = self.__state.options
+
+ def get_state(self, *attrs: str, copy: bool = False) -> dict[str, Any]:
+ # require attrs, since we inherit a *ton* of stuff which we don't want to copy
+ assert attrs
+ res: dict[str, Any] = {}
+ for attr in attrs:
+ value = getattr(self, attr)
+ if copy and hasattr(value, "copy"):
+ value = value.copy()
+ res[attr] = value
+ return res
+
+ def set_state(self, attrs: dict[str, Any], copy: bool = False):
+ for attr, value in attrs.items():
+ if copy and hasattr(value, "copy"): # pragma: no cover
+ # not used by visitors yet
+ value = value.copy()
+ setattr(self, attr, value)
+
+ def save_state(self, node: cst.BaseStatement, *attrs: str, copy: bool = False):
+ state = self.get_state(*attrs, copy=copy)
+ if node in self.outer:
+ # not currently used, and not gonna bother adding dedicated test
+ # visitors atm
+ self.outer[node].update(state) # pragma: no cover
+ else:
+ self.outer[node] = state
+
+ def error(
+ self,
+ node: cst.CSTNode,
+ *args: str | Statement | int,
+ error_code: str | None = None,
+ ):
+ if error_code is None:
+ assert (
+ len(self.error_codes) == 1
+ ), "No error code defined, but class has multiple codes"
+ error_code = next(iter(self.error_codes))
+ # don't emit an error if this code is disabled in a multi-code visitor
+ elif not re.match(
+ self.options.enable_visitor_codes_regex, error_code
+ ): # pragma: no cover
+ return
+ pos = self.get_metadata(PositionProvider, node).start
+
+ self.__state.problems.append(
+ Error(
+ # 7 == len('TRIO...'), so alt messages raise the original code
+ error_code[:7],
+ pos.line,
+ pos.column,
+ self.error_codes[error_code],
+ *args,
+ )
+ )
diff --git a/flake8_trio/visitors/helpers.py b/flake8_trio/visitors/helpers.py
index 5bf407b4..58a22ad5 100644
--- a/flake8_trio/visitors/helpers.py
+++ b/flake8_trio/visitors/helpers.py
@@ -7,17 +7,26 @@
import ast
from fnmatch import fnmatch
-from typing import TYPE_CHECKING, TypeVar
+from typing import TYPE_CHECKING, NamedTuple, TypeVar
+
+import libcst as cst
+import libcst.matchers as m
from ..base import Statement
-from . import ERROR_CLASSES, default_disabled_error_codes, utility_visitors
+from . import (
+ ERROR_CLASSES,
+ ERROR_CLASSES_CST,
+ default_disabled_error_codes,
+ utility_visitors,
+)
if TYPE_CHECKING:
- from collections.abc import Iterable
+ from collections.abc import Iterable, Iterator, Sequence
- from .flake8triovisitor import Flake8TrioVisitor, HasLineCol
+ from .flake8triovisitor import Flake8TrioVisitor, Flake8TrioVisitor_cst, HasLineCol
T = TypeVar("T", bound=Flake8TrioVisitor)
+ T_CST = TypeVar("T_CST", bound=Flake8TrioVisitor_cst)
def error_class(error_class: type[T]) -> type[T]:
@@ -26,6 +35,12 @@ def error_class(error_class: type[T]) -> type[T]:
return error_class
+def error_class_cst(error_class: type[T_CST]) -> type[T_CST]:
+ assert error_class.error_codes
+ ERROR_CLASSES_CST.add(error_class)
+ return error_class
+
+
def disabled_by_default(error_class: type[T]) -> type[T]:
assert error_class.error_codes
default_disabled_error_codes.extend(error_class.error_codes)
@@ -174,3 +189,66 @@ def get_matching_call(
):
return node, node.func.attr, node.func.value.id
return None
+
+
+# ___ CST helpers ___
+def oneof_names(*names: str):
+ return m.OneOf(*map(m.Name, names))
+
+
+CSTNode_T = TypeVar("CSTNode_T", bound=cst.CSTNode)
+
+
+def list_contains(
+ seq: Sequence[CSTNode_T], matcher: m.BaseMatcherNode
+) -> Iterator[CSTNode_T]:
+ yield from (item for item in seq if m.matches(item, matcher))
+
+
+class AttributeCall(NamedTuple):
+ node: cst.Call
+ base: str
+ function: str
+
+
+def with_has_call(
+ node: cst.With, *names: str, base: Iterable[str] = ("trio", "anyio")
+) -> list[AttributeCall]:
+ res_list: list[AttributeCall] = []
+ for item in node.items:
+ if res := m.extract(
+ item.item,
+ m.Call(
+ func=m.Attribute(
+ value=m.SaveMatchedNode(m.Name(), name="library"),
+ attr=m.SaveMatchedNode(
+ oneof_names(*names),
+ name="function",
+ ),
+ )
+ ),
+ ):
+ assert isinstance(item.item, cst.Call)
+ assert isinstance(res["library"], cst.Name)
+ assert isinstance(res["function"], cst.Name)
+ if res["library"].value not in base:
+ continue
+ res_list.append(
+ AttributeCall(item.item, res["library"].value, res["function"].value)
+ )
+ return res_list
+
+
+def func_has_decorator(func: cst.FunctionDef, *names: str) -> bool:
+ return any(
+ list_contains(
+ func.decorators,
+ m.Decorator(
+ decorator=m.OneOf(
+ oneof_names(*names),
+ m.Attribute(attr=oneof_names(*names)),
+ m.Call(func=m.Attribute(attr=oneof_names(*names))),
+ )
+ ),
+ )
+ )
diff --git a/flake8_trio/visitors/visitor100.py b/flake8_trio/visitors/visitor100.py
new file mode 100644
index 00000000..b5bf4cbc
--- /dev/null
+++ b/flake8_trio/visitors/visitor100.py
@@ -0,0 +1,67 @@
+"""Contains visitor for TRIO100.
+
+A `with trio.fail_after(...):` or `with trio.move_on_after(...):`
+context does not contain any `await` statements. This makes it pointless, as
+the timeout can only be triggered by a checkpoint.
+Checkpoints on Await, Async For and Async With
+"""
+# if future annotations are imported then shed will reformat away the Union use
+from typing import Any, Union
+
+import libcst as cst
+import libcst.matchers as m
+
+from .flake8triovisitor import Flake8TrioVisitor_cst
+from .helpers import AttributeCall, error_class_cst, with_has_call
+
+
+@error_class_cst
+class Visitor100_libcst(Flake8TrioVisitor_cst):
+ error_codes = {
+ "TRIO100": (
+ "{0}.{1} context contains no checkpoints, remove the context or add"
+ " `await {0}.lowlevel.checkpoint()`."
+ ),
+ }
+
+ def __init__(self, *args: Any, **kwargs: Any):
+ super().__init__(*args, **kwargs)
+ self.has_checkpoint_stack: list[bool] = []
+ self.node_dict: dict[cst.With, list[AttributeCall]] = {}
+
+ def visit_With(self, node: cst.With) -> None:
+ if m.matches(node, m.With(asynchronous=m.Asynchronous())):
+ self.checkpoint_node(node)
+ if res := with_has_call(
+ node, "fail_after", "fail_at", "move_on_after", "move_on_at", "CancelScope"
+ ):
+ self.node_dict[node] = res
+
+ self.has_checkpoint_stack.append(False)
+ else:
+ self.has_checkpoint_stack.append(True)
+
+ def leave_With(self, original_node: cst.With, updated_node: cst.With) -> cst.With:
+ if not self.has_checkpoint_stack.pop():
+ for res in self.node_dict[original_node]:
+ self.error(res.node, res.base, res.function)
+ # if: autofixing is enabled for this code
+ # then: remove the with and pop out it's body
+ return updated_node
+
+ @m.visit(m.Await() | m.For(asynchronous=m.Asynchronous()))
+ # can't use m.call_if_inside(m.With), since it matches parents *or* the node itself
+ # need to use Union due to https://github.com/Instagram/LibCST/issues/870
+ def checkpoint_node(self, node: Union[cst.Await, cst.For, cst.With]):
+ if self.has_checkpoint_stack:
+ self.has_checkpoint_stack[-1] = True
+
+ def visit_FunctionDef(self, node: cst.FunctionDef):
+ self.save_state(node, "has_checkpoint_stack", copy=True)
+ self.has_checkpoint_stack = []
+
+ def leave_FunctionDef(
+ self, original_node: cst.FunctionDef, updated_node: cst.FunctionDef
+ ) -> cst.FunctionDef:
+ self.set_state(self.outer.pop(original_node, {}))
+ return updated_node
diff --git a/flake8_trio/visitors/visitor101.py b/flake8_trio/visitors/visitor101.py
new file mode 100644
index 00000000..31ad0382
--- /dev/null
+++ b/flake8_trio/visitors/visitor101.py
@@ -0,0 +1,64 @@
+"""Contains visitor for TRIO101.
+
+`yield` inside a nursery or cancel scope is only safe when implementing a context manager
+- otherwise, it breaks exception handling.
+"""
+from __future__ import annotations
+
+from typing import Any
+
+import libcst as cst
+import libcst.matchers as m
+
+from .flake8triovisitor import Flake8TrioVisitor_cst
+from .helpers import (
+ cancel_scope_names,
+ error_class_cst,
+ func_has_decorator,
+ with_has_call,
+)
+
+
+@error_class_cst
+class Visitor101(Flake8TrioVisitor_cst):
+ error_codes = {
+ "TRIO101": (
+ "`yield` inside a nursery or cancel scope is only safe when implementing "
+ "a context manager - otherwise, it breaks exception handling."
+ ),
+ }
+
+ def __init__(self, *args: Any, **kwargs: Any):
+ super().__init__(*args, **kwargs)
+ self._yield_is_error = False
+ self._safe_decorator = False
+
+ def visit_With(self, node: cst.With):
+ self.save_state(node, "_yield_is_error", copy=True)
+ # if there's no safe decorator,
+ # and it's not yet been determined that yield is error
+ # and this withitem opens a cancelscope:
+ # then yielding is unsafe
+ self._yield_is_error = (
+ not self._safe_decorator
+ and not self._yield_is_error
+ and bool(with_has_call(node, "open_nursery", *cancel_scope_names))
+ )
+
+ @m.leave(m.OneOf(m.With(), m.FunctionDef()))
+ def restore_state(
+ self, original_node: cst.BaseStatement, updated_node: cst.BaseStatement
+ ) -> cst.BaseStatement:
+ self.set_state(self.outer.pop(original_node, {}))
+ return updated_node
+
+ def visit_FunctionDef(self, node: cst.FunctionDef):
+ self.save_state(node, "_yield_is_error", "_safe_decorator")
+ self._yield_is_error = False
+ self._safe_decorator = func_has_decorator(
+ node, "contextmanager", "asynccontextmanager", "fixture"
+ )
+
+ def visit_Yield(self, node: cst.Yield):
+ if self._yield_is_error:
+ self.error(node)
diff --git a/flake8_trio/visitors/visitors.py b/flake8_trio/visitors/visitors.py
index 28981dc4..4fe292d0 100644
--- a/flake8_trio/visitors/visitors.py
+++ b/flake8_trio/visitors/visitors.py
@@ -6,84 +6,7 @@
from typing import Any, cast
from .flake8triovisitor import Flake8TrioVisitor
-from .helpers import (
- cancel_scope_names,
- disabled_by_default,
- error_class,
- get_matching_call,
- has_decorator,
-)
-
-# used in 100
-checkpoint_node_types = (ast.Await, ast.AsyncFor, ast.AsyncWith)
-
-
-@error_class
-class Visitor100(Flake8TrioVisitor):
- error_codes = {
- "TRIO100": (
- "{0}.{1} context contains no checkpoints, remove the context or add"
- " `await {0}.lowlevel.checkpoint()`."
- ),
- }
-
- def visit_With(self, node: ast.With | ast.AsyncWith):
- for item in (i.context_expr for i in node.items):
- call = get_matching_call(item, *cancel_scope_names)
- if call and not any(
- isinstance(x, checkpoint_node_types) and x != node
- for x in ast.walk(node)
- ):
- # type checking has been done inside get_matching_call
- self.error(item, call[2], call[1])
-
- visit_AsyncWith = visit_With
-
-
-@error_class
-class Visitor101(Flake8TrioVisitor):
- error_codes = {
- "TRIO101": (
- "`yield` inside a nursery or cancel scope is only safe when implementing "
- "a context manager - otherwise, it breaks exception handling."
- ),
- }
-
- def __init__(self, *args: Any, **kwargs: Any):
- super().__init__(*args, **kwargs)
- self._yield_is_error = False
- self._safe_decorator = False
-
- def visit_With(self, node: ast.With | ast.AsyncWith):
- self.save_state(node, "_yield_is_error", copy=True)
- for item in node.items:
- # if there's no safe decorator,
- # and it's not yet been determined that yield is error
- # and this withitem opens a cancelscope:
- # then yielding is unsafe
- if (
- not self._safe_decorator
- and not self._yield_is_error
- and get_matching_call(
- item.context_expr, "open_nursery", *cancel_scope_names
- )
- is not None
- ):
- self._yield_is_error = True
-
- def visit_FunctionDef(self, node: ast.FunctionDef | ast.AsyncFunctionDef):
- self.save_state(node)
- self._yield_is_error = False
- self._safe_decorator = has_decorator(
- node, "contextmanager", "asynccontextmanager", "fixture"
- )
-
- visit_AsyncWith = visit_With
- visit_AsyncFunctionDef = visit_FunctionDef
-
- def visit_Yield(self, node: ast.Yield):
- if self._yield_is_error:
- self.error(node)
+from .helpers import disabled_by_default, error_class, get_matching_call, has_decorator
@error_class
diff --git a/tests/eval_files/trio100.py b/tests/eval_files/trio100.py
index fcb3174d..525be7e8 100644
--- a/tests/eval_files/trio100.py
+++ b/tests/eval_files/trio100.py
@@ -1,12 +1,21 @@
# type: ignore
+
import trio
-with trio.move_on_after(10): # error: 5,"trio", "move_on_after"
+with trio.move_on_after(10): # error: 5, "trio", "move_on_after"
pass
async def function_name():
- async with trio.fail_after(10): # error: 15,"trio", "fail_after"
+ async with trio.fail_after(10): # error: 15, "trio", "fail_after"
+ pass
+ async with trio.fail_at(10): # error: 15, "trio", "fail_at"
+ pass
+ async with trio.move_on_after(10): # error: 15, "trio", "move_on_after"
+ pass
+ async with trio.move_on_at(10): # error: 15, "trio", "move_on_at"
+ pass
+ async with trio.CancelScope(...): # error: 15, "trio", "CancelScope"
pass
with trio.move_on_after(10):
@@ -24,7 +33,7 @@ async def function_name():
with open("filename") as _:
pass
- with trio.fail_after(10): # error: 9,"trio", "fail_after"
+ with trio.fail_after(10): # error: 9, "trio", "fail_after"
pass
send_channel, receive_channel = trio.open_memory_channel(0)
@@ -35,6 +44,22 @@ async def function_name():
async for _ in receive_channel:
pass
+ # fix missed alarm when function is defined inside the with scope
+ with trio.move_on_after(10): # error: 9, "trio", "move_on_after"
+
+ 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)
+
+ async with random_ignored_library.fail_after(10):
+ pass
+
async def function_name2():
with (
diff --git a/tests/test_flake8_trio.py b/tests/test_flake8_trio.py
index dbecc45f..3ec9172e 100644
--- a/tests/test_flake8_trio.py
+++ b/tests/test_flake8_trio.py
@@ -24,7 +24,7 @@
from flake8_trio import Plugin
from flake8_trio.base import Error, Statement
-from flake8_trio.visitors import ERROR_CLASSES
+from flake8_trio.visitors import ERROR_CLASSES, ERROR_CLASSES_CST
if TYPE_CHECKING:
from collections.abc import Iterable, Sequence
@@ -60,7 +60,7 @@ def check_version(test: str):
ERROR_CODES = {
err_code: err_class
- for err_class in ERROR_CLASSES
+ for err_class in (*ERROR_CLASSES, *ERROR_CLASSES_CST)
for err_code in err_class.error_codes.keys()
}
@@ -75,7 +75,7 @@ def test_eval(test: str, path: Path):
if "# TRIO_NO_ERROR" in content:
expected = []
- plugin = Plugin(ast.parse(content))
+ plugin = Plugin.from_source(content)
_ = assert_expected_errors(plugin, *expected, args=parsed_args)
@@ -105,7 +105,7 @@ def test_eval_anyio(test: str, path: Path):
# initialize plugin and check errors, ignoring columns since they occasionally are
# wrong due to len("anyio") > len("trio")
- plugin = Plugin(ast.parse(content))
+ plugin = Plugin.from_source(content)
if "# ANYIO_NO_ERROR" in content:
expected = []
@@ -200,7 +200,9 @@ def _parse_eval_file(test: str, content: str) -> tuple[list[Error], list[str]]:
raise ParseError(msg) from e
for error in expected:
- assert re.match(visitor_codes_regex, error.code)
+ assert re.match(
+ visitor_codes_regex, error.code
+ ), "Expected error code not enabled"
return expected, parsed_args
@@ -259,7 +261,7 @@ def test_noerror_on_sync_code(test: str, path: Path):
+ "))"
)
_ = assert_expected_errors(
- Plugin(tree),
+ Plugin(tree, [ast.unparse(tree)]),
args=[f"--enable-visitor-codes-regex={ignored_codes_regex}"],
)
@@ -443,7 +445,7 @@ async def foo():
finally:
await foo() | ... | return | None
"""
- plugin = Plugin(ast.AST())
+ plugin = Plugin(ast.AST(), [])
initialize_options(plugin, args=["--enable-visitor-codes-regex=TRIO910"])
check = "await foo()"
@@ -502,7 +504,7 @@ async def foo():
def test_114_raises_on_invalid_parameter(capsys: pytest.CaptureFixture[str]):
- plugin = Plugin(ast.AST())
+ plugin = Plugin(ast.AST(), [])
# flake8 will reraise ArgumentError as SystemExit
for arg in "blah.foo", "foo*", "*":
with pytest.raises(SystemExit):
@@ -513,7 +515,7 @@ def test_114_raises_on_invalid_parameter(capsys: pytest.CaptureFixture[str]):
def test_200_options(capsys: pytest.CaptureFixture[str]):
- plugin = Plugin(ast.AST())
+ plugin = Plugin(ast.AST(), [])
for i, arg in (0, "foo"), (2, "foo->->bar"), (2, "foo->bar->fee"):
with pytest.raises(SystemExit):
initialize_options(plugin, args=[f"--trio200-blocking-calls={arg}"])
@@ -646,7 +648,7 @@ def test_does_not_crash_on_any_valid_code(self, syntax_tree: ast.AST):
# Given any syntatically-valid source code, the checker should
# not crash. This tests doesn't check that we do the *right* thing,
# just that we don't crash on valid-if-poorly-styled code!
- plugin = Plugin(syntax_tree)
+ plugin = Plugin(syntax_tree, [ast.unparse(syntax_tree)])
initialize_options(
plugin, [f"--enable-visitor-codes-regex={enable_visitor_codes_regex}"]
)