From 4b6e253fbb87df9f5abd0f88461edb0b74e8ead9 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Fri, 5 Aug 2022 17:16:04 +0200 Subject: [PATCH 1/6] add trio[302], nurseries and async context manager nesting --- CHANGELOG.md | 3 +++ README.md | 1 + flake8_trio.py | 22 +++++++++++++++++-- tests/trio302.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 tests/trio302.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e5ad2de..36dd5355 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog *[CalVer, YY.month.patch](https://calver.org/)* +## Future +- Added TRIO302: async context manager inside nursery. Nurseries should be outermost. + ## 22.8.4 - Fix TRIO108 raising errors on yields in some sync code. - TRIO109 now skips all decorated functions to avoid false alarms diff --git a/README.md b/README.md index 17c8c986..73dc0ec6 100644 --- a/README.md +++ b/README.md @@ -33,3 +33,4 @@ pip install flake8-trio Checkpoints are `await`, `async for`, and `async with` (on one of enter/exit). - **TRIO109**: Async function definition with a `timeout` parameter - use `trio.[fail/move_on]_[after/at]` instead - **TRIO110**: `while : await trio.sleep()` should be replaced by a `trio.Event`. +- **TRIO302**: async context manager inside nursery. Nurseries should be outermost. diff --git a/flake8_trio.py b/flake8_trio.py index 2088ed88..652fe669 100644 --- a/flake8_trio.py +++ b/flake8_trio.py @@ -29,6 +29,7 @@ "TRIO108": "{0} from async iterable with no guaranteed checkpoint since {1.name} on line {1.lineno}", "TRIO109": "Async function definition with a `timeout` parameter - use `trio.[fail/move_on]_[after/at]` instead", "TRIO110": "`while : await trio.sleep()` should be replaced by a `trio.Event`.", + "TRIO302": "async context manager inside nursery opened on line {}. Nurseries should be outermost.", } @@ -161,7 +162,7 @@ def get_trio_scope(node: ast.AST, *names: str) -> Optional[TrioScope]: and isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name) and node.func.value.id == "trio" - and node.func.attr in names + and (node.func.attr in names or not names) ): return TrioScope(node, node.func.attr) return None @@ -184,6 +185,7 @@ def __init__(self): # variables only used for 101 self._yield_is_error = False self._safe_decorator = False + self._inside_nursery: Optional[int] = None # ---- 100, 101 ---- def visit_With(self, node: Union[ast.With, ast.AsyncWith]): @@ -208,7 +210,11 @@ def visit_With(self, node: Union[ast.With, ast.AsyncWith]): # reset yield_is_error self.set_state(outer) - visit_AsyncWith = visit_With + def visit_AsyncWith(self, node: ast.AsyncWith): + outer = self._inside_nursery + self.check_for_trio302(node.items) + self.visit_With(node) + self._inside_nursery = outer # ---- 100 ---- def check_for_trio100(self, node: Union[ast.With, ast.AsyncWith]): @@ -225,6 +231,7 @@ def check_for_trio100(self, node: Union[ast.With, ast.AsyncWith]): def visit_FunctionDef(self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]): outer = self.get_state() self._yield_is_error = False + self._inside_nursery = None # check for @ and @. if has_decorator(node.decorator_list, *context_manager_names): @@ -280,6 +287,17 @@ def check_for_110(self, node: ast.While): ): self.error("TRIO110", node) + def check_for_trio302(self, withitems: List[ast.withitem]): + calls = [w.context_expr for w in withitems] + for call in calls: + ss = get_trio_scope(call) + if not ss: + continue + if ss.funcname == "open_nursery": + self._inside_nursery = ss.node.lineno + elif self._inside_nursery is not None: + self.error("TRIO302", ss.node, self._inside_nursery) + def critical_except(node: ast.ExceptHandler) -> Optional[Statement]: def has_exception(node: Optional[ast.expr]) -> str: diff --git a/tests/trio302.py b/tests/trio302.py new file mode 100644 index 00000000..9e5c9308 --- /dev/null +++ b/tests/trio302.py @@ -0,0 +1,56 @@ +import trio +import trio as noterror + + +async def foo(): + async with trio.open_nursery(): + async with trio.open_process(): # error: 19, 6 + ... + + async with trio.open_process(): + async with trio.open_nursery(): + ... + + async with trio.open_nursery(): + + async def bar(): + async with trio.open_process(): # safe + ... + + async with trio.open_nursery(): + with trio.anything(): # safe (not async) + ... + + async with trio.open_nursery(): + async with noterror.booboo(): # safe + ... + + async with trio.open_nursery(): + async with trio.anything.anything.anything(): # ??? - currently safe + ... + + async with trio.open_nursery(): + async with trio.open_nursery(): # safe + ... + async with trio.anything(): # error: 19, 32 + ... + + async with trio.anything(): + async with trio.open_nursery(): # safe + async with trio.open_nursery(): # safe + async with trio.anything(): # error: 27, 40 + async with trio.anything(): # error: 31, 40 + ... + + async with noterror.booboo(), trio.open_nursery(): + async with noterror.booboo(), trio.anything(): # error: 38, 45 + ... + + async with trio.open_nursery(), trio.anything(): # error: 36, 49 + ... + + async with trio.anything(), trio.open_nursery(): # safe + ... + + async with trio.open_nursery(), trio.anything(), trio.open_nursery(): # error: 36, 55 + ... From fcc02d46d5ecc6884e385ea0010d40383ef54fc6 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Tue, 9 Aug 2022 15:00:57 +0200 Subject: [PATCH 2/6] rework 302 according to new specifications --- flake8_trio.py | 98 +++++++++++++------ tests/test_flake8_trio.py | 19 ++-- tests/trio302.py | 201 ++++++++++++++++++++++++++++++-------- 3 files changed, 240 insertions(+), 78 deletions(-) diff --git a/flake8_trio.py b/flake8_trio.py index 652fe669..0874ccc2 100644 --- a/flake8_trio.py +++ b/flake8_trio.py @@ -11,7 +11,7 @@ import ast import tokenize -from typing import Any, Dict, Iterable, List, NamedTuple, Optional, Set, Union +from typing import Any, Dict, Iterable, List, NamedTuple, Optional, Set, Tuple, Union # CalVer: YY.month.patch, e.g. first release of July 2022 == "22.7.1" __version__ = "22.8.4" @@ -29,7 +29,7 @@ "TRIO108": "{0} from async iterable with no guaranteed checkpoint since {1.name} on line {1.lineno}", "TRIO109": "Async function definition with a `timeout` parameter - use `trio.[fail/move_on]_[after/at]` instead", "TRIO110": "`while : await trio.sleep()` should be replaced by a `trio.Event`.", - "TRIO302": "async context manager inside nursery opened on line {}. Nurseries should be outermost.", + "TRIO302": "call to nursery.start/start_soon with resource from context manager opened on line {} something something nursery on line {}", } @@ -40,7 +40,7 @@ class Statement(NamedTuple): # ignore col offset since many tests don't supply that def __eq__(self, other: Any) -> bool: - return isinstance(other, Statement) and self[:2] == other[:2] + return isinstance(other, Statement) and self[:2] == other[:2] # type: ignore HasLineInfo = Union[ast.expr, ast.stmt, ast.arg, ast.excepthandler, Statement] @@ -140,10 +140,19 @@ def error(self, error: str, node: HasLineInfo, *args: object): if not self.suppress_errors: self._problems.append(Error(error, node.lineno, node.col_offset, *args)) - def get_state(self, *attrs: str) -> Dict[str, Any]: + def get_state(self, *attrs: str, copy: bool = False) -> Dict[str, Any]: if not attrs: attrs = tuple(self.__dict__.keys()) - return {attr: getattr(self, attr) for attr in attrs if attr != "_problems"} + res: Dict[str, Any] = {} + for attr in attrs: + if attr == "_problems": + continue + value = getattr(self, attr) + if copy and hasattr(value, "copy"): + value = value.copy() + res[attr] = value + return res + # return {attr: getattr(self, attr) for attr in attrs if attr != "_problems"} def set_state(self, attrs: Dict[str, Any], copy: bool = False): for attr, value in attrs.items(): @@ -185,36 +194,41 @@ def __init__(self): # variables only used for 101 self._yield_is_error = False self._safe_decorator = False - self._inside_nursery: Optional[int] = None + self._context_manager_stack: List[Tuple[ast.expr, str, bool]] = [] - # ---- 100, 101 ---- + # ---- 100, 101, 302 ---- def visit_With(self, node: Union[ast.With, ast.AsyncWith]): - # 100 self.check_for_trio100(node) - # 101 for rest of function - outer = self.get_state("_yield_is_error") + outer = self.get_state("_yield_is_error", "_context_manager_stack", copy=True) # Check for a `with trio.` - if not self._safe_decorator: - for item in (i.context_expr for i in node.items): + for item in node.items: + # 101 + if not self._safe_decorator and not self._yield_is_error: if ( - get_trio_scope(item, "open_nursery", *cancel_scope_names) + get_trio_scope( + item.context_expr, "open_nursery", *cancel_scope_names + ) is not None ): self._yield_is_error = True - break + # 302 + if isinstance(item.optional_vars, ast.Name) and isinstance( + item.context_expr, ast.Call + ): + is_nursery = ( + get_trio_scope(item.context_expr, "open_nursery") is not None + ) + poop = (item.context_expr.func, item.optional_vars.id, is_nursery) + self._context_manager_stack.append(poop) self.generic_visit(node) # reset yield_is_error self.set_state(outer) - def visit_AsyncWith(self, node: ast.AsyncWith): - outer = self._inside_nursery - self.check_for_trio302(node.items) - self.visit_With(node) - self._inside_nursery = outer + visit_AsyncWith = visit_With # ---- 100 ---- def check_for_trio100(self, node: Union[ast.With, ast.AsyncWith]): @@ -231,7 +245,7 @@ def check_for_trio100(self, node: Union[ast.With, ast.AsyncWith]): def visit_FunctionDef(self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]): outer = self.get_state() self._yield_is_error = False - self._inside_nursery = None + self._context_manager_stack = [] # check for @ and @. if has_decorator(node.decorator_list, *context_manager_names): @@ -287,16 +301,40 @@ def check_for_110(self, node: ast.While): ): self.error("TRIO110", node) - def check_for_trio302(self, withitems: List[ast.withitem]): - calls = [w.context_expr for w in withitems] - for call in calls: - ss = get_trio_scope(call) - if not ss: - continue - if ss.funcname == "open_nursery": - self._inside_nursery = ss.node.lineno - elif self._inside_nursery is not None: - self.error("TRIO302", ss.node, self._inside_nursery) + def visit_Call(self, node: ast.Call): + def get_id(node: ast.AST) -> Optional[ast.Name]: + if isinstance(node, ast.Name): + return node + if isinstance(node, ast.Attribute): + return get_id(node.value) + if isinstance(node, ast.keyword): + return get_id(node.value) + return None + + if ( + isinstance(node.func, ast.Attribute) + and isinstance(node.func.value, ast.Name) + and node.func.attr in ("start", "start_soon") + ): + called_vars: Dict[str, ast.Name] = {} + for arg in (*node.args, *node.keywords): + name = get_id(arg) + if name: + called_vars[name.id] = name + + nursery_call = None + for expr, cm_name, is_nursery in self._context_manager_stack: + if node.func.value.id == cm_name: + if not is_nursery: + break + nursery_call = expr + continue + if nursery_call is None: + continue + if cm_name in called_vars: + self.error("TRIO302", node, expr.lineno, nursery_call.lineno) + + self.generic_visit(node) def critical_except(node: ast.ExceptHandler) -> Optional[Statement]: diff --git a/tests/test_flake8_trio.py b/tests/test_flake8_trio.py index 2165fdd4..cd4f70f0 100644 --- a/tests/test_flake8_trio.py +++ b/tests/test_flake8_trio.py @@ -79,7 +79,10 @@ def test_eval(test: str, path: str): except Exception as e: print(f"lineno: {lineno}, line: {line}", file=sys.stderr) raise e - col, *args = args + if args: + col, *args = args + else: + col = 0 assert isinstance( col, int ), f'invalid column "{col}" @L{lineno}, in "{line}"' @@ -157,13 +160,15 @@ def assert_expected_errors(plugin: Plugin, include: Iterable[str], *expected: Er def print_first_diff(errors: Sequence[Error], expected: Sequence[Error]): first_error_line: List[Error] = [] - for e in errors: - if e.line == errors[0].line: - first_error_line.append(e) first_expected_line: List[Error] = [] - for e in expected: - if e.line == expected[0].line: - first_expected_line.append(e) + for err, exp in zip(errors, expected): + if err == exp: + continue + if not first_error_line or err.line == first_error_line[0]: + first_error_line.append(err) + if not first_expected_line or exp.line == first_expected_line[0]: + first_expected_line.append(exp) + if first_expected_line != first_error_line: print( "First lines with different errors", diff --git a/tests/trio302.py b/tests/trio302.py index 9e5c9308..d8477032 100644 --- a/tests/trio302.py +++ b/tests/trio302.py @@ -1,56 +1,175 @@ +from typing import Any + import trio import trio as noterror async def foo(): async with trio.open_nursery(): - async with trio.open_process(): # error: 19, 6 - ... + ... - async with trio.open_process(): - async with trio.open_nursery(): - ... + # async nursery + async with trio.open_nursery() as nursery: + # async context manager + async with trio.open_process() as bar: + nursery.start(bar) # error: 12, line-1, line-3 + nursery.start(foo=bar) # error: 12, line-2, line-4 + nursery.start(..., ..., bar, ...) # error: 12, line-3, line-5 - async with trio.open_nursery(): + nursery.start_soon(bar) # error: 12, line-5, line-7 - async def bar(): - async with trio.open_process(): # safe - ... + # sync context manager + with open("") as bar: + nursery.start(bar) # error: 12, line-1, line-11 + nursery.start(foo=bar) # error: 12, line-2, line-12 + nursery.start(..., ..., bar, ...) # error: 12, line-3, line-13 - async with trio.open_nursery(): - with trio.anything(): # safe (not async) - ... + nursery.start_soon(bar) # error: 12, line-5, line-15 - async with trio.open_nursery(): - async with noterror.booboo(): # safe - ... + # sync nursery + with trio.open_nursery() as nursery: + # async context manager + async with trio.open_process() as bar: + nursery.start(bar) # error: 12, line-1, line-3 + nursery.start(foo=bar) # error: 12, line-2, line-4 + nursery.start(..., ..., bar, ...) # error: 12, line-3, line-5 - async with trio.open_nursery(): - async with trio.anything.anything.anything(): # ??? - currently safe - ... + nursery.start_soon(bar) # error: 12, line-5, line-7 - async with trio.open_nursery(): - async with trio.open_nursery(): # safe - ... - async with trio.anything(): # error: 19, 32 - ... - - async with trio.anything(): - async with trio.open_nursery(): # safe - async with trio.open_nursery(): # safe - async with trio.anything(): # error: 27, 40 - async with trio.anything(): # error: 31, 40 - ... - - async with noterror.booboo(), trio.open_nursery(): - async with noterror.booboo(), trio.anything(): # error: 38, 45 - ... - - async with trio.open_nursery(), trio.anything(): # error: 36, 49 - ... + # sync context manager + with open("") as bar: + nursery.start(bar) # error: 12, line-1, line-11 + nursery.start(foo=bar) # error: 12, line-2, line-12 + nursery.start(..., ..., bar, ...) # error: 12, line-3, line-13 - async with trio.anything(), trio.open_nursery(): # safe - ... + nursery.start_soon(bar) # error: 12, line-5, line-15 - async with trio.open_nursery(), trio.anything(), trio.open_nursery(): # error: 36, 55 - ... + # nursery inside context manager + async with trio.open_process() as bar: + async with trio.open_nursery() as nursery: + nursery.start(bar) # safe + with trio.open_process() as bar: + async with trio.open_nursery() as nursery: + nursery.start(bar) # safe + async with trio.open_process() as bar: + with trio.open_nursery() as nursery: + nursery.start(bar) # safe + with trio.open_process() as bar: + with trio.open_nursery() as nursery: + nursery.start(bar) # safe + + # reset variables on nested function + async with trio.open_nursery() as nursery: + + async def foo_1(): + nursery = noterror.something + async with trio.open_process() as bar_2: + nursery.start(bar_2) # safe + + def foo_2(): + nursery = noterror.something + with trio.open_process() as bar_2: + nursery.start(bar_2) # safe + + # specifically check for trio.open_nursery + async with noterror.open_nursery() as nursery: + async with trio.open("") as bar: + nursery.start(bar) + + bar_1: Any = "" + bar_2: Any = "" + nursery_2: Any = "" + + async with trio.open_nursery() as nursery_1: + nursery_1.start(bar_1) + nursery_1.start(bar_2) + nursery_2.start(bar_1) + nursery_2.start(bar_2) + async with trio.open_nursery() as nursery_2: + nursery_1.start(bar_1) + nursery_1.start(bar_2) + nursery_2.start(bar_1) + nursery_2.start(bar_2) + with open("") as bar_1: + nursery_1.start(bar_1) # error: 16, line-1, line-11 + nursery_1.start(bar_2) + nursery_2.start(bar_1) # error: 16, line-3, line-8 + nursery_2.start(bar_2) + async with trio.open("") as bar_2: + nursery_1.start(bar_1) # error: 20, line-6, line-16 + nursery_1.start(bar_2) # error: 20, line-2, line-17 + nursery_2.start(bar_1) # error: 20, line-8, line-13 + nursery_2.start(bar_2) # error: 20, line-4, line-14 + nursery_1.start(bar_1) # error: 16, line-10, line-20 + nursery_1.start(bar_2) + nursery_2.start(bar_1) # error: 16, line-12, line-17 + nursery_2.start(bar_2) + nursery_1.start(bar_1) + nursery_1.start(bar_2) + nursery_2.start(bar_1) + nursery_2.start(bar_2) + nursery_1.start(bar_1) + nursery_1.start(bar_2) + nursery_2.start(bar_1) + nursery_2.start(bar_2) + + async with trio.open_nursery() as nursery_1: + nursery_1.start(bar_1) + nursery_1.start(bar_2) + nursery_2.start(bar_1) + nursery_2.start(bar_2) + with open("") as bar_1: + nursery_1.start(bar_1) # error: 12, line-1, line-6 + nursery_1.start(bar_2) + nursery_2.start(bar_1) + nursery_2.start(bar_2) + async with trio.open_nursery() as nursery_2: + nursery_1.start(bar_1) # error: 16, line-6, line-11 + nursery_1.start(bar_2) + nursery_2.start(bar_1) + nursery_2.start(bar_2) + async with trio.open("") as bar_2: + nursery_1.start(bar_1) # error: 20, line-11, line-16 + nursery_1.start(bar_2) # error: 20, line-2, line-17 + nursery_2.start(bar_1) + nursery_2.start(bar_2) # error: 20, line-4, line-9 + nursery_1.start(bar_1) # error: 16, line-15, line-20 + nursery_1.start(bar_2) + nursery_2.start(bar_1) + nursery_2.start(bar_2) + nursery_1.start(bar_1) # error: 12, line-19, line-24 + nursery_1.start(bar_2) + nursery_2.start(bar_1) + nursery_2.start(bar_2) + nursery_1.start(bar_1) + nursery_1.start(bar_2) + nursery_2.start(bar_1) + nursery_2.start(bar_2) + + async with trio.open_nursery() as nursery_1, trio.anything() as bar_1, trio.open_nursery() as nursery_2, trio.anything() as bar_2: + nursery_1.start(bar_1) # error: 8, line-1, line-1 + nursery_1.start(bar_2) # error: 8, line-2, line-2 + nursery_2.start(bar_1) + nursery_2.start(bar_2) # error: 8, line-4, line-4 + + async with trio.open_nursery() as nursery: + async with trio.anything() as bar: + nursery.start(noterror.bar) # safe + nursery.start(bar.anything) # error: 12, line-2, line-3 + nursery.start(bar.anything.anything) # error: 12, line-3, line-4 + + # I think this is an error + async with trio.open_nursery() as nursery: + async with trio.open_nursery() as nursery_2: + nursery.start(nursery_2) # error: 12, line-1, line-2 + + # in theory safe + async with trio.open_nursery() as nursery: + nursery = noterror.anything + async with trio.anything() as bar: + nursery.start_soon(bar) # error: 12, line-1, line-3 + + async with trio.open_nursery() as nursery: + async with trio.anything() as nursery: + async with trio.anything() as bar: + nursery.start_soon(bar) From e01300b11ac35e52cb74130840cb8a09a486088d Mon Sep 17 00:00:00 2001 From: jakkdl Date: Tue, 9 Aug 2022 21:45:47 +0200 Subject: [PATCH 3/6] fixed issues --- flake8_trio.py | 67 +++++++++++--------- tests/test_flake8_trio.py | 22 ++++--- tests/trio302.py | 125 ++++++++++++++++++++++++++------------ 3 files changed, 138 insertions(+), 76 deletions(-) diff --git a/flake8_trio.py b/flake8_trio.py index 0874ccc2..690fddf7 100644 --- a/flake8_trio.py +++ b/flake8_trio.py @@ -29,7 +29,7 @@ "TRIO108": "{0} from async iterable with no guaranteed checkpoint since {1.name} on line {1.lineno}", "TRIO109": "Async function definition with a `timeout` parameter - use `trio.[fail/move_on]_[after/at]` instead", "TRIO110": "`while : await trio.sleep()` should be replaced by a `trio.Event`.", - "TRIO302": "call to nursery.start/start_soon with resource from context manager opened on line {} something something nursery on line {}", + "TRIO302": "variable {2}, from context manager on line {0}, passed to {3} from nursery opened on {1}, might get closed while in use", } @@ -191,10 +191,16 @@ class VisitorMiscChecks(Flake8TrioVisitor): def __init__(self): super().__init__() - # variables only used for 101 + # 101 self._yield_is_error = False self._safe_decorator = False + + # 302 self._context_manager_stack: List[Tuple[ast.expr, str, bool]] = [] + self._nursery_call_index: Optional[int] = None + self._nursery_call_name: Optional[str] = None + + self.defaults = self.get_state() # ---- 100, 101, 302 ---- def visit_With(self, node: Union[ast.With, ast.AsyncWith]): @@ -244,8 +250,7 @@ def check_for_trio100(self, node: Union[ast.With, ast.AsyncWith]): # ---- 101 ---- def visit_FunctionDef(self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]): outer = self.get_state() - self._yield_is_error = False - self._context_manager_stack = [] + self.set_state(self.defaults, copy=True) # check for @ and @. if has_decorator(node.decorator_list, *context_manager_names): @@ -260,6 +265,12 @@ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef): self.check_109(node) self.visit_FunctionDef(node) + def visit_Lambda(self, node: ast.Lambda): + outer = self.get_state() + self.set_state(self.defaults, copy=True) + self.generic_visit(node) + self.set_state(outer) + # ---- 101 ---- def visit_Yield(self, node: ast.Yield): if self._yield_is_error: @@ -286,6 +297,7 @@ def visit_Import(self, node: ast.Import): for name in node.names: if name.name == "trio" and name.asname is not None: self.error("TRIO106", node) + self.generic_visit(node) # ---- 110 ---- def visit_While(self, node: ast.While): @@ -302,38 +314,37 @@ def check_for_110(self, node: ast.While): self.error("TRIO110", node) def visit_Call(self, node: ast.Call): - def get_id(node: ast.AST) -> Optional[ast.Name]: - if isinstance(node, ast.Name): - return node - if isinstance(node, ast.Attribute): - return get_id(node.value) - if isinstance(node, ast.keyword): - return get_id(node.value) - return None + outer = self.get_state("_nursery_call_index", "_nursery_call_name") if ( isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name) and node.func.attr in ("start", "start_soon") ): - called_vars: Dict[str, ast.Name] = {} - for arg in (*node.args, *node.keywords): - name = get_id(arg) - if name: - called_vars[name.id] = name - - nursery_call = None - for expr, cm_name, is_nursery in self._context_manager_stack: + self._nursery_call_index = None + for i, (_, cm_name, is_nursery) in enumerate(self._context_manager_stack): if node.func.value.id == cm_name: - if not is_nursery: - break - nursery_call = expr - continue - if nursery_call is None: - continue - if cm_name in called_vars: - self.error("TRIO302", node, expr.lineno, nursery_call.lineno) + if is_nursery: + self._nursery_call_index = i + self._nursery_call_name = node.func.attr + else: + self._nursery_call_index = self._nursery_call_name = None + + self.generic_visit(node) + self.set_state(outer) + def visit_Name(self, node: ast.Name): + if self._nursery_call_index is not None: + for i, (expr, cm_name, _) in enumerate(self._context_manager_stack): + if cm_name == node.id and i > self._nursery_call_index: + self.error( + "TRIO302", + node, + expr.lineno, + self._context_manager_stack[self._nursery_call_index][0].lineno, + node.id, + self._nursery_call_name, + ) self.generic_visit(node) diff --git a/tests/test_flake8_trio.py b/tests/test_flake8_trio.py index cd4f70f0..cafa2715 100644 --- a/tests/test_flake8_trio.py +++ b/tests/test_flake8_trio.py @@ -66,15 +66,19 @@ def test_eval(test: str, path: str): try: # Append a bunch of empty strings so string formatting gives garbage # instead of throwing an exception - args = eval( - f"[{reg_match}]", - { - "lineno": lineno, - "line": lineno, - "Statement": Statement, - "Stmt": Statement, - }, - ) + try: + args = eval( + f"[{reg_match}]", + { + "lineno": lineno, + "line": lineno, + "Statement": Statement, + "Stmt": Statement, + }, + ) + except NameError: + print(f"failed to eval on line {lineno}", file=sys.stderr) + raise except Exception as e: print(f"lineno: {lineno}, line: {line}", file=sys.stderr) diff --git a/tests/trio302.py b/tests/trio302.py index d8477032..7d3ab48b 100644 --- a/tests/trio302.py +++ b/tests/trio302.py @@ -4,6 +4,7 @@ import trio as noterror +# fmt: off async def foo(): async with trio.open_nursery(): ... @@ -12,37 +13,37 @@ async def foo(): async with trio.open_nursery() as nursery: # async context manager async with trio.open_process() as bar: - nursery.start(bar) # error: 12, line-1, line-3 - nursery.start(foo=bar) # error: 12, line-2, line-4 - nursery.start(..., ..., bar, ...) # error: 12, line-3, line-5 + nursery.start(bar) # error: 26, line-1, line-3, "bar", "start" + nursery.start(foo=bar) # error: 30, line-2, line-4, "bar", "start" + nursery.start(..., ..., bar, ...) # error: 36, line-3, line-5, "bar", "start" - nursery.start_soon(bar) # error: 12, line-5, line-7 + nursery.start_soon(bar) # error: 31, line-5, line-7, "bar", "start_soon" # sync context manager with open("") as bar: - nursery.start(bar) # error: 12, line-1, line-11 - nursery.start(foo=bar) # error: 12, line-2, line-12 - nursery.start(..., ..., bar, ...) # error: 12, line-3, line-13 + nursery.start(bar) # error: 26, line-1, line-11, "bar", "start" + nursery.start(foo=bar) # error: 30, line-2, line-12, "bar", "start" + nursery.start(..., ..., bar, ...) # error: 36, line-3, line-13, "bar", "start" - nursery.start_soon(bar) # error: 12, line-5, line-15 + nursery.start_soon(bar) # error: 31, line-5, line-15, "bar", "start_soon" # sync nursery with trio.open_nursery() as nursery: # async context manager async with trio.open_process() as bar: - nursery.start(bar) # error: 12, line-1, line-3 - nursery.start(foo=bar) # error: 12, line-2, line-4 - nursery.start(..., ..., bar, ...) # error: 12, line-3, line-5 + nursery.start(bar) # error: 26, line-1, line-3, "bar", "start" + nursery.start(foo=bar) # error: 30, line-2, line-4, "bar", "start" + nursery.start(..., ..., bar, ...) # error: 36, line-3, line-5, "bar", "start" - nursery.start_soon(bar) # error: 12, line-5, line-7 + nursery.start_soon(bar) # error: 31, line-5, line-7, "bar", "start_soon" # sync context manager with open("") as bar: - nursery.start(bar) # error: 12, line-1, line-11 - nursery.start(foo=bar) # error: 12, line-2, line-12 - nursery.start(..., ..., bar, ...) # error: 12, line-3, line-13 + nursery.start(bar) # error: 26, line-1, line-11, "bar", "start" + nursery.start(foo=bar) # error: 30, line-2, line-12, "bar", "start" + nursery.start(..., ..., bar, ...) # error: 36, line-3, line-13, "bar", "start" - nursery.start_soon(bar) # error: 12, line-5, line-15 + nursery.start_soon(bar) # error: 31, line-5, line-15, "bar", "start_soon" # nursery inside context manager async with trio.open_process() as bar: @@ -91,18 +92,18 @@ def foo_2(): nursery_2.start(bar_1) nursery_2.start(bar_2) with open("") as bar_1: - nursery_1.start(bar_1) # error: 16, line-1, line-11 + nursery_1.start(bar_1) # error: 32, line-1, line-11, "bar_1", "start" nursery_1.start(bar_2) - nursery_2.start(bar_1) # error: 16, line-3, line-8 + nursery_2.start(bar_1) # error: 32, line-3, line-8, "bar_1", "start" nursery_2.start(bar_2) async with trio.open("") as bar_2: - nursery_1.start(bar_1) # error: 20, line-6, line-16 - nursery_1.start(bar_2) # error: 20, line-2, line-17 - nursery_2.start(bar_1) # error: 20, line-8, line-13 - nursery_2.start(bar_2) # error: 20, line-4, line-14 - nursery_1.start(bar_1) # error: 16, line-10, line-20 + nursery_1.start(bar_1) # error: 36, line-6, line-16, "bar_1", "start" + nursery_1.start(bar_2) # error: 36, line-2, line-17, "bar_2", "start" + nursery_2.start(bar_1) # error: 36, line-8, line-13, "bar_1", "start" + nursery_2.start(bar_2) # error: 36, line-4, line-14, "bar_2", "start" + nursery_1.start(bar_1) # error: 32, line-10, line-20, "bar_1", "start" nursery_1.start(bar_2) - nursery_2.start(bar_1) # error: 16, line-12, line-17 + nursery_2.start(bar_1) # error: 32, line-12, line-17, "bar_1", "start" nursery_2.start(bar_2) nursery_1.start(bar_1) nursery_1.start(bar_2) @@ -119,25 +120,25 @@ def foo_2(): nursery_2.start(bar_1) nursery_2.start(bar_2) with open("") as bar_1: - nursery_1.start(bar_1) # error: 12, line-1, line-6 + nursery_1.start(bar_1) # error: 28, line-1, line-6, "bar_1", "start" nursery_1.start(bar_2) nursery_2.start(bar_1) nursery_2.start(bar_2) async with trio.open_nursery() as nursery_2: - nursery_1.start(bar_1) # error: 16, line-6, line-11 + nursery_1.start(bar_1) # error: 32, line-6, line-11, "bar_1", "start" nursery_1.start(bar_2) nursery_2.start(bar_1) nursery_2.start(bar_2) async with trio.open("") as bar_2: - nursery_1.start(bar_1) # error: 20, line-11, line-16 - nursery_1.start(bar_2) # error: 20, line-2, line-17 + nursery_1.start(bar_1) # error: 36, line-11, line-16, "bar_1", "start" + nursery_1.start(bar_2) # error: 36, line-2, line-17, "bar_2", "start" nursery_2.start(bar_1) - nursery_2.start(bar_2) # error: 20, line-4, line-9 - nursery_1.start(bar_1) # error: 16, line-15, line-20 + nursery_2.start(bar_2) # error: 36, line-4, line-9, "bar_2", "start" + nursery_1.start(bar_1) # error: 32, line-15, line-20, "bar_1", "start" nursery_1.start(bar_2) nursery_2.start(bar_1) nursery_2.start(bar_2) - nursery_1.start(bar_1) # error: 12, line-19, line-24 + nursery_1.start(bar_1) # error: 28, line-19, line-24, "bar_1", "start" nursery_1.start(bar_2) nursery_2.start(bar_1) nursery_2.start(bar_2) @@ -147,29 +148,75 @@ def foo_2(): nursery_2.start(bar_2) async with trio.open_nursery() as nursery_1, trio.anything() as bar_1, trio.open_nursery() as nursery_2, trio.anything() as bar_2: - nursery_1.start(bar_1) # error: 8, line-1, line-1 - nursery_1.start(bar_2) # error: 8, line-2, line-2 + nursery_1.start(bar_1) # error: 24, line-1, line-1, "bar_1", "start" + nursery_1.start(bar_2) # error: 24, line-2, line-2, "bar_2", "start" nursery_2.start(bar_1) - nursery_2.start(bar_2) # error: 8, line-4, line-4 + nursery_2.start(bar_2) # error: 24, line-4, line-4, "bar_2", "start" async with trio.open_nursery() as nursery: async with trio.anything() as bar: nursery.start(noterror.bar) # safe - nursery.start(bar.anything) # error: 12, line-2, line-3 - nursery.start(bar.anything.anything) # error: 12, line-3, line-4 + nursery.start(bar.anything) # error: 26, line-2, line-3, "bar", "start" + nursery.start(bar.anything.anything) # error: 26, line-3, line-4, "bar", "start" # I think this is an error async with trio.open_nursery() as nursery: async with trio.open_nursery() as nursery_2: - nursery.start(nursery_2) # error: 12, line-1, line-2 + nursery.start(nursery_2) # error: 26, line-1, line-2, "nursery_2", "start" + nursery_2.start(nursery) - # in theory safe + # in theory safe-ish, but treated as error async with trio.open_nursery() as nursery: nursery = noterror.anything async with trio.anything() as bar: - nursery.start_soon(bar) # error: 12, line-1, line-3 + nursery.start_soon(bar) # error: 31, line-1, line-3, "bar", "start_soon" async with trio.open_nursery() as nursery: async with trio.anything() as nursery: async with trio.anything() as bar: nursery.start_soon(bar) + + # weird calls + # async nursery + async with trio.open_nursery() as nursery: + # async context manager + async with trio.open_process() as bar: + nursery.start(*bar) # error: 27, line-1, line-3, "bar", "start" + nursery.start(foo=[*bar]) # error: 32, line-2, line-4, "bar", "start" + nursery.start(..., ..., *bar, ...) # error: 37, line-3, line-5, "bar", "start" + nursery.start_soon(*bar) # error: 32, line-4, line-6, "bar", "start_soon" + + # async nursery + async with trio.open_nursery() as nursery: + # async context manager + async with trio.open_process() as bar: + nursery.start(**bar) # error: 28, line-1, line-3, "bar", "start" + nursery.start(foo={**bar}) # error: 33, line-2, line-4, "bar", "start" + nursery.start(..., ..., **bar, foo=...) # error: 38, line-3, line-5, "bar", "start" + nursery.start_soon(**bar) # error: 33, line-4, line-6, "bar", "start_soon" + + # async nursery + async with trio.open_nursery() as nursery: + # async context manager + async with trio.open_process() as bar: + nursery.start( + ..., + bar, # error: 16, line-3, line-5, "bar", "start" + ..., + *bar, # error: 17, line-5, line-7, "bar", "start" + ..., + **bar, # error: 18, line-7, line-9, "bar", "start" + ) + + async with trio.open_nursery() as nursery: + # async context manager + async with trio.open_process() as bar: + nursery.start(list((tuple([0]), (bar)))) # error: 45, line-1, line-3, "bar", "start" + + nursery.start("bar") + nursery.start(lambda bar: bar+1) + + def myfun(nursery, bar): + nursery.start(bar) + +# fmt: on From e590e765577dd0bfc43abea9740af5bb1949c3ab Mon Sep 17 00:00:00 2001 From: jakkdl Date: Wed, 10 Aug 2022 14:55:03 +0200 Subject: [PATCH 4/6] clean up, comments, and deindent test statements that needn't be inside async function --- CHANGELOG.md | 4 +- README.md | 2 +- flake8_trio.py | 158 ++++++++++++++++---------------- tests/trio111.py | 228 +++++++++++++++++++++++++++++++++++++++++++++++ tests/trio302.py | 222 --------------------------------------------- 5 files changed, 313 insertions(+), 301 deletions(-) create mode 100644 tests/trio111.py delete mode 100644 tests/trio302.py diff --git a/CHANGELOG.md b/CHANGELOG.md index be21a869..24be7b0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,8 @@ *[CalVer, YY.month.patch](https://calver.org/)* ## Future -- Added TRIO302: async context manager inside nursery. Nurseries should be outermost. -- add TRIO112, nursery body with only a call to `nursery.start[_soon]` and not passing itself as a parameter can be replaced with a regular function call. +- Add TRIO111: async context manager inside nursery. Nurseries should be outermost. +- Add TRIO112: nursery body with only a call to `nursery.start[_soon]` and not passing itself as a parameter can be replaced with a regular function call. ## 22.8.4 - Fix TRIO108 raising errors on yields in some sync code. diff --git a/README.md b/README.md index f26d349c..49f2f1d2 100644 --- a/README.md +++ b/README.md @@ -33,5 +33,5 @@ pip install flake8-trio Checkpoints are `await`, `async for`, and `async with` (on one of enter/exit). - **TRIO109**: Async function definition with a `timeout` parameter - use `trio.[fail/move_on]_[after/at]` instead - **TRIO110**: `while : await trio.sleep()` should be replaced by a `trio.Event`. -- **TRIO302**: async context manager inside nursery. Nurseries should be outermost. +- **TRIO111**: Variable from context manager opened inside nursery passed to `start[_soon]` might get closed while in use. - **TRIO112**: nursery body with only a call to `nursery.start[_soon]` and not passing itself as a parameter can be replaced with a regular function call. diff --git a/flake8_trio.py b/flake8_trio.py index a5c7f0d8..5cf3e270 100644 --- a/flake8_trio.py +++ b/flake8_trio.py @@ -11,9 +11,6 @@ import ast import tokenize -<<<<<<< HEAD -from typing import Any, Dict, Iterable, List, NamedTuple, Optional, Set, Tuple, Union -======= from typing import ( Any, Dict, @@ -26,7 +23,6 @@ Union, cast, ) ->>>>>>> trio112_single_statement_nursery # CalVer: YY.month.patch, e.g. first release of July 2022 == "22.7.1" __version__ = "22.8.4" @@ -59,11 +55,11 @@ "`trio.[fail/move_on]_[after/at]` instead" ), "TRIO110": "`while : await trio.sleep()` should be replaced by a `trio.Event`.", -<<<<<<< HEAD - "TRIO302": "variable {2}, from context manager on line {0}, passed to {3} from nursery opened on {1}, might get closed while in use", -======= + "TRIO111": ( + "variable {2}, from context manager on line {0}, " + "passed to {3} from nursery opened on {1}, might get closed while in use" + ), "TRIO112": "Redundant nursery {}, consider replacing with a regular function call", ->>>>>>> trio112_single_statement_nursery } @@ -182,7 +178,6 @@ def get_state(self, *attrs: str, copy: bool = False) -> Dict[str, Any]: value = value.copy() res[attr] = value return res - # return {attr: getattr(self, attr) for attr in attrs if attr != "_problems"} def set_state(self, attrs: Dict[str, Any], copy: bool = False): for attr, value in attrs.items(): @@ -195,21 +190,6 @@ def walk(self, *body: ast.AST) -> Iterable[ast.AST]: yield from ast.walk(b) -<<<<<<< HEAD -def get_trio_scope(node: ast.AST, *names: str) -> Optional[TrioScope]: - if ( - isinstance(node, ast.Call) - and isinstance(node.func, ast.Attribute) - and isinstance(node.func.value, ast.Name) - and node.func.value.id == "trio" - and (node.func.attr in names or not names) - ): - return TrioScope(node, node.func.attr) - return None - - -======= ->>>>>>> trio112_single_statement_nursery def has_decorator(decorator_list: List[ast.expr], *names: str): for dec in decorator_list: if (isinstance(dec, ast.Name) and dec.id in names) or ( @@ -219,8 +199,17 @@ def has_decorator(decorator_list: List[ast.expr], *names: str): return False -# handles 100, 101, 106, 109, 110 +# handles 100, 101, 106, 109, 110, 111, 112 class VisitorMiscChecks(Flake8TrioVisitor): + class NurseryCall(NamedTuple): + stack_index: int + name: str + + class TrioContextManager(NamedTuple): + lineno: int + name: str + is_nursery: bool + def __init__(self): super().__init__() @@ -228,48 +217,50 @@ def __init__(self): self._yield_is_error = False self._safe_decorator = False - # 302 - self._context_manager_stack: List[Tuple[ast.expr, str, bool]] = [] - self._nursery_call_index: Optional[int] = None - self._nursery_call_name: Optional[str] = None + # 111 + self._context_managers: List[VisitorMiscChecks.TrioContextManager] = [] + self._nursery_call: Optional[VisitorMiscChecks.NurseryCall] = None - self.defaults = self.get_state() + self.defaults = self.get_state(copy=True) - # ---- 100, 101, 302 ---- + # ---- 100, 101, 111, 112 ---- def visit_With(self, node: Union[ast.With, ast.AsyncWith]): self.check_for_trio100(node) self.check_for_trio112(node) - outer = self.get_state("_yield_is_error", "_context_manager_stack", copy=True) + outer = self.get_state("_yield_is_error", "_context_managers", copy=True) - # Check for a `with trio.` for item in node.items: # 101 - if not self._safe_decorator and not self._yield_is_error: - if ( -<<<<<<< HEAD - get_trio_scope( - item.context_expr, "open_nursery", *cancel_scope_names - ) -======= - get_matching_call(item, "open_nursery", *cancel_scope_names) ->>>>>>> trio112_single_statement_nursery - is not None - ): - self._yield_is_error = True - # 302 - if isinstance(item.optional_vars, ast.Name) and isinstance( - item.context_expr, ast.Call + # 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 ): - is_nursery = ( - get_trio_scope(item.context_expr, "open_nursery") is not None + self._yield_is_error = True + + # 111 + # if a withitem is saved in a variable, + # push its line, variable, and whether it's a trio nursery + # to the _context_managers stack, + if isinstance(item.optional_vars, ast.Name): + self._context_managers.append( + self.TrioContextManager( + item.context_expr.lineno, + item.optional_vars.id, + get_matching_call(item.context_expr, "open_nursery") + is not None, + ) ) - poop = (item.context_expr.func, item.optional_vars.id, is_nursery) - self._context_manager_stack.append(poop) self.generic_visit(node) - - # reset yield_is_error self.set_state(outer) visit_AsyncWith = visit_With @@ -318,8 +309,11 @@ def visit_Yield(self, node: ast.Yield): # ---- 109 ---- def check_for_trio109(self, node: ast.AsyncFunctionDef): + # pending configuration or a more sophisticated check, ignore + # all functions with a decorator if node.decorator_list: return + args = node.args for arg in (*args.posonlyargs, *args.args, *args.kwonlyargs): if arg.arg == "timeout": @@ -351,41 +345,53 @@ def check_for_trio110(self, node: ast.While): ): self.error("TRIO110", node) -<<<<<<< HEAD + # ---- 111 ---- + # if it's a .start[_soon] call + # and is a nursery listed in self._context_managers: + # Save 's index in self._context_managers to guard against cm's higher in the + # stack being passed as parameters to it. (and save for the error message) def visit_Call(self, node: ast.Call): - outer = self.get_state("_nursery_call_index", "_nursery_call_name") + outer = self.get_state("_nursery_call") if ( isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name) and node.func.attr in ("start", "start_soon") ): - self._nursery_call_index = None - for i, (_, cm_name, is_nursery) in enumerate(self._context_manager_stack): - if node.func.value.id == cm_name: - if is_nursery: - self._nursery_call_index = i - self._nursery_call_name = node.func.attr + self._nursery_call = None + for i, cm in enumerate(self._context_managers): + if node.func.value.id == cm.name: + # don't break upon finding a nursery in case there's multiple cm's + # on the stack with the same name + if cm.is_nursery: + self._nursery_call = self.NurseryCall(i, node.func.attr) else: - self._nursery_call_index = self._nursery_call_name = None + self._nursery_call = None self.generic_visit(node) self.set_state(outer) + # If we're inside a .start[_soon] call (where is a nursery), + # and we're accessing a variable cm that's on the self._context_managers stack, + # with a higher index than : + # Raise error since the scope of cm may close before the function passed to the + # nursery finishes. def visit_Name(self, node: ast.Name): - if self._nursery_call_index is not None: - for i, (expr, cm_name, _) in enumerate(self._context_manager_stack): - if cm_name == node.id and i > self._nursery_call_index: - self.error( - "TRIO302", - node, - expr.lineno, - self._context_manager_stack[self._nursery_call_index][0].lineno, - node.id, - self._nursery_call_name, - ) self.generic_visit(node) -======= + if self._nursery_call is None: + return + + for i, cm in enumerate(self._context_managers): + if cm.name == node.id and i > self._nursery_call.stack_index: + self.error( + "TRIO111", + node, + cm.lineno, + self._context_managers[self._nursery_call.stack_index].lineno, + node.id, + self._nursery_call.name, + ) + # if with has a withitem `trio.open_nursery() as `, # and the body is only a single expression .start[_soon](), # and does not pass as a parameter to the expression @@ -415,9 +421,9 @@ def check_for_trio112(self, node: Union[ast.With, ast.AsyncWith]): ) ): self.error("TRIO112", item.context_expr, var_name) ->>>>>>> trio112_single_statement_nursery +# used in 102, 103 and 104 def critical_except(node: ast.ExceptHandler) -> Optional[Statement]: def has_exception(node: Optional[ast.expr]) -> str: if isinstance(node, ast.Name) and node.id == "BaseException": diff --git a/tests/trio111.py b/tests/trio111.py new file mode 100644 index 00000000..14a3515f --- /dev/null +++ b/tests/trio111.py @@ -0,0 +1,228 @@ +from typing import Any + +import trio +import trio as noterror + + +# shed/black breaks up a *ton* of lines since adding more detailed error messages, so +# disable formatting to avoid having to adjust a ton of line references +# fmt: off +async def foo(): + async with trio.open_nursery(): + ... + + # async nursery + async with trio.open_nursery() as nursery: + # async context manager + async with trio.open_process() as bar: + nursery.start(bar) # error: 26, line-1, line-3, "bar", "start" + nursery.start(foo=bar) # error: 30, line-2, line-4, "bar", "start" + nursery.start(..., ..., bar, ...) # error: 36, line-3, line-5, "bar", "start" + + nursery.start_soon(bar) # error: 31, line-5, line-7, "bar", "start_soon" + + # sync context manager + with open("") as bar: + nursery.start(bar) # error: 26, line-1, line-11, "bar", "start" + nursery.start(foo=bar) # error: 30, line-2, line-12, "bar", "start" + nursery.start(..., ..., bar, ...) # error: 36, line-3, line-13, "bar", "start" + + nursery.start_soon(bar) # error: 31, line-5, line-15, "bar", "start_soon" + + # sync nursery + with trio.open_nursery() as nursery: + # async context manager + async with trio.open_process() as bar: + nursery.start(bar) # error: 26, line-1, line-3, "bar", "start" + nursery.start(foo=bar) # error: 30, line-2, line-4, "bar", "start" + nursery.start(..., ..., bar, ...) # error: 36, line-3, line-5, "bar", "start" + + nursery.start_soon(bar) # error: 31, line-5, line-7, "bar", "start_soon" + + # sync context manager + with open("") as bar: + nursery.start(bar) # error: 26, line-1, line-11, "bar", "start" + nursery.start(foo=bar) # error: 30, line-2, line-12, "bar", "start" + nursery.start(..., ..., bar, ...) # error: 36, line-3, line-13, "bar", "start" + + nursery.start_soon(bar) # error: 31, line-5, line-15, "bar", "start_soon" + +# nursery inside context manager +with trio.open_process() as bar: + with trio.open_nursery() as nursery: + nursery.start(bar) # safe +with trio.open_process() as bar: + with trio.open_nursery() as nursery: + nursery.start(bar) # safe +with trio.open_process() as bar: + with trio.open_nursery() as nursery: + nursery.start(bar) # safe +with trio.open_process() as bar: + with trio.open_nursery() as nursery: + nursery.start(bar) # safe + +# reset variables on nested function +with trio.open_nursery() as nursery: + + def foo_1(): + nursery = noterror.something + with trio.open_process() as bar_2: + nursery.start(bar_2) # safe + + def foo_2(): + nursery = noterror.something + with trio.open_process() as bar_2: + nursery.start(bar_2) # safe + +# specifically check for trio.open_nursery +with noterror.open_nursery() as nursery: + with trio.open("") as bar: + nursery.start(bar) + +bar_1: Any = "" +bar_2: Any = "" +nursery_2: Any = "" + +with trio.open_nursery() as nursery_1: + nursery_1.start(bar_1) + nursery_1.start(bar_2) + nursery_2.start(bar_1) + nursery_2.start(bar_2) + with trio.open_nursery() as nursery_2: + nursery_1.start(bar_1) + nursery_1.start(bar_2) + nursery_2.start(bar_1) + nursery_2.start(bar_2) + with open("") as bar_1: + nursery_1.start(bar_1) # error: 28, line-1, line-11, "bar_1", "start" + nursery_1.start(bar_2) + nursery_2.start(bar_1) # error: 28, line-3, line-8, "bar_1", "start" + nursery_2.start(bar_2) + with trio.open("") as bar_2: + nursery_1.start(bar_1) # error: 32, line-6, line-16, "bar_1", "start" + nursery_1.start(bar_2) # error: 32, line-2, line-17, "bar_2", "start" + nursery_2.start(bar_1) # error: 32, line-8, line-13, "bar_1", "start" + nursery_2.start(bar_2) # error: 32, line-4, line-14, "bar_2", "start" + nursery_1.start(bar_1) # error: 28, line-10, line-20, "bar_1", "start" + nursery_1.start(bar_2) + nursery_2.start(bar_1) # error: 28, line-12, line-17, "bar_1", "start" + nursery_2.start(bar_2) + nursery_1.start(bar_1) + nursery_1.start(bar_2) + nursery_2.start(bar_1) + nursery_2.start(bar_2) + nursery_1.start(bar_1) + nursery_1.start(bar_2) + nursery_2.start(bar_1) + nursery_2.start(bar_2) + +with trio.open_nursery() as nursery_1: + nursery_1.start(bar_1) + nursery_1.start(bar_2) + nursery_2.start(bar_1) + nursery_2.start(bar_2) + with open("") as bar_1: + nursery_1.start(bar_1) # error: 24, line-1, line-6, "bar_1", "start" + nursery_1.start(bar_2) + nursery_2.start(bar_1) + nursery_2.start(bar_2) + with trio.open_nursery() as nursery_2: + nursery_1.start(bar_1) # error: 28, line-6, line-11, "bar_1", "start" + nursery_1.start(bar_2) + nursery_2.start(bar_1) + nursery_2.start(bar_2) + with trio.open("") as bar_2: + nursery_1.start(bar_1) # error: 32, line-11, line-16, "bar_1", "start" + nursery_1.start(bar_2) # error: 32, line-2, line-17, "bar_2", "start" + nursery_2.start(bar_1) + nursery_2.start(bar_2) # error: 32, line-4, line-9, "bar_2", "start" + nursery_1.start(bar_1) # error: 28, line-15, line-20, "bar_1", "start" + nursery_1.start(bar_2) + nursery_2.start(bar_1) + nursery_2.start(bar_2) + nursery_1.start(bar_1) # error: 24, line-19, line-24, "bar_1", "start" + nursery_1.start(bar_2) + nursery_2.start(bar_1) + nursery_2.start(bar_2) + nursery_1.start(bar_1) + nursery_1.start(bar_2) + nursery_2.start(bar_1) + nursery_2.start(bar_2) + +with trio.open_nursery() as nursery_1, trio.anything() as bar_1, trio.open_nursery() as nursery_2, trio.anything() as bar_2: + nursery_1.start(bar_1) # error: 20, line-1, line-1, "bar_1", "start" + nursery_1.start(bar_2) # error: 20, line-2, line-2, "bar_2", "start" + nursery_2.start(bar_1) + nursery_2.start(bar_2) # error: 20, line-4, line-4, "bar_2", "start" + +with trio.open_nursery() as nursery: + with trio.anything() as bar: + nursery.start(noterror.bar) # safe + nursery.start(bar.anything) # error: 22, line-2, line-3, "bar", "start" + nursery.start(bar.anything.anything) # error: 22, line-3, line-4, "bar", "start" + +# nursery passed as parameter +with trio.open_nursery() as nursery: + with trio.open_nursery() as nursery_2: + nursery.start(nursery_2) # error: 22, line-1, line-2, "nursery_2", "start" + nursery_2.start(nursery) + +# in theory safe-ish, but treated as error and likely an error with typechecking +with trio.open_nursery() as nursery: + nursery = noterror.anything + with trio.anything() as bar: + nursery.start_soon(bar) # error: 27, line-1, line-3, "bar", "start_soon" + +with trio.open_nursery() as nursery: + with trio.anything() as nursery: + with trio.anything() as bar: + nursery.start_soon(bar) + +# weird calls +with trio.open_nursery() as nursery: + with trio.open_process() as bar: + nursery.start(*bar) # error: 23, line-1, line-2, "bar", "start" + nursery.start(foo=[*bar]) # error: 28, line-2, line-3, "bar", "start" + nursery.start(..., ..., *bar, ...) # error: 33, line-3, line-4, "bar", "start" + nursery.start_soon(*bar) # error: 28, line-4, line-5, "bar", "start_soon" + +with trio.open_nursery() as nursery: + with trio.open_process() as bar: + nursery.start(**bar) # error: 24, line-1, line-2, "bar", "start" + nursery.start(foo={**bar}) # error: 29, line-2, line-3, "bar", "start" + nursery.start(..., ..., **bar, foo=...) # error: 34, line-3, line-4, "bar", "start" + nursery.start_soon(**bar) # error: 29, line-4, line-5, "bar", "start_soon" + +with trio.open_nursery() as nursery: + with trio.open_process() as bar: + nursery.start( + ..., + bar, # error: 12, line-3, line-4, "bar", "start" + ..., + *bar, # error: 13, line-5, line-6, "bar", "start" + ..., + **bar, # error: 14, line-7, line-8, "bar", "start" + ) + +# variable nested deep inside parameter list +with trio.open_nursery() as nursery: + with trio.open_process() as bar: + nursery.start(list((tuple([0]), (bar)))) # error: 41, line-1, line-2, "bar", "start" + +# tricky cases +with trio.open_nursery() as nursery: + with trio.open_process() as bar: + nursery.start("bar") + nursery.start(lambda bar: bar+1) + + def myfun(nursery, bar): + nursery.start(bar) + +# nursery overriden by non-expression context manager +b = trio.open() +with trio.open_nursery() as nursery: + with b as nursery: + with open("") as f: + nursery.start(f) + +# fmt: on diff --git a/tests/trio302.py b/tests/trio302.py deleted file mode 100644 index 7d3ab48b..00000000 --- a/tests/trio302.py +++ /dev/null @@ -1,222 +0,0 @@ -from typing import Any - -import trio -import trio as noterror - - -# fmt: off -async def foo(): - async with trio.open_nursery(): - ... - - # async nursery - async with trio.open_nursery() as nursery: - # async context manager - async with trio.open_process() as bar: - nursery.start(bar) # error: 26, line-1, line-3, "bar", "start" - nursery.start(foo=bar) # error: 30, line-2, line-4, "bar", "start" - nursery.start(..., ..., bar, ...) # error: 36, line-3, line-5, "bar", "start" - - nursery.start_soon(bar) # error: 31, line-5, line-7, "bar", "start_soon" - - # sync context manager - with open("") as bar: - nursery.start(bar) # error: 26, line-1, line-11, "bar", "start" - nursery.start(foo=bar) # error: 30, line-2, line-12, "bar", "start" - nursery.start(..., ..., bar, ...) # error: 36, line-3, line-13, "bar", "start" - - nursery.start_soon(bar) # error: 31, line-5, line-15, "bar", "start_soon" - - # sync nursery - with trio.open_nursery() as nursery: - # async context manager - async with trio.open_process() as bar: - nursery.start(bar) # error: 26, line-1, line-3, "bar", "start" - nursery.start(foo=bar) # error: 30, line-2, line-4, "bar", "start" - nursery.start(..., ..., bar, ...) # error: 36, line-3, line-5, "bar", "start" - - nursery.start_soon(bar) # error: 31, line-5, line-7, "bar", "start_soon" - - # sync context manager - with open("") as bar: - nursery.start(bar) # error: 26, line-1, line-11, "bar", "start" - nursery.start(foo=bar) # error: 30, line-2, line-12, "bar", "start" - nursery.start(..., ..., bar, ...) # error: 36, line-3, line-13, "bar", "start" - - nursery.start_soon(bar) # error: 31, line-5, line-15, "bar", "start_soon" - - # nursery inside context manager - async with trio.open_process() as bar: - async with trio.open_nursery() as nursery: - nursery.start(bar) # safe - with trio.open_process() as bar: - async with trio.open_nursery() as nursery: - nursery.start(bar) # safe - async with trio.open_process() as bar: - with trio.open_nursery() as nursery: - nursery.start(bar) # safe - with trio.open_process() as bar: - with trio.open_nursery() as nursery: - nursery.start(bar) # safe - - # reset variables on nested function - async with trio.open_nursery() as nursery: - - async def foo_1(): - nursery = noterror.something - async with trio.open_process() as bar_2: - nursery.start(bar_2) # safe - - def foo_2(): - nursery = noterror.something - with trio.open_process() as bar_2: - nursery.start(bar_2) # safe - - # specifically check for trio.open_nursery - async with noterror.open_nursery() as nursery: - async with trio.open("") as bar: - nursery.start(bar) - - bar_1: Any = "" - bar_2: Any = "" - nursery_2: Any = "" - - async with trio.open_nursery() as nursery_1: - nursery_1.start(bar_1) - nursery_1.start(bar_2) - nursery_2.start(bar_1) - nursery_2.start(bar_2) - async with trio.open_nursery() as nursery_2: - nursery_1.start(bar_1) - nursery_1.start(bar_2) - nursery_2.start(bar_1) - nursery_2.start(bar_2) - with open("") as bar_1: - nursery_1.start(bar_1) # error: 32, line-1, line-11, "bar_1", "start" - nursery_1.start(bar_2) - nursery_2.start(bar_1) # error: 32, line-3, line-8, "bar_1", "start" - nursery_2.start(bar_2) - async with trio.open("") as bar_2: - nursery_1.start(bar_1) # error: 36, line-6, line-16, "bar_1", "start" - nursery_1.start(bar_2) # error: 36, line-2, line-17, "bar_2", "start" - nursery_2.start(bar_1) # error: 36, line-8, line-13, "bar_1", "start" - nursery_2.start(bar_2) # error: 36, line-4, line-14, "bar_2", "start" - nursery_1.start(bar_1) # error: 32, line-10, line-20, "bar_1", "start" - nursery_1.start(bar_2) - nursery_2.start(bar_1) # error: 32, line-12, line-17, "bar_1", "start" - nursery_2.start(bar_2) - nursery_1.start(bar_1) - nursery_1.start(bar_2) - nursery_2.start(bar_1) - nursery_2.start(bar_2) - nursery_1.start(bar_1) - nursery_1.start(bar_2) - nursery_2.start(bar_1) - nursery_2.start(bar_2) - - async with trio.open_nursery() as nursery_1: - nursery_1.start(bar_1) - nursery_1.start(bar_2) - nursery_2.start(bar_1) - nursery_2.start(bar_2) - with open("") as bar_1: - nursery_1.start(bar_1) # error: 28, line-1, line-6, "bar_1", "start" - nursery_1.start(bar_2) - nursery_2.start(bar_1) - nursery_2.start(bar_2) - async with trio.open_nursery() as nursery_2: - nursery_1.start(bar_1) # error: 32, line-6, line-11, "bar_1", "start" - nursery_1.start(bar_2) - nursery_2.start(bar_1) - nursery_2.start(bar_2) - async with trio.open("") as bar_2: - nursery_1.start(bar_1) # error: 36, line-11, line-16, "bar_1", "start" - nursery_1.start(bar_2) # error: 36, line-2, line-17, "bar_2", "start" - nursery_2.start(bar_1) - nursery_2.start(bar_2) # error: 36, line-4, line-9, "bar_2", "start" - nursery_1.start(bar_1) # error: 32, line-15, line-20, "bar_1", "start" - nursery_1.start(bar_2) - nursery_2.start(bar_1) - nursery_2.start(bar_2) - nursery_1.start(bar_1) # error: 28, line-19, line-24, "bar_1", "start" - nursery_1.start(bar_2) - nursery_2.start(bar_1) - nursery_2.start(bar_2) - nursery_1.start(bar_1) - nursery_1.start(bar_2) - nursery_2.start(bar_1) - nursery_2.start(bar_2) - - async with trio.open_nursery() as nursery_1, trio.anything() as bar_1, trio.open_nursery() as nursery_2, trio.anything() as bar_2: - nursery_1.start(bar_1) # error: 24, line-1, line-1, "bar_1", "start" - nursery_1.start(bar_2) # error: 24, line-2, line-2, "bar_2", "start" - nursery_2.start(bar_1) - nursery_2.start(bar_2) # error: 24, line-4, line-4, "bar_2", "start" - - async with trio.open_nursery() as nursery: - async with trio.anything() as bar: - nursery.start(noterror.bar) # safe - nursery.start(bar.anything) # error: 26, line-2, line-3, "bar", "start" - nursery.start(bar.anything.anything) # error: 26, line-3, line-4, "bar", "start" - - # I think this is an error - async with trio.open_nursery() as nursery: - async with trio.open_nursery() as nursery_2: - nursery.start(nursery_2) # error: 26, line-1, line-2, "nursery_2", "start" - nursery_2.start(nursery) - - # in theory safe-ish, but treated as error - async with trio.open_nursery() as nursery: - nursery = noterror.anything - async with trio.anything() as bar: - nursery.start_soon(bar) # error: 31, line-1, line-3, "bar", "start_soon" - - async with trio.open_nursery() as nursery: - async with trio.anything() as nursery: - async with trio.anything() as bar: - nursery.start_soon(bar) - - # weird calls - # async nursery - async with trio.open_nursery() as nursery: - # async context manager - async with trio.open_process() as bar: - nursery.start(*bar) # error: 27, line-1, line-3, "bar", "start" - nursery.start(foo=[*bar]) # error: 32, line-2, line-4, "bar", "start" - nursery.start(..., ..., *bar, ...) # error: 37, line-3, line-5, "bar", "start" - nursery.start_soon(*bar) # error: 32, line-4, line-6, "bar", "start_soon" - - # async nursery - async with trio.open_nursery() as nursery: - # async context manager - async with trio.open_process() as bar: - nursery.start(**bar) # error: 28, line-1, line-3, "bar", "start" - nursery.start(foo={**bar}) # error: 33, line-2, line-4, "bar", "start" - nursery.start(..., ..., **bar, foo=...) # error: 38, line-3, line-5, "bar", "start" - nursery.start_soon(**bar) # error: 33, line-4, line-6, "bar", "start_soon" - - # async nursery - async with trio.open_nursery() as nursery: - # async context manager - async with trio.open_process() as bar: - nursery.start( - ..., - bar, # error: 16, line-3, line-5, "bar", "start" - ..., - *bar, # error: 17, line-5, line-7, "bar", "start" - ..., - **bar, # error: 18, line-7, line-9, "bar", "start" - ) - - async with trio.open_nursery() as nursery: - # async context manager - async with trio.open_process() as bar: - nursery.start(list((tuple([0]), (bar)))) # error: 45, line-1, line-3, "bar", "start" - - nursery.start("bar") - nursery.start(lambda bar: bar+1) - - def myfun(nursery, bar): - nursery.start(bar) - -# fmt: on From f49e4985317690f30a9317705bbb7192f0122971 Mon Sep 17 00:00:00 2001 From: John Litborn <11260241+jakkdl@users.noreply.github.com> Date: Thu, 11 Aug 2022 12:13:36 +0200 Subject: [PATCH 5/6] Apply suggestions from code review Co-authored-by: Zac Hatfield-Dodds --- CHANGELOG.md | 2 +- flake8_trio.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24be7b0a..f3a996f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## Future - Add TRIO111: async context manager inside nursery. Nurseries should be outermost. -- Add TRIO112: nursery body with only a call to `nursery.start[_soon]` and not passing itself as a parameter can be replaced with a regular function call. +- Add TRIO112: this single-task nursery could be replaced by awaiting the function call directly. ## 22.8.4 - Fix TRIO108 raising errors on yields in some sync code. diff --git a/flake8_trio.py b/flake8_trio.py index 5cf3e270..7f382028 100644 --- a/flake8_trio.py +++ b/flake8_trio.py @@ -56,8 +56,9 @@ ), "TRIO110": "`while : await trio.sleep()` should be replaced by a `trio.Event`.", "TRIO111": ( - "variable {2}, from context manager on line {0}, " - "passed to {3} from nursery opened on {1}, might get closed while in use" + "variable {} is usable within the context manager on line {}, but that " + "will close before nursery opened on line {} - this is usually a bug. " + "Nurseries should generally be the inner-most context manager." ), "TRIO112": "Redundant nursery {}, consider replacing with a regular function call", } From d655eb73f37b60a8b1d4bdda4072910cd5cc2eb8 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Thu, 11 Aug 2022 13:48:42 +0200 Subject: [PATCH 6/6] update messages, clean up and comment tests --- CHANGELOG.md | 4 ++-- README.md | 2 +- flake8_trio.py | 8 ++++---- tests/trio111.py | 50 ++++++++++++++++++++++++++++++------------------ 4 files changed, 38 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3a996f8..35cd7247 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Changelog *[CalVer, YY.month.patch](https://calver.org/)* -## Future -- Add TRIO111: async context manager inside nursery. Nurseries should be outermost. +## 22.8.5 +- Add TRIO111: Variable, from context manager opened inside nursery, passed to `start[_soon]` might be invalidly accesed while in use, due to context manager closing before the nursery. This is usually a bug, and nurseries should generally be the inner-most context manager. - Add TRIO112: this single-task nursery could be replaced by awaiting the function call directly. ## 22.8.4 diff --git a/README.md b/README.md index 49f2f1d2..41e9791d 100644 --- a/README.md +++ b/README.md @@ -33,5 +33,5 @@ pip install flake8-trio Checkpoints are `await`, `async for`, and `async with` (on one of enter/exit). - **TRIO109**: Async function definition with a `timeout` parameter - use `trio.[fail/move_on]_[after/at]` instead - **TRIO110**: `while : await trio.sleep()` should be replaced by a `trio.Event`. -- **TRIO111**: Variable from context manager opened inside nursery passed to `start[_soon]` might get closed while in use. +- **TRIO111**: Variable, from context manager opened inside nursery, passed to `start[_soon]` might be invalidly accesed while in use, due to context manager closing before the nursery. This is usually a bug, and nurseries should generally be the inner-most context manager. - **TRIO112**: nursery body with only a call to `nursery.start[_soon]` and not passing itself as a parameter can be replaced with a regular function call. diff --git a/flake8_trio.py b/flake8_trio.py index 7f382028..2046d27e 100644 --- a/flake8_trio.py +++ b/flake8_trio.py @@ -25,7 +25,7 @@ ) # CalVer: YY.month.patch, e.g. first release of July 2022 == "22.7.1" -__version__ = "22.8.4" +__version__ = "22.8.5" Error_codes = { @@ -56,11 +56,11 @@ ), "TRIO110": "`while : await trio.sleep()` should be replaced by a `trio.Event`.", "TRIO111": ( - "variable {} is usable within the context manager on line {}, but that " - "will close before nursery opened on line {} - this is usually a bug. " + "variable {2} is usable within the context manager on line {0}, but that " + "will close before nursery opened on line {1} - this is usually a bug. " "Nurseries should generally be the inner-most context manager." ), - "TRIO112": "Redundant nursery {}, consider replacing with a regular function call", + "TRIO112": "Redundant nursery {}, consider replacing with directly awaiting the function call", } diff --git a/tests/trio111.py b/tests/trio111.py index 14a3515f..2352f193 100644 --- a/tests/trio111.py +++ b/tests/trio111.py @@ -47,42 +47,46 @@ async def foo(): nursery.start_soon(bar) # error: 31, line-5, line-15, "bar", "start_soon" -# nursery inside context manager -with trio.open_process() as bar: - with trio.open_nursery() as nursery: - nursery.start(bar) # safe -with trio.open_process() as bar: - with trio.open_nursery() as nursery: - nursery.start(bar) # safe -with trio.open_process() as bar: - with trio.open_nursery() as nursery: - nursery.start(bar) # safe -with trio.open_process() as bar: - with trio.open_nursery() as nursery: - nursery.start(bar) # safe + # check all safe async/sync permutations + async with trio.open_process() as bar: + async with trio.open_nursery() as nursery: + nursery.start(bar) # safe + async with trio.open_process() as bar: + with trio.open_nursery() as nursery: + nursery.start(bar) # safe + with trio.open_process() as bar: + async with trio.open_nursery() as nursery: + nursery.start(bar) # safe + with trio.open_process() as bar: + with trio.open_nursery() as nursery: + nursery.start(bar) # safe # reset variables on nested function with trio.open_nursery() as nursery: - def foo_1(): nursery = noterror.something with trio.open_process() as bar_2: nursery.start(bar_2) # safe - def foo_2(): + async def foo_2(): nursery = noterror.something - with trio.open_process() as bar_2: + async with trio.open_process() as bar_2: nursery.start(bar_2) # safe -# specifically check for trio.open_nursery +# specifically check for *trio*.open_nursery with noterror.open_nursery() as nursery: with trio.open("") as bar: nursery.start(bar) +# specifically check for trio.*open_nursery* +with trio.open_nurse() as nursery: + with trio.open("") as bar: + nursery.start(bar) + + bar_1: Any = "" bar_2: Any = "" nursery_2: Any = "" - with trio.open_nursery() as nursery_1: nursery_1.start(bar_1) nursery_1.start(bar_2) @@ -116,6 +120,7 @@ def foo_2(): nursery_2.start(bar_1) nursery_2.start(bar_2) +# same as above, except second and third scope swapped with trio.open_nursery() as nursery_1: nursery_1.start(bar_1) nursery_1.start(bar_2) @@ -149,12 +154,14 @@ def foo_2(): nursery_2.start(bar_1) nursery_2.start(bar_2) +# multiple withitems with trio.open_nursery() as nursery_1, trio.anything() as bar_1, trio.open_nursery() as nursery_2, trio.anything() as bar_2: nursery_1.start(bar_1) # error: 20, line-1, line-1, "bar_1", "start" nursery_1.start(bar_2) # error: 20, line-2, line-2, "bar_2", "start" nursery_2.start(bar_1) nursery_2.start(bar_2) # error: 20, line-4, line-4, "bar_2", "start" +# attribute/name parameter modifications with trio.open_nursery() as nursery: with trio.anything() as bar: nursery.start(noterror.bar) # safe @@ -173,12 +180,13 @@ def foo_2(): with trio.anything() as bar: nursery.start_soon(bar) # error: 27, line-1, line-3, "bar", "start_soon" +# context manager with same variable name overrides nursery with trio.open_nursery() as nursery: with trio.anything() as nursery: with trio.anything() as bar: nursery.start_soon(bar) -# weird calls +# list unpack with trio.open_nursery() as nursery: with trio.open_process() as bar: nursery.start(*bar) # error: 23, line-1, line-2, "bar", "start" @@ -186,6 +194,7 @@ def foo_2(): nursery.start(..., ..., *bar, ...) # error: 33, line-3, line-4, "bar", "start" nursery.start_soon(*bar) # error: 28, line-4, line-5, "bar", "start_soon" +# dict unpack with trio.open_nursery() as nursery: with trio.open_process() as bar: nursery.start(**bar) # error: 24, line-1, line-2, "bar", "start" @@ -193,6 +202,7 @@ def foo_2(): nursery.start(..., ..., **bar, foo=...) # error: 34, line-3, line-4, "bar", "start" nursery.start_soon(**bar) # error: 29, line-4, line-5, "bar", "start_soon" +# multi-line call with multiple errors with trio.open_nursery() as nursery: with trio.open_process() as bar: nursery.start( @@ -208,6 +218,8 @@ def foo_2(): with trio.open_nursery() as nursery: with trio.open_process() as bar: nursery.start(list((tuple([0]), (bar)))) # error: 41, line-1, line-2, "bar", "start" + from functools import partial + nursery.start(partial(noterror.bar, foo=bar)) # error: 48, line-3, line-4, "bar", "start" # tricky cases with trio.open_nursery() as nursery: