flake8-async 24.11.4__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 (151) hide show
  1. {flake8_async-24.11.4 → flake8_async-25.2.2}/PKG-INFO +12 -3
  2. {flake8_async-24.11.4 → flake8_async-25.2.2}/flake8_async/__init__.py +1 -1
  3. {flake8_async-24.11.4 → flake8_async-25.2.2}/flake8_async/visitors/visitor103_104.py +2 -2
  4. {flake8_async-24.11.4 → flake8_async-25.2.2}/flake8_async/visitors/visitor2xx.py +4 -2
  5. {flake8_async-24.11.4 → flake8_async-25.2.2}/flake8_async/visitors/visitor91x.py +141 -25
  6. {flake8_async-24.11.4 → flake8_async-25.2.2}/flake8_async/visitors/visitors.py +33 -16
  7. {flake8_async-24.11.4 → flake8_async-25.2.2}/flake8_async.egg-info/PKG-INFO +12 -3
  8. {flake8_async-24.11.4 → flake8_async-25.2.2}/flake8_async.egg-info/SOURCES.txt +6 -10
  9. {flake8_async-24.11.4 → flake8_async-25.2.2}/pyproject.toml +3 -2
  10. {flake8_async-24.11.4 → flake8_async-25.2.2}/setup.py +1 -0
  11. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/autofix_files/async100.py +74 -0
  12. flake8_async-25.2.2/tests/autofix_files/async124.py +159 -0
  13. {flake8_async-24.11.4 → 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.4/tests/test_changelog_and_version.py → flake8_async-25.2.2/tests/check_changelog_and_version.py +26 -10
  16. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async100.py +74 -0
  17. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async101.py +11 -4
  18. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async111.py +0 -2
  19. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async112.py +0 -1
  20. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async112_asyncio.py +1 -1
  21. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async113.py +32 -15
  22. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async113_trio.py +5 -9
  23. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async121.py +0 -1
  24. {flake8_async-24.11.4 → 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.4 → flake8_async-25.2.2}/tests/eval_files/async251.py +0 -2
  28. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async910.py +20 -2
  29. {flake8_async-24.11.4 → 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.4 → flake8_async-25.2.2}/tests/test_flake8_async.py +25 -2
  32. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/trio_options.py +8 -1
  33. {flake8_async-24.11.4 → flake8_async-25.2.2}/tox.ini +1 -1
  34. flake8_async-24.11.4/tests/autofix_files/async100_anyio.py +0 -26
  35. flake8_async-24.11.4/tests/autofix_files/async100_trio.py +0 -75
  36. flake8_async-24.11.4/tests/eval_files/async100_anyio.py +0 -26
  37. flake8_async-24.11.4/tests/eval_files/async100_trio.py +0 -75
  38. flake8_async-24.11.4/tests/eval_files/async101_anyio.py +0 -10
  39. flake8_async-24.11.4/tests/eval_files/async101_trio.py +0 -17
  40. flake8_async-24.11.4/tests/eval_files/async111_anyio.py +0 -21
  41. flake8_async-24.11.4/tests/eval_files/async112_anyio.py +0 -28
  42. flake8_async-24.11.4/tests/eval_files/async113_anyio.py +0 -22
  43. {flake8_async-24.11.4 → flake8_async-25.2.2}/CHANGELOG.md +0 -0
  44. {flake8_async-24.11.4 → flake8_async-25.2.2}/CONTRIBUTING.md +0 -0
  45. {flake8_async-24.11.4 → flake8_async-25.2.2}/LICENSE +0 -0
  46. {flake8_async-24.11.4 → flake8_async-25.2.2}/MANIFEST.in +0 -0
  47. {flake8_async-24.11.4 → flake8_async-25.2.2}/README.md +0 -0
  48. {flake8_async-24.11.4 → flake8_async-25.2.2}/flake8_async/__main__.py +0 -0
  49. {flake8_async-24.11.4 → flake8_async-25.2.2}/flake8_async/base.py +0 -0
  50. {flake8_async-24.11.4 → flake8_async-25.2.2}/flake8_async/runner.py +0 -0
  51. {flake8_async-24.11.4 → flake8_async-25.2.2}/flake8_async/visitors/__init__.py +0 -0
  52. {flake8_async-24.11.4 → flake8_async-25.2.2}/flake8_async/visitors/flake8asyncvisitor.py +0 -0
  53. {flake8_async-24.11.4 → flake8_async-25.2.2}/flake8_async/visitors/helpers.py +0 -0
  54. {flake8_async-24.11.4 → flake8_async-25.2.2}/flake8_async/visitors/visitor101.py +0 -0
  55. {flake8_async-24.11.4 → flake8_async-25.2.2}/flake8_async/visitors/visitor102_120.py +0 -0
  56. {flake8_async-24.11.4 → flake8_async-25.2.2}/flake8_async/visitors/visitor105.py +0 -0
  57. {flake8_async-24.11.4 → flake8_async-25.2.2}/flake8_async/visitors/visitor111.py +0 -0
  58. {flake8_async-24.11.4 → flake8_async-25.2.2}/flake8_async/visitors/visitor118.py +0 -0
  59. {flake8_async-24.11.4 → flake8_async-25.2.2}/flake8_async/visitors/visitor123.py +0 -0
  60. {flake8_async-24.11.4 → flake8_async-25.2.2}/flake8_async/visitors/visitor_utility.py +0 -0
  61. {flake8_async-24.11.4 → flake8_async-25.2.2}/flake8_async.egg-info/dependency_links.txt +0 -0
  62. {flake8_async-24.11.4 → flake8_async-25.2.2}/flake8_async.egg-info/entry_points.txt +0 -0
  63. {flake8_async-24.11.4 → flake8_async-25.2.2}/flake8_async.egg-info/not-zip-safe +0 -0
  64. {flake8_async-24.11.4 → flake8_async-25.2.2}/flake8_async.egg-info/requires.txt +0 -0
  65. {flake8_async-24.11.4 → flake8_async-25.2.2}/flake8_async.egg-info/top_level.txt +0 -0
  66. {flake8_async-24.11.4 → flake8_async-25.2.2}/setup.cfg +0 -0
  67. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/__init__.py +0 -0
  68. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/autofix_files/async100_asyncio.py +0 -0
  69. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/autofix_files/async100_simple_autofix.py +0 -0
  70. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/autofix_files/async910_insert_library.py +0 -0
  71. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/autofix_files/async911.py +0 -0
  72. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/autofix_files/async911_insert_library.py +0 -0
  73. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/autofix_files/async913.py +0 -0
  74. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/autofix_files/async91x_autofix.py +0 -0
  75. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/autofix_files/exception_suppress_context_manager.py +0 -0
  76. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/autofix_files/exception_suppress_context_manager_import_star.py +0 -0
  77. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/autofix_files/noqa.py +0 -0
  78. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/autofix_files/noqa_testing.py +0 -0
  79. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/conftest.py +0 -0
  80. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/anyio_trio.py +0 -0
  81. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async100_asyncio.py +0 -0
  82. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async100_noautofix.py +0 -0
  83. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async100_simple_autofix.py +0 -0
  84. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async101_asyncio.py +0 -0
  85. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async102.py +0 -0
  86. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async102_aclose.py +0 -0
  87. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async102_aclose_args.py +0 -0
  88. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async102_anyio.py +0 -0
  89. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async102_asyncio.py +0 -0
  90. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async102_trio.py +0 -0
  91. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async103.py +0 -0
  92. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async103_all_imported.py +0 -0
  93. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async103_both_imported.py +0 -0
  94. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async103_no_104.py +0 -0
  95. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async103_trio.py +0 -0
  96. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async104.py +0 -0
  97. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async104_anyio.py +0 -0
  98. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async104_trio.py +0 -0
  99. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async105.py +0 -0
  100. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async105_anyio.py +0 -0
  101. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async106.py +0 -0
  102. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async109.py +0 -0
  103. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async110.py +0 -0
  104. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async111_asyncio.py +0 -0
  105. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async113_asyncio.py +0 -0
  106. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async114.py +0 -0
  107. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async115.py +0 -0
  108. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async116.py +0 -0
  109. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async118.py +0 -0
  110. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async119.py +0 -0
  111. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async120.py +0 -0
  112. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async121_asyncio.py +0 -0
  113. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async122.py +0 -0
  114. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async123.py +0 -0
  115. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async123_py311.py +0 -0
  116. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async200.py +0 -0
  117. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async210.py +0 -0
  118. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async211.py +0 -0
  119. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async212.py +0 -0
  120. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async22x.py +0 -0
  121. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async22x_asyncio.py +0 -0
  122. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async232.py +0 -0
  123. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async232_asyncio.py +0 -0
  124. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async23x.py +0 -0
  125. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async23x_asyncio.py +0 -0
  126. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async240.py +0 -0
  127. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async250.py +0 -0
  128. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async250_multi_library.py +0 -0
  129. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async251_multi_library.py +0 -0
  130. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async300.py +0 -0
  131. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async900.py +0 -0
  132. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async910_insert_library.py +0 -0
  133. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async911.py +0 -0
  134. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async911_insert_library.py +0 -0
  135. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async912_asyncio.py +0 -0
  136. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async913.py +0 -0
  137. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async91x_autofix.py +0 -0
  138. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/async91x_noautofix.py +0 -0
  139. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/exception_suppress_context_manager.py +0 -0
  140. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/exception_suppress_context_manager_import_star.py +0 -0
  141. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/no_library.py +0 -0
  142. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/noqa.py +0 -0
  143. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/noqa_no_autofix.py +0 -0
  144. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/noqa_testing.py +0 -0
  145. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/eval_files/trio_anyio.py +0 -0
  146. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/test_all_visitors_imported.py +0 -0
  147. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/test_config_and_args.py +0 -0
  148. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/test_decorator.py +0 -0
  149. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/test_exception_on_invalid_code.py +0 -0
  150. {flake8_async-24.11.4 → flake8_async-25.2.2}/tests/test_formatting.py +0 -0
  151. {flake8_async-24.11.4 → 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.4
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.4"
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
@@ -64,6 +70,92 @@ def func_empty_body(node: cst.FunctionDef) -> bool:
64
70
  )
65
71
 
66
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
+
67
159
  @dataclass
68
160
  class LoopState:
69
161
  infinite_loop: bool = False
@@ -262,10 +354,10 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
262
354
  "on line {1.lineno}."
263
355
  ),
