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: