flake8-async 24.11.3__tar.gz → 25.2.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 (149) hide show
  1. {flake8_async-24.11.3 → flake8_async-25.2.2}/PKG-INFO +12 -3
  2. {flake8_async-24.11.3 → flake8_async-25.2.2}/flake8_async/__init__.py +1 -1
  3. {flake8_async-24.11.3 → flake8_async-25.2.2}/flake8_async/visitors/visitor103_104.py +2 -2
  4. {flake8_async-24.11.3 → flake8_async-25.2.2}/flake8_async/visitors/visitor2xx.py +4 -2
  5. {flake8_async-24.11.3 → flake8_async-25.2.2}/flake8_async/visitors/visitor91x.py +208 -31
  6. {flake8_async-24.11.3 → flake8_async-25.2.2}/flake8_async/visitors/visitors.py +33 -16
  7. {flake8_async-24.11.3 → flake8_async-25.2.2}/flake8_async.egg-info/PKG-INFO +12 -3
  8. {flake8_async-24.11.3 → flake8_async-25.2.2}/flake8_async.egg-info/SOURCES.txt +6 -8
  9. {flake8_async-24.11.3 → flake8_async-25.2.2}/pyproject.toml +3 -2
  10. {flake8_async-24.11.3 → flake8_async-25.2.2}/setup.py +1 -0
  11. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/autofix_files/async100.py +74 -6
  12. flake8_async-25.2.2/tests/autofix_files/async124.py +159 -0
  13. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/autofix_files/async910.py +21 -2
  14. flake8_async-25.2.2/tests/autofix_files/async913_trio_anyio.py +12 -0
  15. flake8_async-24.11.3/tests/test_changelog_and_version.py → flake8_async-25.2.2/tests/check_changelog_and_version.py +26 -10
  16. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async100.py +74 -6
  17. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async101.py +11 -4
  18. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async111.py +0 -2
  19. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async112.py +0 -1
  20. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async112_asyncio.py +1 -1
  21. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async113.py +32 -15
  22. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async113_trio.py +5 -9
  23. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async121.py +0 -1
  24. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async121_anyio.py +0 -1
  25. flake8_async-25.2.2/tests/eval_files/async124.py +145 -0
  26. flake8_async-25.2.2/tests/eval_files/async124_no_autofix.py +9 -0
  27. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async251.py +0 -2
  28. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async910.py +20 -2
  29. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async912.py +8 -1
  30. flake8_async-25.2.2/tests/eval_files/async913_trio_anyio.py +11 -0
  31. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/test_flake8_async.py +25 -2
  32. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/trio_options.py +8 -1
  33. {flake8_async-24.11.3 → flake8_async-25.2.2}/tox.ini +1 -1
  34. flake8_async-24.11.3/tests/autofix_files/async100_trio.py +0 -10
  35. flake8_async-24.11.3/tests/eval_files/async100_trio.py +0 -10
  36. flake8_async-24.11.3/tests/eval_files/async101_anyio.py +0 -10
  37. flake8_async-24.11.3/tests/eval_files/async101_trio.py +0 -17
  38. flake8_async-24.11.3/tests/eval_files/async111_anyio.py +0 -21
  39. flake8_async-24.11.3/tests/eval_files/async112_anyio.py +0 -28
  40. flake8_async-24.11.3/tests/eval_files/async113_anyio.py +0 -22
  41. {flake8_async-24.11.3 → flake8_async-25.2.2}/CHANGELOG.md +0 -0
  42. {flake8_async-24.11.3 → flake8_async-25.2.2}/CONTRIBUTING.md +0 -0
  43. {flake8_async-24.11.3 → flake8_async-25.2.2}/LICENSE +0 -0
  44. {flake8_async-24.11.3 → flake8_async-25.2.2}/MANIFEST.in +0 -0
  45. {flake8_async-24.11.3 → flake8_async-25.2.2}/README.md +0 -0
  46. {flake8_async-24.11.3 → flake8_async-25.2.2}/flake8_async/__main__.py +0 -0
  47. {flake8_async-24.11.3 → flake8_async-25.2.2}/flake8_async/base.py +0 -0
  48. {flake8_async-24.11.3 → flake8_async-25.2.2}/flake8_async/runner.py +0 -0
  49. {flake8_async-24.11.3 → flake8_async-25.2.2}/flake8_async/visitors/__init__.py +0 -0
  50. {flake8_async-24.11.3 → flake8_async-25.2.2}/flake8_async/visitors/flake8asyncvisitor.py +0 -0
  51. {flake8_async-24.11.3 → flake8_async-25.2.2}/flake8_async/visitors/helpers.py +0 -0
  52. {flake8_async-24.11.3 → flake8_async-25.2.2}/flake8_async/visitors/visitor101.py +0 -0
  53. {flake8_async-24.11.3 → flake8_async-25.2.2}/flake8_async/visitors/visitor102_120.py +0 -0
  54. {flake8_async-24.11.3 → flake8_async-25.2.2}/flake8_async/visitors/visitor105.py +0 -0
  55. {flake8_async-24.11.3 → flake8_async-25.2.2}/flake8_async/visitors/visitor111.py +0 -0
  56. {flake8_async-24.11.3 → flake8_async-25.2.2}/flake8_async/visitors/visitor118.py +0 -0
  57. {flake8_async-24.11.3 → flake8_async-25.2.2}/flake8_async/visitors/visitor123.py +0 -0
  58. {flake8_async-24.11.3 → flake8_async-25.2.2}/flake8_async/visitors/visitor_utility.py +0 -0
  59. {flake8_async-24.11.3 → flake8_async-25.2.2}/flake8_async.egg-info/dependency_links.txt +0 -0
  60. {flake8_async-24.11.3 → flake8_async-25.2.2}/flake8_async.egg-info/entry_points.txt +0 -0
  61. {flake8_async-24.11.3 → flake8_async-25.2.2}/flake8_async.egg-info/not-zip-safe +0 -0
  62. {flake8_async-24.11.3 → flake8_async-25.2.2}/flake8_async.egg-info/requires.txt +0 -0
  63. {flake8_async-24.11.3 → flake8_async-25.2.2}/flake8_async.egg-info/top_level.txt +0 -0
  64. {flake8_async-24.11.3 → flake8_async-25.2.2}/setup.cfg +0 -0
  65. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/__init__.py +0 -0
  66. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/autofix_files/async100_asyncio.py +0 -0
  67. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/autofix_files/async100_simple_autofix.py +0 -0
  68. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/autofix_files/async910_insert_library.py +0 -0
  69. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/autofix_files/async911.py +0 -0
  70. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/autofix_files/async911_insert_library.py +0 -0
  71. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/autofix_files/async913.py +0 -0
  72. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/autofix_files/async91x_autofix.py +0 -0
  73. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/autofix_files/exception_suppress_context_manager.py +0 -0
  74. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/autofix_files/exception_suppress_context_manager_import_star.py +0 -0
  75. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/autofix_files/noqa.py +0 -0
  76. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/autofix_files/noqa_testing.py +0 -0
  77. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/conftest.py +0 -0
  78. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/anyio_trio.py +0 -0
  79. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async100_asyncio.py +0 -0
  80. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async100_noautofix.py +0 -0
  81. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async100_simple_autofix.py +0 -0
  82. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async101_asyncio.py +0 -0
  83. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async102.py +0 -0
  84. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async102_aclose.py +0 -0
  85. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async102_aclose_args.py +0 -0
  86. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async102_anyio.py +0 -0
  87. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async102_asyncio.py +0 -0
  88. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async102_trio.py +0 -0
  89. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async103.py +0 -0
  90. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async103_all_imported.py +0 -0
  91. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async103_both_imported.py +0 -0
  92. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async103_no_104.py +0 -0
  93. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async103_trio.py +0 -0
  94. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async104.py +0 -0
  95. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async104_anyio.py +0 -0
  96. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async104_trio.py +0 -0
  97. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async105.py +0 -0
  98. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async105_anyio.py +0 -0
  99. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async106.py +0 -0
  100. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async109.py +0 -0
  101. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async110.py +0 -0
  102. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async111_asyncio.py +0 -0
  103. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async113_asyncio.py +0 -0
  104. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async114.py +0 -0
  105. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async115.py +0 -0
  106. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async116.py +0 -0
  107. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async118.py +0 -0
  108. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async119.py +0 -0
  109. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async120.py +0 -0
  110. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async121_asyncio.py +0 -0
  111. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async122.py +0 -0
  112. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async123.py +0 -0
  113. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async123_py311.py +0 -0
  114. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async200.py +0 -0
  115. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async210.py +0 -0
  116. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async211.py +0 -0
  117. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async212.py +0 -0
  118. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async22x.py +0 -0
  119. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async22x_asyncio.py +0 -0
  120. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async232.py +0 -0
  121. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async232_asyncio.py +0 -0
  122. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async23x.py +0 -0
  123. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async23x_asyncio.py +0 -0
  124. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async240.py +0 -0
  125. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async250.py +0 -0
  126. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async250_multi_library.py +0 -0
  127. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async251_multi_library.py +0 -0
  128. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async300.py +0 -0
  129. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async900.py +0 -0
  130. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async910_insert_library.py +0 -0
  131. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async911.py +0 -0
  132. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async911_insert_library.py +0 -0
  133. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async912_asyncio.py +0 -0
  134. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async913.py +0 -0
  135. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async91x_autofix.py +0 -0
  136. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/async91x_noautofix.py +0 -0
  137. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/exception_suppress_context_manager.py +0 -0
  138. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/exception_suppress_context_manager_import_star.py +0 -0
  139. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/no_library.py +0 -0
  140. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/noqa.py +0 -0
  141. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/noqa_no_autofix.py +0 -0
  142. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/noqa_testing.py +0 -0
  143. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/eval_files/trio_anyio.py +0 -0
  144. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/test_all_visitors_imported.py +0 -0
  145. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/test_config_and_args.py +0 -0
  146. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/test_decorator.py +0 -0
  147. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/test_exception_on_invalid_code.py +0 -0
  148. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/test_formatting.py +0 -0
  149. {flake8_async-24.11.3 → flake8_async-25.2.2}/tests/test_messages_documented.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: flake8-async
