@@ -98,9 +98,8 @@ def get_trio_scope(node: ast.AST, *names: str) -> Optional[TrioScope]:
98
98
and isinstance (node .func , ast .Attribute )
99
99
and isinstance (node .func .value , ast .Name )
100
100
and node .func .value .id == "trio"
101
- and node .func .attr in names
101
+ and ( node .func .attr in names or not names )
102
102
):
103
- # return "trio." + node.func.attr
104
103
return TrioScope (node , node .func .attr , node .func .value .id )
105
104
return None
106
105
@@ -120,6 +119,7 @@ def __init__(self):
120
119
super ().__init__ ()
121
120
self ._yield_is_error = False
122
121
self ._safe_decorator = False
122
+ self ._inside_nursery : Optional [int ] = None
123
123
124
124
def visit_With (self , node : Union [ast .With , ast .AsyncWith ]):
125
125
self .check_for_trio100 (node )
@@ -142,19 +142,23 @@ def visit_With(self, node: Union[ast.With, ast.AsyncWith]):
142
142
self ._yield_is_error = outer_yie
143
143
144
144
def visit_AsyncWith (self , node : ast .AsyncWith ):
145
+ outer = self ._inside_nursery
146
+ self .check_for_trio302 (node .items )
145
147
self .visit_With (node )
148
+ self ._inside_nursery = outer
146
149
147
150
def visit_FunctionDef (self , node : Union [ast .FunctionDef , ast .AsyncFunctionDef ]):
148
- outer = self ._safe_decorator , self ._yield_is_error
151
+ outer = self ._safe_decorator , self ._yield_is_error , self . _inside_nursery
149
152
self ._yield_is_error = False
153
+ self ._inside_nursery = None
150
154
151
155
# check for @<context_manager_name> and @<library>.<context_manager_name>
152
156
if has_decorator (node .decorator_list , * context_manager_names ):
153
157
self ._safe_decorator = True
154
158
155
159
self .generic_visit (node )
156
160
157
- self ._safe_decorator , self ._yield_is_error = outer
161
+ self ._safe_decorator , self ._yield_is_error , self . _inside_nursery = outer
158
162
159
163
def visit_AsyncFunctionDef (self , node : ast .AsyncFunctionDef ):
160
164
self .visit_FunctionDef (node )
@@ -187,6 +191,19 @@ def visit_Import(self, node: ast.Import):
187
191
if name .name == "trio" and name .asname is not None :
188
192
self .problems .append (make_error (TRIO106 , node .lineno , node .col_offset ))
189
193
194
+ def check_for_trio302 (self , withitems : List [ast .withitem ]):
195
+ calls = [w .context_expr for w in withitems ]
196
+ for call in calls :
197
+ ss = get_trio_scope (call )
198
+ if not ss :
199
+ continue
200
+ if ss .funcname == "open_nursery" :
201
+ self ._inside_nursery = ss .node .lineno
202
+ elif self ._inside_nursery is not None :
203
+ self .error (
204
+ TRIO302 , ss .node .lineno , ss .node .col_offset , self ._inside_nursery
205
+ )
206
+
190
207
191
208
def critical_except (node : ast .ExceptHandler ) -> Optional [Tuple [int , int , str ]]:
192
209
def has_exception (node : Optional [ast .expr ]) -> str :
@@ -629,3 +646,4 @@ def run(self) -> Generator[Tuple[int, int, str, Type[Any]], None, None]:
629
646
TRIO106 = "TRIO106: trio must be imported with `import trio` for the linter to work"
630
647
TRIO107 = "TRIO107: Async functions must have at least one checkpoint on every code path, unless an exception is raised"
631
648
TRIO108 = "TRIO108: Early return from async function must have at least one checkpoint on every code path before it."
649
+ TRIO302 = "TRIO302: async context manager inside nursery opened on line {}. Nurseries should be outermost."
0 commit comments