flake8-async 25.4.2__tar.gz → 25.4.4__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 (154) hide show
  1. {flake8_async-25.4.2 → flake8_async-25.4.4}/PKG-INFO +1 -1
  2. {flake8_async-25.4.2 → flake8_async-25.4.4}/flake8_async/__init__.py +1 -1
  3. {flake8_async-25.4.2 → flake8_async-25.4.4}/flake8_async/runner.py +5 -5
  4. {flake8_async-25.4.2 → flake8_async-25.4.4}/flake8_async/visitors/helpers.py +28 -13
  5. {flake8_async-25.4.2 → flake8_async-25.4.4}/flake8_async/visitors/visitor91x.py +138 -63
  6. {flake8_async-25.4.2 → flake8_async-25.4.4}/flake8_async/visitors/visitors.py +8 -5
  7. {flake8_async-25.4.2 → flake8_async-25.4.4}/flake8_async.egg-info/PKG-INFO +1 -1
  8. {flake8_async-25.4.2 → flake8_async-25.4.4}/flake8_async.egg-info/SOURCES.txt +1 -1
  9. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/autofix_files/async100.py +32 -0
  10. flake8_async-25.4.4/tests/autofix_files/async100_asyncio.py +31 -0
  11. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async100.py +38 -0
  12. flake8_async-25.4.4/tests/eval_files/async100_asyncio.py +32 -0
  13. flake8_async-25.4.4/tests/eval_files/async100_asyncio_noautofix.py +21 -0
  14. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async900.py +6 -3
  15. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async912_asyncio.py +6 -17
  16. {flake8_async-25.4.2 → flake8_async-25.4.4}/tox.ini +3 -3
  17. flake8_async-25.4.2/tests/autofix_files/async100_asyncio.py +0 -22
  18. flake8_async-25.4.2/tests/eval_files/async100_asyncio.py +0 -22
  19. flake8_async-25.4.2/tests/eval_files/async100_noautofix.py +0 -25
  20. {flake8_async-25.4.2 → flake8_async-25.4.4}/CHANGELOG.md +0 -0
  21. {flake8_async-25.4.2 → flake8_async-25.4.4}/CONTRIBUTING.md +0 -0
  22. {flake8_async-25.4.2 → flake8_async-25.4.4}/LICENSE +0 -0
  23. {flake8_async-25.4.2 → flake8_async-25.4.4}/MANIFEST.in +0 -0
  24. {flake8_async-25.4.2 → flake8_async-25.4.4}/README.md +0 -0
  25. {flake8_async-25.4.2 → flake8_async-25.4.4}/flake8_async/__main__.py +0 -0
  26. {flake8_async-25.4.2 → flake8_async-25.4.4}/flake8_async/base.py +0 -0
  27. {flake8_async-25.4.2 → flake8_async-25.4.4}/flake8_async/visitors/__init__.py +0 -0
  28. {flake8_async-25.4.2 → flake8_async-25.4.4}/flake8_async/visitors/flake8asyncvisitor.py +0 -0
  29. {flake8_async-25.4.2 → flake8_async-25.4.4}/flake8_async/visitors/visitor101.py +0 -0
  30. {flake8_async-25.4.2 → flake8_async-25.4.4}/flake8_async/visitors/visitor102_120.py +0 -0
  31. {flake8_async-25.4.2 → flake8_async-25.4.4}/flake8_async/visitors/visitor103_104.py +0 -0
  32. {flake8_async-25.4.2 → flake8_async-25.4.4}/flake8_async/visitors/visitor105.py +0 -0
  33. {flake8_async-25.4.2 → flake8_async-25.4.4}/flake8_async/visitors/visitor111.py +0 -0
  34. {flake8_async-25.4.2 → flake8_async-25.4.4}/flake8_async/visitors/visitor118.py +0 -0
  35. {flake8_async-25.4.2 → flake8_async-25.4.4}/flake8_async/visitors/visitor123.py +0 -0
  36. {flake8_async-25.4.2 → flake8_async-25.4.4}/flake8_async/visitors/visitor2xx.py +0 -0
  37. {flake8_async-25.4.2 → flake8_async-25.4.4}/flake8_async/visitors/visitor_utility.py +0 -0
  38. {flake8_async-25.4.2 → flake8_async-25.4.4}/flake8_async.egg-info/dependency_links.txt +0 -0
  39. {flake8_async-25.4.2 → flake8_async-25.4.4}/flake8_async.egg-info/entry_points.txt +0 -0
  40. {flake8_async-25.4.2 → flake8_async-25.4.4}/flake8_async.egg-info/not-zip-safe +0 -0
  41. {flake8_async-25.4.2 → flake8_async-25.4.4}/flake8_async.egg-info/requires.txt +0 -0
  42. {flake8_async-25.4.2 → flake8_async-25.4.4}/flake8_async.egg-info/top_level.txt +0 -0
  43. {flake8_async-25.4.2 → flake8_async-25.4.4}/pyproject.toml +0 -0
  44. {flake8_async-25.4.2 → flake8_async-25.4.4}/setup.cfg +0 -0
  45. {flake8_async-25.4.2 → flake8_async-25.4.4}/setup.py +0 -0
  46. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/__init__.py +0 -0
  47. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/autofix_files/async100_simple_autofix.py +0 -0
  48. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/autofix_files/async124.py +0 -0
  49. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/autofix_files/async910.py +0 -0
  50. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/autofix_files/async910_insert_library.py +0 -0
  51. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/autofix_files/async911.py +0 -0
  52. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/autofix_files/async911_insert_library.py +0 -0
  53. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/autofix_files/async913.py +0 -0
  54. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/autofix_files/async913_trio_anyio.py +0 -0
  55. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/autofix_files/async91x_autofix.py +0 -0
  56. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/autofix_files/async91x_py310.py +0 -0
  57. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/autofix_files/async91x_py311.py +0 -0
  58. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/autofix_files/exception_suppress_context_manager.py +0 -0
  59. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/autofix_files/exception_suppress_context_manager_import_star.py +0 -0
  60. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/autofix_files/noqa.py +0 -0
  61. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/autofix_files/noqa_testing.py +0 -0
  62. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/check_changelog_and_version.py +0 -0
  63. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/conftest.py +0 -0
  64. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/anyio_trio.py +0 -0
  65. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async100_simple_autofix.py +0 -0
  66. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async101.py +0 -0
  67. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async101_asyncio.py +0 -0
  68. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async102.py +0 -0
  69. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async102_120_py311.py +0 -0
  70. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async102_aclose.py +0 -0
  71. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async102_aclose_args.py +0 -0
  72. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async102_anyio.py +0 -0
  73. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async102_asyncio.py +0 -0
  74. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async102_trio.py +0 -0
  75. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async103.py +0 -0
  76. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async103_104_py310.py +0 -0
  77. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async103_104_py311.py +0 -0
  78. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async103_all_imported.py +0 -0
  79. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async103_both_imported.py +0 -0
  80. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async103_no_104.py +0 -0
  81. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async103_trio.py +0 -0
  82. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async104.py +0 -0
  83. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async104_anyio.py +0 -0
  84. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async104_trio.py +0 -0
  85. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async105.py +0 -0
  86. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async105_anyio.py +0 -0
  87. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async106.py +0 -0
  88. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async109.py +0 -0
  89. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async110.py +0 -0
  90. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async111.py +0 -0
  91. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async111_asyncio.py +0 -0
  92. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async112.py +0 -0
  93. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async112_asyncio.py +0 -0
  94. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async113.py +0 -0
  95. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async113_asyncio.py +0 -0
  96. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async113_trio.py +0 -0
  97. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async114.py +0 -0
  98. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async115.py +0 -0
  99. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async116.py +0 -0
  100. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async118.py +0 -0
  101. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async119.py +0 -0
  102. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async120.py +0 -0
  103. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async121.py +0 -0
  104. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async121_anyio.py +0 -0
  105. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async121_asyncio.py +0 -0
  106. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async122.py +0 -0
  107. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async123.py +0 -0
  108. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async123_py311.py +0 -0
  109. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async124.py +0 -0
  110. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async124_no_autofix.py +0 -0
  111. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async125.py +0 -0
  112. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async200.py +0 -0
  113. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async210.py +0 -0
  114. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async211.py +0 -0
  115. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async212.py +0 -0
  116. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async22x.py +0 -0
  117. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async22x_asyncio.py +0 -0
  118. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async232.py +0 -0
  119. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async232_asyncio.py +0 -0
  120. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async23x.py +0 -0
  121. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async23x_asyncio.py +0 -0
  122. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async240.py +0 -0
  123. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async250.py +0 -0
  124. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async250_multi_library.py +0 -0
  125. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async251.py +0 -0
  126. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async251_multi_library.py +0 -0
  127. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async300.py +0 -0
  128. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async910.py +0 -0
  129. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async910_insert_library.py +0 -0
  130. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async911.py +0 -0
  131. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async911_insert_library.py +0 -0
  132. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async912.py +0 -0
  133. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async912_py311.py +0 -0
  134. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async913.py +0 -0
  135. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async913_trio_anyio.py +0 -0
  136. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async91x_autofix.py +0 -0
  137. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async91x_noautofix.py +0 -0
  138. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async91x_py310.py +0 -0
  139. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/async91x_py311.py +0 -0
  140. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/exception_suppress_context_manager.py +0 -0
  141. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/exception_suppress_context_manager_import_star.py +0 -0
  142. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/no_library.py +0 -0
  143. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/noqa.py +0 -0
  144. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/noqa_no_autofix.py +0 -0
  145. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/noqa_testing.py +0 -0
  146. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/eval_files/trio_anyio.py +0 -0
  147. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/test_all_visitors_imported.py +0 -0
  148. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/test_config_and_args.py +0 -0
  149. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/test_decorator.py +0 -0
  150. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/test_exception_on_invalid_code.py +0 -0
  151. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/test_flake8_async.py +0 -0
  152. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/test_formatting.py +0 -0
  153. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/test_messages_documented.py +0 -0
  154. {flake8_async-25.4.2 → flake8_async-25.4.4}/tests/trio_options.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flake8-async
