flake8-async 25.2.2__tar.gz → 25.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. {flake8_async-25.2.2 → flake8_async-25.3.1}/PKG-INFO +4 -2
  2. {flake8_async-25.2.2 → flake8_async-25.3.1}/flake8_async/__init__.py +41 -7
  3. {flake8_async-25.2.2 → flake8_async-25.3.1}/flake8_async/visitors/helpers.py +1 -1
  4. {flake8_async-25.2.2 → flake8_async-25.3.1}/flake8_async/visitors/visitor102_120.py +3 -1
  5. {flake8_async-25.2.2 → flake8_async-25.3.1}/flake8_async/visitors/visitor103_104.py +3 -1
  6. {flake8_async-25.2.2 → flake8_async-25.3.1}/flake8_async/visitors/visitor123.py +1 -2
  7. {flake8_async-25.2.2 → flake8_async-25.3.1}/flake8_async/visitors/visitor91x.py +25 -11
  8. {flake8_async-25.2.2 → flake8_async-25.3.1}/flake8_async.egg-info/PKG-INFO +4 -2
  9. {flake8_async-25.2.2 → flake8_async-25.3.1}/flake8_async.egg-info/SOURCES.txt +5 -0
  10. {flake8_async-25.2.2 → flake8_async-25.3.1}/flake8_async.egg-info/requires.txt +3 -1
  11. {flake8_async-25.2.2 → flake8_async-25.3.1}/pyproject.toml +32 -0
  12. {flake8_async-25.2.2 → flake8_async-25.3.1}/setup.py +2 -1
  13. flake8_async-25.3.1/tests/autofix_files/async91x_py311.py +97 -0
  14. flake8_async-25.3.1/tests/eval_files/async102_120_py311.py +29 -0
  15. flake8_async-25.3.1/tests/eval_files/async103_104_py311.py +40 -0
  16. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async119.py +1 -1
  17. flake8_async-25.3.1/tests/eval_files/async912_py311.py +16 -0
  18. flake8_async-25.3.1/tests/eval_files/async91x_py311.py +92 -0
  19. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/test_config_and_args.py +58 -3
  20. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/test_decorator.py +24 -9
  21. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/test_flake8_async.py +5 -0
  22. {flake8_async-25.2.2 → flake8_async-25.3.1}/tox.ini +5 -29
  23. {flake8_async-25.2.2 → flake8_async-25.3.1}/CHANGELOG.md +0 -0
  24. {flake8_async-25.2.2 → flake8_async-25.3.1}/CONTRIBUTING.md +0 -0
  25. {flake8_async-25.2.2 → flake8_async-25.3.1}/LICENSE +0 -0
  26. {flake8_async-25.2.2 → flake8_async-25.3.1}/MANIFEST.in +0 -0
  27. {flake8_async-25.2.2 → flake8_async-25.3.1}/README.md +0 -0
  28. {flake8_async-25.2.2 → flake8_async-25.3.1}/flake8_async/__main__.py +0 -0
  29. {flake8_async-25.2.2 → flake8_async-25.3.1}/flake8_async/base.py +0 -0
  30. {flake8_async-25.2.2 → flake8_async-25.3.1}/flake8_async/runner.py +0 -0
  31. {flake8_async-25.2.2 → flake8_async-25.3.1}/flake8_async/visitors/__init__.py +0 -0
  32. {flake8_async-25.2.2 → flake8_async-25.3.1}/flake8_async/visitors/flake8asyncvisitor.py +0 -0
  33. {flake8_async-25.2.2 → flake8_async-25.3.1}/flake8_async/visitors/visitor101.py +0 -0
  34. {flake8_async-25.2.2 → flake8_async-25.3.1}/flake8_async/visitors/visitor105.py +0 -0
  35. {flake8_async-25.2.2 → flake8_async-25.3.1}/flake8_async/visitors/visitor111.py +0 -0
  36. {flake8_async-25.2.2 → flake8_async-25.3.1}/flake8_async/visitors/visitor118.py +0 -0
  37. {flake8_async-25.2.2 → flake8_async-25.3.1}/flake8_async/visitors/visitor2xx.py +0 -0
  38. {flake8_async-25.2.2 → flake8_async-25.3.1}/flake8_async/visitors/visitor_utility.py +0 -0
  39. {flake8_async-25.2.2 → flake8_async-25.3.1}/flake8_async/visitors/visitors.py +0 -0
  40. {flake8_async-25.2.2 → flake8_async-25.3.1}/flake8_async.egg-info/dependency_links.txt +0 -0
  41. {flake8_async-25.2.2 → flake8_async-25.3.1}/flake8_async.egg-info/entry_points.txt +0 -0
  42. {flake8_async-25.2.2 → flake8_async-25.3.1}/flake8_async.egg-info/not-zip-safe +0 -0
  43. {flake8_async-25.2.2 → flake8_async-25.3.1}/flake8_async.egg-info/top_level.txt +0 -0
  44. {flake8_async-25.2.2 → flake8_async-25.3.1}/setup.cfg +0 -0
  45. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/__init__.py +0 -0
  46. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/autofix_files/async100.py +0 -0
  47. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/autofix_files/async100_asyncio.py +0 -0
  48. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/autofix_files/async100_simple_autofix.py +0 -0
  49. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/autofix_files/async124.py +0 -0
  50. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/autofix_files/async910.py +0 -0
  51. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/autofix_files/async910_insert_library.py +0 -0
  52. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/autofix_files/async911.py +0 -0
  53. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/autofix_files/async911_insert_library.py +0 -0
  54. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/autofix_files/async913.py +0 -0
  55. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/autofix_files/async913_trio_anyio.py +0 -0
  56. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/autofix_files/async91x_autofix.py +0 -0
  57. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/autofix_files/exception_suppress_context_manager.py +0 -0
  58. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/autofix_files/exception_suppress_context_manager_import_star.py +0 -0
  59. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/autofix_files/noqa.py +0 -0
  60. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/autofix_files/noqa_testing.py +0 -0
  61. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/check_changelog_and_version.py +0 -0
  62. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/conftest.py +0 -0
  63. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/anyio_trio.py +0 -0
  64. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async100.py +0 -0
  65. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async100_asyncio.py +0 -0
  66. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async100_noautofix.py +0 -0
  67. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async100_simple_autofix.py +0 -0
  68. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async101.py +0 -0
  69. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async101_asyncio.py +0 -0
  70. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async102.py +0 -0
  71. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async102_aclose.py +0 -0
  72. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async102_aclose_args.py +0 -0
  73. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async102_anyio.py +0 -0
  74. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async102_asyncio.py +0 -0
  75. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async102_trio.py +0 -0
  76. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async103.py +0 -0
  77. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async103_all_imported.py +0 -0
  78. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async103_both_imported.py +0 -0
  79. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async103_no_104.py +0 -0
  80. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async103_trio.py +0 -0
  81. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async104.py +0 -0
  82. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async104_anyio.py +0 -0
  83. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async104_trio.py +0 -0
  84. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async105.py +0 -0
  85. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async105_anyio.py +0 -0
  86. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async106.py +0 -0
  87. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async109.py +0 -0
  88. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async110.py +0 -0
  89. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async111.py +0 -0
  90. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async111_asyncio.py +0 -0
  91. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async112.py +0 -0
  92. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async112_asyncio.py +0 -0
  93. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async113.py +0 -0
  94. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async113_asyncio.py +0 -0
  95. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async113_trio.py +0 -0
  96. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async114.py +0 -0
  97. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async115.py +0 -0
  98. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async116.py +0 -0
  99. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async118.py +0 -0
  100. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async120.py +0 -0
  101. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async121.py +0 -0
  102. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async121_anyio.py +0 -0
  103. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async121_asyncio.py +0 -0
  104. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async122.py +0 -0
  105. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async123.py +0 -0
  106. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async123_py311.py +0 -0
  107. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async124.py +0 -0
  108. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async124_no_autofix.py +0 -0
  109. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async200.py +0 -0
  110. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async210.py +0 -0
  111. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async211.py +0 -0
  112. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async212.py +0 -0
  113. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async22x.py +0 -0
  114. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async22x_asyncio.py +0 -0
  115. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async232.py +0 -0
  116. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async232_asyncio.py +0 -0
  117. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async23x.py +0 -0
  118. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async23x_asyncio.py +0 -0
  119. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async240.py +0 -0
  120. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async250.py +0 -0
  121. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async250_multi_library.py +0 -0
  122. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async251.py +0 -0
  123. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async251_multi_library.py +0 -0
  124. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async300.py +0 -0
  125. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async900.py +0 -0
  126. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async910.py +0 -0
  127. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async910_insert_library.py +0 -0
  128. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async911.py +0 -0
  129. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async911_insert_library.py +0 -0
  130. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async912.py +0 -0
  131. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async912_asyncio.py +0 -0
  132. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async913.py +0 -0
  133. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async913_trio_anyio.py +0 -0
  134. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async91x_autofix.py +0 -0
  135. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/async91x_noautofix.py +0 -0
  136. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/exception_suppress_context_manager.py +0 -0
  137. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/exception_suppress_context_manager_import_star.py +0 -0
  138. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/no_library.py +0 -0
  139. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/noqa.py +0 -0
  140. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/noqa_no_autofix.py +0 -0
  141. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/noqa_testing.py +0 -0
  142. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/eval_files/trio_anyio.py +0 -0
  143. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/test_all_visitors_imported.py +0 -0
  144. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/test_exception_on_invalid_code.py +0 -0
  145. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/test_formatting.py +0 -0
  146. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/test_messages_documented.py +0 -0
  147. {flake8_async-25.2.2 → flake8_async-25.3.1}/tests/trio_options.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: flake8-async