264
356
  "ASYNC912": (
265
- "CancelScope with no guaranteed checkpoint. This makes it potentially "
357
+ "CancelScope with no guaranteed cancel point. This makes it potentially "
266
358
  "impossible to cancel."
267
359
  ),
268
- "ASYNC913": ("Indefinite loop with no guaranteed checkpoint."),
360
+ "ASYNC913": "Indefinite loop with no guaranteed cancel points.",
269
361
  "ASYNC100": (
270
362
  "{0}.{1} context contains no checkpoints, remove the context or add"
271
363
  " `await {0}.lowlevel.checkpoint()`."
@@ -275,7 +367,6 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
275
367
  def __init__(self, *args: Any, **kwargs: Any):
276
368
  super().__init__(*args, **kwargs)
277
369
  self.has_yield = False
278
- self.safe_decorator = False
279
370
  self.async_function = False
280
371
  self.uncheckpointed_statements: set[Statement] = set()
281
372
  self.comp_unknown = False
@@ -291,6 +382,9 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
291
382
  # --exception-suppress-context-manager
292
383
  self.suppress_imported_as: list[str] = []
293
384
 
385
+ # used to transfer new body between visit_FunctionDef and leave_FunctionDef
386
+ self.new_body: cst.BaseSuite | None = None
387
+
294
388
  def should_autofix(self, node: cst.CSTNode, code: str | None = None) -> bool:
295
389
  if code is None: # pragma: no branch
296
390
  code = "ASYNC911" if self.has_yield else "ASYNC910"
@@ -307,10 +401,16 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
307
401
  self.taskgroup_has_start_soon.clear()
308
402
 
309
403
  def checkpoint_schedule_point(self) -> None:
310
- self.uncheckpointed_statements = set()
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
+ }
311
411
 
