flake8-async 24.4.2__tar.gz → 24.5.2__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 (125) hide show
  1. {flake8_async-24.4.2 → flake8_async-24.5.2}/CHANGELOG.md +9 -0
  2. {flake8_async-24.4.2 → flake8_async-24.5.2}/CONTRIBUTING.md +9 -5
  3. {flake8_async-24.4.2 → flake8_async-24.5.2}/PKG-INFO +12 -2
  4. {flake8_async-24.4.2 → flake8_async-24.5.2}/README.md +2 -1
  5. {flake8_async-24.4.2 → flake8_async-24.5.2}/flake8_async/__init__.py +1 -1
  6. {flake8_async-24.4.2 → flake8_async-24.5.2}/flake8_async/visitors/__init__.py +0 -1
  7. {flake8_async-24.4.2 → flake8_async-24.5.2}/flake8_async/visitors/flake8asyncvisitor.py +11 -3
  8. {flake8_async-24.4.2 → flake8_async-24.5.2}/flake8_async/visitors/helpers.py +68 -18
  9. {flake8_async-24.4.2 → flake8_async-24.5.2}/flake8_async/visitors/visitor101.py +12 -2
  10. {flake8_async-24.4.2 → flake8_async-24.5.2}/flake8_async/visitors/visitor111.py +12 -5
  11. {flake8_async-24.4.2 → flake8_async-24.5.2}/flake8_async/visitors/visitor91x.py +99 -15
  12. {flake8_async-24.4.2 → flake8_async-24.5.2}/flake8_async/visitors/visitors.py +18 -6
  13. {flake8_async-24.4.2 → flake8_async-24.5.2}/flake8_async.egg-info/PKG-INFO +12 -2
  14. {flake8_async-24.4.2 → flake8_async-24.5.2}/flake8_async.egg-info/SOURCES.txt +10 -1
  15. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/autofix_files/async100.py +4 -3
  16. flake8_async-24.5.2/tests/autofix_files/async100_asyncio.py +22 -0
  17. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/autofix_files/async910.py +1 -0
  18. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/autofix_files/async911.py +1 -0
  19. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/autofix_files/async91x_autofix.py +20 -0
  20. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/autofix_files/noqa_testing.py +3 -0
  21. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async100.py +4 -3
  22. flake8_async-24.5.2/tests/eval_files/async100_asyncio.py +22 -0
  23. flake8_async-24.5.2/tests/eval_files/async101.py +129 -0
  24. flake8_async-24.5.2/tests/eval_files/async101_anyio.py +10 -0
  25. flake8_async-24.5.2/tests/eval_files/async101_asyncio.py +82 -0
  26. flake8_async-24.5.2/tests/eval_files/async101_trio.py +17 -0
  27. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async111.py +9 -1
  28. flake8_async-24.5.2/tests/eval_files/async111_anyio.py +21 -0
  29. flake8_async-24.5.2/tests/eval_files/async111_asyncio.py +23 -0
  30. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async112.py +4 -4
  31. flake8_async-24.5.2/tests/eval_files/async112_anyio.py +28 -0
  32. flake8_async-24.5.2/tests/eval_files/async112_asyncio.py +24 -0
  33. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async113_trio.py +12 -0
  34. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async910.py +1 -0
  35. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async911.py +1 -0
  36. flake8_async-24.5.2/tests/eval_files/async912.py +184 -0
  37. flake8_async-24.5.2/tests/eval_files/async912_asyncio.py +76 -0
  38. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async91x_autofix.py +19 -0
  39. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/noqa_testing.py +3 -0
  40. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/test_flake8_async.py +38 -13
  41. flake8_async-24.4.2/flake8_async/visitors/visitor100.py +0 -90
  42. flake8_async-24.4.2/tests/eval_files/async100_asyncio.py +0 -23
  43. flake8_async-24.4.2/tests/eval_files/async101.py +0 -74
  44. {flake8_async-24.4.2 → flake8_async-24.5.2}/LICENSE +0 -0
  45. {flake8_async-24.4.2 → flake8_async-24.5.2}/MANIFEST.in +0 -0
  46. {flake8_async-24.4.2 → flake8_async-24.5.2}/flake8_async/__main__.py +0 -0
  47. {flake8_async-24.4.2 → flake8_async-24.5.2}/flake8_async/base.py +0 -0
  48. {flake8_async-24.4.2 → flake8_async-24.5.2}/flake8_async/runner.py +0 -0
  49. {flake8_async-24.4.2 → flake8_async-24.5.2}/flake8_async/visitors/visitor102.py +0 -0
  50. {flake8_async-24.4.2 → flake8_async-24.5.2}/flake8_async/visitors/visitor103_104.py +0 -0
  51. {flake8_async-24.4.2 → flake8_async-24.5.2}/flake8_async/visitors/visitor105.py +0 -0
  52. {flake8_async-24.4.2 → flake8_async-24.5.2}/flake8_async/visitors/visitor118.py +0 -0
  53. {flake8_async-24.4.2 → flake8_async-24.5.2}/flake8_async/visitors/visitor2xx.py +0 -0
  54. {flake8_async-24.4.2 → flake8_async-24.5.2}/flake8_async/visitors/visitor_utility.py +0 -0
  55. {flake8_async-24.4.2 → flake8_async-24.5.2}/flake8_async.egg-info/dependency_links.txt +0 -0
  56. {flake8_async-24.4.2 → flake8_async-24.5.2}/flake8_async.egg-info/entry_points.txt +0 -0
  57. {flake8_async-24.4.2 → flake8_async-24.5.2}/flake8_async.egg-info/not-zip-safe +0 -0
  58. {flake8_async-24.4.2 → flake8_async-24.5.2}/flake8_async.egg-info/requires.txt +0 -0
  59. {flake8_async-24.4.2 → flake8_async-24.5.2}/flake8_async.egg-info/top_level.txt +0 -0
  60. {flake8_async-24.4.2 → flake8_async-24.5.2}/pyproject.toml +0 -0
  61. {flake8_async-24.4.2 → flake8_async-24.5.2}/setup.cfg +0 -0
  62. {flake8_async-24.4.2 → flake8_async-24.5.2}/setup.py +0 -0
  63. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/__init__.py +0 -0
  64. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/autofix_files/async100_simple_autofix.py +0 -0
  65. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/autofix_files/noqa.py +0 -0
  66. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/conftest.py +0 -0
  67. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/anyio_trio.py +0 -0
  68. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async100_noautofix.py +0 -0
  69. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async100_simple_autofix.py +0 -0
  70. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async102.py +0 -0
  71. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async102_aclose.py +0 -0
  72. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async102_aclose_args.py +0 -0
  73. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async102_anyio.py +0 -0
  74. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async102_asyncio.py +0 -0
  75. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async102_trio.py +0 -0
  76. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async103.py +0 -0
  77. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async103_all_imported.py +0 -0
  78. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async103_both_imported.py +0 -0
  79. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async103_no_104.py +0 -0
  80. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async103_trio.py +0 -0
  81. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async104.py +0 -0
  82. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async104_anyio.py +0 -0
  83. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async104_trio.py +0 -0
  84. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async105.py +0 -0
  85. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async105_anyio.py +0 -0
  86. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async106.py +0 -0
  87. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async109.py +0 -0
  88. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async110.py +0 -0
  89. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async113.py +0 -0
  90. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async113_anyio.py +0 -0
  91. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async114.py +0 -0
  92. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async115.py +0 -0
  93. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async116.py +0 -0
  94. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async118.py +0 -0
  95. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async119.py +0 -0
  96. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async200.py +0 -0
  97. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async210.py +0 -0
  98. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async211.py +0 -0
  99. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async212.py +0 -0
  100. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async22x.py +0 -0
  101. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async22x_asyncio.py +0 -0
  102. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async232.py +0 -0
  103. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async232_asyncio.py +0 -0
  104. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async23x.py +0 -0
  105. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async23x_asyncio.py +0 -0
  106. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async240.py +0 -0
  107. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async250.py +0 -0
  108. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async250_multi_library.py +0 -0
  109. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async251.py +0 -0
  110. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async251_multi_library.py +0 -0
  111. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async900.py +0 -0
  112. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/async91x_noautofix.py +0 -0
  113. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/no_library.py +0 -0
  114. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/noqa.py +0 -0
  115. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/noqa_no_autofix.py +0 -0
  116. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/eval_files/trio_anyio.py +0 -0
  117. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/test_all_visitors_imported.py +0 -0
  118. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/test_changelog_and_version.py +0 -0
  119. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/test_config_and_args.py +0 -0
  120. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/test_decorator.py +0 -0
  121. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/test_exception_on_invalid_code.py +0 -0
  122. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/test_formatting.py +0 -0
  123. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/test_messages_documented.py +0 -0
  124. {flake8_async-24.4.2 → flake8_async-24.5.2}/tests/trio_options.py +0 -0
  125. {flake8_async-24.4.2 → flake8_async-24.5.2}/tox.ini +0 -0
@@ -1,6 +1,15 @@
1
1
  # Changelog
2
2
  *[CalVer, YY.month.patch](https://calver.org/)*
3
3
 
4
+ ## 24.5.2
5
+ - ASYNC101 now also warns on anyio & asyncio taskgroup.s
6
+ - Fixed a bug where ASYNC101 and ASYNC91x would not recognize decorators with parameters directly imported. I.e. `@fixture(...)` will now suppress errors.
7
+
8
+ ## 24.5.1
9
+ - Add ASYNC912: no checkpoints in with statement are guaranteed to run.
10
+ - ASYNC100 now properly treats async for comprehensions as checkpoints.
11
+ - ASYNC100 now supports autofixing on asyncio.
12
+
4
13
  ## 24.4.2
5
14
  - Add ASYNC119: yield in contextmanager in async generator.
6
15
 
@@ -44,11 +44,15 @@ You can instead of `error` specify the error code.
44
44
  With `# ARG` lines you can also specify command-line arguments that should be passed to the plugin when parsing a file. Can be specified multiple times for several different arguments.
45
45
  Generated tests will by default `--select` the error code of the file, which will enable any visitors that can generate that code (and if those visitors can raise other codes they might be raised too). This can be overridden by adding an `# ARG --select=...` line.
46
46
 
47
- ### `# NOANYIO` and `# NOTRIO`
48
- Eval files are also evaluated where ~all instances of "trio" is replaced with "anyio" and `--anyio` is prepended to the argument list, to check for compatibility with the [anyio](https://github.com/agronholm/anyio) library - unless they are marked with `# NOANYIO`.
49
- If a file is marked with `# NOTRIO` it will not replace instances of "trio" with "anyio", and the file will not be run by the normal `eval_files` test generator.
50
- #### `# ANYIO_NO_ERROR`
51
- A file which is marked with this will ignore all `# error` or `# TRIO...` comments when running with anyio. Use when an error is trio-specific and replacing "trio" with "anyio" should silence all errors.
47
+ ### Library parametrization
48
+ Eval files are evaluated with each supported library. It does this by replacing all instances of the `BASE_LIBRARY` ("trio" by default) with the two other libraries, and setting the corresponding flag (`--anyio` or `--asyncio`).
49
+ ### `# BASE_LIBRARY anyio` / `# BASE_LIBRARY asyncio`
50
+ Defaults to `trio`. Used to specify the primary library an eval file is testing.
51
+
52
+ #### `# ANYIO_NO_ERROR`, `# TRIO_NO_ERROR`, `# ASYNCIO_NO_ERROR`
53
+ A file which is marked with this will ignore all `# error` or `# TRIO...` comments when running with anyio. Use when an error is library-specific and replacing all instances means the file should no longer raise any errors.
54
+ ### `# NOANYIO`, `# NOTRIO`, `#NOASYNCIO`
55
+ Disables checking a file with the specified library. Should be used somewhat sparingly, and always have a comment motivating its use.
52
56
 
53
57
  ## Running pytest outside tox
54
58
  If you don't want to bother with tox to quickly test stuff, you'll need to install the following dependencies:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: flake8-async
3
- Version: 24.4.2
3
+ Version: 24.5.2
4
4
  Summary: A highly opinionated flake8 plugin for Trio-related problems.
5
5
  Home-page: https://github.com/python-trio/flake8-async
6
6
  Author: Zac Hatfield-Dodds, John Litborn, and Contributors
@@ -89,6 +89,7 @@ Note: 22X, 23X and 24X has not had asyncio-specific suggestions written.
89
89
  - **ASYNC910**: Exit or `return` from async function with no guaranteed checkpoint or exception since function definition. You might want to enable this on a codebase to make it easier to reason about checkpoints, and make the logic of ASYNC911 correct.
90
90
  - **ASYNC911**: Exit, `yield` or `return` from async iterable with no guaranteed checkpoint since possible function entry (yield or function definition)
91
91
  Checkpoints are `await`, `async for`, and `async with` (on one of enter/exit).
92
+ - **ASYNC912**: Timeout/Cancelscope has no awaits that are guaranteed to run. If the scope has no checkpoints at all, then `ASYNC100` will be raised instead.
92
93
 
93
94
  ### Removed Warnings
94
95
  - **TRIOxxx**: All error codes are now renamed ASYNCxxx
@@ -110,7 +111,7 @@ adding the following to your `.pre-commit-config.yaml`:
110
111
  minimum_pre_commit_version: '2.9.0'
111
112
  repos:
112
113
  - repo: https://github.com/python-trio/flake8-async
113
- rev: 24.4.2
114
+ rev: 24.5.2
114
115
  hooks:
115
116
  - id: flake8-async
116
117
  # args: [--enable=ASYNC, --disable=ASYNC9, --autofix=ASYNC]
@@ -225,6 +226,15 @@ async def my_function():
225
226
  # Changelog
226
227
  *[CalVer, YY.month.patch](https://calver.org/)*
227
228
 
229
+ ## 24.5.2
230
+ - ASYNC101 now also warns on anyio & asyncio taskgroup.s
231
+ - Fixed a bug where ASYNC101 and ASYNC91x would not recognize decorators with parameters directly imported. I.e. `@fixture(...)` will now suppress errors.
232
+
233
+ ## 24.5.1
234
+ - Add ASYNC912: no checkpoints in with statement are guaranteed to run.
235
+ - ASYNC100 now properly treats async for comprehensions as checkpoints.
236
+ - ASYNC100 now supports autofixing on asyncio.
237
+
228
238
  ## 24.4.2
229
239
  - Add ASYNC119: yield in contextmanager in async generator.
230
240
 
@@ -64,6 +64,7 @@ Note: 22X, 23X and 24X has not had asyncio-specific suggestions written.
64
64
  - **ASYNC910**: Exit or `return` from async function with no guaranteed checkpoint or exception since function definition. You might want to enable this on a codebase to make it easier to reason about checkpoints, and make the logic of ASYNC911 correct.
65
65
  - **ASYNC911**: Exit, `yield` or `return` from async iterable with no guaranteed checkpoint since possible function entry (yield or function definition)
66
66
  Checkpoints are `await`, `async for`, and `async with` (on one of enter/exit).
67
+ - **ASYNC912**: Timeout/Cancelscope has no awaits that are guaranteed to run. If the scope has no checkpoints at all, then `ASYNC100` will be raised instead.
67
68
 
68
69
  ### Removed Warnings
69
70
  - **TRIOxxx**: All error codes are now renamed ASYNCxxx
@@ -85,7 +86,7 @@ adding the following to your `.pre-commit-config.yaml`:
85
86
  minimum_pre_commit_version: '2.9.0'
86
87
  repos:
87
88
  - repo: https://github.com/python-trio/flake8-async
88
- rev: 24.4.2
89
+ rev: 24.5.2
89
90
  hooks:
90
91
  - id: flake8-async
91
92
  # args: [--enable=ASYNC, --disable=ASYNC9, --autofix=ASYNC]
@@ -37,7 +37,7 @@ if TYPE_CHECKING:
37
37
 
38
38
 
39
39
  # CalVer: YY.month.patch, e.g. first release of July 2022 == "22.7.1"
40
- __version__ = "24.4.2"
40
+ __version__ = "24.5.2"
41
41
 
42
42
 
43
43
  # taken from https://github.com/Zac-HD/shed
@@ -30,7 +30,6 @@ utility_visitors_cst: set[type[Flake8AsyncVisitor_cst]] = set()
30
30
  from . import (
31
31
  visitor2xx,
32
32
  visitor91x,
33
- visitor100,
34
33
  visitor101,
35
34
  visitor102,
36
35
  visitor103_104,
@@ -98,7 +98,11 @@ class Flake8AsyncVisitor(ast.NodeVisitor, ABC):
98
98
  ), "No error code defined, but class has multiple codes"
99
99
  error_code = next(iter(self.error_codes))
100
100
  # don't emit an error if this code is disabled in a multi-code visitor
101
- elif strip_error_subidentifier(error_code) not in self.options.enabled_codes:
101
+ elif (
102
+ (ec_no_sub := strip_error_subidentifier(error_code))
103
+ not in self.options.enabled_codes
104
+ and ec_no_sub not in self.options.autofix_codes
105
+ ):
102
106
  return
103
107
 
104
108
  self.__state.problems.append(
@@ -217,7 +221,11 @@ class Flake8AsyncVisitor_cst(cst.CSTTransformer, ABC):
217
221
  error_code = next(iter(self.error_codes))
218
222
  # don't emit an error if this code is disabled in a multi-code visitor
219
223
  # TODO: write test for only one of 910/911 enabled/autofixed
220
- elif strip_error_subidentifier(error_code) not in self.options.enabled_codes:
224
+ elif (
225
+ (ec_no_sub := strip_error_subidentifier(error_code))
226
+ not in self.options.enabled_codes
227
+ and ec_no_sub not in self.options.autofix_codes
228
+ ):
221
229
  return False # pragma: no cover
222
230
 
223
231
  if self.is_noqa(node, error_code):
@@ -237,7 +245,7 @@ class Flake8AsyncVisitor_cst(cst.CSTTransformer, ABC):
237
245
  return True
238
246
 
239
247
  def should_autofix(self, node: cst.CSTNode, code: str | None = None) -> bool:
240
- if code is None:
248
+ if code is None: # pragma: no cover
241
249
  assert len(self.error_codes) == 1
242
250
  code = next(iter(self.error_codes))
243
251
  # this does not currently need to check for `noqa`s, as error() does that
@@ -51,6 +51,7 @@ def error_class_cst(error_class: type[T_CST]) -> type[T_CST]:
51
51
 
52
52
 
53
53
  def disabled_by_default(error_class: type[T_EITHER]) -> type[T_EITHER]:
54
+ """Default-disables all error codes in a class."""
54
55
  assert error_class.error_codes # type: ignore[attr-defined]
55
56
  default_disabled_error_codes.extend(
56
57
  error_class.error_codes # type: ignore[attr-defined]
@@ -58,6 +59,11 @@ def disabled_by_default(error_class: type[T_EITHER]) -> type[T_EITHER]:
58
59
  return error_class
59
60
 
60
61
 
62
+ def disable_codes_by_default(*codes: str) -> None:
63
+ """Default-disables only specified codes."""
64
+ default_disabled_error_codes.extend(codes)
65
+
66
+
61
67
  def utility_visitor(c: type[T]) -> type[T]:
62
68
  assert not hasattr(c, "error_codes")
63
69
  c.error_codes = {}
@@ -83,7 +89,8 @@ def _get_identifier(node: ast.expr) -> str:
83
89
 
84
90
 
85
91
  # ignores module and only checks the unqualified name of the decorator
86
- # used in 101, 113, 900 and 910/911
92
+ # used in 113. cst version used in 101, 900 and 910/911
93
+ # matches @name, @foo.name, @name(...), and @foo.name(...)
87
94
  def has_decorator(node: ast.FunctionDef | ast.AsyncFunctionDef, *names: str):
88
95
  return any(_get_identifier(dec) in names for dec in node.decorator_list)
89
96
 
@@ -317,30 +324,68 @@ class AttributeCall(NamedTuple):
317
324
  function: str
318
325
 
319
326
 
327
+ # the custom __or__ in libcst breaks pyright type checking. It's possible to use
328
+ # `Union` as a workaround ... except pyupgrade will automatically replace that.
329
+ # So we have to resort to specifying one of the base classes.
330
+ # See https://github.com/Instagram/LibCST/issues/1143
331
+ def build_cst_matcher(attr: str) -> m.BaseExpression:
332
+ """Build a cst matcher structure with attributes&names matching a string `a.b.c`."""
333
+ if "." not in attr:
334
+ return m.Name(value=attr)
335
+ body, tail = attr.rsplit(".")
336
+ return m.Attribute(value=build_cst_matcher(body), attr=m.Name(value=tail))
337
+
338
+
339
+ def identifier_to_string(attr: cst.Name | cst.Attribute) -> str:
340
+ if isinstance(attr, cst.Name):
341
+ return attr.value
342
+ assert isinstance(attr.value, (cst.Attribute, cst.Name))
343
+ return identifier_to_string(attr.value) + "." + attr.attr.value
344
+
345
+
320
346
  def with_has_call(
321
- node: cst.With, *names: str, base: Iterable[str] = ("trio", "anyio")
347
+ node: cst.With, *names: str, base: Iterable[str] | str = ("trio", "anyio")
322
348
  ) -> list[AttributeCall]:
349
+ """Check if a with statement has a matching call, returning a list with matches.
350
+
351
+ `names` specify the names of functions to match, `base` specifies the
352
+ library/module(s) the function must be in.
353
+ The list elements in the return value are named tuples with the matched node,
354
+ base and function.
355
+
356
+ Examples_
357
+
358
+ `with_has_call(node, "bar", base="foo")` matches foo.bar.
359
+ `with_has_call(node, "bar", "bee", base=("foo", "a.b.c")` matches
360
+ `foo.bar`, `foo.bee`, `a.b.c.bar`, and `a.b.c.bee`.
361
+
362
+ """
363
+ if isinstance(base, str):
364
+ base = (base,) # pragma: no cover
365
+
366
+ # build matcher, using SaveMatchedNode to save the base and the function name.
367
+ matcher = m.Call(
368
+ func=m.Attribute(
369
+ value=m.SaveMatchedNode(
370
+ m.OneOf(*(build_cst_matcher(b) for b in base)), name="base"
371
+ ),
372
+ attr=m.SaveMatchedNode(
373
+ oneof_names(*names),
374
+ name="function",
375
+ ),
376
+ )
377
+ )
378
+
323
379
  res_list: list[AttributeCall] = []
324
380
  for item in node.items:
325
- if res := m.extract(
326
- item.item,
327
- m.Call(
328
- func=m.Attribute(
329
- value=m.SaveMatchedNode(m.Name(), name="library"),
330
- attr=m.SaveMatchedNode(
331
- oneof_names(*names),
332
- name="function",
333
- ),
334
- )
335
- ),
336
- ):
381
+ if res := m.extract(item.item, matcher):
337
382
  assert isinstance(item.item, cst.Call)
338
- assert isinstance(res["library"], cst.Name)
383
+ assert isinstance(res["base"], (cst.Name, cst.Attribute))
339
384
  assert isinstance(res["function"], cst.Name)
340
- if res["library"].value not in base:
341
- continue
342
385
  res_list.append(
343
- AttributeCall(item.item, res["library"].value, res["function"].value)
386
+ AttributeCall(
387
+ item.item, identifier_to_string(res["base"]), res["function"].value
388
+ )
344
389
  )
345
390
  return res_list
346
391
 
@@ -351,8 +396,13 @@ def func_has_decorator(func: cst.FunctionDef, *names: str) -> bool:
351
396
  func.decorators,
352
397
  m.Decorator(
353
398
  decorator=m.OneOf(
399
+ # @name
354
400
  oneof_names(*names),
401
+ # @foo.name
355
402
  m.Attribute(attr=oneof_names(*names)),
403
+ # @name(...)
404
+ m.Call(func=oneof_names(*names)),
405
+ # @foo.name(...)
356
406
  m.Call(func=m.Attribute(attr=oneof_names(*names))),
357
407
  )
358
408
  ),
@@ -40,12 +40,22 @@ class Visitor101(Flake8AsyncVisitor_cst):
40
40
  self.save_state(node, "_yield_is_error", copy=True)
41
41
  # if there's no safe decorator,
42
42
  # and it's not yet been determined that yield is error
43
- # and this withitem opens a cancelscope:
43
+ # and this withitem opens a nursery/taskgroup/cancelscope:
44
44
  # then yielding is unsafe
45
45
  self._yield_is_error = (
46
46
  not self._safe_decorator
47
47
  and not self._yield_is_error
48
- and bool(with_has_call(node, "open_nursery", *cancel_scope_names))
48
+ # It's not strictly necessary to specify the base, as raising errors on
49
+ # e.g. anyio.open_nursery isn't much of a problem.
50
+ and bool(
51
+ # nursery/taskgroup
52
+ with_has_call(node, "open_nursery", base="trio")
53
+ or with_has_call(node, "create_task_group", base="anyio")
54
+ or with_has_call(node, "TaskGroup", base="asyncio")
55
+ # cancel scopes
56
+ or with_has_call(node, "timeout", "timeout_at", base="asyncio")
57
+ or with_has_call(node, *cancel_scope_names, base=("trio", "anyio"))
58
+ )
49
59
  )
50
60
 
51
61
  def leave_With(
@@ -12,13 +12,21 @@ if TYPE_CHECKING:
12
12
  from collections.abc import Mapping
13
13
 
14
14
 
15
+ def is_nursery_like(node: ast.expr) -> bool:
16
+ return bool(
17
+ get_matching_call(node, "open_nursery", base="trio")
18
+ or get_matching_call(node, "create_task_group", base="anyio")
19
+ or get_matching_call(node, "TaskGroup", base="asyncio")
20
+ )
21
+
22
+
15
23
  @error_class
16
24
  class Visitor111(Flake8AsyncVisitor):
17
25
  error_codes: Mapping[str, str] = {
18
26
  "ASYNC111": (
19
27
  "variable {2} is usable within the context manager on line {0}, but that "
20
- "will close before nursery opened on line {1} - this is usually a bug. "
21
- "Nurseries should generally be the inner-most context manager."
28
+ "will close before nursery/taskgroup opened on line {1} - this is usually "
29
+ "a bug. Nursery/TaskGroup should generally be the inner-most context manager."
22
30
  ),
23
31
  }
24
32
 
@@ -48,8 +56,7 @@ class Visitor111(Flake8AsyncVisitor):
48
56
  self.TrioContextManager(
49
57
  item.context_expr.lineno,
50
58
  item.optional_vars.id,
51
- get_matching_call(item.context_expr, "open_nursery")
52
- is not None,
59
+ is_nursery_like(item.context_expr),
53
60
  )
54
61
  )
55
62
 
@@ -75,7 +82,7 @@ class Visitor111(Flake8AsyncVisitor):
75
82
  if (
76
83
  isinstance(node.func, ast.Attribute)
77
84
  and isinstance(node.func.value, ast.Name)
78
- and node.func.attr in ("start", "start_soon")
85
+ and node.func.attr in ("start", "start_soon", "create_task")
79
86
  ):
80
87
  self._nursery_call = None
81
88
  for i, cm in enumerate(self._context_managers):
@@ -18,19 +18,32 @@ from libcst.metadata import PositionProvider
18
18
  from ..base import Statement
19
19
  from .flake8asyncvisitor import Flake8AsyncVisitor_cst
20
20
  from .helpers import (
21
- disabled_by_default,
21
+ AttributeCall,
22
+ cancel_scope_names,
23
+ disable_codes_by_default,
22
24
  error_class_cst,
25
+ flatten_preserving_comments,
23
26
  fnmatch_qualified_name_cst,
24
27
  func_has_decorator,
25
28
  iter_guaranteed_once_cst,
29
+ with_has_call,
26
30
  )
27
31
 
28
32
  if TYPE_CHECKING:
29
33
  from collections.abc import Mapping, Sequence
30
34
 
31
35
 
36
+ class ArtificialStatement(Statement):
37
+ """Statement that should not trigger 910/911 on function exit.
38
+
39
+ Used by loops and `with` statements.
40
+ """
41
+
42
+
32
43
  # Statement injected at the start of loops to track missed checkpoints.
33
- ARTIFICIAL_STATEMENT = Statement("artificial", -1)
44
+ ARTIFICIAL_STATEMENT = ArtificialStatement("artificial", -1)
45
+ # There's no particular reason why loops use a globally instanced statement, but
46
+ # `with` does not - mostly just an artifact of them being implemented at different times.
34
47
 
35
48
 
36
49
  def func_empty_body(node: cst.FunctionDef) -> bool:
@@ -231,8 +244,10 @@ class InsertCheckpointsInLoopBody(CommonVisitors):
231
244
  leave_Return = leave_Yield # type: ignore
232
245
 
233
246
 
247
+ disable_codes_by_default("ASYNC910", "ASYNC911", "ASYNC912")
248
+
249
+
234
250
  @error_class_cst
235
- @disabled_by_default
236
251
  class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
237
252
  error_codes: Mapping[str, str] = {
238
253
  "ASYNC910": (
@@ -243,6 +258,14 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
243
258
  "{0} from async iterable with no guaranteed checkpoint since {1.name} "
244
259
  "on line {1.lineno}."
245
260
  ),
261
+ "ASYNC912": (
262
+ "CancelScope with no guaranteed checkpoint. This makes it potentially "
263
+ "impossible to cancel."
264
+ ),
265
+ "ASYNC100": (
266
+ "{0}.{1} context contains no checkpoints, remove the context or add"
267
+ " `await {0}.lowlevel.checkpoint()`."
268
+ ),
246
269
  }
247
270
 
248
271
  def __init__(self, *args: Any, **kwargs: Any):
@@ -256,15 +279,24 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
256
279
  self.loop_state = LoopState()
257
280
  self.try_state = TryState()
258
281
 
282
+ # ASYNC100
283
+ self.has_checkpoint_stack: list[bool] = []
284
+ self.node_dict: dict[cst.With, list[AttributeCall]] = {}
285
+
259
286
  def should_autofix(self, node: cst.CSTNode, code: str | None = None) -> bool:
287
+ if code is None: # pragma: no branch
288
+ code = "ASYNC911" if self.has_yield else "ASYNC910"
289
+
260
290
  return (
261
291
  not self.noautofix
262
- and super().should_autofix(
263
- node, "ASYNC911" if self.has_yield else "ASYNC910"
264
- )
292
+ and super().should_autofix(node, code)
265
293
  and self.library != ("asyncio",)
266
294
  )
267
295
 
296
+ def checkpoint(self) -> None:
297
+ self.uncheckpointed_statements = set()
298
+ self.has_checkpoint_stack = [True] * len(self.has_checkpoint_stack)
299
+
268
300
  def checkpoint_statement(self) -> cst.SimpleStatementLine:
269
301
  return checkpoint_statement(self.library[0])
270
302
 
@@ -283,9 +315,11 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
283
315
  "uncheckpointed_statements",
284
316
  "loop_state",
285
317
  "try_state",
318
+ "has_checkpoint_stack",
286
319
  copy=True,
287
320
  )
288
321
  self.uncheckpointed_statements = set()
322
+ self.has_checkpoint_stack = []
289
323
  self.has_yield = self.safe_decorator = False
290
324
  self.loop_state = LoopState()
291
325
 
@@ -359,7 +393,7 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
359
393
  any_errors = False
360
394
  # raise the actual errors
361
395
  for statement in self.uncheckpointed_statements:
362
- if statement == ARTIFICIAL_STATEMENT:
396
+ if isinstance(statement, ArtificialStatement):
363
397
  continue
364
398
  any_errors |= self.error_91x(original_node, statement)
365
399
 
@@ -376,6 +410,7 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
376
410
  self.add_statement = self.checkpoint_statement()
377
411
  # avoid duplicate error messages
378
412
  self.uncheckpointed_statements = set()
413
+ # we don't treat it as a checkpoint for ASYNC100
379
414
 
380
415
  # return original node to avoid problems with identity equality
381
416
  assert original_node.deep_equals(updated_node)
@@ -386,7 +421,7 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
386
421
  node: cst.Return | cst.FunctionDef | cst.Yield,
387
422
  statement: Statement,
388
423
  ) -> bool:
389
- assert statement != ARTIFICIAL_STATEMENT
424
+ assert not isinstance(statement, ArtificialStatement)
390
425
 
391
426
  if isinstance(node, cst.FunctionDef):
392
427
  msg = "exit"
@@ -407,7 +442,7 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
407
442
  # so only set checkpoint after the await node
408
443
 
409
444
  # all nodes are now checkpointed
410
- self.uncheckpointed_statements = set()
445
+ self.checkpoint()
411
446
  return updated_node
412
447
 
413
448
  # raising exception means we don't need to checkpoint so we can treat it as one
@@ -419,9 +454,54 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
419
454
  # missing-checkpoint warning when there might in fact be one (i.e. a false alarm).
420
455
  def visit_With_body(self, node: cst.With):
421
456
  if getattr(node, "asynchronous", None):
422
- self.uncheckpointed_statements = set()
423
-
424
- leave_With_body = visit_With_body
457
+ self.checkpoint()
458
+ if res := (
459
+ with_has_call(node, *cancel_scope_names)
460
+ or with_has_call(
461
+ node, "timeout", "timeout_at", base=("asyncio", "asyncio.timeouts")
462
+ )
463
+ ):
464
+ pos = self.get_metadata(PositionProvider, node).start # pyright: ignore
465
+ line: int = pos.line # pyright: ignore
466
+ column: int = pos.column # pyright: ignore
467
+ self.uncheckpointed_statements.add(
468
+ ArtificialStatement("with", line, column)
469
+ )
470
+ self.node_dict[node] = res
471
+ self.has_checkpoint_stack.append(False)
472
+ else:
473
+ self.has_checkpoint_stack.append(True)
474
+
475
+ def leave_With(self, original_node: cst.With, updated_node: cst.With):
476
+ # Uses leave_With instead of leave_With_body because we need access to both
477
+ # original and updated node
478
+ # ASYNC100
479
+ if not self.has_checkpoint_stack.pop():
480
+ autofix = len(updated_node.items) == 1
481
+ for res in self.node_dict[original_node]:
482
+ # bypass 910 & 911's should_autofix logic, which excludes asyncio
483
+ # (TODO: and uses self.noautofix ... which I don't remember what it's for)
484
+ autofix &= self.error(
485
+ res.node, res.base, res.function, error_code="ASYNC100"
486
+ ) and super().should_autofix(res.node, code="ASYNC100")
487
+
488
+ if autofix:
489
+ return flatten_preserving_comments(updated_node)
490
+ # ASYNC912
491
+ else:
492
+ pos = self.get_metadata( # pyright: ignore
493
+ PositionProvider, original_node
494
+ ).start # pyright: ignore
495
+ line: int = pos.line # pyright: ignore
496
+ column: int = pos.column # pyright: ignore
497
+ s = ArtificialStatement("with", line, column)
498
+ if s in self.uncheckpointed_statements:
499
+ self.uncheckpointed_statements.remove(s)
500
+ for res in self.node_dict[original_node]:
501
+ self.error(res.node, error_code="ASYNC912")
502
+ if getattr(original_node, "asynchronous", None):
503
+ self.checkpoint()
504
+ return updated_node
425
505
 
426
506
  # error if no checkpoint since earlier yield or function entry
427
507
  def leave_Yield(
@@ -431,6 +511,10 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
431
511
  return updated_node
432
512
  self.has_yield = True
433
513
 
514
+ # Treat as a checkpoint for ASYNC100, since the context we yield to
515
+ # may checkpoint.
516
+ self.has_checkpoint_stack = [True] * len(self.has_checkpoint_stack)
517
+
434
518
  if self.check_function_exit(original_node) and self.should_autofix(
435
519
  original_node
436
520
  ):
@@ -605,7 +689,7 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
605
689
  # appropriate errors if the loop doesn't checkpoint
606
690
 
607
691
  if getattr(node, "asynchronous", None):
608
- self.uncheckpointed_statements = set()
692
+ self.checkpoint()
609
693
  else:
610
694
  self.uncheckpointed_statements = {ARTIFICIAL_STATEMENT}
611
695
 
@@ -651,7 +735,7 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
651
735
  # AsyncFor guarantees checkpoint on running out of iterable
652
736
  # so reset checkpoint state at end of loop. (but not state at break)
653
737
  if getattr(node, "asynchronous", None):
654
- self.uncheckpointed_statements = set()
738
+ self.checkpoint()
655
739
  else:
656
740
  # enter orelse with worst case:
657
741
  # loop body might execute fully before entering orelse
@@ -780,7 +864,7 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
780
864
 
781
865
  # if async comprehension, checkpoint
782
866
  if node.asynchronous:
783
- self.uncheckpointed_statements = set()
867
+ self.checkpoint()
784
868
  self.comp_unknown = False
785
869
  return False
786
870
 
@@ -104,17 +104,29 @@ class Visitor112(Flake8AsyncVisitor):
104
104
  continue
105
105
  var_name = item.optional_vars.id
106
106
 
107
- # check for trio.open_nursery
107
+ # check for trio.open_nursery and anyio.create_task_group
108
108
  nursery = get_matching_call(
109
- item.context_expr, "open_nursery", base=("trio",)
110
- )
109
+ item.context_expr, "open_nursery", base="trio"
110
+ ) or get_matching_call(item.context_expr, "create_task_group", base="anyio")
111
+ start_methods: tuple[str, ...] = ("start", "start_soon")
112
+ if nursery is None:
113
+ # check for asyncio.TaskGroup
114
+ nursery = get_matching_call(
115
+ item.context_expr, "TaskGroup", base="asyncio"
116
+ )
117
+ if nursery is None:
118
+ continue
119
+ start_methods = ("create_task",)
120
+
121
+ body_call = node.body[0].value
122
+ if isinstance(body_call, ast.Await):
123
+ body_call = body_call.value
111
124
 
112
125
  # `isinstance(..., ast.Call)` is done in get_matching_call
113
- body_call = cast("ast.Call", node.body[0].value)
126
+ body_call = cast("ast.Call", body_call)
114
127
 
115
128
  if (
116
- nursery is not None
117
- and get_matching_call(body_call, "start", "start_soon", base=var_name)
129
+ get_matching_call(body_call, *start_methods, base=var_name)
118
130
  # check for presence of <X> as parameter
119
131
  and not any(
120
132
  (isinstance(n, ast.Name) and n.id == var_name)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: flake8-async
3
- Version: 24.4.2
3
+ Version: 24.5.2
4
4
  Summary: A highly opinionated flake8 plugin for Trio-related problems.
5
5
  Home-page: https://github.com/python-trio/flake8-async
6
6
  Author: Zac Hatfield-Dodds, John Litborn, and Contributors
@@ -89,6 +89,7 @@ Note: 22X, 23X and 24X has not had asyncio-specific suggestions written.
89
89
  - **ASYNC910**: Exit or `return` from async function with no guaranteed checkpoint or exception since function definition. You might want to enable this on a codebase to make it easier to reason about checkpoints, and make the logic of ASYNC911 correct.
90
90
  - **ASYNC911**: Exit, `yield` or `return` from async iterable with no guaranteed checkpoint since possible function entry (yield or function definition)
91
91
  Checkpoints are `await`, `async for`, and `async with` (on one of enter/exit).
92
+ - **ASYNC912**: Timeout/Cancelscope has no awaits that are guaranteed to run. If the scope has no checkpoints at all, then `ASYNC100` will be raised instead.
92
93
 
93
94
  ### Removed Warnings
94
95
  - **TRIOxxx**: All error codes are now renamed ASYNCxxx
@@ -110,7 +111,7 @@ adding the following to your `.pre-commit-config.yaml`:
110
111
  minimum_pre_commit_version: '2.9.0'
111
112
  repos:
112
113
  - repo: https://github.com/python-trio/flake8-async
113
- rev: 24.4.2
114
+ rev: 24.5.2
114
115
  hooks:
115
116
  - id: flake8-async
116
117
  # args: [--enable=ASYNC, --disable=ASYNC9, --autofix=ASYNC]
@@ -225,6 +226,15 @@ async def my_function():
225
226
  # Changelog
226
227
  *[CalVer, YY.month.patch](https://calver.org/)*
227
228
 
229
+ ## 24.5.2
230
+ - ASYNC101 now also warns on anyio & asyncio taskgroup.s
231
+ - Fixed a bug where ASYNC101 and ASYNC91x would not recognize decorators with parameters directly imported. I.e. `@fixture(...)` will now suppress errors.
232
+
233
+ ## 24.5.1
234
+ - Add ASYNC912: no checkpoints in with statement are guaranteed to run.
235
+ - ASYNC100 now properly treats async for comprehensions as checkpoints.
236
+ - ASYNC100 now supports autofixing on asyncio.
237
+
228
238
  ## 24.4.2
229
239
  - Add ASYNC119: yield in contextmanager in async generator.
230
240