3
- Version: 25.2.2
3
+ Version: 25.3.1
4
4
  Summary: A highly opinionated flake8 plugin for Trio-related problems.
5
5
  Author: Zac Hatfield-Dodds, John Litborn, and Contributors
6
6
  Author-email: zac@zhd.dev
@@ -21,8 +21,9 @@ Classifier: Programming Language :: Python :: 3.12
21
21
  Classifier: Programming Language :: Python :: 3.13
22
22
  Requires-Python: >=3.9
23
23
  Description-Content-Type: text/markdown
24
- Requires-Dist: flake8>=6
25
24
  Requires-Dist: libcst>=1.0.1
25
+ Provides-Extra: flake8
26
+ Requires-Dist: flake8>=6; extra == "flake8"
26
27
  Dynamic: author
27
28
  Dynamic: author-email
28
29
  Dynamic: classifier
@@ -30,6 +31,7 @@ Dynamic: description
30
31
  Dynamic: description-content-type
31
32
  Dynamic: license
32
33
  Dynamic: project-url
34
+ Dynamic: provides-extra
33
35
  Dynamic: requires-dist
34
36
  Dynamic: requires-python
35
37
  Dynamic: summary
@@ -38,7 +38,7 @@ if TYPE_CHECKING:
38
38
 
39
39
 
40
40
  # CalVer: YY.month.patch, e.g. first release of July 2022 == "22.7.1"
41
- __version__ = "25.2.2"
41
+ __version__ = "25.3.1"
42
42
 
43
43
 
44
44
  # taken from https://github.com/Zac-HD/shed
@@ -127,26 +127,32 @@ class Plugin:
127
127
  assert self._options is not None
128
128
  return self._options
129
129
 
130
- def __init__(self, tree: ast.AST, lines: Sequence[str]):
130
+ def __init__(
131
+ self, tree: ast.AST, lines: Sequence[str], filename: str | None = None
132
+ ):
131
133
  super().__init__()
134
+ self.filename: str | None = filename
132
135
  self._tree = tree
133
136
  source = "".join(lines)
134
137
 
135
138
  self.module: cst.Module = cst_parse_module_native(source)
136
139
 
137
140
  @classmethod
138
- def from_filename(cls, filename: str | PathLike[str]) -> Plugin: # pragma: no cover
141
+ def from_filename(cls, filename: str | PathLike[str]) -> Plugin:
139
142
  # only used with --runslow
140
143
  with tokenize.open(filename) as f:
141
144
  source = f.read()
142
- return cls.from_source(source)
145
+ return cls.from_source(source, filename=filename)
143
146
 
144
147
  # alternative `__init__` to avoid re-splitting and/or re-joining lines
145
148
  @classmethod
146
- def from_source(cls, source: str) -> Plugin:
149
+ def from_source(
150
+ cls, source: str, filename: str | PathLike[str] | None = None
151
+ ) -> Plugin:
147
152
  plugin = Plugin.__new__(cls)
148
153
  super(Plugin, plugin).__init__()
149
154
  plugin._tree = ast.parse(source)
155
+ plugin.filename = str(filename) if filename else None
150
156
  plugin.module = cst_parse_module_native(source)
151
157
  return plugin
152
158
 
@@ -198,7 +204,7 @@ class Plugin:
198
204
  'lines with "# noqa" at the end.'
199
205
  ),
200
206
  )
201
- else: # if run as a flake8 plugin
207
+ else: # pragma: no-cov-no-flake8
202
208
  Plugin.standalone = False
203
209
  # Disable ASYNC9xx calls by default
204
210
  option_manager.extend_default_ignore(default_disabled_error_codes)
@@ -231,6 +237,13 @@ class Plugin:
231
237
  " errors."
232
238
  ),
233
239
  )
240
+ add_argument(
241
+ "--per-file-disable",
242
+ type=parse_per_file_disable,
243
+ default={},
244
+ required=False,
245
+ help=("..."),
246
+ )
234
247
  add_argument(
235
248
  "--autofix",
236
249
  type=comma_separated_list,
@@ -367,7 +380,7 @@ class Plugin:
367
380
  }
368
381
  assert all_codes
369
382
 
370
- if options.autofix and not Plugin.standalone:
383
+ if options.autofix and not Plugin.standalone: # pragma: no-cov-no-flake8
371
384
  print("Cannot autofix when run as a flake8 plugin.", file=sys.stderr)
372
385
  sys.exit(1)
373
386
  autofix_codes = set(get_matching_codes(options.autofix, all_codes))
@@ -441,3 +454,24 @@ def parse_async200_dict(raw_value: str) -> dict[str, str]:
441
454
  )
442
455
  res[split_values[0]] = split_values[1]
443
456
  return res
457
+
458
+
459
+ # not run if flake8 is installed
460
+ # TODO: this is not tested at all atm, I'm not even sure if it works
461
+ def parse_per_file_disable( # pragma: no cover
462
+ raw_value: str,
463
+ ) -> dict[str, tuple[str, ...]]:
464
+ res: dict[str, tuple[str, ...]] = {}
465
+ splitter = "->"
466
+ values = [s.strip() for s in raw_value.split(" \t\n") if s.strip()]
467
+ for value in values:
468
+ split_values = list(map(str.strip, value.split(splitter)))
469
+ if len(split_values) != 2:
470
+ # argparse will eat this error message and spit out its own
471
+ # if we raise it as ValueError
472
+ raise ArgumentTypeError(
473
+ f"Invalid number ({len(split_values)-1}) of splitter "
474
+ f"tokens {splitter!r} in {value!r}"
475
+ )
476
+ res[split_values[0]] = tuple(split_values[1].split(","))
477
+ return res
@@ -371,7 +371,7 @@ def with_has_call(
371
371
 
372
372
  """
373
373
  if isinstance(base, str):
374
- base = (base,) # pragma: no cover
374
+ base = (base,)
375
375
 
376
376
  # build matcher, using SaveMatchedNode to save the base and the function name.
377
377
  matcher = m.Call(
@@ -149,7 +149,7 @@ class Visitor102(Flake8AsyncVisitor):
149
149
  break
150
150
  self.visit_With(node)
151
151
 
152
- def visit_Try(self, node: ast.Try):
152
+ def visit_Try(self, node: ast.Try | ast.TryStar): # type: ignore[name-defined]
153
153
  self.save_state(
154
154
  node, "_critical_scope", "_trio_context_managers", "cancelled_caught"
155
155
  )
@@ -165,6 +165,8 @@ class Visitor102(Flake8AsyncVisitor):
165
165
  self._critical_scope = Statement("try/finally", node.lineno, node.col_offset)
166
166
  self.visit_nodes(node.finalbody)
167
167
 
168
+ visit_TryStar = visit_Try
169
+
168
170
  def visit_ExceptHandler(self, node: ast.ExceptHandler):
169
171
  # if we're inside a critical scope, a nested except should never override that
170
172
  if self._critical_scope is not None and self._critical_scope.name != "except":
@@ -160,7 +160,7 @@ class Visitor103_104(Flake8AsyncVisitor):
160
160
  visit_Yield = visit_Return
161
161
 
162
162
  # Treat Try's as fully covering only if `finally` always raises.
163
- def visit_Try(self, node: ast.Try):
163
+ def visit_Try(self, node: ast.Try | ast.TryStar): # type: ignore[name-defined]
164
164
  self.save_state(node, "cancelled_caught", copy=True)
165
165
  self.cancelled_caught = set()
166
166
 
@@ -179,6 +179,8 @@ class Visitor103_104(Flake8AsyncVisitor):
179
179
  # but it's fine if we raise in finally
180
180
  self.visit_nodes(node.finalbody)
181
181
 
182
+ visit_TryStar = visit_Try
183
+
182
184
  # Treat if's as fully covering if both `if` and `else` raise.
183
185
  # `elif` is parsed by the ast as a new if statement inside the else.
184
186
  def visit_If(self, node: ast.If):
@@ -78,8 +78,7 @@ class Visitor123(Flake8AsyncVisitor):
78
78
  self.exception_group_names = {node.name}
79
79
 
80
80
  # ast.TryStar added in py311
81
- # we run strict codecov on all python versions, this one doesn't run on <py311
82
- def visit_TryStar(self, node: ast.TryStar): # type: ignore[name-defined] # pragma: no cover
81
+ def visit_TryStar(self, node: ast.TryStar): # type: ignore[name-defined] # pragma: no-cov-py-lt-311
83
82
  self.save_state(node, "try_star", copy=False)
84
83
  self.try_star = True
85
84
 
@@ -386,7 +386,7 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
386
386
  self.new_body: cst.BaseSuite | None = None
387
387
 
388
388
  def should_autofix(self, node: cst.CSTNode, code: str | None = None) -> bool:
389
- if code is None: # pragma: no branch
389
+ if code is None:
390
390
  code = "ASYNC911" if self.has_yield else "ASYNC910"
391
391
 
392
392
  return (
@@ -768,7 +768,7 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
768
768
  # try can jump into any except or into the finally* at any point during it's
769
769
  # execution so we need to make sure except & finally can handle worst-case
770
770
  # * unless there's a bare except / except BaseException - not implemented.
771
- def visit_Try(self, node: cst.Try):
771
+ def visit_Try(self, node: cst.Try | cst.TryStar):
772
772
  if not self.async_function:
773
773
  return
774
774
  self.save_state(node, "try_state", copy=True)
@@ -784,39 +784,41 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
784
784
  Statement("yield", pos.line, pos.column) # type: ignore
785
785
  )
786
786
 
787
- def leave_Try_body(self, node: cst.Try):
787
+ def leave_Try_body(self, node: cst.Try | cst.TryStar):
788
788
  # save state at end of try for entering else
789
789
  self.try_state.try_checkpoint = self.uncheckpointed_statements
790
790
 
791
791
  # check that all except handlers checkpoint (await or most likely raise)
792
792
  self.try_state.except_uncheckpointed_statements = set()
793
793
 
794
- def visit_ExceptHandler(self, node: cst.ExceptHandler):
794
+ def visit_ExceptHandler(self, node: cst.ExceptHandler | cst.ExceptStarHandler):
795
795
  # enter with worst case of try
796
796
  self.uncheckpointed_statements = (
797
797
  self.try_state.body_uncheckpointed_statements.copy()
798
798
  )
799
799
 
800
800
  def leave_ExceptHandler(
801
- self, original_node: cst.ExceptHandler, updated_node: cst.ExceptHandler
802
- ) -> cst.ExceptHandler:
801
+ self,
802
+ original_node: cst.ExceptHandler | cst.ExceptStarHandler,
803
+ updated_node: cst.ExceptHandler | cst.ExceptStarHandler,
804
+ ) -> Any: # not worth creating a TypeVar to handle correctly
803
805
  self.try_state.except_uncheckpointed_statements.update(
804
806
  self.uncheckpointed_statements
805
807
  )
806
808
  return updated_node
807
809
 
808
- def visit_Try_orelse(self, node: cst.Try):
810
+ def visit_Try_orelse(self, node: cst.Try | cst.TryStar):
809
811
  # check else
810
812
  # if else runs it's after all of try, so restore state to back then
811
813
  self.uncheckpointed_statements = self.try_state.try_checkpoint
812
814
 
813
- def leave_Try_orelse(self, node: cst.Try):
815
+ def leave_Try_orelse(self, node: cst.Try | cst.TryStar):
814
816
  # checkpoint if else checkpoints, and all excepts checkpoint
815
817
  self.uncheckpointed_statements.update(
816
818
  self.try_state.except_uncheckpointed_statements
817
819
  )
818
820
 
819
- def visit_Try_finalbody(self, node: cst.Try):
821
+ def visit_Try_finalbody(self, node: cst.Try | cst.TryStar):
820
822
  if node.finalbody:
821
823
  self.try_state.added = (
822
824
  self.try_state.body_uncheckpointed_statements.difference(
@@ -835,14 +837,26 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
835
837
  ):
836
838
  self.uncheckpointed_statements.update(self.try_state.added)
837
839
 
838
- def leave_Try_finalbody(self, node: cst.Try):
840
+ def leave_Try_finalbody(self, node: cst.Try | cst.TryStar):
839
841
  if node.finalbody:
840
842
  self.uncheckpointed_statements.difference_update(self.try_state.added)
841
843
 
842
- def leave_Try(self, original_node: cst.Try, updated_node: cst.Try) -> cst.Try:
844
+ def leave_Try(
845
+ self, original_node: cst.Try | cst.TryStar, updated_node: cst.Try | cst.TryStar
846
+ ) -> cst.Try | cst.TryStar:
843
847
  self.restore_state(original_node)
844
848
  return updated_node
845
849
 
850
+ visit_TryStar = visit_Try
851
+ leave_TryStar = leave_Try
852
+ leave_TryStar_body = leave_Try_body
853
+ visit_TryStar_orelse = visit_Try_orelse
854
+ leave_TryStar_orelse = leave_Try_orelse
855
+ visit_TryStar_finalbody = visit_Try_finalbody
856
+ leave_TryStar_finalbody = leave_Try_finalbody
857
+ visit_ExceptStarHandler = visit_ExceptHandler
858
+ leave_ExceptStarHandler = leave_ExceptHandler
859
+
846
860
  def leave_If_test(self, node: cst.If | cst.IfExp) -> None:
847
861
  if not self.async_function:
848
862
  return
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: flake8-async
3
- Version: 25.2.2
3
+ Version: 25.3.1
4
4
  Summary: A highly opinionated flake8 plugin for Trio-related problems.
5
5
  Author: Zac Hatfield-Dodds, John Litborn, and Contributors
6
6
  Author-email: zac@zhd.dev
@@ -21,8 +21,9 @@ Classifier: Programming Language :: Python :: 3.12
21
21
  Classifier: Programming Language :: Python :: 3.13
22
22
  Requires-Python: >=3.9
23
23
  Description-Content-Type: text/markdown
24
- Requires-Dist: flake8>=6
25
24
  Requires-Dist: libcst>=1.0.1
25
+ Provides-Extra: flake8
26
+ Requires-Dist: flake8>=6; extra == "flake8"
26
27
  Dynamic: author
27
28
  Dynamic: author-email
28
29
  Dynamic: classifier
@@ -30,6 +31,7 @@ Dynamic: description
30
31
  Dynamic: description-content-type
31
32
  Dynamic: license
32
33
  Dynamic: project-url
34
+ Dynamic: provides-extra
33
35
  Dynamic: requires-dist
34
36
  Dynamic: requires-python
35
37
  Dynamic: summary
@@ -53,6 +53,7 @@ tests/autofix_files/async911_insert_library.py
53
53
  tests/autofix_files/async913.py
54
54
  tests/autofix_files/async913_trio_anyio.py
55
55
  tests/autofix_files/async91x_autofix.py
56
+ tests/autofix_files/async91x_py311.py
56
57
  tests/autofix_files/exception_suppress_context_manager.py
57
58
  tests/autofix_files/exception_suppress_context_manager_import_star.py
58
59
  tests/autofix_files/noqa.py
@@ -65,12 +66,14 @@ tests/eval_files/async100_simple_autofix.py
65
66
  tests/eval_files/async101.py
66
67
  tests/eval_files/async101_asyncio.py
67
68
  tests/eval_files/async102.py
69
+ tests/eval_files/async102_120_py311.py
68
70
  tests/eval_files/async102_aclose.py
69
71
  tests/eval_files/async102_aclose_args.py
70
72
  tests/eval_files/async102_anyio.py
71
73
  tests/eval_files/async102_asyncio.py
72
74
  tests/eval_files/async102_trio.py
73
75
  tests/eval_files/async103.py
76
+ tests/eval_files/async103_104_py311.py
74
77
  tests/eval_files/async103_all_imported.py
75
78
  tests/eval_files/async103_both_imported.py
76
79
  tests/eval_files/async103_no_104.py
@@ -127,10 +130,12 @@ tests/eval_files/async911.py
127
130
  tests/eval_files/async911_insert_library.py
128
131
  tests/eval_files/async912.py
129
132
  tests/eval_files/async912_asyncio.py
133
+ tests/eval_files/async912_py311.py
130
134
  tests/eval_files/async913.py
131
135
  tests/eval_files/async913_trio_anyio.py
132
136
  tests/eval_files/async91x_autofix.py
133
137
  tests/eval_files/async91x_noautofix.py
138
+ tests/eval_files/async91x_py311.py
134
139
  tests/eval_files/exception_suppress_context_manager.py
135
140
  tests/eval_files/exception_suppress_context_manager_import_star.py
136
141
  tests/eval_files/no_library.py
@@ -1,2 +1,4 @@
1
- flake8>=6
2
1
  libcst>=1.0.1
2
+
3
+ [flake8]
4
+ flake8>=6
@@ -14,6 +14,34 @@ force-exclude = "tests/autofix_files/.*.py"
14
14
  [tool.codespell]
15
15
  ignore-words-list = 'spawnve'
16
16
 
17
+ [tool.coverage.coverage_conditional_plugin.rules]
18
+ no-cov-has-flake8 = "is_installed('flake8')"
19
+ no-cov-no-flake8 = "not is_installed('flake8')"
20
+ no-cov-py-lt-311 = "sys.version_info < (3, 11)"
21
+
22
+ [tool.coverage.paths]
23
+ source = [
24
+ "flake8_async",
25
+ "*/site-packages/flake8_async"
26
+ ]
27
+
28
+ [tool.coverage.report]
29
+ exclude_lines = [
30
+ # Have to re-enable the standard pragma
31
+ "pragma: no cover",
32
+ # Don't complain about abstract methods, they aren't run:
33
+ "@(abc\\.)?abstractmethod",
34
+ # Don't check guarded type imports
35
+ "if (typing.)?TYPE_CHECKING:"
36
+ ]
37
+ fail_under = 100
38
+ skip_covered = true
39
+ skip_empty = true
40
+
41
+ [tool.coverage.run]
42
+ branch = true
43
+ plugins = ["coverage_conditional_plugin"]
44
+
17
45
  [tool.isort]
18
46
  only_modified = true
19
47
  profile = "black"
@@ -45,6 +73,10 @@ reportUnnecessaryTypeIgnoreComment = false
45
73
  reportUnusedCallResult = false
46
74
  strict = ["*.py", "tests/*.py", "flake8_async/**/*.py"]
47
75
 
76
+ [tool.pytest.ini_options]
77
+ filterwarnings = ["error"]
78
+ testpaths = ["tests"]
79
+
48
80
  [tool.ruff]
49
81
  extend-exclude = [
50
82
  ".*",
@@ -34,7 +34,8 @@ setup(
34
34
  license_files=[], # https://github.com/pypa/twine/issues/1216
35
35
  description="A highly opinionated flake8 plugin for Trio-related problems.",
36
36
  zip_safe=False,
37
- install_requires=["flake8>=6", "libcst>=1.0.1"],
37
+ install_requires=["libcst>=1.0.1"],
38
+ extras_require={"flake8": ["flake8>=6"]},
38
39
  python_requires=">=3.9",
39
40
  classifiers=[
40
41
  "Development Status :: 3 - Alpha",
@@ -0,0 +1,97 @@
1
+ """Test for ASYNC91x rules with except* blocks.
2
+
3
+ ASYNC910: async-function-no-checkpoint
4
+ ASYNC911: async-generator-no-checkpoint
5
+ ASYNC913: indefinite-loop-no-guaranteed-checkpoint
6
+
7
+ async912 handled in separate file
8
+ """
9
+
10
+ # ARG --enable=ASYNC910,ASYNC911,ASYNC913
11
+ # AUTOFIX
12
+ # ASYNCIO_NO_AUTOFIX
13
+ import trio
14
+
15
+
16
+ async def foo(): ...
17
+
18
+
19
+ async def foo_try_except_star_1(): # ASYNC910: 0, "exit", Statement("function definition", lineno)
20
+ try:
21
+ await foo()
22
+ except* ValueError:
23
+ ...
24
+ except* RuntimeError:
25
+ raise
26
+ else:
27
+ await foo()
28
+ await trio.lowlevel.checkpoint()
29
+
30
+
31
+ async def foo_try_except_star_2(): # safe
32
+ try:
33
+ ...
34
+ except* ValueError:
35
+ ...
36
+ finally:
37
+ await foo()
38
+
39
+
40
+ async def foo_try_except_star_3(): # safe
41
+ try:
42
+ await foo()
43
+ except* ValueError:
44
+ raise
45
+
46
+
47
+ # Multiple except* handlers - should all guarantee checkpoint/raise
48
+ async def foo_try_except_star_4():
49
+ try:
50
+ await foo()
51
+ except* ValueError:
52
+ await foo()
53
+ except* TypeError:
54
+ raise
55
+ except* Exception:
56
+ raise
57
+
58
+
59
+ async def try_else_no_raise_in_except(): # ASYNC910: 0, "exit", Statement("function definition", lineno)
60
+ try:
61
+ ...
62
+ except* ValueError:
63
+ ...
64
+ else:
65
+ await foo()
66
+ await trio.lowlevel.checkpoint()
67
+
68
+
69
+ async def try_else_raise_in_except():
70
+ try:
71
+ ...
72
+ except* ValueError:
73
+ raise
74
+ else:
75
+ await foo()
76
+
77
+
78
+ async def check_async911(): # ASYNC911: 0, "exit", Statement("yield", lineno+7)
79
+ try:
80
+ await foo()
81
+ except* ValueError:
82
+ ...
83
+ except* RuntimeError:
84
+ raise
85
+ await trio.lowlevel.checkpoint()
86
+ yield # ASYNC911: 4, "yield", Statement("function definition", lineno-7)
87
+ await trio.lowlevel.checkpoint()
88
+
89
+
90
+ async def check_async913():
91
+ while True: # ASYNC913: 4
92
+ await trio.lowlevel.checkpoint()
93
+ try:
94
+ await foo()
95
+ except* ValueError:
96
+ # Missing checkpoint
97
+ ...
@@ -0,0 +1,29 @@
1
+ """Test for ASYNC102/ASYNC120 with except*
2
+
3
+ ASYNC102: await-in-finally-or-cancelled
4
+
5
+ ASYNC120: await-in-except
6
+ """
7
+
8
+ # type: ignore
9
+ # ARG --enable=ASYNC102,ASYNC120
10
+ # NOASYNCIO # TODO: support asyncio shields
11
+ import trio
12
+
13
+
14
+ async def foo():
15
+ try:
16
+ ...
17
+ except* ValueError:
18
+ await foo() # ASYNC120: 8, Statement("except", lineno-1)
19
+ raise
20
+ except* BaseException:
21
+ await foo() # ASYNC102: 8, Statement("BaseException", lineno-1)
22
+ finally:
23
+ await foo() # ASYNC102: 8, Statement("try/finally", lineno-8)
24
+
25
+ try:
26
+ ...
27
+ except* BaseException:
28
+ with trio.move_on_after(30, shield=True):
29
+ await foo()
@@ -0,0 +1,40 @@
1
+ """Test for ASYNC103/ASYNC104 with except* blocks.
2
+
3
+ ASYNC103: no-reraise-cancelled
4
+ ASYNC104: cancelled-not-raised
5
+ """
6
+
7
+ # ARG --enable=ASYNC103,ASYNC104
8
+
9
+ try:
10
+ ...
11
+ except* BaseException: # ASYNC103_trio: 8, "BaseException"
12
+ ...
13
+
14
+ try:
15
+ ...
16
+ except* BaseException:
17
+ raise
18
+
19
+ try:
20
+ ...
21
+ except* ValueError:
22
+ ...
23
+ except* BaseException: # ASYNC103_trio: 8, "BaseException"
24
+ ...
25
+
26
+ try:
27
+ ...
28
+ except* BaseException:
29
+ raise ValueError # ASYNC104: 4
30
+
31
+
32
+ def foo():
33
+ try:
34
+ ...
35
+ except* BaseException: # ASYNC103_trio: 12, "BaseException"
36
+ return # ASYNC104: 8
37
+ try:
38
+ ...
39
+ except* BaseException:
40
+ raise ValueError # ASYNC104: 8
@@ -15,7 +15,7 @@ async def async_with():
15
15
  yield # error: 8
16
16
 
17
17
 
18
- async def warn_on_yeach_yield():
18
+ async def warn_on_each_yield():
19
19
  with open(""):
20
20
  yield # error: 8
21
21
  yield # error: 8
@@ -0,0 +1,16 @@
1
+ # ASYNC912 can't be tested with the other 91x rules since there's no universal
2
+ # cancelscope name across trio/asyncio/anyio - so we need ASYNCIO_NO_ERROR
3
+
4
+
5
+ # ASYNCIO_NO_ERROR
6
+ async def foo(): ...
7
+
8
+
9
+ async def check_async912():
10
+ with trio.move_on_after(30): # ASYNC912: 9
11
+ try:
12
+ await foo()
13
+ except* ValueError:
14
+ # Missing checkpoint
15
+ ...
16
+ await foo()