312
412
  def checkpoint(self) -> None:
313
- self.checkpoint_schedule_point()
413
+ self.uncheckpointed_statements = set()
314
414
  self.checkpoint_cancel_point()
315
415
 
316
416
  def checkpoint_statement(self) -> cst.SimpleStatementLine:
@@ -349,6 +449,10 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
349
449
  return
350
450
 
351
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
+
352
456
  # don't lint functions whose bodies solely consist of pass or ellipsis
353
457
  # @overload functions are also guaranteed to be empty
354
458
  # we also ignore pytest fixtures
@@ -358,7 +462,6 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
358
462
  self.save_state(
359
463
  node,
360
464
  "has_yield",
361
- "safe_decorator",
362
465
  "async_function",
363
466
  "uncheckpointed_statements",
364
467
  # comp_unknown does not need to be saved
@@ -370,11 +473,11 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
370
473
  "suppress_imported_as", # a copy is saved, but state is not reset
371
474
  copy=True,
372
475
  )
373
- self.has_yield = self.safe_decorator = False
374
476
  self.uncheckpointed_statements = set()
477
+ self.has_checkpoint_stack = []
478
+ self.has_yield = False
375
479
  self.loop_state = LoopState()
376
480
  # try_state is reset upon entering try
377
- self.has_checkpoint_stack = []
378
481
  self.taskgroup_has_start_soon = {}