3
- Version: 24.11.3
3
+ Version: 25.2.2
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,9 +21,18 @@ 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
- License-File: LICENSE
25
24
  Requires-Dist: flake8>=6
26
25
  Requires-Dist: libcst>=1.0.1
26
+ Dynamic: author
27
+ Dynamic: author-email
28
+ Dynamic: classifier
29
+ Dynamic: description
30
+ Dynamic: description-content-type
31
+ Dynamic: license
32
+ Dynamic: project-url
33
+ Dynamic: requires-dist
34
+ Dynamic: requires-python
35
+ Dynamic: summary
27
36
 
28
37
  [![Documentation](https://img.shields.io/badge/docs-read%20now-blue.svg)](https://flake8-async.readthedocs.io)
29
38
  [![Latest PyPi version](https://img.shields.io/pypi/v/flake8-async.svg)](https://pypi.org/project/flake8-async)
@@ -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__ = "24.11.3"
41
+ __version__ = "25.2.2"
42
42
 
43
43
 
44
44
  # taken from https://github.com/Zac-HD/shed
@@ -51,9 +51,9 @@ _error_codes = {
51
51
  "ASYNC103": _async103_common_msg,
52
52
  "ASYNC104": "Cancelled (and therefore BaseException) must be re-raised.",
53
53
  }
54
- for poss_library in _suggestion_dict:
54
+ for poss_library, _lib_suggest in _suggestion_dict.items():
55
55
  _error_codes[f"ASYNC103_{'_'.join(poss_library)}"] = (
56
- _async103_common_msg + _suggestion.format(_suggestion_dict[poss_library])
56
+ _async103_common_msg + _suggestion.format(_lib_suggest)
57
57
  )
58
58
 
59
59
 
@@ -186,7 +186,9 @@ class Visitor22X(Visitor200):
186
186
  "Sync call {} in async function, use "
187
187
  "`asyncio.create_subprocess_[exec/shell]."
188
188
  ),
189
- "ASYNC222": "Sync call {} in async function, wrap in `{}.to_thread.run_sync()`.",
189
+ "ASYNC222": (
190
+ "Sync call {} in async function, wrap in `{}.to_thread.run_sync()`."
191
+ ),
190
192
  "ASYNC222_asyncio": (
191
193
  "Sync call {} in async function, use `asyncio.loop.run_in_executor`."
192
194
  ),
@@ -397,7 +399,7 @@ wrappers: Mapping[str, str] = {
397
399
  @error_class
398
400
  class Visitor25X(Visitor200):
399
401
  error_codes: Mapping[str, str] = {
400
- "ASYNC250": ("Blocking sync call `input()` in async function. Wrap in `{}`."),
402
+ "ASYNC250": "Blocking sync call `input()` in async function. Wrap in `{}`.",
401
403
  "ASYNC251": (
402
404
  "Blocking sync call `time.sleep(...)` in async function."
403
405
  " Use `await {}.sleep(...)`."
@@ -1,8 +1,14 @@
1
- """Contains Visitor91X.
1
+ """Contains Visitor91X and Visitor124.
2
2
 
3
- 910 looks for async functions without guaranteed checkpoints (or exceptions), and 911 does
4
- the same except for async iterables - while also requiring that they checkpoint between
5
- each yield.
3
+ Visitor91X contains checks for
4
+ * ASYNC100 cancel-scope-no-checkpoint
5
+ * ASYNC910 async-function-no-checkpoint
6
+ * ASYNC911 async-generator-no-checkpoint
7
+ * ASYNC912 cancel-scope-no-guaranteed-checkpoint
8
+ * ASYNC913 indefinite-loop-no-guaranteed-checkpoint
9
+
10
+ Visitor124 contains
11
+ * ASYNC124 async-function-could-be-sync
6
12
  """
7
13
 
8
14
  from __future__ import annotations
@@ -25,6 +31,7 @@ from .helpers import (
25
31
  flatten_preserving_comments,
26
32
  fnmatch_qualified_name_cst,
27
33
  func_has_decorator,
34
+ identifier_to_string,
28
35
  iter_guaranteed_once_cst,
29
36
  with_has_call,
30
37
  )
@@ -63,6 +70,92 @@ def func_empty_body(node: cst.FunctionDef) -> bool:
63
70
  )
64
71
 
65
72
 
73
+ # this could've been implemented as part of visitor91x, but /shrug
74
+ @error_class_cst
75
+ class Visitor124(Flake8AsyncVisitor_cst):
76
+ error_codes: Mapping[str, str] = {
77
+ "ASYNC124": (
78
+ "Async function with no `await` could be sync."
79
+ " Async functions are more expensive to call."
80
+ )
81
+ }
82
+
83
+ def __init__(self, *args: Any, **kwargs: Any):
84
+ super().__init__(*args, **kwargs)
85
+ self.has_await = False
86
+ self.in_class = False
87
+
88
+ def visit_ClassDef(self, node: cst.ClassDef):
89
+ self.save_state(node, "in_class", copy=False)
90
+ self.in_class = True
91
+
92
+ def leave_ClassDef(
93
+ self, original_node: cst.ClassDef, updated_node: cst.ClassDef
94
+ ) -> cst.ClassDef:
95
+ self.restore_state(original_node)
96
+ return updated_node
97
+
98
+ # await in sync defs are not valid, but handling this will make ASYNC124
99
+ # correctly pop up in parent func as if the child function was async
100
+ def visit_FunctionDef(self, node: cst.FunctionDef):
101
+ # default values are evaluated in parent scope
102
+ # this visitor has no autofixes, so we can throw away return value
103
+ _ = node.params.visit(self)
104
+
105
+ self.save_state(node, "has_await", "in_class", copy=False)
106
+
107
+ # ignore class methods
108
+ self.has_await = self.in_class
109
+
110
+ # but not nested functions
111
+ self.in_class = False
112
+
113
+ _ = node.body.visit(self)
114
+
115
+ # we've manually visited subnodes (that we care about).
116
+ return False
117
+
118
+ def leave_FunctionDef(
119
+ self, original_node: cst.FunctionDef, updated_node: cst.FunctionDef
120
+ ) -> cst.FunctionDef:
121
+ if (
122
+ original_node.asynchronous is not None
123
+ and not self.has_await
124
+ and not func_empty_body(original_node)
125
+ and not func_has_decorator(original_node, "overload")
126
+ # skip functions with @fixture and params since they may be relying
127
+ # on async fixtures.
128
+ and not (
129
+ original_node.params.params
130
+ and func_has_decorator(original_node, "fixture")
131
+ )
132
+ # ignore functions with no_checkpoint_warning_decorators
133
+ and not fnmatch_qualified_name_cst(
134
+ original_node.decorators, *self.options.no_checkpoint_warning_decorators
135
+ )
136
+ ):
137
+ self.error(original_node)
138
+ self.restore_state(original_node)
139
+ return updated_node
140
+
141
+ def visit_Await(self, node: cst.Await):
142
+ self.has_await = True
143
+
144
+ def visit_With(self, node: cst.With | cst.For | cst.CompFor):
145
+ if node.asynchronous is not None:
146
+ self.has_await = True
147
+
148
+ visit_For = visit_With
149
+ visit_CompFor = visit_With
150
+
151
+ # The generator target will be immediately evaluated, but the other
152
+ # elements will not be evaluated at the point of defining the GenExp.
153
+ # To consume those needs an explicit syntactic checkpoint
154
+ def visit_GeneratorExp(self, node: cst.GeneratorExp):
155
+ node.for_in.iter.visit(self)
156
+ return False
157
+
158
+
66
159
  @dataclass
67
160
  class LoopState:
68
161
  infinite_loop: bool = False
@@ -261,10 +354,10 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
261
354
  "on line {1.lineno}."
262
355
  ),
263
356
  "ASYNC912": (
264
- "CancelScope with no guaranteed checkpoint. This makes it potentially "
357
+ "CancelScope with no guaranteed cancel point. This makes it potentially "
265
358
  "impossible to cancel."
266
359
  ),
267
- "ASYNC913": ("Indefinite loop with no guaranteed checkpoint."),
360
+ "ASYNC913": "Indefinite loop with no guaranteed cancel points.",
268
361
  "ASYNC100": (
269
362
  "{0}.{1} context contains no checkpoints, remove the context or add"
270
363
  " `await {0}.lowlevel.checkpoint()`."
@@ -274,7 +367,6 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
274
367
  def __init__(self, *args: Any, **kwargs: Any):
275
368
  super().__init__(*args, **kwargs)
276
369
  self.has_yield = False
277
- self.safe_decorator = False
278
370
  self.async_function = False
279
371
  self.uncheckpointed_statements: set[Statement] = set()
280
372
  self.comp_unknown = False
@@ -285,10 +377,14 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
285
377
  # ASYNC100
286
378
  self.has_checkpoint_stack: list[bool] = []
287
379
  self.node_dict: dict[cst.With, list[AttributeCall]] = {}
380
+ self.taskgroup_has_start_soon: dict[str, bool] = {}
288
381
 
289
382
  # --exception-suppress-context-manager
290
383
  self.suppress_imported_as: list[str] = []
291
384
 
385
+ # used to transfer new body between visit_FunctionDef and leave_FunctionDef
386
+ self.new_body: cst.BaseSuite | None = None
387
+
292
388
  def should_autofix(self, node: cst.CSTNode, code: str | None = None) -> bool:
293
389
  if code is None: # pragma: no branch
294
390
  code = "ASYNC911" if self.has_yield else "ASYNC910"
@@ -299,13 +395,37 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
299
395
  and self.library != ("asyncio",)
300
396
  )
301
397
 
398
+ def checkpoint_cancel_point(self) -> None:
399
+ self.has_checkpoint_stack = [True] * len(self.has_checkpoint_stack)
400
+ # don't need to look for any .start_soon() calls
401
+ self.taskgroup_has_start_soon.clear()
402
+
403
+ def checkpoint_schedule_point(self) -> None:
404
+ # ASYNC912&ASYNC913 only cares about cancel points, so don't remove
405
+ # them if we only do a schedule point
406
+ self.uncheckpointed_statements = {
407
+ s
408
+ for s in self.uncheckpointed_statements
409
+ if isinstance(s, ArtificialStatement)
410
+ }
411
+
302
412
  def checkpoint(self) -> None:
303
413
  self.uncheckpointed_statements = set()
304
- self.has_checkpoint_stack = [True] * len(self.has_checkpoint_stack)
414
+ self.checkpoint_cancel_point()
305
415
 
306
416
  def checkpoint_statement(self) -> cst.SimpleStatementLine:
307
417
  return checkpoint_statement(self.library[0])
308
418
 
419
+ def visit_Call(self, node: cst.Call) -> None:
420
+ # [Nursery/TaskGroup].start_soon introduces a cancel point
421
+ if (
422
+ isinstance(node.func, cst.Attribute)
423
+ and isinstance(node.func.value, cst.Name)
424
+ and node.func.attr.value == "start_soon"
425
+ and node.func.value.value in self.taskgroup_has_start_soon
426
+ ):
427
+ self.taskgroup_has_start_soon[node.func.value.value] = True
428
+
309
429
  def visit_ImportFrom(self, node: cst.ImportFrom) -> None:
310
430
  # Semi-crude approach to handle `from contextlib import suppress`.
311
431
  # It does not handle the identifier being overridden, or assigned
@@ -329,6 +449,10 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
329
449
  return
330
450
 
331
451
  def visit_FunctionDef(self, node: cst.FunctionDef) -> bool:
452
+ # `await` in default values happen in parent scope
453
+ # we also know we don't ever modify parameters so we can ignore the return value
454
+ _ = node.params.visit(self)
455
+
332
456
  # don't lint functions whose bodies solely consist of pass or ellipsis
333
457
  # @overload functions are also guaranteed to be empty
334
458
  # we also ignore pytest fixtures
@@ -338,19 +462,23 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
338
462
  self.save_state(
339
463
  node,
340
464
  "has_yield",
341
- "safe_decorator",
342
465
  "async_function",
343
466
  "uncheckpointed_statements",
467
+ # comp_unknown does not need to be saved
344
468
  "loop_state",
345
469
  "try_state",
346
470
  "has_checkpoint_stack",
347
- "suppress_imported_as",
471
+ # node_dict is cleaned up and don't need to be saved
472
+ "taskgroup_has_start_soon",
473
+ "suppress_imported_as", # a copy is saved, but state is not reset
348
474
  copy=True,
349
475
  )
350
476
  self.uncheckpointed_statements = set()
351
477
  self.has_checkpoint_stack = []
352
- self.has_yield = self.safe_decorator = False
478
+ self.has_yield = False
353
479
  self.loop_state = LoopState()
480
+ # try_state is reset upon entering try
481
+ self.taskgroup_has_start_soon = {}
354
482
 
355
483
  self.async_function = (
356
484
  node.asynchronous is not None
@@ -358,36 +486,49 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
358
486
  node.decorators, *self.options.no_checkpoint_warning_decorators
359
487
  )
360
488
  )
361
- if not self.async_function:
362
- # only visit subnodes if there is an async function defined inside
363
- # this should improve performance on codebases with many sync functions
364
- return any(m.findall(node, m.FunctionDef(asynchronous=m.Asynchronous())))
489
+ # only visit subnodes if there is an async function defined inside
490
+ # this should improve performance on codebases with many sync functions
491
+ if not self.async_function and not any(
492
+ m.findall(node, m.FunctionDef(asynchronous=m.Asynchronous()))
493
+ ):
494
+ return False
365
495
 
366
496
  pos = self.get_metadata(PositionProvider, node).start # type: ignore
367
497
  self.uncheckpointed_statements = {
368
498
  Statement("function definition", pos.line, pos.column) # type: ignore
369
499
  }
370
- return True
500
+
501
+ # visit body
502
+ # we're not gonna get FlattenSentinel or RemovalSentinel
503
+ self.new_body = cast(cst.BaseSuite, node.body.visit(self))
504
+
505
+ # we know that leave_FunctionDef for this FunctionDef will run immediately after
506
+ # this function exits so we don't need to worry about save_state for new_body
507
+ return False
371
508
 
372
509
  def leave_FunctionDef(
373
510
  self, original_node: cst.FunctionDef, updated_node: cst.FunctionDef
374
511
  ) -> cst.FunctionDef:
375
512
  if (
376
- self.async_function
513
+ self.new_body is not None
514
+ and self.async_function
377
515
  # updated_node does not have a position, so we must send original_node
378
516
  and self.check_function_exit(original_node)
379
517
  and self.should_autofix(original_node)
380
- and isinstance(updated_node.body, cst.IndentedBlock)
518
+ and isinstance(self.new_body, cst.IndentedBlock)
381
519
  ):
382
520
  # insert checkpoint at the end of body
383
- new_body = list(updated_node.body.body)
384
- new_body.append(self.checkpoint_statement())
385
- indentedblock = updated_node.body.with_changes(body=new_body)
386
- updated_node = updated_node.with_changes(body=indentedblock)
521
+ new_body_block = list(self.new_body.body)
522
+ new_body_block.append(self.checkpoint_statement())
523
+ self.new_body = self.new_body.with_changes(body=new_body_block)
387
524
 
388
525
  self.ensure_imported_library()
389
526
 
527
+ if self.new_body is not None:
528
+ updated_node = updated_node.with_changes(body=self.new_body)
390
529
  self.restore_state(original_node)
530
+ # reset self.new_body
531
+ self.new_body = None
391
532
  return updated_node
392
533
 
393
534
  # error if function exit/return/yields with uncheckpointed statements
@@ -440,8 +581,8 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
440
581
  ):
441
582
  self.add_statement = self.checkpoint_statement()
442
583
  # avoid duplicate error messages
443
- self.uncheckpointed_statements = set()
444
- # we don't treat it as a checkpoint for ASYNC100
584
+ # but don't see it as a cancel point for ASYNC100
585
+ self.checkpoint_schedule_point()
445
586
 
446
587
  # return original node to avoid problems with identity equality
447
588
  assert original_node.deep_equals(updated_node)
@@ -491,12 +632,47 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
491
632
  is not None
492
633
  )
493
634
 
635
+ def _checkpoint_with(self, node: cst.With, entry: bool):
636
+ """Conditionally checkpoints entry/exit of With.
637
+
638
+ If the `with` only contains calls to open_nursery/create_task_group, it's a
639
+ schedule point but not a cancellation point, so we treat it as a checkpoint
640
+ for async91x but not for async100.
641
+
642
+ Saves the name of the taskgroup/nursery if entry is set
643
+ """
644
+ if not getattr(node, "asynchronous", None):
645
+ return
646
+
647
+ for item in node.items:
648
+ if isinstance(item.item, cst.Call) and identifier_to_string(
649
+ item.item.func
650
+ ) in (
651
+ "trio.open_nursery",
652
+ "anyio.create_task_group",
653
+ ):
654
+ if item.asname is not None and isinstance(item.asname.name, cst.Name):
655
+ # save the nursery/taskgroup to see if it has a `.start_soon`
656
+ if entry:
657
+ self.taskgroup_has_start_soon[item.asname.name.value] = False
658
+ elif self.taskgroup_has_start_soon.pop(
659
+ item.asname.name.value, False
660
+ ):
661
+ self.checkpoint()
662
+ return
663
+ else:
664
+ self.checkpoint()
665
+ break
666
+ else:
667
+ # only taskgroup/nursery calls
668
+ self.checkpoint_schedule_point()
669
+
494
670
  # Async context managers can reasonably checkpoint on either or both of entry and
495
671
  # exit. Given that we can't tell which, we assume "both" to avoid raising a
496
672
  # missing-checkpoint warning when there might in fact be one (i.e. a false alarm).
497
673
  def visit_With_body(self, node: cst.With):
498
- if getattr(node, "asynchronous", None):
499
- self.checkpoint()
674
+ self.save_state(node, "taskgroup_has_start_soon", copy=True)
675
+ self._checkpoint_with(node, entry=True)
500
676
 
501
677
  # if this might suppress exceptions, we cannot treat anything inside it as
502
678
  # checkpointing.
@@ -548,6 +724,8 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
548
724
  for res in self.node_dict[original_node]:
549
725
  self.error(res.node, error_code="ASYNC912")
550
726
 
727
+ self.node_dict.pop(original_node, None)
728
+
551
729
  # if exception-suppressing, restore all uncheckpointed statements from
552
730
  # before the `with`.
553
731
  if self._is_exception_suppressing_context_manager(original_node):
@@ -555,8 +733,7 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
555
733
  self.restore_state(original_node)
556
734
  self.uncheckpointed_statements.update(prev_checkpoints)
557
735
 
558
- if getattr(original_node, "asynchronous", None):
559
- self.checkpoint()
736
+ self._checkpoint_with(original_node, entry=False)
560
737
  return updated_node
561
738
 
562
739
  # error if no checkpoint since earlier yield or function entry
@@ -569,7 +746,7 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
569
746
 
570
747
  # Treat as a checkpoint for ASYNC100, since the context we yield to
571
748
  # may checkpoint.
572
- self.has_checkpoint_stack = [True] * len(self.has_checkpoint_stack)
749
+ self.checkpoint_cancel_point()
573
750
 
574
751
  if self.check_function_exit(original_node) and self.should_autofix(
575
752
  original_node
@@ -978,8 +1155,8 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
978
1155
  return False
979
1156
 
980
1157
  # The generator target will be immediately evaluated, but the other
981
- # elements will be lazily evaluated as the generator is consumed so we don't
982
- # visit them as any checkpoints in them are not guaranteed to execute.
1158
+ # elements will not be evaluated at the point of defining the GenExp.
1159
+ # To consume those needs an explicit syntactic checkpoint
983
1160
  def visit_GeneratorExp(self, node: cst.GeneratorExp):
984
1161
  node.for_in.iter.visit(self)
985
1162
  return False
@@ -121,10 +121,10 @@ class Visitor112(Flake8AsyncVisitor):
121
121
  elif get_matching_call(
122
122
  item.context_expr, "create_task_group", base="anyio"
123
123
  ):
124
- nursery_type = "taskgroup"
124
+ nursery_type = "task group"
125
125
  # check for asyncio.TaskGroup
126
126
  elif get_matching_call(item.context_expr, "TaskGroup", base="asyncio"):
127
- nursery_type = "taskgroup"
127
+ nursery_type = "task group"
128
128
  start_methods = ("create_task",)
129
129
  else:
130
130
  # incorrectly marked as not covered on py39
@@ -151,12 +151,12 @@ class Visitor112(Flake8AsyncVisitor):
151
151
 
152
152
 
153
153
  # used in 113 and 114
154
- STARTABLE_CALLS = (
154
+ STARTABLE_CALLS = ("serve",)
155
+ TRIO_STARTABLE_CALLS = (
155
156
  "run_process",
156
157
  "serve_ssl_over_tcp",
157
158
  "serve_tcp",
158
159
  "serve_listeners",
159
- "serve",
160
160
  )
161
161
 
162
162
 
@@ -201,7 +201,11 @@ class Visitor113(Flake8AsyncVisitor):
201
201
  if isinstance(n, ast.Name):
202
202
  return n.id in startable_list
203
203
  if isinstance(n, ast.Attribute):
204
- return n.attr in startable_list
204
+ return n.attr in startable_list and not (
205
+ n.attr in TRIO_STARTABLE_CALLS
206
+ and isinstance(n.value, ast.Name)
207
+ and n.value.id != "trio"
208
+ )
205
209
  if isinstance(n, ast.Call):
206
210
  return any(is_startable(nn, *startable_list) for nn in n.args)
207
211
  return False
@@ -213,12 +217,16 @@ class Visitor113(Flake8AsyncVisitor):
213
217
  ):
214
218
  return False
215
219
  var = ast.unparse(node.value)
216
- return ("trio" in self.library and var.endswith("nursery")) or (
217
- self.variables.get(var, "")
218
- in (
219
- "trio.Nursery",
220
- "anyio.TaskGroup",
221
- "asyncio.TaskGroup",
220
+ return (
221
+ ("trio" in self.library and var.endswith("nursery"))
222
+ or ("anyio" in self.library and var.endswith("task_group"))
223
+ or (
224
+ self.variables.get(var, "")
225
+ in (
226
+ "trio.Nursery",
227
+ "anyio.TaskGroup",
228
+ "asyncio.TaskGroup",
229
+ )
222
230
  )
223
231
  )
224
232
 
@@ -229,6 +237,7 @@ class Visitor113(Flake8AsyncVisitor):
229
237
  and is_startable(
230
238
  node.args[0],
231
239
  *STARTABLE_CALLS,
240
+ *TRIO_STARTABLE_CALLS,
232
241
  *self.options.startable_in_context_manager,
233
242
  )
234
243
  ):
@@ -254,7 +263,11 @@ class Visitor114(Flake8AsyncVisitor):
254
263
  for n in self.walk(*node.args.args, *node.args.kwonlyargs)
255
264
  ) and not any(
256
265
  node.name == opt
257
- for opt in (*self.options.startable_in_context_manager, *STARTABLE_CALLS)
266
+ for opt in (
267
+ *self.options.startable_in_context_manager,
268
+ *STARTABLE_CALLS,
269
+ *TRIO_STARTABLE_CALLS,
270
+ )
258
271
  ):
259
272
  self.error(node, node.name)
260
273
 
@@ -319,8 +332,10 @@ class Visitor116(Flake8AsyncVisitor):
319
332
  @error_class
320
333
  class Visitor119(Flake8AsyncVisitor):
321
334
  error_codes: Mapping[str, str] = {
322
- "ASYNC119": "Yield in contextmanager in async generator might not trigger"
323
- " cleanup. Use `@asynccontextmanager` or refactor."
335
+ "ASYNC119": (
336
+ "Yield in contextmanager in async generator might not trigger"
337
+ " cleanup. Use `@asynccontextmanager` or refactor."
338
+ )
324
339
  }
325
340
 
326
341
  def __init__(self, *args: Any, **kwargs: Any):
@@ -490,8 +505,10 @@ class Visitor300(Flake8AsyncVisitor_cst):
490
505
  @disabled_by_default
491
506
  class Visitor900(Flake8AsyncVisitor):
492
507
  error_codes: Mapping[str, str] = {
493
- "ASYNC900": "Async generator not allowed, unless transformed "
494
- "by a known decorator (one of: {})."
508
+ "ASYNC900": (
509
+ "Async generator not allowed, unless transformed "
510
+ "by a known decorator (one of: {})."
511
+ )
495
512
  }
496
513
 
497
514
  def __init__(self, *args: Any, **kwargs: Any):
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: flake8-async
3
- Version: 24.11.3
3
+ Version: 25.2.2
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,9 +21,18 @@ 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
- License-File: LICENSE
25
24
  Requires-Dist: flake8>=6
26
25
  Requires-Dist: libcst>=1.0.1
26
+ Dynamic: author
27
+ Dynamic: author-email
28
+ Dynamic: classifier
29
+ Dynamic: description
30
+ Dynamic: description-content-type
31
+ Dynamic: license
32
+ Dynamic: project-url
33
+ Dynamic: requires-dist
34
+ Dynamic: requires-python
35
+ Dynamic: summary
27
36
 
28
37
  [![Documentation](https://img.shields.io/badge/docs-read%20now-blue.svg)](https://flake8-async.readthedocs.io)
29
38
  [![Latest PyPi version](https://img.shields.io/pypi/v/flake8-async.svg)](https://pypi.org/project/flake8-async)
@@ -32,9 +32,9 @@ flake8_async/visitors/visitor91x.py
32
32
  flake8_async/visitors/visitor_utility.py
33
33
  flake8_async/visitors/visitors.py
34
34
  tests/__init__.py
35
+ tests/check_changelog_and_version.py
35
36
  tests/conftest.py
36
37
  tests/test_all_visitors_imported.py
37
- tests/test_changelog_and_version.py
38
38
  tests/test_config_and_args.py
39
39
  tests/test_decorator.py
40
40
  tests/test_exception_on_invalid_code.py
@@ -45,12 +45,13 @@ tests/trio_options.py
45
45
  tests/autofix_files/async100.py
46
46
  tests/autofix_files/async100_asyncio.py
47
47
  tests/autofix_files/async100_simple_autofix.py
48
- tests/autofix_files/async100_trio.py
48
+ tests/autofix_files/async124.py
49
49
  tests/autofix_files/async910.py
50
50
  tests/autofix_files/async910_insert_library.py
51
51
  tests/autofix_files/async911.py
52
52
  tests/autofix_files/async911_insert_library.py
53
53
  tests/autofix_files/async913.py
54
+ tests/autofix_files/async913_trio_anyio.py
54
55
  tests/autofix_files/async91x_autofix.py
55
56
  tests/autofix_files/exception_suppress_context_manager.py
56
57
  tests/autofix_files/exception_suppress_context_manager_import_star.py
@@ -61,11 +62,8 @@ tests/eval_files/async100.py
61
62
  tests/eval_files/async100_asyncio.py
62
63
  tests/eval_files/async100_noautofix.py
63
64
  tests/eval_files/async100_simple_autofix.py
64
- tests/eval_files/async100_trio.py
65
65
  tests/eval_files/async101.py
66
- tests/eval_files/async101_anyio.py
67
66
  tests/eval_files/async101_asyncio.py
68
- tests/eval_files/async101_trio.py
69
67
  tests/eval_files/async102.py
70
68
  tests/eval_files/async102_aclose.py
71
69
  tests/eval_files/async102_aclose_args.py
@@ -86,13 +84,10 @@ tests/eval_files/async106.py
86
84
  tests/eval_files/async109.py
87
85
  tests/eval_files/async110.py
88
86
  tests/eval_files/async111.py
89
- tests/eval_files/async111_anyio.py
90
87
  tests/eval_files/async111_asyncio.py
91
88
  tests/eval_files/async112.py
92
- tests/eval_files/async112_anyio.py
93
89
  tests/eval_files/async112_asyncio.py
94
90
  tests/eval_files/async113.py
95
- tests/eval_files/async113_anyio.py
96
91
  tests/eval_files/async113_asyncio.py
97
92
  tests/eval_files/async113_trio.py
98
93
  tests/eval_files/async114.py
@@ -107,6 +102,8 @@ tests/eval_files/async121_asyncio.py
107
102
  tests/eval_files/async122.py
108
103
  tests/eval_files/async123.py
109
104
  tests/eval_files/async123_py311.py
105
+ tests/eval_files/async124.py
106
+ tests/eval_files/async124_no_autofix.py
110
107
  tests/eval_files/async200.py
111
108
  tests/eval_files/async210.py
112
109
  tests/eval_files/async211.py
@@ -131,6 +128,7 @@ tests/eval_files/async911_insert_library.py
131
128
  tests/eval_files/async912.py
132
129
  tests/eval_files/async912_asyncio.py
133
130
  tests/eval_files/async913.py
131
+ tests/eval_files/async913_trio_anyio.py
134
132
  tests/eval_files/async91x_autofix.py
135
133
  tests/eval_files/async91x_noautofix.py
136
134
  tests/eval_files/exception_suppress_context_manager.py