Skip to content

Commit 022fc3a

Browse files
committed
don't autofix noqa'd errors, add --disable-noqa, add noqa test parameter
to eval_files
1 parent 35e94e0 commit 022fc3a

19 files changed

+433
-97
lines changed

flake8_trio/__init__.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -150,21 +150,24 @@ def from_source(cls, source: str) -> Plugin:
150150
return plugin
151151

152152
def run(self) -> Iterable[Error]:
153-
problems_ast = Flake8TrioRunner.run(self._tree, self.options)
154-
155-
cst_runner = Flake8TrioRunner_cst(self.options, self.module)
156-
problems_cst = cst_runner.run()
157-
158153
# when run as a flake8 plugin, flake8 handles suppressing errors from `noqa`.
159154
# it's therefore important we don't suppress any errors for compatibility with
160155
# flake8-noqa
161156
if not self.standalone:
157+
self.options.disable_noqa = True
158+
159+
cst_runner = Flake8TrioRunner_cst(self.options, self.module)
160+
# any noqa'd errors are suppressed upon being generated
161+
yield from cst_runner.run()
162+
163+
problems_ast = Flake8TrioRunner.run(self._tree, self.options)
164+
if self.options.disable_noqa:
162165
yield from problems_ast
163-
yield from problems_cst
164166
return
165167

166-
for problem in (*problems_ast, *problems_cst):
167-
noqa = cst_runner.state.noqas.get(problem.line)
168+
for problem in problems_ast:
169+
# access the stored noqas in cst_runner
170+
noqa = cst_runner.noqas.get(problem.line)
168171
# if there's a noqa comment, and it's bare or this code is listed in it
169172
if noqa is not None and (noqa == set() or problem.code in noqa):
170173
continue
@@ -184,6 +187,16 @@ def add_options(option_manager: OptionManager | ArgumentParser):
184187
dest="files",
185188
help="Files(s) to format, instead of autodetection.",
186189
)
190+
add_argument(
191+
"--disable-noqa",
192+
required=False,
193+
default=False,
194+
action="store_true",
195+
help=(
196+
'Disable the effect of "# noqa". This will report errors on '
197+
'lines with "# noqa" at the end.'
198+
),
199+
)
187200
else: # if run as a flake8 plugin
188201
Plugin.standalone = False
189202
# Disable TRIO9xx calls by default
@@ -326,6 +339,7 @@ def get_matching_codes(
326339
startable_in_context_manager=options.startable_in_context_manager,
327340
trio200_blocking_calls=options.trio200_blocking_calls,
328341
anyio=options.anyio,
342+
disable_noqa=options.disable_noqa,
329343
)
330344

331345

flake8_trio/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class Options:
2121
startable_in_context_manager: Collection[str]
2222
trio200_blocking_calls: dict[str, str]
2323
anyio: bool
24+
disable_noqa: bool
2425

2526

2627
class Statement(NamedTuple):

flake8_trio/runner.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
utility_visitors,
1919
utility_visitors_cst,
2020
)
21+
from .visitors.visitor_utility import NoqaHandler
2122

2223
if TYPE_CHECKING:
2324
from collections.abc import Iterable
@@ -113,22 +114,35 @@ class Flake8TrioRunner_cst(__CommonRunner):
113114
def __init__(self, options: Options, module: Module):
114115
super().__init__(options)
115116
self.options = options
117+
self.noqas: dict[int, set[str]] = {}
118+
119+
utility_visitors = utility_visitors_cst.copy()
120+
if self.options.disable_noqa:
121+
utility_visitors.remove(NoqaHandler)
116122

117123
# Could possibly enable/disable utility visitors here, if visitors declared
118124
# dependencies
119125
self.utility_visitors: tuple[Flake8TrioVisitor_cst, ...] = tuple(
120-
v(self.state) for v in utility_visitors_cst
126+
v(self.state) for v in utility_visitors
121127
)
122128

129+
# sort the error classes to get predictable behaviour when multiple autofixers
130+
# are enabled
131+
sorted_error_classes_cst = sorted(ERROR_CLASSES_CST, key=lambda x: x.__name__)
123132
self.visitors: tuple[Flake8TrioVisitor_cst, ...] = tuple(
124-
v(self.state) for v in ERROR_CLASSES_CST if self.selected(v.error_codes)
133+
v(self.state)
134+
for v in sorted_error_classes_cst
135+
if self.selected(v.error_codes)
125136
)
126137
self.module = module
127138

128139
def run(self) -> Iterable[Error]:
129-
if not self.visitors:
130-
return
131140
for v in (*self.utility_visitors, *self.visitors):
132141
self.module = cst.MetadataWrapper(self.module).visit(v)
133142

134143
yield from self.state.problems
144+
145+
# expose the noqa's parsed by the last visitor, so they can be used to filter
146+
# ast problems
147+
if not self.options.disable_noqa:
148+
self.noqas = v.noqas # type: ignore[reportUnboundVariable]

flake8_trio/visitors/flake8triovisitor.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,12 +197,19 @@ def save_state(self, node: cst.CSTNode, *attrs: str, copy: bool = False):
197197
def restore_state(self, node: cst.CSTNode):
198198
self.set_state(self.outer.pop(node, {}))
199199

200+
def is_noqa(self, node: cst.CSTNode, code: str):
201+
if self.options.disable_noqa:
202+
return False
203+
pos = self.get_metadata(PositionProvider, node).start
204+
noqas = self.noqas.get(pos.line)
205+
return noqas is not None and (noqas == set() or code in noqas)
206+
200207
def error(
201208
self,
202209
node: cst.CSTNode,
203210
*args: str | Statement | int,
204211
error_code: str | None = None,
205-
):
212+
) -> bool:
206213
if error_code is None:
207214
assert (
208215
len(self.error_codes) == 1
@@ -211,9 +218,12 @@ def error(
211218
# don't emit an error if this code is disabled in a multi-code visitor
212219
# TODO: write test for only one of 910/911 enabled/autofixed
213220
elif error_code[:7] not in self.options.enabled_codes:
214-
return # pragma: no cover
215-
pos = self.get_metadata(PositionProvider, node).start
221+
return False # pragma: no cover
222+
223+
if self.is_noqa(node, error_code):
224+
return False
216225

226+
pos = self.get_metadata(PositionProvider, node).start
217227
self.__state.problems.append(
218228
Error(
219229
# 7 == len('TRIO...'), so alt messages raise the original code
@@ -224,11 +234,15 @@ def error(
224234
*args,
225235
)
226236
)
237+
return True
227238

228-
def should_autofix(self, code: str | None = None):
239+
def should_autofix(self, node: cst.CSTNode, code: str | None = None) -> bool:
229240
if code is None:
230241
assert len(self.error_codes) == 1
231242
code = next(iter(self.error_codes))
243+
# this does not currently need to check for `noqa`s, as error() does that
244+
# and no codes tries to autofix if there's no error. This might change if/when
245+
# "printing an error" and "autofixing" becomes independent. See issue #192
232246
return code in self.options.autofix_codes
233247

234248
@property

flake8_trio/visitors/visitor100.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,13 @@ def leave_With(
5555
self, original_node: cst.With, updated_node: cst.With
5656
) -> cst.BaseStatement | cst.FlattenSentinel[cst.BaseStatement]:
5757
if not self.has_checkpoint_stack.pop():
58+
autofix = len(updated_node.items) == 1
5859
for res in self.node_dict[original_node]:
59-
self.error(res.node, res.base, res.function)
60+
autofix &= self.error(
61+
res.node, res.base, res.function
62+
) and self.should_autofix(res.node)
6063

61-
if self.should_autofix() and len(updated_node.items) == 1:
64+
if autofix:
6265
return flatten_preserving_comments(updated_node)
6366

6467
return updated_node

flake8_trio/visitors/visitor101.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def visit_FunctionDef(self, node: cst.FunctionDef):
6060
node, "contextmanager", "asynccontextmanager", "fixture"
6161
)
6262

63+
# trigger on leaving yield so any comments are parsed for noqas
6364
def visit_Yield(self, node: cst.Yield):
6465
if self._yield_is_error:
6566
self.error(node)

flake8_trio/visitors/visitor91x.py

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -118,14 +118,15 @@ def library(self) -> tuple[str, ...]:
118118
...
119119

120120
@abstractmethod
121-
def should_autofix(self) -> bool:
121+
def should_autofix(self, node: cst.CSTNode, code: str | None = None) -> bool:
122122
...
123123

124124
# instead of trying to exclude yields found in all the weird places from
125125
# setting self.add_statement, we instead clear it upon each new line.
126126
# Several of them *could* be handled, e.g. `if ...: yield`, but
127127
# that's uncommon enough we don't care about it.
128128
def visit_SimpleStatementLine(self, node: cst.SimpleStatementLine):
129+
super().visit_SimpleStatementLine(node)
129130
self.add_statement = None
130131

131132
# insert checkpoint before yield with a flattensentinel, if indicated
@@ -134,10 +135,12 @@ def leave_SimpleStatementLine(
134135
original_node: cst.SimpleStatementLine,
135136
updated_node: cst.SimpleStatementLine,
136137
) -> cst.SimpleStatementLine | cst.FlattenSentinel[cst.SimpleStatementLine]:
138+
_ = super().leave_SimpleStatementLine(original_node, updated_node)
139+
137140
# possible TODO: generate an error if transforming+visiting is done in a
138141
# single pass and emit-error-on-transform can be enabled/disabled. The error can't
139142
# be generated in the yield/return since it doesn't know if it will be autofixed.
140-
if self.add_statement is None or not self.should_autofix():
143+
if self.add_statement is None or not self.should_autofix(original_node):
141144
return updated_node
142145
curr_add_statement = self.add_statement
143146
self.add_statement = None
@@ -196,8 +199,8 @@ def __init__(
196199
def library(self) -> tuple[str, ...]:
197200
return self.__library if self.__library else ("trio",)
198201

199-
def should_autofix(self) -> bool:
200-
return True
202+
def should_autofix(self, node: cst.CSTNode, code: str | None = None) -> bool:
203+
return not self.noautofix
201204

202205
def leave_Yield(
203206
self,
@@ -206,7 +209,9 @@ def leave_Yield(
206209
) -> cst.Yield:
207210
# Needs to be passed *original* node, since updated node is a copy
208211
# which loses identity equality
209-
if original_node in self.nodes_needing_checkpoint and not self.noautofix:
212+
if original_node in self.nodes_needing_checkpoint and self.should_autofix(
213+
original_node
214+
):
210215
self.add_statement = checkpoint_statement(self.library[0])
211216
return updated_node
212217

@@ -239,8 +244,10 @@ def __init__(self, *args: Any, **kwargs: Any):
239244
self.loop_state = LoopState()
240245
self.try_state = TryState()
241246

242-
def should_autofix(self, code: str | None = None) -> bool:
243-
return super().should_autofix("TRIO911" if self.has_yield else "TRIO910")
247+
def should_autofix(self, node: cst.CSTNode, code: str | None = None) -> bool:
248+
return not self.noautofix and super().should_autofix(
249+
node, "TRIO911" if self.has_yield else "TRIO910"
250+
)
244251

245252
def checkpoint_statement(self) -> cst.SimpleStatementLine:
246253
return checkpoint_statement(self.library[0])
@@ -290,7 +297,7 @@ def leave_FunctionDef(
290297
self.async_function
291298
# updated_node does not have a position, so we must send original_node
292299
and self.check_function_exit(original_node)
293-
and self.should_autofix()
300+
and self.should_autofix(original_node)
294301
and isinstance(updated_node.body, cst.IndentedBlock)
295302
):
296303
# insert checkpoint at the end of body
@@ -327,17 +334,20 @@ def check_function_exit(
327334
# Add this as a node potentially needing checkpoints only if it
328335
# missing checkpoints solely depends on whether the artificial statement is
329336
# "real"
330-
if len(self.uncheckpointed_statements) == 1 and not self.noautofix:
337+
if len(self.uncheckpointed_statements) == 1 and self.should_autofix(
338+
original_node
339+
):
331340
self.loop_state.nodes_needing_checkpoints.append(original_node)
332341
return False
333342

343+
any_errors = False
334344
# raise the actual errors
335345
for statement in self.uncheckpointed_statements:
336346
if statement == ARTIFICIAL_STATEMENT:
337347
continue
338-
self.error_91x(original_node, statement)
348+
any_errors |= self.error_91x(original_node, statement)
339349

340-
return True
350+
return any_errors
341351

342352
def leave_Return(
343353
self, original_node: cst.Return, updated_node: cst.Return
@@ -357,15 +367,15 @@ def error_91x(
357367
self,
358368
node: cst.Return | cst.FunctionDef | cst.Yield,
359369
statement: Statement,
360-
):
370+
) -> bool:
361371
assert statement != ARTIFICIAL_STATEMENT
362372

363373
if isinstance(node, cst.FunctionDef):
364374
msg = "exit"
365375
else:
366376
msg = node.__class__.__name__.lower()
367377

368-
self.error(
378+
return self.error(
369379
node,
370380
msg,
371381
statement,
@@ -403,7 +413,9 @@ def leave_Yield(
403413
return updated_node
404414
self.has_yield = True
405415

406-
if self.check_function_exit(original_node) and not self.noautofix:
416+
if self.check_function_exit(original_node) and self.should_autofix(
417+
original_node
418+
):
407419
self.add_statement = self.checkpoint_statement()
408420

409421
# mark as requiring checkpoint after
@@ -596,8 +608,7 @@ def leave_While_body(self, node: cst.For | cst.While):
596608
):
597609
if stmt == ARTIFICIAL_STATEMENT:
598610
continue
599-
any_error = True
600-
self.error_91x(err_node, stmt)
611+
any_error |= self.error_91x(err_node, stmt)
601612

602613
# if there's no errors from artificial statements, we don't need to insert
603614
# the potential checkpoints
@@ -664,7 +675,7 @@ def leave_While(
664675
| cst.FlattenSentinel[cst.For | cst.While]
665676
| cst.RemovalSentinel
666677
):
667-
if self.loop_state.nodes_needing_checkpoints and self.should_autofix():
678+
if self.loop_state.nodes_needing_checkpoints:
668679
transformer = InsertCheckpointsInLoopBody(
669680
self.loop_state.nodes_needing_checkpoints, self.library
670681
)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ warn_unused_ignores = false
2828
[tool.pyright]
2929
exclude = ["**/node_modules", "**/__pycache__", "**/.*"]
3030
reportCallInDefaultInitializer = true
31-
reportImplicitStringConcatenation = true
31+
reportImplicitStringConcatenation = false # black generates implicit string concats
3232
reportMissingSuperCall = true
3333
reportPropertyTypeMismatch = true
3434
reportShadowedImports = true

0 commit comments

Comments
 (0)