379
482
 
380
483
  self.async_function = (
@@ -383,36 +486,49 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
383
486
  node.decorators, *self.options.no_checkpoint_warning_decorators
384
487
  )
385
488
  )
386
- if not self.async_function:
387
- # only visit subnodes if there is an async function defined inside
388
- # this should improve performance on codebases with many sync functions
389
- 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
390
495
 
391
496
  pos = self.get_metadata(PositionProvider, node).start # type: ignore
392
497
  self.uncheckpointed_statements = {
393
498
  Statement("function definition", pos.line, pos.column) # type: ignore
394
499
  }
395
- 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
396
508
 
397
509
  def leave_FunctionDef(
398
510
  self, original_node: cst.FunctionDef, updated_node: cst.FunctionDef
399
511
  ) -> cst.FunctionDef:
400
512
  if (
401
- self.async_function
513
+ self.new_body is not None
514
+ and self.async_function
402
515
  # updated_node does not have a position, so we must send original_node
403
516
  and self.check_function_exit(original_node)
404
517
  and self.should_autofix(original_node)
405
- and isinstance(updated_node.body, cst.IndentedBlock)
518
+ and isinstance(self.new_body, cst.IndentedBlock)
406
519
  ):
407
520
  # insert checkpoint at the end of body
408
- new_body = list(updated_node.body.body)
409
- new_body.append(self.checkpoint_statement())
410
- indentedblock = updated_node.body.with_changes(body=new_body)
411
- 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)
412
524
 
413
525
  self.ensure_imported_library()
414
526
 
527
+ if self.new_body is not None:
528
+ updated_node = updated_node.with_changes(body=self.new_body)
415
529
  self.restore_state(original_node)
530
+ # reset self.new_body
531
+ self.new_body = None
416
532
  return updated_node
417
533
 
418
534
  # error if function exit/return/yields with uncheckpointed statements
@@ -1039,8 +1155,8 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
1039
1155
  return False
1040
1156
 
1041
1157
  # The generator target will be immediately evaluated, but the other
1042
- # elements will be lazily evaluated as the generator is consumed so we don't
1043
- # 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
1044
1160
  def visit_GeneratorExp(self, node: cst.GeneratorExp):
1045
1161
  node.for_in.iter.visit(self)
1046
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.4
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
@@ -43,15 +43,15 @@ tests/test_formatting.py
43
43
  tests/test_messages_documented.py
44
44
  tests/trio_options.py
45
45
  tests/autofix_files/async100.py
46
- tests/autofix_files/async100_anyio.py
47
46
  tests/autofix_files/async100_asyncio.py
48
47
  tests/autofix_files/async100_simple_autofix.py
49
- tests/autofix_files/async100_trio.py
48
+ tests/autofix_files/async124.py
50
49
  tests/autofix_files/async910.py
51
50
  tests/autofix_files/async910_insert_library.py
52
51
  tests/autofix_files/async911.py
53
52
  tests/autofix_files/async911_insert_library.py
54
53
  tests/autofix_files/async913.py
54
+ tests/autofix_files/async913_trio_anyio.py
55
55
  tests/autofix_files/async91x_autofix.py
56
56
  tests/autofix_files/exception_suppress_context_manager.py
57
57
  tests/autofix_files/exception_suppress_context_manager_import_star.py
@@ -59,15 +59,11 @@ tests/autofix_files/noqa.py
59
59
  tests/autofix_files/noqa_testing.py
60
60
  tests/eval_files/anyio_trio.py
61
61
  tests/eval_files/async100.py
62
- tests/eval_files/async100_anyio.py
63
62
  tests/eval_files/async100_asyncio.py
64
63
  tests/eval_files/async100_noautofix.py
65
64
  tests/eval_files/async100_simple_autofix.py
66
- tests/eval_files/async100_trio.py
67
65
  tests/eval_files/async101.py
68
- tests/eval_files/async101_anyio.py
69
66
  tests/eval_files/async101_asyncio.py
70
- tests/eval_files/async101_trio.py
71
67
  tests/eval_files/async102.py
72
68
  tests/eval_files/async102_aclose.py
73
69
  tests/eval_files/async102_aclose_args.py
@@ -88,13 +84,10 @@ tests/eval_files/async106.py
88
84
  tests/eval_files/async109.py
89
85
  tests/eval_files/async110.py
90
86
  tests/eval_files/async111.py
91
- tests/eval_files/async111_anyio.py
92
87
  tests/eval_files/async111_asyncio.py
93
88
  tests/eval_files/async112.py
94
- tests/eval_files/async112_anyio.py
95
89
  tests/eval_files/async112_asyncio.py
96
90
  tests/eval_files/async113.py
97
- tests/eval_files/async113_anyio.py
98
91
  tests/eval_files/async113_asyncio.py
99
92
  tests/eval_files/async113_trio.py
100
93
  tests/eval_files/async114.py
@@ -109,6 +102,8 @@ tests/eval_files/async121_asyncio.py
109
102
  tests/eval_files/async122.py
110
103
  tests/eval_files/async123.py
111
104
  tests/eval_files/async123_py311.py
105
+ tests/eval_files/async124.py
106
+ tests/eval_files/async124_no_autofix.py
112
107
  tests/eval_files/async200.py
113
108
  tests/eval_files/async210.py
114
109
  tests/eval_files/async211.py
@@ -133,6 +128,7 @@ tests/eval_files/async911_insert_library.py
133
128
  tests/eval_files/async912.py
134
129
  tests/eval_files/async912_asyncio.py
135
130
  tests/eval_files/async913.py
131
+ tests/eval_files/async913_trio_anyio.py
136
132
  tests/eval_files/async91x_autofix.py
137
133
  tests/eval_files/async91x_noautofix.py
138
134
  tests/eval_files/exception_suppress_context_manager.py
@@ -17,7 +17,8 @@ ignore-words-list = 'spawnve'
17
17
  [tool.isort]
18
18
  only_modified = true
19
19
  profile = "black"
20
- py_version = "311"
20
+ # isort is slow to release official support for new python versions
21
+ py_version = "312"
21
22
  quiet = true
22
23
  skip_gitignore = true
23
24
  skip_glob = "tests/*_files/*"
@@ -32,7 +33,7 @@ warn_unused_ignores = false
32
33
 
33
34
  [tool.pyright]
34
35
  exclude = ["**/node_modules", "**/__pycache__", "**/.*", "tests/eval_files/*", "tests/autofix_files/*"] # TODO: fix errors in eval/autofix files
35
- pythonVersion = "3.12"
36
+ pythonVersion = "3.13"
36
37
  reportCallInDefaultInitializer = true
37
38
  reportImplicitStringConcatenation = false # black generates implicit string concats
38
39
  reportMissingSuperCall = true
@@ -31,6 +31,7 @@ setup(
31
31
  "Changelog": "https://flake8-async.readthedocs.io/en/latest/changelog.html",
32
32
  },
33
33
  license="MIT",
34
+ license_files=[], # https://github.com/pypa/twine/issues/1216
34
35
  description="A highly opinionated flake8 plugin for Trio-related problems.",
35
36
  zip_safe=False,
36
37
  install_requires=["flake8>=6", "libcst>=1.0.1"],
@@ -140,3 +140,77 @@ async def dont_crash_on_non_name_or_attr_call():
140
140
  async def another_weird_with_call():
141
141
  async with a().b():
142
142
  ...
143
+
144
+
145
+ # ---open_nursery / create_task_group stuff---
146
+
147
+
148
+ async def nursery_no_cancel_point():
149
+ # error: 9, "trio", "CancelScope"
150
+ async with trio.open_nursery():
151
+ ...
152
+
153
+
154
+ # but it is a cancel point if the nursery contains a call to start_soon()
155
+
156
+
157
+ async def nursery_start_soon():
158
+ with trio.CancelScope():
159
+ async with trio.open_nursery() as n:
160
+ n.start_soon(trio.sleep, 0)
161
+
162
+
163
+ async def nursery_start_soon_misnested():
164
+ async with trio.open_nursery() as n:
165
+ # error: 13, "trio", "CancelScope"
166
+ n.start_soon(trio.sleep, 0)
167
+
168
+
169
+ async def nested_scope():
170
+ with trio.CancelScope():
171
+ with trio.CancelScope():
172
+ async with trio.open_nursery() as n:
173
+ n.start_soon(trio.sleep, 0)
174
+
175
+
176
+ async def nested_nursery():
177
+ with trio.CancelScope():
178
+ async with trio.open_nursery() as n:
179
+ async with trio.open_nursery() as n2:
180
+ n2.start_soon(trio.sleep, 0)
181
+
182
+
183
+ async def nested_function_call():
184
+
185
+ # error: 9, "trio", "CancelScope"
186
+ async with trio.open_nursery() as n:
187
+
188
+ def foo():
189
+ n.start_soon(trio.sleep, 0)
190
+
191
+ # a false alarm in case we call foo()... but we can't check if they do
192
+ foo()
193
+
194
+
195
+ # insert cancel point on nursery exit, not at the start_soon call
196
+ async def cancel_point_on_nursery_exit():
197
+ with trio.CancelScope():
198
+ async with trio.open_nursery() as n:
199
+ # error: 17, "trio", "CancelScope"
200
+ n.start_soon(trio.sleep, 0)
201
+
202
+
203
+ # async100 does not consider *redundant* cancel scopes
204
+ async def redundant_cancel_scope():
205
+ with trio.CancelScope():
206
+ with trio.CancelScope():
207
+ await trio.lowlevel.checkpoint()
208
+
209
+
210
+ # but if it did then none of these scopes should be marked redundant
211
+ # The inner checks task startup, the outer checks task exit
212
+ async def nursery_exit_blocks_with_start():
213
+ with trio.CancelScope():
214
+ async with trio.open_nursery() as n:
215
+ with trio.CancelScope():
216
+ await n.start(trio.sleep, 0)