3
- Version: 25.4.2
3
+ Version: 25.4.4
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
@@ -38,7 +38,7 @@ if TYPE_CHECKING:
38
38
 
39
39
 
40
40
  # CalVer: YY.month.patch, e.g. first release of July 2022 == "22.7.1"
41
- __version__ = "25.4.2"
41
+ __version__ = "25.4.4"
42
42
 
43
43
 
44
44
  # taken from https://github.com/Zac-HD/shed
@@ -12,6 +12,7 @@ from typing import TYPE_CHECKING
12
12
 
13
13
  import libcst as cst
14
14
 
15
+ from .base import Error, Options
15
16
  from .visitors import (
16
17
  ERROR_CLASSES,
17
18
  ERROR_CLASSES_CST,
@@ -25,18 +26,17 @@ if TYPE_CHECKING:
25
26
 
26
27
  from libcst import Module
27
28
 
28
- from .base import Error, Options
29
29
  from .visitors.flake8asyncvisitor import Flake8AsyncVisitor, Flake8AsyncVisitor_cst
30
30
 
31
31
 
32
32
  @dataclass
33
33
  class SharedState:
34
34
  options: Options
35
- problems: list[Error] = field(default_factory=list)
36
- noqas: dict[int, set[str]] = field(default_factory=dict)
35
+ problems: list[Error] = field(default_factory=list[Error])
36
+ noqas: dict[int, set[str]] = field(default_factory=dict[int, set[str]])
37
37
  library: tuple[str, ...] = ()
38
- typed_calls: dict[str, str] = field(default_factory=dict)
39
- variables: dict[str, str] = field(default_factory=dict)
38
+ typed_calls: dict[str, str] = field(default_factory=dict[str, str])
39
+ variables: dict[str, str] = field(default_factory=dict[str, str])
40
40
 
41
41
 
42
42
  class __CommonRunner:
@@ -8,7 +8,7 @@ from __future__ import annotations
8
8
  import ast
9
9
  from dataclasses import dataclass
10
10
  from fnmatch import fnmatch
11
- from typing import TYPE_CHECKING, NamedTuple, TypeVar, Union
11
+ from typing import TYPE_CHECKING, Generic, TypeVar, Union
12
12
 
13
13
  import libcst as cst
14
14
  import libcst.matchers as m
@@ -38,6 +38,8 @@ if TYPE_CHECKING:
38
38
  "T_EITHER", bound=Union[Flake8AsyncVisitor, Flake8AsyncVisitor_cst]
39
39
  )
40
40
 
41
+ T_Call = TypeVar("T_Call", bound=Union[cst.Call, ast.Call])
42
+
41
43
 
42
44
  def error_class(error_class: type[T]) -> type[T]:
43
45
  assert error_class.error_codes
@@ -289,8 +291,8 @@ cancel_scope_names = (
289
291
 
290
292
 
291
293
  @dataclass
292
- class MatchingCall:
293
- node: ast.Call
294
+ class MatchingCall(Generic[T_Call]):
295
+ node: T_Call
294
296
  name: str
295
297
  base: str
296
298
 
@@ -301,7 +303,7 @@ class MatchingCall:
301
303
  # convenience function used in a lot of visitors
302
304
  def get_matching_call(
303
305
  node: ast.AST, *names: str, base: Iterable[str] = ("trio", "anyio")
304
- ) -> MatchingCall | None:
306
+ ) -> MatchingCall[ast.Call] | None:
305
307
  if isinstance(base, str):
306
308
  base = (base,)
307
309
  if (
@@ -316,6 +318,23 @@ def get_matching_call(
316
318
 
317
319
 
318
320
  # ___ CST helpers ___
321
+ def get_matching_call_cst(
322
+ node: cst.CSTNode, *names: str, base: Iterable[str] = ("trio", "anyio")
323
+ ) -> MatchingCall[cst.Call] | None:
324
+ if isinstance(base, str):
325
+ base = (base,)
326
+ if (
327
+ isinstance(node, cst.Call)
328
+ and isinstance(node.func, cst.Attribute)
329
+ and node.func.attr.value in names
330
+ and isinstance(node.func.value, (cst.Name, cst.Attribute))
331
+ ):
332
+ attr_base = identifier_to_string(node.func.value)
333
+ if attr_base is not None and attr_base in base:
334
+ return MatchingCall(node, node.func.attr.value, attr_base)
335
+ return None
336
+
337
+
319
338
  def oneof_names(*names: str):
320
339
  return m.OneOf(*map(m.Name, names))
321
340
 
@@ -329,12 +348,6 @@ def list_contains(
329
348
  yield from (item for item in seq if m.matches(item, matcher))
330
349
 
331
350
 
332
- class AttributeCall(NamedTuple):
333
- node: cst.Call
334
- base: str
335
- function: str
336
-
337
-
338
351
  # the custom __or__ in libcst breaks pyright type checking. It's possible to use
339
352
  # `Union` as a workaround ... except pyupgrade will automatically replace that.
340
353
  # So we have to resort to specifying one of the base classes.
@@ -365,7 +378,7 @@ def identifier_to_string(node: cst.CSTNode) -> str | None:
365
378
 
366
379
  def with_has_call(
367
380
  node: cst.With, *names: str, base: Iterable[str] | str = ("trio", "anyio")
368
- ) -> list[AttributeCall]:
381
+ ) -> list[MatchingCall[cst.Call]]:
369
382
  """Check if a with statement has a matching call, returning a list with matches.
370
383
 
371
384
  `names` specify the names of functions to match, `base` specifies the
@@ -396,7 +409,7 @@ def with_has_call(
396
409
  )
397
410
  )
398
411
 
399
- res_list: list[AttributeCall] = []
412
+ res_list: list[MatchingCall[cst.Call]] = []
400
413
  for item in node.items:
401
414
  if res := m.extract(item.item, matcher):
402
415
  assert isinstance(item.item, cst.Call)
@@ -405,7 +418,9 @@ def with_has_call(
405
418
  base_string = identifier_to_string(res["base"])
406
419
  assert base_string is not None, "subscripts should never get matched"
407
420
  res_list.append(
408
- AttributeCall(item.item, base_string, res["function"].value)
421
+ MatchingCall(
422
+ node=item.item, base=base_string, name=res["function"].value
423
+ )
409
424
  )
410
425
  return res_list
411
426
 
@@ -19,21 +19,21 @@ from typing import TYPE_CHECKING, Any, cast
19
19
 
20
20
  import libcst as cst
21
21
  import libcst.matchers as m
22
- from libcst.metadata import PositionProvider
22
+ from libcst.metadata import CodeRange, PositionProvider
23
23
 
24
24
  from ..base import Statement
25
25
  from .flake8asyncvisitor import Flake8AsyncVisitor_cst
26
26
  from .helpers import (
27
- AttributeCall,
27
+ MatchingCall,
28
28
  cancel_scope_names,
29
29
  disable_codes_by_default,
30
30
  error_class_cst,
31
31
  flatten_preserving_comments,
32
32
  fnmatch_qualified_name_cst,
33
33
  func_has_decorator,
34
+ get_matching_call_cst,
34
35
  identifier_to_string,
35
36
  iter_guaranteed_once_cst,
36
- with_has_call,
37
37
  )
38
38
 
39
39
  if TYPE_CHECKING:
@@ -162,13 +162,21 @@ class LoopState:
162
162
  body_guaranteed_once: bool = False
163
163
  has_break: bool = False
164
164
 
165
- uncheckpointed_before_continue: set[Statement] = field(default_factory=set)
166
- uncheckpointed_before_break: set[Statement] = field(default_factory=set)
167
-
168
- artificial_errors: set[cst.Return | cst.Yield] = field(default_factory=set)
169
- nodes_needing_checkpoints: list[cst.Return | cst.Yield | ArtificialStatement] = (
170
- field(default_factory=list)
165
+ uncheckpointed_before_continue: set[Statement] = field(
166
+ default_factory=set[Statement]
171
167
  )
168
+ uncheckpointed_before_break: set[Statement] = field(default_factory=set[Statement])
169
+ # pyright emits reportUnknownVariableType, requiring the generic to default_factory
170
+ # to be specified.
171
+ # But for these we require a union, and `|` doesn't work on py39, and uses of
172
+ # `Union` gets autofixed by ruff.
173
+ # So.... let's just ignore the error for now
174
+ artificial_errors: set[ # pyright: ignore[reportUnknownVariableType]
175
+ cst.Return | cst.Yield
176
+ ] = field(default_factory=set)
177
+ nodes_needing_checkpoints: list[ # pyright: ignore[reportUnknownVariableType]
178
+ cst.Return | cst.Yield | ArtificialStatement
179
+ ] = field(default_factory=list)
172
180
 
173
181
  def copy(self):
174
182
  return LoopState(
@@ -184,10 +192,14 @@ class LoopState:
184
192
 
185
193
  @dataclass
186
194
  class TryState:
187
- body_uncheckpointed_statements: set[Statement] = field(default_factory=set)
188
- try_checkpoint: set[Statement] = field(default_factory=set)
189
- except_uncheckpointed_statements: set[Statement] = field(default_factory=set)
190
- added: set[Statement] = field(default_factory=set)
195
+ body_uncheckpointed_statements: set[Statement] = field(
196
+ default_factory=set[Statement]
197
+ )
198
+ try_checkpoint: set[Statement] = field(default_factory=set[Statement])
199
+ except_uncheckpointed_statements: set[Statement] = field(
200
+ default_factory=set[Statement]
201
+ )
202
+ added: set[Statement] = field(default_factory=set[Statement])
191
203
 
192
204
  def copy(self):
193
205
  return TryState(
@@ -202,8 +214,12 @@ class TryState:
202
214
  class MatchState:
203
215
  # TryState, LoopState, and MatchState all do fairly similar things. It would be nice
204
216
  # to harmonize them and share logic.
205
- base_uncheckpointed_statements: set[Statement] = field(default_factory=set)
206
- case_uncheckpointed_statements: set[Statement] = field(default_factory=set)
217
+ base_uncheckpointed_statements: set[Statement] = field(
218
+ default_factory=set[Statement]
219
+ )
220
+ case_uncheckpointed_statements: set[Statement] = field(
221
+ default_factory=set[Statement]
222
+ )
207
223
  has_fallback: bool = False
208
224
 
209
225
  def copy(self):
@@ -358,6 +374,14 @@ class InsertCheckpointsInLoopBody(CommonVisitors):
358
374
  disable_codes_by_default("ASYNC910", "ASYNC911", "ASYNC912", "ASYNC913")
359
375
 
360
376
 
377
+ @dataclass
378
+ class ContextManager:
379
+ has_checkpoint: bool | None = None
380
+ call: MatchingCall[cst.Call] | None = None
381
+ line: int | None = None
382
+ column: int | None = None
383
+
384
+
361
385
  @error_class_cst
362
386
  class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
363
387
  error_codes: Mapping[str, str] = {
@@ -392,8 +416,7 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
392
416
  self.match_state = MatchState()
393
417
 
394
418
  # ASYNC100
395
- self.has_checkpoint_stack: list[bool] = []
396
- self.node_dict: dict[cst.With, list[AttributeCall]] = {}
419
+ self.has_checkpoint_stack: list[ContextManager] = []
397
420
  self.taskgroup_has_start_soon: dict[str, bool] = {}
398
421
 
399
422
  # --exception-suppress-context-manager
@@ -413,7 +436,11 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
413
436
  )
414
437
 
415
438
  def checkpoint_cancel_point(self) -> None:
416
- self.has_checkpoint_stack = [True] * len(self.has_checkpoint_stack)
439
+ for cm in reversed(self.has_checkpoint_stack):
440
+ if cm.has_checkpoint:
441
+ # Everything further down in the stack is already True.
442
+ break
443
+ cm.has_checkpoint = True
417
444
  # don't need to look for any .start_soon() calls
418
445
  self.taskgroup_has_start_soon.clear()
419
446
 
@@ -689,59 +716,106 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
689
716
  # missing-checkpoint warning when there might in fact be one (i.e. a false alarm).
690
717
  def visit_With_body(self, node: cst.With):
691
718
  self.save_state(node, "taskgroup_has_start_soon", copy=True)
692
- self._checkpoint_with(node, entry=True)
719
+
720
+ is_suppressing = False
693
721
 
694
722
  # if this might suppress exceptions, we cannot treat anything inside it as
695
723
  # checkpointing.
696
724
  if self._is_exception_suppressing_context_manager(node):
697
725
  self.save_state(node, "uncheckpointed_statements", copy=True)
698
726
 
699
- if res := (
700
- with_has_call(node, *cancel_scope_names)
701
- or with_has_call(
702
- node, "timeout", "timeout_at", base=("asyncio", "asyncio.timeouts")
703
- )
704
- ):
705
- pos = self.get_metadata(PositionProvider, node).start # pyright: ignore
706
- line: int = pos.line # pyright: ignore
707
- column: int = pos.column # pyright: ignore
708
- self.uncheckpointed_statements.add(
709
- ArtificialStatement("with", line, column)
710
- )
711
- self.node_dict[node] = res
712
- self.has_checkpoint_stack.append(False)
713
- else:
714
- self.has_checkpoint_stack.append(True)
727
+ for withitem in node.items:
728
+ self.has_checkpoint_stack.append(ContextManager())
729
+ if get_matching_call_cst(
730
+ withitem.item, "open_nursery", "create_task_group"
731
+ ):
732
+ if withitem.asname is not None and isinstance(
733
+ withitem.asname.name, cst.Name
734
+ ):
735
+ self.taskgroup_has_start_soon[withitem.asname.name.value] = False
736
+ self.checkpoint_schedule_point()
737
+ # Technically somebody could set open_nursery or create_task_group as
738
+ # suppressing context managers, but we're not add logic for that.
739
+ continue
740
+
741
+ if bool(getattr(node, "asynchronous", False)):
742
+ self.checkpoint()
743
+
744
+ # not a clean function call
745
+ if not isinstance(withitem.item, cst.Call) or not isinstance(
746
+ withitem.item.func, (cst.Name, cst.Attribute)
747
+ ):
748
+ continue
749
+
750
+ if (
751
+ fnmatch_qualified_name_cst(
752
+ (withitem.item.func,),
753
+ "contextlib.suppress",
754
+ *self.suppress_imported_as,
755
+ *self.options.exception_suppress_context_managers,
756
+ )
757
+ is not None
758
+ ):
759
+ # Don't re-update state if there's several suppressing cm's.
760
+ if not is_suppressing:
761
+ self.save_state(node, "uncheckpointed_statements", copy=True)
762
+ is_suppressing = True
763
+ continue
764
+
765
+ if res := (
766
+ get_matching_call_cst(withitem.item, *cancel_scope_names)
767
+ or get_matching_call_cst(
768
+ withitem.item,
769
+ "timeout",
770
+ "timeout_at",
771
+ base="asyncio",
772
+ )
773
+ ):
774
+ # typing issue: https://github.com/Instagram/LibCST/issues/1107
775
+ pos = cst.ensure_type(
776
+ self.get_metadata(PositionProvider, withitem),
777
+ CodeRange,
778
+ ).start
779
+ self.uncheckpointed_statements.add(
780
+ ArtificialStatement("withitem", pos.line, pos.column)
781
+ )
782
+
783
+ cm = self.has_checkpoint_stack[-1]
784
+ cm.line = pos.line
785
+ cm.column = pos.column
786
+ cm.call = res
787
+ cm.has_checkpoint = False
715
788
 
716
789
  def leave_With(self, original_node: cst.With, updated_node: cst.With):
717
- # Uses leave_With instead of leave_With_body because we need access to both
718
- # original and updated node
719
- # ASYNC100
720
- if not self.has_checkpoint_stack.pop():
721
- autofix = len(updated_node.items) == 1
722
- for res in self.node_dict[original_node]:
790
+ withitems = list(updated_node.items)
791
+ for i in reversed(range(len(updated_node.items))):
792
+ cm = self.has_checkpoint_stack.pop()
793
+ # ASYNC100
794
+ if cm.has_checkpoint is False:
795
+ res = cm.call
796
+ assert res is not None
723
797
  # bypass 910 & 911's should_autofix logic, which excludes asyncio
724
- # (TODO: and uses self.noautofix ... which I don't remember what it's for)
725
- autofix &= self.error(
726
- res.node, res.base, res.function, error_code="ASYNC100"
727
- ) and super().should_autofix(res.node, code="ASYNC100")
728
-
729
- if autofix:
730
- return flatten_preserving_comments(updated_node)
731
- # ASYNC912
732
- else:
733
- pos = self.get_metadata( # pyright: ignore
734
- PositionProvider, original_node
735
- ).start # pyright: ignore
736
- line: int = pos.line # pyright: ignore
737
- column: int = pos.column # pyright: ignore
738
- s = ArtificialStatement("with", line, column)
739
- if s in self.uncheckpointed_statements:
740
- self.uncheckpointed_statements.remove(s)
741
- for res in self.node_dict[original_node]:
742
- self.error(res.node, error_code="ASYNC912")
743
-
744
- self.node_dict.pop(original_node, None)
798
+ if self.error(
799
+ res.node, res.base, res.name, error_code="ASYNC100"
800
+ ) and super().should_autofix(res.node, code="ASYNC100"):
801
+ if len(withitems) == 1:
802
+ # Remove this With node, bypassing later logic.
803
+ return flatten_preserving_comments(updated_node)
804
+ if i == len(withitems) - 1:
805
+ # preserve trailing comma, or remove comma if there was none
806
+ withitems[-2] = withitems[-2].with_changes(
807
+ comma=withitems[-1].comma
808
+ )
809
+ withitems.pop(i)
810
+
811
+ # ASYNC912
812
+ elif cm.call is not None:
813
+ assert cm.line is not None
814
+ assert cm.column is not None
815
+ s = ArtificialStatement("withitem", cm.line, cm.column)
816
+ if s in self.uncheckpointed_statements:
817
+ self.uncheckpointed_statements.remove(s)
818
+ self.error(cm.call.node, error_code="ASYNC912")
745
819
 
746
820
  # if exception-suppressing, restore all uncheckpointed statements from
747
821
  # before the `with`.
@@ -751,7 +825,8 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
751
825
  self.uncheckpointed_statements.update(prev_checkpoints)
752
826
 
753
827
  self._checkpoint_with(original_node, entry=False)
754
- return updated_node
828
+
829
+ return updated_node.with_changes(items=withitems)
755
830
 
756
831
  # error if no checkpoint since earlier yield or function entry
757
832
  def leave_Yield(
@@ -555,18 +555,21 @@ class Visitor900(Flake8AsyncVisitor):
555
555
  def __init__(self, *args: Any, **kwargs: Any):
556
556
  super().__init__(*args, **kwargs)
557
557
  self.unsafe_function: ast.AsyncFunctionDef | None = None
558
- self.transform_decorators = (
559
- "asynccontextmanager",
560
- "fixture",
558
+ self.transform_decorators = [
559
+ "contextlib.asynccontextmanager",
560
+ "pytest.fixture",
561
561
  *self.options.transform_async_generator_decorators,
562
- )
562
+ ]
563
+ # only recommend if using trio
564
+ if "trio" in self.library:
565
+ self.transform_decorators.insert(0, "trio.as_safe_channel")
563
566
 
564
567
  def visit_AsyncFunctionDef(
565
568
  self, node: ast.AsyncFunctionDef | ast.FunctionDef | ast.Lambda
566
569
  ):
567
570
  self.save_state(node, "unsafe_function")
568
571
  if isinstance(node, ast.AsyncFunctionDef) and not has_decorator(
569
- node, *self.transform_decorators
572
+ node, *(d.split(".")[-1] for d in self.transform_decorators)
570
573
  ):
571
574
  self.unsafe_function = node
572
575
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flake8-async
3
- Version: 25.4.2
3
+ Version: 25.4.4
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
@@ -62,7 +62,7 @@ tests/autofix_files/noqa_testing.py
62
62
  tests/eval_files/anyio_trio.py
63
63
  tests/eval_files/async100.py
64
64
  tests/eval_files/async100_asyncio.py
65
- tests/eval_files/async100_noautofix.py
65
+ tests/eval_files/async100_asyncio_noautofix.py
66
66
  tests/eval_files/async100_simple_autofix.py
67
67
  tests/eval_files/async101.py
68
68
  tests/eval_files/async101_asyncio.py
@@ -2,8 +2,14 @@
2
2
  # AUTOFIX
3
3
  # ASYNCIO_NO_ERROR # timeout primitives are named differently in asyncio
4
4
 
5
+ import contextlib
5
6
  import trio
6
7
 
8
+
9
+ def condition() -> bool:
10
+ return False
11
+
12
+
7
13
  # error: 5, "trio", "move_on_after"
8
14
  ...
9
15
 
@@ -214,3 +220,29 @@ async def nursery_exit_blocks_with_start():
214
220
  async with trio.open_nursery() as n:
215
221
  with trio.CancelScope():
216
222
  await n.start(trio.sleep, 0)
223
+
224
+
225
+ async def autofix_multi_withitem():
226
+ with open("foo"): # error: 9, "trio", "CancelScope"
227
+ ...
228
+
229
+ # this one is completely removed
230
+ # error: 8, "trio", "CancelScope"
231
+ # error: 8, "trio", "CancelScope"
232
+ ...
233
+
234
+ # these keep the `open`
235
+ with (
236
+ open("aa") as _, # error: 8, "trio", "fail_after"
237
+ ):
238
+ ...
239
+
240
+ with (
241
+ open("bb") as _, # error: 8, "trio", "move_on_after"
242
+ ):
243
+ ...
244
+
245
+ with (
246
+ open("cc") as f,
247
+ ):
248
+ ...
@@ -0,0 +1,31 @@
1
+ # TRIO_NO_ERROR
2
+ # ANYIO_NO_ERROR
3
+ # BASE_LIBRARY asyncio
4
+
5
+ # asyncio.timeout[_at] added in py3.11
6
+ # mypy: disable-error-code=attr-defined
7
+ # AUTOFIX
8
+
9
+ import asyncio
10
+
11
+
12
+ async def foo():
13
+ # error: 9, "asyncio", "timeout_at"
14
+ ...
15
+ # error: 9, "asyncio", "timeout"
16
+ ...
17
+
18
+
19
+ # this is technically only a problem with asyncio, since timeout primitives in trio/anyio
20
+ # are sync cm's
21
+ async def multi_withitem():
22
+ with open("foo"): # error: 9, "asyncio", "timeout"
23
+ ...
24
+ with open("foo"): # error: 22, "asyncio", "timeout"
25
+ ...
26
+ # retain explicit trailing comma (?)
27
+ with (
28
+ open("foo"),
29
+ open("bar"), # error: 8, "asyncio", "timeout"
30
+ ):
31
+ ...
@@ -2,8 +2,14 @@
2
2
  # AUTOFIX
3
3
  # ASYNCIO_NO_ERROR # timeout primitives are named differently in asyncio
4
4
 
5
+ import contextlib
5
6
  import trio
6
7
 
8
+
9
+ def condition() -> bool:
10
+ return False
11
+
12
+
7
13
  with trio.move_on_after(10): # error: 5, "trio", "move_on_after"
8
14
  ...
9
15
 
@@ -214,3 +220,35 @@ async def nursery_exit_blocks_with_start():
214
220
  async with trio.open_nursery() as n:
215
221
  with trio.CancelScope():
216
222
  await n.start(trio.sleep, 0)
223
+
224
+
225
+ async def autofix_multi_withitem():
226
+ with trio.CancelScope(), open("foo"): # error: 9, "trio", "CancelScope"
227
+ ...
228
+
229
+ # this one is completely removed
230
+ with (
231
+ trio.CancelScope(), # error: 8, "trio", "CancelScope"
232
+ trio.CancelScope(), # error: 8, "trio", "CancelScope"
233
+ ):
234
+ ...
235
+
236
+ # these keep the `open`
237
+ with (
238
+ open("aa") as _,
239
+ trio.fail_after(10), # error: 8, "trio", "fail_after"
240
+ ):
241
+ ...
242
+
243
+ with (
244
+ trio.fail_after(5), # error: 8, "trio", "fail_after"
245
+ open("bb") as _,
246
+ trio.move_on_after(5), # error: 8, "trio", "move_on_after"
247
+ ):
248
+ ...
249
+
250
+ with (
251
+ trio.move_on_after(10), # error: 8, "trio", "move_on_after"
252
+ open("cc") as f,
253
+ ):
254
+ ...
@@ -0,0 +1,32 @@
1
+ # TRIO_NO_ERROR
2
+ # ANYIO_NO_ERROR
3
+ # BASE_LIBRARY asyncio
4
+
5
+ # asyncio.timeout[_at] added in py3.11
6
+ # mypy: disable-error-code=attr-defined
7
+ # AUTOFIX
8
+
9
+ import asyncio
10
+
11
+
12
+ async def foo():
13
+ with asyncio.timeout_at(10): # error: 9, "asyncio", "timeout_at"
14
+ ...
15
+ with asyncio.timeout(10): # error: 9, "asyncio", "timeout"
16
+ ...
17
+
18
+
19
+ # this is technically only a problem with asyncio, since timeout primitives in trio/anyio
20
+ # are sync cm's
21
+ async def multi_withitem():
22
+ with asyncio.timeout(10), open("foo"): # error: 9, "asyncio", "timeout"
23
+ ...
24
+ with open("foo"), asyncio.timeout(10): # error: 22, "asyncio", "timeout"
25
+ ...
26
+ # retain explicit trailing comma (?)
27
+ with (
28
+ open("foo"),
29
+ open("bar"),
30
+ asyncio.timeout(10), # error: 8, "asyncio", "timeout"
31
+ ):
32
+ ...
@@ -0,0 +1,21 @@
1
+ # TRIO_NO_ERROR
2
+ # ANYIO_NO_ERROR
3
+ # BASE_LIBRARY asyncio
4
+
5
+ # We remove the last timeout, but don't re-evaluate the whole with statement,
6
+ # so the test still raises an error.
7
+ # NOAUTOFIX
8
+
9
+ # asyncio.timeout[_at] added in py3.11
10
+ # mypy: disable-error-code=attr-defined
11
+
12
+ import asyncio
13
+
14
+
15
+ async def multi_withitem():
16
+ async with asyncio.timeout(
17
+ 10
18
+ ), asyncio.timeout_at( # error: 7, "asyncio", "timeout_at"
19
+ 10
20
+ ):
21
+ ...