python-constricter 0.4.1__cp312-cp312-win32.whl

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 (261) hide show
  1. constricter/__init__.py +55 -0
  2. constricter/__main__.py +26 -0
  3. constricter/_version.py +4 -0
  4. constricter/cli/__init__.py +2 -0
  5. constricter/cli/baseline.py +107 -0
  6. constricter/cli/collecting.py +54 -0
  7. constricter/cli/command.py +547 -0
  8. constricter/cli/config.py +322 -0
  9. constricter/cli/explain.py +104 -0
  10. constricter/cli/lsp/__init__.py +2 -0
  11. constricter/cli/lsp/connection.py +475 -0
  12. constricter/cli/lsp/edits.py +188 -0
  13. constricter/cli/lsp/guard.py +133 -0
  14. constricter/cli/lsp/hints.py +391 -0
  15. constricter/cli/lsp/protocol.py +192 -0
  16. constricter/cli/lsp/remembered.py +81 -0
  17. constricter/cli/lsp/stored.py +200 -0
  18. constricter/cli/options.py +799 -0
  19. constricter/cli/paths.py +151 -0
  20. constricter/cli/reading.py +36 -0
  21. constricter/cli/report.py +397 -0
  22. constricter/cli/rounds.py +328 -0
  23. constricter/cli/runs.py +90 -0
  24. constricter/cli/schedule.py +463 -0
  25. constricter/cli/traced.py +401 -0
  26. constricter/cli/workers.py +286 -0
  27. constricter/empty.py +66 -0
  28. constricter/fix/__init__.cp312-win32.pyd +0 -0
  29. constricter/fix/__init__.py +2 -0
  30. constricter/fix/core/__init__.cp312-win32.pyd +0 -0
  31. constricter/fix/core/__init__.py +2 -0
  32. constricter/fix/core/asked.cp312-win32.pyd +0 -0
  33. constricter/fix/core/asked.py +56 -0
  34. constricter/fix/core/fixes.cp312-win32.pyd +0 -0
  35. constricter/fix/core/fixes.py +317 -0
  36. constricter/fix/core/imports.cp312-win32.pyd +0 -0
  37. constricter/fix/core/imports.py +580 -0
  38. constricter/fix/core/inherited.cp312-win32.pyd +0 -0
  39. constricter/fix/core/inherited.py +311 -0
  40. constricter/fix/core/known.cp312-win32.pyd +0 -0
  41. constricter/fix/core/known.py +730 -0
  42. constricter/fix/core/signatures.cp312-win32.pyd +0 -0
  43. constricter/fix/core/signatures.py +132 -0
  44. constricter/fix/index/__init__.cp312-win32.pyd +0 -0
  45. constricter/fix/index/__init__.py +2 -0
  46. constricter/fix/index/awaits.cp312-win32.pyd +0 -0
  47. constricter/fix/index/awaits.py +90 -0
  48. constricter/fix/index/beyond.cp312-win32.pyd +0 -0
  49. constricter/fix/index/beyond.py +149 -0
  50. constricter/fix/index/callers.cp312-win32.pyd +0 -0
  51. constricter/fix/index/callers.py +155 -0
  52. constricter/fix/index/crossed.cp312-win32.pyd +0 -0
  53. constricter/fix/index/crossed.py +327 -0
  54. constricter/fix/index/decorated.cp312-win32.pyd +0 -0
  55. constricter/fix/index/decorated.py +140 -0
  56. constricter/fix/index/fixtures.cp312-win32.pyd +0 -0
  57. constricter/fix/index/fixtures.py +176 -0
  58. constricter/fix/index/linked.cp312-win32.pyd +0 -0
  59. constricter/fix/index/linked.py +142 -0
  60. constricter/fix/index/loose.cp312-win32.pyd +0 -0
  61. constricter/fix/index/loose.py +103 -0
  62. constricter/fix/index/managed.cp312-win32.pyd +0 -0
  63. constricter/fix/index/managed.py +63 -0
  64. constricter/fix/index/modules.cp312-win32.pyd +0 -0
  65. constricter/fix/index/modules.py +741 -0
  66. constricter/fix/index/offers.cp312-win32.pyd +0 -0
  67. constricter/fix/index/offers.py +252 -0
  68. constricter/fix/index/order.cp312-win32.pyd +0 -0
  69. constricter/fix/index/order.py +155 -0
  70. constricter/fix/index/origins.cp312-win32.pyd +0 -0
  71. constricter/fix/index/origins.py +238 -0
  72. constricter/fix/index/own_methods.cp312-win32.pyd +0 -0
  73. constricter/fix/index/own_methods.py +91 -0
  74. constricter/fix/index/own_overloads.cp312-win32.pyd +0 -0
  75. constricter/fix/index/own_overloads.py +70 -0
  76. constricter/fix/index/own_types.cp312-win32.pyd +0 -0
  77. constricter/fix/index/own_types.py +330 -0
  78. constricter/fix/index/params.cp312-win32.pyd +0 -0
  79. constricter/fix/index/params.py +81 -0
  80. constricter/fix/index/plain.cp312-win32.pyd +0 -0
  81. constricter/fix/index/plain.py +221 -0
  82. constricter/fix/index/project.cp312-win32.pyd +0 -0
  83. constricter/fix/index/project.py +797 -0
  84. constricter/fix/index/sides.cp312-win32.pyd +0 -0
  85. constricter/fix/index/sides.py +160 -0
  86. constricter/fix/index/strings.cp312-win32.pyd +0 -0
  87. constricter/fix/index/strings.py +46 -0
  88. constricter/fix/index/stubs/__init__.cp312-win32.pyd +0 -0
  89. constricter/fix/index/stubs/__init__.py +2 -0
  90. constricter/fix/index/stubs/atoms.cp312-win32.pyd +0 -0
  91. constricter/fix/index/stubs/atoms.py +425 -0
  92. constricter/fix/index/stubs/chained.cp312-win32.pyd +0 -0
  93. constricter/fix/index/stubs/chained.py +69 -0
  94. constricter/fix/index/stubs/classnames.cp312-win32.pyd +0 -0
  95. constricter/fix/index/stubs/classnames.py +124 -0
  96. constricter/fix/index/stubs/declared.cp312-win32.pyd +0 -0
  97. constricter/fix/index/stubs/declared.py +508 -0
  98. constricter/fix/index/stubs/installed.cp312-win32.pyd +0 -0
  99. constricter/fix/index/stubs/installed.py +425 -0
  100. constricter/fix/index/stubs/names.cp312-win32.pyd +0 -0
  101. constricter/fix/index/stubs/names.py +200 -0
  102. constricter/fix/index/stubs/read.cp312-win32.pyd +0 -0
  103. constricter/fix/index/stubs/read.py +70 -0
  104. constricter/fix/index/stubs/signatures.cp312-win32.pyd +0 -0
  105. constricter/fix/index/stubs/signatures.py +656 -0
  106. constricter/fix/index/stubs/stubbed.cp312-win32.pyd +0 -0
  107. constricter/fix/index/stubs/stubbed.py +208 -0
  108. constricter/fix/index/taken.cp312-win32.pyd +0 -0
  109. constricter/fix/index/taken.py +45 -0
  110. constricter/fix/index/tuples.cp312-win32.pyd +0 -0
  111. constricter/fix/index/tuples.py +112 -0
  112. constricter/fix/libraries/__init__.cp312-win32.pyd +0 -0
  113. constricter/fix/libraries/__init__.py +2 -0
  114. constricter/fix/libraries/library.cp312-win32.pyd +0 -0
  115. constricter/fix/libraries/library.py +420 -0
  116. constricter/fix/libraries/opened.cp312-win32.pyd +0 -0
  117. constricter/fix/libraries/opened.py +147 -0
  118. constricter/fix/libraries/overloads.cp312-win32.pyd +0 -0
  119. constricter/fix/libraries/overloads.py +960 -0
  120. constricter/fix/libraries/receivers.cp312-win32.pyd +0 -0
  121. constricter/fix/libraries/receivers.py +274 -0
  122. constricter/fix/libraries/stdlib.cp312-win32.pyd +0 -0
  123. constricter/fix/libraries/stdlib.py +752 -0
  124. constricter/fix/libraries/walks.cp312-win32.pyd +0 -0
  125. constricter/fix/libraries/walks.py +49 -0
  126. constricter/fix/tables/aliases.json +147 -0
  127. constricter/fix/tables/attributes.json +700 -0
  128. constricter/fix/tables/awaited.json +69 -0
  129. constricter/fix/tables/awaited_overloads.json +14 -0
  130. constricter/fix/tables/bases.json +888 -0
  131. constricter/fix/tables/classes.json +1941 -0
  132. constricter/fix/tables/elements.json +156 -0
  133. constricter/fix/tables/functions.json +58 -0
  134. constricter/fix/tables/generic_attributes.json +254 -0
  135. constricter/fix/tables/method_overloads.json +461 -0
  136. constricter/fix/tables/method_signatures.json +954 -0
  137. constricter/fix/tables/methods.json +954 -0
  138. constricter/fix/tables/overloads.json +540 -0
  139. constricter/fix/tables/platform.json +37 -0
  140. constricter/fix/tables/returns.json +1362 -0
  141. constricter/fix/tables/scalar_members.json +16 -0
  142. constricter/fix/tables/scalars.json +2707 -0
  143. constricter/fix/tables/source.json +28 -0
  144. constricter/fix/tables/subscriptable.json +77 -0
  145. constricter/fix/tables/type_parameters.json +178 -0
  146. constricter/fix/tables/variables.json +2962 -0
  147. constricter/fix/values/__init__.cp312-win32.pyd +0 -0
  148. constricter/fix/values/__init__.py +2 -0
  149. constricter/fix/values/aliased.cp312-win32.pyd +0 -0
  150. constricter/fix/values/aliased.py +195 -0
  151. constricter/fix/values/bodies.cp312-win32.pyd +0 -0
  152. constricter/fix/values/bodies.py +76 -0
  153. constricter/fix/values/callables.cp312-win32.pyd +0 -0
  154. constricter/fix/values/callables.py +323 -0
  155. constricter/fix/values/called.cp312-win32.pyd +0 -0
  156. constricter/fix/values/called.py +194 -0
  157. constricter/fix/values/classvars.cp312-win32.pyd +0 -0
  158. constricter/fix/values/classvars.py +629 -0
  159. constricter/fix/values/decided.cp312-win32.pyd +0 -0
  160. constricter/fix/values/decided.py +429 -0
  161. constricter/fix/values/displays.cp312-win32.pyd +0 -0
  162. constricter/fix/values/displays.py +101 -0
  163. constricter/fix/values/doubts.cp312-win32.pyd +0 -0
  164. constricter/fix/values/doubts.py +677 -0
  165. constricter/fix/values/entered.cp312-win32.pyd +0 -0
  166. constricter/fix/values/entered.py +332 -0
  167. constricter/fix/values/fills.cp312-win32.pyd +0 -0
  168. constricter/fix/values/fills.py +446 -0
  169. constricter/fix/values/guesses.cp312-win32.pyd +0 -0
  170. constricter/fix/values/guesses.py +376 -0
  171. constricter/fix/values/hinted.cp312-win32.pyd +0 -0
  172. constricter/fix/values/hinted.py +811 -0
  173. constricter/fix/values/inference.cp312-win32.pyd +0 -0
  174. constricter/fix/values/inference.py +698 -0
  175. constricter/fix/values/loops.cp312-win32.pyd +0 -0
  176. constricter/fix/values/loops.py +335 -0
  177. constricter/fix/values/members.cp312-win32.pyd +0 -0
  178. constricter/fix/values/members.py +578 -0
  179. constricter/fix/values/narrowed.cp312-win32.pyd +0 -0
  180. constricter/fix/values/narrowed.py +220 -0
  181. constricter/fix/values/operated.cp312-win32.pyd +0 -0
  182. constricter/fix/values/operated.py +202 -0
  183. constricter/fix/values/ordered.cp312-win32.pyd +0 -0
  184. constricter/fix/values/ordered.py +74 -0
  185. constricter/fix/values/returned.cp312-win32.pyd +0 -0
  186. constricter/fix/values/returned.py +479 -0
  187. constricter/fix/values/returns.cp312-win32.pyd +0 -0
  188. constricter/fix/values/returns.py +237 -0
  189. constricter/fix/values/shapes.cp312-win32.pyd +0 -0
  190. constricter/fix/values/shapes.py +654 -0
  191. constricter/fix/values/stored.cp312-win32.pyd +0 -0
  192. constricter/fix/values/stored.py +564 -0
  193. constricter/fix/values/targets.cp312-win32.pyd +0 -0
  194. constricter/fix/values/targets.py +419 -0
  195. constricter/fix/values/unchecked.cp312-win32.pyd +0 -0
  196. constricter/fix/values/unchecked.py +278 -0
  197. constricter/jsonc.py +50 -0
  198. constricter/noqa.py +47 -0
  199. constricter/notebook.py +115 -0
  200. constricter/offences.py +436 -0
  201. constricter/overrides.py +24 -0
  202. constricter/plugins/__init__.py +2 -0
  203. constricter/plugins/flake8.py +133 -0
  204. constricter/plugins/pylint.py +186 -0
  205. constricter/py.typed +0 -0
  206. constricter/recording.py +660 -0
  207. constricter/rules/__init__.cp312-win32.pyd +0 -0
  208. constricter/rules/__init__.py +2 -0
  209. constricter/rules/annotations.cp312-win32.pyd +0 -0
  210. constricter/rules/annotations.py +296 -0
  211. constricter/rules/binding.cp312-win32.pyd +0 -0
  212. constricter/rules/binding.py +652 -0
  213. constricter/rules/calls.cp312-win32.pyd +0 -0
  214. constricter/rules/calls.py +460 -0
  215. constricter/rules/checker.cp312-win32.pyd +0 -0
  216. constricter/rules/checker.py +536 -0
  217. constricter/rules/declarations.cp312-win32.pyd +0 -0
  218. constricter/rules/declarations.py +730 -0
  219. constricter/rules/decorators.cp312-win32.pyd +0 -0
  220. constricter/rules/decorators.py +351 -0
  221. constricter/rules/flow.cp312-win32.pyd +0 -0
  222. constricter/rules/flow.py +502 -0
  223. constricter/rules/keys.cp312-win32.pyd +0 -0
  224. constricter/rules/keys.py +79 -0
  225. constricter/rules/late.cp312-win32.pyd +0 -0
  226. constricter/rules/late.py +969 -0
  227. constricter/rules/narrowing.cp312-win32.pyd +0 -0
  228. constricter/rules/narrowing.py +98 -0
  229. constricter/rules/parsed.cp312-win32.pyd +0 -0
  230. constricter/rules/parsed.py +136 -0
  231. constricter/rules/quoted.cp312-win32.pyd +0 -0
  232. constricter/rules/quoted.py +141 -0
  233. constricter/rules/rebinding.cp312-win32.pyd +0 -0
  234. constricter/rules/rebinding.py +169 -0
  235. constricter/rules/recorded.cp312-win32.pyd +0 -0
  236. constricter/rules/recorded.py +168 -0
  237. constricter/rules/redundant.cp312-win32.pyd +0 -0
  238. constricter/rules/redundant.py +160 -0
  239. constricter/rules/scope.cp312-win32.pyd +0 -0
  240. constricter/rules/scope.py +802 -0
  241. constricter/rules/scoped.cp312-win32.pyd +0 -0
  242. constricter/rules/scoped.py +232 -0
  243. constricter/rules/scoping.cp312-win32.pyd +0 -0
  244. constricter/rules/scoping.py +346 -0
  245. constricter/rules/settings.cp312-win32.pyd +0 -0
  246. constricter/rules/settings.py +189 -0
  247. constricter/rules/syntax.cp312-win32.pyd +0 -0
  248. constricter/rules/syntax.py +472 -0
  249. constricter/rules/tables.cp312-win32.pyd +0 -0
  250. constricter/rules/tables.py +169 -0
  251. constricter/rules/walked.cp312-win32.pyd +0 -0
  252. constricter/rules/walked.py +127 -0
  253. constricter/rules/widened.cp312-win32.pyd +0 -0
  254. constricter/rules/widened.py +322 -0
  255. constricter/trace.py +374 -0
  256. e45ce06b2fdd5c43f782__mypyc.cp312-win32.pyd +0 -0
  257. python_constricter-0.4.1.dist-info/METADATA +400 -0
  258. python_constricter-0.4.1.dist-info/RECORD +261 -0
  259. python_constricter-0.4.1.dist-info/WHEEL +4 -0
  260. python_constricter-0.4.1.dist-info/entry_points.txt +5 -0
  261. python_constricter-0.4.1.dist-info/licenses/LICENSE.md +21 -0
@@ -0,0 +1,168 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """What a finished function's scope says its `return`s, `yield`s and stores to `self`'s attributes give.
3
+
4
+ Recorded for `constricter.fix.values.returned`, which types the function's calls and its class's attributes
5
+ from them.
6
+ """
7
+
8
+ import ast
9
+ from typing import TYPE_CHECKING, Final
10
+
11
+ from constricter.fix.core.known import ImportPlan, Inference
12
+ from constricter.fix.values import bodies, entered, fills, stored
13
+ from constricter.fix.values.inference import RETURNED, inference
14
+ from constricter.fix.values.loops import looped
15
+ from constricter.fix.values.targets import iterated
16
+ from constricter.rules.scope import Scope, guesses_in
17
+ from constricter.rules.syntax import FunctionDef, own_nodes
18
+ from constricter.rules.walked import walk
19
+
20
+ if TYPE_CHECKING:
21
+ from collections.abc import Mapping
22
+
23
+ _GENERATOR: Final = "collections.abc.Generator"
24
+
25
+
26
+ def returns(scope: Scope, func: FunctionDef, module: ast.Module) -> list[stored.Recorded]:
27
+ """Record a finished function's `return` statements; a generator's one type instead (see `_generator`).
28
+
29
+ `module`: the function's, whose `yield`s say which functions to look in for one.
30
+
31
+ Returns:
32
+ Each one's value.
33
+
34
+ """
35
+ if isinstance(func, ast.FunctionDef) and bodies.is_generator(module, func):
36
+ return _generator(scope, func, bare=entered.is_manager(func))
37
+ return [
38
+ (None, frozenset()) if value is None else _recorded(scope, value) for value in scope.inferred.returns
39
+ ]
40
+
41
+
42
+ def assigned(scope: Scope) -> list[stored.Assigned]:
43
+ """Record a finished function's `self.x = value` assignments, each value as a `return`'s is.
44
+
45
+ But a value reading a local bound more than once is unknown: the scope's type for it is its last
46
+ binding's, not what reaches the assignment (`x = None`, `if c: x = 1`, then `self.x = x`).
47
+
48
+ Returns:
49
+ Each attribute, and its value's inference and guesses.
50
+
51
+ """
52
+ return [
53
+ (attr, (None, frozenset()) if _rebound_in(scope, value) else _recorded(scope, value))
54
+ for attr, value in scope.inferred.assigned
55
+ ]
56
+
57
+
58
+ def used(scope: Scope, func: FunctionDef, module: ast.Module) -> list[stored.Used]:
59
+ """Record a finished method's reads of the attributes its class binds to an empty container.
60
+
61
+ Each as `fills.stored` judges it; what one adds is typed as an assigned value is (see `assigned`).
62
+
63
+ Returns:
64
+ Each attribute, and what the read adds to it.
65
+
66
+ """
67
+ kinds: Mapping[str, str]
68
+ if not (kinds := stored.empties(module, func)):
69
+ return []
70
+ return [
71
+ (attr, _added(scope, use, kinds[attr]) if isinstance(use, fills.Fill) else (() if use else None))
72
+ for attr, use in fills.stored(func.body, kinds)
73
+ ]
74
+
75
+
76
+ def _added(scope: Scope, use: fills.Fill, kind: str) -> stored.Added:
77
+ """Record what one fill of an empty container's attribute adds to it.
78
+
79
+ Returns:
80
+ Each part's inference and guesses (a `dict`'s key and value, any other's element).
81
+
82
+ """
83
+ values: list[ast.expr] = (
84
+ iterated(use.value) if use.spread else [part for part in (use.key, use.value) if part is not None]
85
+ )
86
+ if any(_rebound_in(scope, value) for value in values):
87
+ return ((None, frozenset()),)
88
+ origins: frozenset[str] = guesses_in(scope, values)[1]
89
+ return tuple(
90
+ (part, origins) for part in fills.added(use, kind, scope.settings.known, scope.inferred.types)
91
+ )
92
+
93
+
94
+ def _recorded(scope: Scope, value: ast.expr) -> stored.Recorded:
95
+ """Record a value as its finished function's scope sees it.
96
+
97
+ Returns:
98
+ Its inference (`None` for an unknown value), and what that rests on if it's a guess.
99
+
100
+ """
101
+ found: Inference | None = inference(value, scope.settings.known, scope.inferred.types)
102
+ # What it rests on counts only for a typed value (see `returned`).
103
+ return found, frozenset() if found is None else guesses_in(scope, [value])[1]
104
+
105
+
106
+ def _generator(scope: Scope, func: ast.FunctionDef, *, bare: bool = False) -> list[stored.Recorded]:
107
+ """Record what calling a generator function gives, if its `yield`s decide it.
108
+
109
+ Each a statement of its own (nothing is sent in), all of one type `T`, and no `return` of a
110
+ value: a `Generator[T, None, None]`, named as the module can; `bare`: `T` itself, what `with`
111
+ gives of one `@contextmanager` makes a manager.
112
+
113
+ Returns:
114
+ That type, as a lone `return`'s value; or nothing.
115
+
116
+ """
117
+ nodes: list[ast.AST] = list(own_nodes(func.body))
118
+ statements: set[int] = {id(node.value) for node in nodes if isinstance(node, ast.Expr)}
119
+ yields: list[ast.Yield | ast.YieldFrom] = [
120
+ node for node in nodes if isinstance(node, ast.Yield | ast.YieldFrom)
121
+ ]
122
+ if any(value is not None for value in scope.inferred.returns) or any(
123
+ id(node) not in statements for node in yields
124
+ ):
125
+ return []
126
+ found: list[stored.Recorded] = [_yielded(scope, node) for node in yields]
127
+ types: set[str | None] = {None if part is None else part.annotation for part, _ in found}
128
+ element: str | None = next(iter(types)) if len(types) == 1 else None
129
+ plan: ImportPlan | None = scope.settings.known.names.plan
130
+ spelled: str | None = None
131
+ if not bare and plan is not None and element is not None:
132
+ spelled = plan.spell(_GENERATOR)
133
+ if element is None or not (bare or spelled):
134
+ return []
135
+ text: str = element if bare else f"{spelled}[{element}, None, None]"
136
+ typed: Inference = Inference(text, "its `yield`s", frozenset({RETURNED}))
137
+ return [(typed, frozenset[str]().union(*(origins for _, origins in found)))]
138
+
139
+
140
+ def _yielded(scope: Scope, node: ast.Yield | ast.YieldFrom) -> stored.Recorded:
141
+ """Record what one `yield` gives: its value, or (`yield from`) each element of it.
142
+
143
+ Unknown for a bare `yield`, and a value reading a local bound more than once (see `assigned`).
144
+
145
+ Returns:
146
+ Its inference, and what that rests on if it's a guess.
147
+
148
+ """
149
+ value: ast.expr | None = node.value
150
+ if value is None or _rebound_in(scope, value):
151
+ return None, frozenset()
152
+ if isinstance(node, ast.Yield):
153
+ return _recorded(scope, value)
154
+ found: Inference | None = looped(value, scope.settings.known, scope.inferred.types, inference)
155
+ return found, frozenset() if found is None else guesses_in(scope, iterated(value))[1]
156
+
157
+
158
+ def _rebound_in(scope: Scope, value: ast.expr) -> bool:
159
+ """Check whether `value` reads a local of `scope` bound more than once.
160
+
161
+ Returns:
162
+ Whether it does.
163
+
164
+ """
165
+ return any(
166
+ isinstance(node, ast.Name) and node.id in scope.flow and len(scope.flow[node.id].bindings) > 1
167
+ for node in walk(value)
168
+ )
@@ -0,0 +1,160 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """LVA007: a name annotated again with the type it already has, in the same straight-line block."""
3
+
4
+ import ast
5
+ from collections.abc import Callable, Iterator
6
+ from typing import Final, cast
7
+
8
+ from constricter.offences import REDUNDANT_TYPE, Edit, Fix, FixPolicy, Offence, at
9
+ from constricter.rules.syntax import BRANCHING, child_statements
10
+ from constricter.rules.walked import classes, of_type
11
+
12
+ _KINDS: Final = frozenset({"redundant"})
13
+ # The nodes with blocks of statements: all `_blocks` finds any in.
14
+ _BLOCKED: Final = (
15
+ ast.Module,
16
+ ast.FunctionDef,
17
+ ast.AsyncFunctionDef,
18
+ ast.ClassDef,
19
+ ast.With,
20
+ ast.AsyncWith,
21
+ *BRANCHING,
22
+ )
23
+
24
+
25
+ def redundant(tree: ast.Module, policy: FixPolicy | None = None) -> list[Offence]:
26
+ """Find a name annotated again with the type it already has, in the same straight-line block.
27
+
28
+ Every block (a function, module or class body; an `if`'s body and its `orelse`; ...) is checked
29
+ on its own: two branches that never run in the same pass typing a name the same way isn't
30
+ redundant, so they're not compared against each other. Each gets a fix, if `policy` allows it,
31
+ dropping the annotation (see `_dropped`).
32
+
33
+ Returns:
34
+ One offence (LVA007) per redundant re-annotation.
35
+
36
+ """
37
+ offences: list[Offence] = []
38
+ node: ast.AST
39
+ block: list[ast.stmt]
40
+ fixing: bool = (policy or FixPolicy()).allows(_KINDS)
41
+ in_class: set[int] = _class_level(tree)
42
+ for node in of_type(tree, *_BLOCKED):
43
+ for block in _blocks(node):
44
+ offences += _redundant_in(block, lambda stmt: fixing and id(stmt) not in in_class)
45
+ return offences
46
+
47
+
48
+ def _class_level(tree: ast.Module) -> set[int]:
49
+ """Find the statements of every class body, through `if`/`try`/..., not in its methods.
50
+
51
+ `--fix` never edits them: a dataclass's annotations are its fields.
52
+
53
+ Returns:
54
+ Their `id`s.
55
+
56
+ """
57
+ return {id(stmt) for node in classes(tree) for stmt in _nested(node.body)}
58
+
59
+
60
+ def _nested(body: list[ast.stmt]) -> Iterator[ast.stmt]:
61
+ """Walk `body`'s statements, and theirs through `if`/`try`/... (not a function's or class's).
62
+
63
+ Yields:
64
+ Each statement.
65
+
66
+ """
67
+ stmt: ast.stmt
68
+ for stmt in body:
69
+ yield stmt
70
+ yield from _nested(child_statements(stmt))
71
+
72
+
73
+ def _blocks(node: ast.AST) -> Iterator[list[ast.stmt]]:
74
+ """Find the straight-line blocks of statements directly in `node`.
75
+
76
+ Yields:
77
+ Each one (an `if`'s body and its `orelse` separately, and likewise for the other compound
78
+ statements with more than one: they run in different passes, if at all).
79
+
80
+ """
81
+ body: list[ast.stmt]
82
+ orelse: list[ast.stmt]
83
+ handlers: list[ast.ExceptHandler]
84
+ finalbody: list[ast.stmt]
85
+ handler: ast.ExceptHandler
86
+ case: ast.match_case
87
+ match node:
88
+ case (
89
+ ast.Module(body=body)
90
+ | ast.FunctionDef(body=body)
91
+ | ast.AsyncFunctionDef(body=body)
92
+ | ast.ClassDef(body=body)
93
+ | ast.With(body=body)
94
+ | ast.AsyncWith(body=body)
95
+ ):
96
+ yield body
97
+ case (
98
+ ast.If(body=body, orelse=orelse)
99
+ | ast.For(body=body, orelse=orelse)
100
+ | ast.AsyncFor(body=body, orelse=orelse)
101
+ | ast.While(body=body, orelse=orelse)
102
+ ):
103
+ yield body
104
+ yield orelse # empty when there's no `else`, which is harmless: nothing to find in it
105
+ case (
106
+ ast.Try(body=body, handlers=handlers, orelse=orelse, finalbody=finalbody)
107
+ | ast.TryStar(body=body, handlers=handlers, orelse=orelse, finalbody=finalbody)
108
+ ):
109
+ yield body
110
+ for handler in handlers:
111
+ yield handler.body
112
+ yield orelse
113
+ yield finalbody
114
+ case _: # a `match`: the last of the kinds with blocks (`_BLOCKED`), the only ones asked about
115
+ for case in cast("ast.Match", node).cases:
116
+ yield case.body
117
+
118
+
119
+ def _redundant_in(block: list[ast.stmt], fixable: Callable[[ast.stmt], bool]) -> list[Offence]:
120
+ """Find a name in `block` annotated the same way twice.
121
+
122
+ Returns:
123
+ One offence per repeat, at the later statement, with a fix where `fixable` says so.
124
+
125
+ """
126
+ offences: list[Offence] = []
127
+ seen: dict[str, str] = {}
128
+ stmt: ast.stmt
129
+ name: str
130
+ annotation: ast.expr
131
+ for stmt in block:
132
+ match stmt:
133
+ case ast.AnnAssign(target=ast.Name(id=name), annotation=annotation):
134
+ text: str = ast.unparse(annotation)
135
+ if seen.get(name) == text:
136
+ edit: Fix | None = _dropped(stmt) if fixable(stmt) else None
137
+ offences.append(Offence(*at(stmt), name, REDUNDANT_TYPE, edit))
138
+ seen[name] = text
139
+ case _:
140
+ pass
141
+ return offences
142
+
143
+
144
+ def _dropped(stmt: ast.AnnAssign) -> Fix | None:
145
+ """Drop a repeated annotation: `x: int = 2` becomes `x = 2`.
146
+
147
+ Returns:
148
+ The fix (writing nothing over `: int`), or `None` for a bare declaration (`x: int`, whose
149
+ whole statement would have to go) or one spread over lines.
150
+
151
+ """
152
+ if stmt.value is None or stmt.annotation.end_lineno != stmt.target.lineno:
153
+ return None
154
+ return Fix(
155
+ "",
156
+ "the type it already has",
157
+ edit=Edit.REPLACE,
158
+ span=(stmt.target.end_col_offset or 0, stmt.annotation.end_col_offset or 0),
159
+ kinds=_KINDS,
160
+ )
Binary file