flake8-async 25.4.4__tar.gz → 25.5.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. {flake8_async-25.4.4 → flake8_async-25.5.2}/PKG-INFO +1 -1
  2. {flake8_async-25.4.4 → flake8_async-25.5.2}/flake8_async/__init__.py +1 -1
  3. {flake8_async-25.4.4 → flake8_async-25.5.2}/flake8_async/visitors/__init__.py +1 -0
  4. {flake8_async-25.4.4 → flake8_async-25.5.2}/flake8_async/visitors/visitor102_120.py +1 -11
  5. flake8_async-25.5.2/flake8_async/visitors/visitor4xx.py +98 -0
  6. {flake8_async-25.4.4 → flake8_async-25.5.2}/flake8_async/visitors/visitors.py +38 -14
  7. {flake8_async-25.4.4 → flake8_async-25.5.2}/flake8_async.egg-info/PKG-INFO +1 -1
  8. {flake8_async-25.4.4 → flake8_async-25.5.2}/flake8_async.egg-info/SOURCES.txt +3 -0
  9. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async102.py +2 -1
  10. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async113.py +58 -0
  11. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async113_trio.py +2 -0
  12. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async120.py +10 -0
  13. flake8_async-25.5.2/tests/eval_files/async400_py311.py +84 -0
  14. flake8_async-25.5.2/tests/eval_files/async900_trio.py +15 -0
  15. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/test_flake8_async.py +12 -0
  16. {flake8_async-25.4.4 → flake8_async-25.5.2}/CHANGELOG.md +0 -0
  17. {flake8_async-25.4.4 → flake8_async-25.5.2}/CONTRIBUTING.md +0 -0
  18. {flake8_async-25.4.4 → flake8_async-25.5.2}/LICENSE +0 -0
  19. {flake8_async-25.4.4 → flake8_async-25.5.2}/MANIFEST.in +0 -0
  20. {flake8_async-25.4.4 → flake8_async-25.5.2}/README.md +0 -0
  21. {flake8_async-25.4.4 → flake8_async-25.5.2}/flake8_async/__main__.py +0 -0
  22. {flake8_async-25.4.4 → flake8_async-25.5.2}/flake8_async/base.py +0 -0
  23. {flake8_async-25.4.4 → flake8_async-25.5.2}/flake8_async/runner.py +0 -0
  24. {flake8_async-25.4.4 → flake8_async-25.5.2}/flake8_async/visitors/flake8asyncvisitor.py +0 -0
  25. {flake8_async-25.4.4 → flake8_async-25.5.2}/flake8_async/visitors/helpers.py +0 -0
  26. {flake8_async-25.4.4 → flake8_async-25.5.2}/flake8_async/visitors/visitor101.py +0 -0
  27. {flake8_async-25.4.4 → flake8_async-25.5.2}/flake8_async/visitors/visitor103_104.py +0 -0
  28. {flake8_async-25.4.4 → flake8_async-25.5.2}/flake8_async/visitors/visitor105.py +0 -0
  29. {flake8_async-25.4.4 → flake8_async-25.5.2}/flake8_async/visitors/visitor111.py +0 -0
  30. {flake8_async-25.4.4 → flake8_async-25.5.2}/flake8_async/visitors/visitor118.py +0 -0
  31. {flake8_async-25.4.4 → flake8_async-25.5.2}/flake8_async/visitors/visitor123.py +0 -0
  32. {flake8_async-25.4.4 → flake8_async-25.5.2}/flake8_async/visitors/visitor2xx.py +0 -0
  33. {flake8_async-25.4.4 → flake8_async-25.5.2}/flake8_async/visitors/visitor91x.py +0 -0
  34. {flake8_async-25.4.4 → flake8_async-25.5.2}/flake8_async/visitors/visitor_utility.py +0 -0
  35. {flake8_async-25.4.4 → flake8_async-25.5.2}/flake8_async.egg-info/dependency_links.txt +0 -0
  36. {flake8_async-25.4.4 → flake8_async-25.5.2}/flake8_async.egg-info/entry_points.txt +0 -0
  37. {flake8_async-25.4.4 → flake8_async-25.5.2}/flake8_async.egg-info/not-zip-safe +0 -0
  38. {flake8_async-25.4.4 → flake8_async-25.5.2}/flake8_async.egg-info/requires.txt +0 -0
  39. {flake8_async-25.4.4 → flake8_async-25.5.2}/flake8_async.egg-info/top_level.txt +0 -0
  40. {flake8_async-25.4.4 → flake8_async-25.5.2}/pyproject.toml +0 -0
  41. {flake8_async-25.4.4 → flake8_async-25.5.2}/setup.cfg +0 -0
  42. {flake8_async-25.4.4 → flake8_async-25.5.2}/setup.py +0 -0
  43. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/__init__.py +0 -0
  44. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/autofix_files/async100.py +0 -0
  45. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/autofix_files/async100_asyncio.py +0 -0
  46. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/autofix_files/async100_simple_autofix.py +0 -0
  47. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/autofix_files/async124.py +0 -0
  48. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/autofix_files/async910.py +0 -0
  49. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/autofix_files/async910_insert_library.py +0 -0
  50. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/autofix_files/async911.py +0 -0
  51. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/autofix_files/async911_insert_library.py +0 -0
  52. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/autofix_files/async913.py +0 -0
  53. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/autofix_files/async913_trio_anyio.py +0 -0
  54. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/autofix_files/async91x_autofix.py +0 -0
  55. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/autofix_files/async91x_py310.py +0 -0
  56. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/autofix_files/async91x_py311.py +0 -0
  57. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/autofix_files/exception_suppress_context_manager.py +0 -0
  58. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/autofix_files/exception_suppress_context_manager_import_star.py +0 -0
  59. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/autofix_files/noqa.py +0 -0
  60. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/autofix_files/noqa_testing.py +0 -0
  61. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/check_changelog_and_version.py +0 -0
  62. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/conftest.py +0 -0
  63. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/anyio_trio.py +0 -0
  64. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async100.py +0 -0
  65. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async100_asyncio.py +0 -0
  66. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async100_asyncio_noautofix.py +0 -0
  67. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async100_simple_autofix.py +0 -0
  68. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async101.py +0 -0
  69. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async101_asyncio.py +0 -0
  70. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async102_120_py311.py +0 -0
  71. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async102_aclose.py +0 -0
  72. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async102_aclose_args.py +0 -0
  73. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async102_anyio.py +0 -0
  74. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async102_asyncio.py +0 -0
  75. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async102_trio.py +0 -0
  76. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async103.py +0 -0
  77. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async103_104_py310.py +0 -0
  78. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async103_104_py311.py +0 -0
  79. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async103_all_imported.py +0 -0
  80. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async103_both_imported.py +0 -0
  81. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async103_no_104.py +0 -0
  82. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async103_trio.py +0 -0
  83. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async104.py +0 -0
  84. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async104_anyio.py +0 -0
  85. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async104_trio.py +0 -0
  86. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async105.py +0 -0
  87. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async105_anyio.py +0 -0
  88. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async106.py +0 -0
  89. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async109.py +0 -0
  90. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async110.py +0 -0
  91. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async111.py +0 -0
  92. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async111_asyncio.py +0 -0
  93. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async112.py +0 -0
  94. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async112_asyncio.py +0 -0
  95. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async113_asyncio.py +0 -0
  96. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async114.py +0 -0
  97. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async115.py +0 -0
  98. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async116.py +0 -0
  99. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async118.py +0 -0
  100. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async119.py +0 -0
  101. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async121.py +0 -0
  102. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async121_anyio.py +0 -0
  103. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async121_asyncio.py +0 -0
  104. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async122.py +0 -0
  105. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async123.py +0 -0
  106. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async123_py311.py +0 -0
  107. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async124.py +0 -0
  108. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async124_no_autofix.py +0 -0
  109. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async125.py +0 -0
  110. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async200.py +0 -0
  111. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async210.py +0 -0
  112. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async211.py +0 -0
  113. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async212.py +0 -0
  114. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async22x.py +0 -0
  115. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async22x_asyncio.py +0 -0
  116. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async232.py +0 -0
  117. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async232_asyncio.py +0 -0
  118. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async23x.py +0 -0
  119. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async23x_asyncio.py +0 -0
  120. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async240.py +0 -0
  121. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async250.py +0 -0
  122. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async250_multi_library.py +0 -0
  123. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async251.py +0 -0
  124. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async251_multi_library.py +0 -0
  125. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async300.py +0 -0
  126. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async900.py +0 -0
  127. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async910.py +0 -0
  128. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async910_insert_library.py +0 -0
  129. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async911.py +0 -0
  130. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async911_insert_library.py +0 -0
  131. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async912.py +0 -0
  132. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async912_asyncio.py +0 -0
  133. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async912_py311.py +0 -0
  134. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async913.py +0 -0
  135. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async913_trio_anyio.py +0 -0
  136. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async91x_autofix.py +0 -0
  137. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async91x_noautofix.py +0 -0
  138. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async91x_py310.py +0 -0
  139. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/async91x_py311.py +0 -0
  140. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/exception_suppress_context_manager.py +0 -0
  141. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/exception_suppress_context_manager_import_star.py +0 -0
  142. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/no_library.py +0 -0
  143. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/noqa.py +0 -0
  144. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/noqa_no_autofix.py +0 -0
  145. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/noqa_testing.py +0 -0
  146. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/eval_files/trio_anyio.py +0 -0
  147. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/test_all_visitors_imported.py +0 -0
  148. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/test_config_and_args.py +0 -0
  149. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/test_decorator.py +0 -0
  150. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/test_exception_on_invalid_code.py +0 -0
  151. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/test_formatting.py +0 -0
  152. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/test_messages_documented.py +0 -0
  153. {flake8_async-25.4.4 → flake8_async-25.5.2}/tests/trio_options.py +0 -0
  154. {flake8_async-25.4.4 → flake8_async-25.5.2}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flake8-async
3
- Version: 25.4.4
3
+ Version: 25.5.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
@@ -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.4"
41
+ __version__ = "25.5.2"
42
42
 
43
43
 
44
44
  # taken from https://github.com/Zac-HD/shed
@@ -29,6 +29,7 @@ utility_visitors_cst: set[type[Flake8AsyncVisitor_cst]] = set()
29
29
  # This has to be done at the end to avoid circular imports
30
30
  from . import (
31
31
  visitor2xx,
32
+ visitor4xx,
32
33
  visitor91x,
33
34
  visitor101,
34
35
  visitor102_120,
@@ -40,16 +40,6 @@ class Visitor102(Flake8AsyncVisitor):
40
40
  self.funcname = funcname
41
41
  self.variable_name: str | None = None
42
42
  self.shielded: bool = False
43
- self.has_timeout: bool = True
44
-
45
- # scope.shielded is assigned to in visit_Assign
46
-
47
- if self.funcname == "CancelScope":
48
- self.has_timeout = False
49
- for kw in node.keywords:
50
- # note: sets to True even if timeout is explicitly set to inf
51
- if kw.arg == "deadline":
52
- self.has_timeout = True
53
43
 
54
44
  # trio 0.27 adds shield parameter to all scope helpers
55
45
  if self.funcname in cancel_scope_names:
@@ -79,7 +69,7 @@ class Visitor102(Flake8AsyncVisitor):
79
69
  self, node: ast.Await | ast.AsyncFor | ast.AsyncWith
80
70
  ) -> None:
81
71
  if self._critical_scope is not None and not any(
82
- cm.has_timeout and cm.shielded for cm in self._trio_context_managers
72
+ cm.shielded for cm in self._trio_context_managers
83
73
  ):
84
74
  # non-critical exception handlers have the statement name set to "except"
85
75
  if self._critical_scope.name == "except":
@@ -0,0 +1,98 @@
1
+ """4XX error classes, which handle exception groups.
2
+
3
+ ASYNC400 except-star-invalid-attribute checks for invalid attribute access on except*
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import ast
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ from .flake8asyncvisitor import Flake8AsyncVisitor
12
+ from .helpers import error_class
13
+
14
+ if TYPE_CHECKING:
15
+ from collections.abc import Mapping
16
+
17
+ EXCGROUP_ATTRS = (
18
+ # from ExceptionGroup
19
+ "message",
20
+ "exceptions",
21
+ "subgroup",
22
+ "split",
23
+ "derive",
24
+ # from BaseException
25
+ "args",
26
+ "with_traceback",
27
+ "add_note",
28
+ # in the backport
29
+ "_is_protocol",
30
+ )
31
+
32
+
33
+ @error_class
34
+ class Visitor4xx(Flake8AsyncVisitor):
35
+
36
+ error_codes: Mapping[str, str] = {
37
+ "ASYNC400": (
38
+ "Accessing attribute {} on ExceptionGroup as if it was a bare Exception."
39
+ )
40
+ }
41
+
42
+ def __init__(self, *args: Any, **kwargs: Any):
43
+ super().__init__(*args, **kwargs)
44
+ self.exception_groups: list[str] = []
45
+ self.trystar = False
46
+
47
+ def visit_TryStar(self, node: ast.TryStar): # type: ignore[name-defined]
48
+ self.save_state(node, "trystar")
49
+ self.trystar = True
50
+
51
+ def visit_Try(self, node: ast.Try):
52
+ self.save_state(node, "trystar")
53
+ self.trystar = False
54
+
55
+ def visit_ExceptHandler(self, node: ast.ExceptHandler):
56
+ if not self.trystar or node.name is None:
57
+ return
58
+ self.save_state(node, "exception_groups", copy=True)
59
+ self.exception_groups.append(node.name)
60
+ self.visit_nodes(node.body)
61
+
62
+ def visit_Attribute(self, node: ast.Attribute):
63
+ if (
64
+ isinstance(node.value, ast.Name)
65
+ and node.value.id in self.exception_groups
66
+ and node.attr not in EXCGROUP_ATTRS
67
+ and not (node.attr.startswith("__") and node.attr.endswith("__"))
68
+ ):
69
+ self.error(node, node.attr)
70
+
71
+ def _clear_if_name(self, node: ast.AST | None):
72
+ if isinstance(node, ast.Name) and node.id in self.exception_groups:
73
+ self.exception_groups.remove(node.id)
74
+
75
+ def _walk_and_clear(self, node: ast.AST | None):
76
+ if node is None:
77
+ return
78
+ for n in ast.walk(node):
79
+ self._clear_if_name(n)
80
+
81
+ def visit_Assign(self, node: ast.Assign):
82
+ for t in node.targets:
83
+ self._walk_and_clear(t)
84
+
85
+ def visit_AnnAssign(self, node: ast.AnnAssign):
86
+ self._clear_if_name(node.target)
87
+
88
+ def visit_withitem(self, node: ast.withitem):
89
+ self._walk_and_clear(node.optional_vars)
90
+
91
+ def visit_FunctionDef(
92
+ self, node: ast.FunctionDef | ast.AsyncFunctionDef | ast.Lambda
93
+ ):
94
+ self.save_state(node, "exception_groups", "trystar", copy=False)
95
+ self.exception_groups = []
96
+
97
+ visit_AsyncFunctionDef = visit_FunctionDef
98
+ visit_Lambda = visit_FunctionDef
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import ast
6
+ from collections import defaultdict
6
7
  from typing import TYPE_CHECKING, Any, cast
7
8
 
8
9
  from .flake8asyncvisitor import Flake8AsyncVisitor, Flake8AsyncVisitor_cst
@@ -181,20 +182,25 @@ class Visitor113(Flake8AsyncVisitor):
181
182
  self.asynccontextmanager = False
182
183
  self.aenter = False
183
184
 
185
+ self.potential_errors: dict[str, list[ast.Call]] = defaultdict(list)
186
+
184
187
  def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
185
- self.save_state(node, "aenter")
188
+ self.save_state(node, "aenter", "asynccontextmanager", "potential_errors")
186
189
 
187
- self.aenter = node.name == "__aenter__" or has_decorator(
188
- node, "asynccontextmanager"
189
- )
190
+ self.aenter = node.name == "__aenter__"
191
+ self.asynccontextmanager = has_decorator(node, "asynccontextmanager")
190
192
 
191
193
  def visit_FunctionDef(self, node: ast.FunctionDef):
192
- self.save_state(node, "aenter")
194
+ self.save_state(node, "aenter", "asynccontextmanager", "potential_errors")
193
195
  # sync function should never be named __aenter__ or have @asynccontextmanager
194
- self.aenter = False
196
+ self.aenter = self.asynccontextmanager = False
195
197
 
196
198
  def visit_Yield(self, node: ast.Yield):
197
- self.aenter = False
199
+ for nodes in self.potential_errors.values():
200
+ for n in nodes:
201
+ self.error(n)
202
+ self.potential_errors.clear()
203
+ self.aenter = self.asynccontextmanager = False
198
204
 
199
205
  def visit_Call(self, node: ast.Call) -> None:
200
206
  def is_startable(n: ast.expr, *startable_list: str) -> bool:
@@ -210,14 +216,14 @@ class Visitor113(Flake8AsyncVisitor):
210
216
  return any(is_startable(nn, *startable_list) for nn in n.args)
211
217
  return False
212
218
 
213
- def is_nursery_call(node: ast.expr):
219
+ def is_nursery_call(node: ast.expr) -> str | None:
214
220
  if not isinstance(node, ast.Attribute) or node.attr not in (
215
221
  "start_soon",
216
222
  "create_task",
217
223
  ):
218
- return False
224
+ return None
219
225
  var = ast.unparse(node.value)
220
- return (
226
+ if (
221
227
  ("trio" in self.library and var.endswith("nursery"))
222
228
  or ("anyio" in self.library and var.endswith("task_group"))
223
229
  or (
@@ -228,11 +234,12 @@ class Visitor113(Flake8AsyncVisitor):
228
234
  "asyncio.TaskGroup",
229
235
  )
230
236
  )
231
- )
237
+ ):
238
+ return var
239
+ return None
232
240
 
233
241
  if (
234
- self.aenter
235
- and is_nursery_call(node.func)
242
+ (var := is_nursery_call(node.func)) is not None
236
243
  and len(node.args) > 0
237
244
  and is_startable(
238
245
  node.args[0],
@@ -241,7 +248,24 @@ class Visitor113(Flake8AsyncVisitor):
241
248
  *self.options.startable_in_context_manager,
242
249
  )
243
250
  ):
244
- self.error(node)
251
+ if self.aenter:
252
+ self.error(node)
253
+ elif self.asynccontextmanager:
254
+ self.potential_errors[var].append(node)
255
+
256
+ def visit_AsyncWith(self, node: ast.AsyncWith | ast.With):
257
+ # Entirely skip any nurseries that doesn't have any yields in them.
258
+ # This fixes an otherwise very thorny false alarm.
259
+ # In the worst case this does mean we iterate over the body twice, but might
260
+ # actually be a performance gain on average due to setting `novisit`
261
+ if not any(isinstance(n, ast.Yield) for b in node.body for n in ast.walk(b)):
262
+ self.novisit = True
263
+ return
264
+
265
+ # open_nursery/create_task_group only works with AsyncWith, but in case somebody
266
+ # is doing something very weird we'll be conservative and possibly avoid
267
+ # some potential false alarms
268
+ visit_With = visit_AsyncWith
245
269
 
246
270
 
247
271
  # Checks that all async functions with a "task_status" parameter have a match in
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flake8-async
3
- Version: 25.4.4
3
+ Version: 25.5.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
@@ -28,6 +28,7 @@ flake8_async/visitors/visitor111.py
28
28
  flake8_async/visitors/visitor118.py
29
29
  flake8_async/visitors/visitor123.py
30
30
  flake8_async/visitors/visitor2xx.py
31
+ flake8_async/visitors/visitor4xx.py
31
32
  flake8_async/visitors/visitor91x.py
32
33
  flake8_async/visitors/visitor_utility.py
33
34
  flake8_async/visitors/visitors.py
@@ -126,7 +127,9 @@ tests/eval_files/async250_multi_library.py
126
127
  tests/eval_files/async251.py
127
128
  tests/eval_files/async251_multi_library.py
128
129
  tests/eval_files/async300.py
130
+ tests/eval_files/async400_py311.py
129
131
  tests/eval_files/async900.py
132
+ tests/eval_files/async900_trio.py
130
133
  tests/eval_files/async910.py
131
134
  tests/eval_files/async910_insert_library.py
132
135
  tests/eval_files/async911.py
@@ -100,8 +100,9 @@ async def foo():
100
100
  try:
101
101
  pass
102
102
  finally:
103
+ # it's now fine to have a shield and no timeout
103
104
  with trio.CancelScope(shield=True):
104
- await foo() # error: 12, Statement("try/finally", lineno-4)
105
+ await foo()
105
106
  try:
106
107
  pass
107
108
  finally:
@@ -24,9 +24,18 @@ async def foo():
24
24
  # we don't check for `async with`
25
25
  with trio.open_nursery() as bar: # type: ignore[attr-defined]
26
26
  bar.start_soon(my_startable) # ASYNC113: 8
27
+ yield
28
+
29
+
30
+ @asynccontextmanager
31
+ async def foo2():
27
32
  async with trio.open_nursery() as bar:
28
33
  bar.start_soon(my_startable) # ASYNC113: 8
34
+ yield
35
+
29
36
 
37
+ @asynccontextmanager
38
+ async def foo3():
30
39
  boo: trio.Nursery = ... # type: ignore
31
40
  boo.start_soon(my_startable) # ASYNC113: 4
32
41
 
@@ -132,3 +141,52 @@ async def foo_nested_sync_def():
132
141
  bar.start_soon(my_startable)
133
142
 
134
143
  yield
144
+
145
+
146
+ @asynccontextmanager
147
+ async def false_alarm():
148
+ async with trio.open_nursery() as nursery:
149
+ nursery.start_soon(my_startable)
150
+ yield
151
+
152
+
153
+ @asynccontextmanager
154
+ async def should_error():
155
+ async with trio.open_nursery() as nursery:
156
+ nursery.start_soon(my_startable) # ASYNC113: 8
157
+ # overrides the nursery variable
158
+ async with trio.open_nursery() as nursery:
159
+ nursery.start_soon(my_startable)
160
+ yield
161
+
162
+
163
+ @asynccontextmanager
164
+ async def foo_sync_with_closed():
165
+ # we don't check for `async with`
166
+ with trio.open_nursery() as bar: # type: ignore[attr-defined]
167
+ bar.start_soon(my_startable)
168
+ yield
169
+
170
+
171
+ # fixed by entirely skipping nurseries without yields in them
172
+ class FalseAlarm:
173
+ async def __aenter__(self):
174
+ with trio.open_nursery() as nursery:
175
+ nursery.start_soon(my_startable)
176
+
177
+
178
+ @asynccontextmanager
179
+ async def yield_before_start_soon():
180
+ with trio.open_nursery() as bar:
181
+ yield
182
+ bar.start_soon(my_startable)
183
+
184
+
185
+ # This was broken when visit_AsyncWith manually visited subnodes due to not
186
+ # letting TypeTrackerVisitor interject.
187
+ @asynccontextmanager
188
+ async def nested():
189
+ with trio.open_nursery() as foo:
190
+ with trio.open_nursery() as bar:
191
+ bar.start_soon(my_startable) # error: 12
192
+ yield
@@ -132,8 +132,10 @@ async def foo4(): ...
132
132
  @asynccontextmanager() # type: ignore[call-arg]
133
133
  async def foo_paren():
134
134
  nursery.start_soon(trio.run_process) # error: 4
135
+ yield
135
136
 
136
137
 
137
138
  @asynccontextmanager(1, 2, 3) # type: ignore[call-arg]
138
139
  async def foo_params():
139
140
  nursery.start_soon(trio.run_process) # error: 4
141
+ yield
@@ -121,3 +121,13 @@ async def foo_nested_funcdef():
121
121
  await foo()
122
122
 
123
123
  raise
124
+
125
+
126
+ # shielded but no timeout no longer triggers async120
127
+ # https://github.com/python-trio/flake8-async/issues/272
128
+ async def foo_shield_no_timeout():
129
+ try:
130
+ ...
131
+ finally:
132
+ with trio.CancelScope(shield=True):
133
+ await foo()
@@ -0,0 +1,84 @@
1
+ try:
2
+ ...
3
+ except* ValueError as e:
4
+ e.anything # error: 4, "anything"
5
+ e.foo() # error: 4, "foo"
6
+ e.bar.zee # error: 4, "bar"
7
+
8
+ # from ExceptionGroup
9
+ e.message
10
+ e.exceptions
11
+ e.subgroup
12
+ e.split
13
+ e.derive
14
+
15
+ # from BaseException
16
+ e.args
17
+ e.with_traceback
18
+ e.add_note
19
+
20
+ # ignore anything that looks like a dunder
21
+ e.__foo__
22
+ e.__bar__
23
+
24
+ e.anything # safe
25
+
26
+ # assigning to the variable clears it
27
+ try:
28
+ ...
29
+ except* ValueError as e:
30
+ e = e.exceptions[0]
31
+ e.ignore # safe
32
+ except* ValueError as e:
33
+ e, f = 1, 2
34
+ e.anything # safe
35
+ except* TypeError as e:
36
+ (e, f) = (1, 2)
37
+ e.anything # safe
38
+ except* ValueError as e:
39
+ with blah as e:
40
+ e.anything
41
+ e.anything
42
+ except* ValueError as e:
43
+ e: int = 1
44
+ e.real
45
+ except* ValueError as e:
46
+ with blah as (e, f):
47
+ e.anything
48
+
49
+ # check state saving
50
+ try:
51
+ ...
52
+ except* ValueError as e:
53
+ ...
54
+ except* BaseException:
55
+ e.error # safe
56
+
57
+ try:
58
+ ...
59
+ except* ValueError as e:
60
+ try:
61
+ ...
62
+ except* TypeError as e:
63
+ ...
64
+ e.anything # error: 4, "anything"
65
+
66
+ try:
67
+ ...
68
+ except* ValueError as e:
69
+
70
+ def foo():
71
+ # possibly problematic, but we minimize false alarms
72
+ e.anything
73
+
74
+ e.anything # error: 4, "anything"
75
+
76
+ def foo(e):
77
+ # this one is more clear it should be treated as safe
78
+ e.anything
79
+
80
+ e.anything # error: 4, "anything"
81
+
82
+ lambda e: e.anything
83
+
84
+ e.anything # error: 4, "anything"
@@ -0,0 +1,15 @@
1
+ import trio
2
+
3
+ # different error message & no as_safe_channel
4
+ # NOANYIO
5
+ # NOASYNCIO
6
+
7
+
8
+ async def foo1(): # ASYNC900: 0, 'trio.as_safe_channel, contextlib.asynccontextmanager, pytest.fixture'
9
+ yield
10
+ yield
11
+
12
+
13
+ @trio.as_safe_channel
14
+ async def foo():
15
+ yield
@@ -26,6 +26,10 @@ from hypothesmith import from_grammar, from_node
26
26
  from flake8_async import Plugin
27
27
  from flake8_async.base import Error, Statement
28
28
  from flake8_async.visitors import ERROR_CLASSES, ERROR_CLASSES_CST
29
+ from flake8_async.visitors.visitor4xx import EXCGROUP_ATTRS
30
+
31
+ if sys.version_info < (3, 11):
32
+ from exceptiongroup import ExceptionGroup
29
33
 
30
34
  if TYPE_CHECKING:
31
35
  from collections.abc import Iterable, Sequence
@@ -512,6 +516,7 @@ error_codes_ignored_when_checking_transformed_sync_code = {
512
516
  "ASYNC123",
513
517
  "ASYNC125",
514
518
  "ASYNC300",
519
+ "ASYNC400",
515
520
  "ASYNC912",
516
521
  }
517
522
 
@@ -845,6 +850,13 @@ def test_910_permutations():
845
850
  assert not errors, "# false alarm:\n" + function_str
846
851
 
847
852
 
853
+ def test_async400_excgroup_attributes():
854
+ for attr in dir(ExceptionGroup):
855
+ if attr.startswith("__") and attr.endswith("__"):
856
+ continue
857
+ assert attr in EXCGROUP_ATTRS
858
+
859
+
848
860
  # from https://docs.python.org/3/library/itertools.html#itertools-recipes
849
861
  def consume(iterator: Iterable[Any]):
850
862
  deque(iterator, maxlen=0)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes