python-constricter 0.4.1__cp313-cp313-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__.cp313-win32.pyd +0 -0
  29. constricter/fix/__init__.py +2 -0
  30. constricter/fix/core/__init__.cp313-win32.pyd +0 -0
  31. constricter/fix/core/__init__.py +2 -0
  32. constricter/fix/core/asked.cp313-win32.pyd +0 -0
  33. constricter/fix/core/asked.py +56 -0
  34. constricter/fix/core/fixes.cp313-win32.pyd +0 -0
  35. constricter/fix/core/fixes.py +317 -0
  36. constricter/fix/core/imports.cp313-win32.pyd +0 -0
  37. constricter/fix/core/imports.py +580 -0
  38. constricter/fix/core/inherited.cp313-win32.pyd +0 -0
  39. constricter/fix/core/inherited.py +311 -0
  40. constricter/fix/core/known.cp313-win32.pyd +0 -0
  41. constricter/fix/core/known.py +730 -0
  42. constricter/fix/core/signatures.cp313-win32.pyd +0 -0
  43. constricter/fix/core/signatures.py +132 -0
  44. constricter/fix/index/__init__.cp313-win32.pyd +0 -0
  45. constricter/fix/index/__init__.py +2 -0
  46. constricter/fix/index/awaits.cp313-win32.pyd +0 -0
  47. constricter/fix/index/awaits.py +90 -0
  48. constricter/fix/index/beyond.cp313-win32.pyd +0 -0
  49. constricter/fix/index/beyond.py +149 -0
  50. constricter/fix/index/callers.cp313-win32.pyd +0 -0
  51. constricter/fix/index/callers.py +155 -0
  52. constricter/fix/index/crossed.cp313-win32.pyd +0 -0
  53. constricter/fix/index/crossed.py +327 -0
  54. constricter/fix/index/decorated.cp313-win32.pyd +0 -0
  55. constricter/fix/index/decorated.py +140 -0
  56. constricter/fix/index/fixtures.cp313-win32.pyd +0 -0
  57. constricter/fix/index/fixtures.py +176 -0
  58. constricter/fix/index/linked.cp313-win32.pyd +0 -0
  59. constricter/fix/index/linked.py +142 -0
  60. constricter/fix/index/loose.cp313-win32.pyd +0 -0
  61. constricter/fix/index/loose.py +103 -0
  62. constricter/fix/index/managed.cp313-win32.pyd +0 -0
  63. constricter/fix/index/managed.py +63 -0
  64. constricter/fix/index/modules.cp313-win32.pyd +0 -0
  65. constricter/fix/index/modules.py +741 -0
  66. constricter/fix/index/offers.cp313-win32.pyd +0 -0
  67. constricter/fix/index/offers.py +252 -0
  68. constricter/fix/index/order.cp313-win32.pyd +0 -0
  69. constricter/fix/index/order.py +155 -0
  70. constricter/fix/index/origins.cp313-win32.pyd +0 -0
  71. constricter/fix/index/origins.py +238 -0
  72. constricter/fix/index/own_methods.cp313-win32.pyd +0 -0
  73. constricter/fix/index/own_methods.py +91 -0
  74. constricter/fix/index/own_overloads.cp313-win32.pyd +0 -0
  75. constricter/fix/index/own_overloads.py +70 -0
  76. constricter/fix/index/own_types.cp313-win32.pyd +0 -0
  77. constricter/fix/index/own_types.py +330 -0
  78. constricter/fix/index/params.cp313-win32.pyd +0 -0
  79. constricter/fix/index/params.py +81 -0
  80. constricter/fix/index/plain.cp313-win32.pyd +0 -0
  81. constricter/fix/index/plain.py +221 -0
  82. constricter/fix/index/project.cp313-win32.pyd +0 -0
  83. constricter/fix/index/project.py +797 -0
  84. constricter/fix/index/sides.cp313-win32.pyd +0 -0
  85. constricter/fix/index/sides.py +160 -0
  86. constricter/fix/index/strings.cp313-win32.pyd +0 -0
  87. constricter/fix/index/strings.py +46 -0
  88. constricter/fix/index/stubs/__init__.cp313-win32.pyd +0 -0
  89. constricter/fix/index/stubs/__init__.py +2 -0
  90. constricter/fix/index/stubs/atoms.cp313-win32.pyd +0 -0
  91. constricter/fix/index/stubs/atoms.py +425 -0
  92. constricter/fix/index/stubs/chained.cp313-win32.pyd +0 -0
  93. constricter/fix/index/stubs/chained.py +69 -0
  94. constricter/fix/index/stubs/classnames.cp313-win32.pyd +0 -0
  95. constricter/fix/index/stubs/classnames.py +124 -0
  96. constricter/fix/index/stubs/declared.cp313-win32.pyd +0 -0
  97. constricter/fix/index/stubs/declared.py +508 -0
  98. constricter/fix/index/stubs/installed.cp313-win32.pyd +0 -0
  99. constricter/fix/index/stubs/installed.py +425 -0
  100. constricter/fix/index/stubs/names.cp313-win32.pyd +0 -0
  101. constricter/fix/index/stubs/names.py +200 -0
  102. constricter/fix/index/stubs/read.cp313-win32.pyd +0 -0
  103. constricter/fix/index/stubs/read.py +70 -0
  104. constricter/fix/index/stubs/signatures.cp313-win32.pyd +0 -0
  105. constricter/fix/index/stubs/signatures.py +656 -0
  106. constricter/fix/index/stubs/stubbed.cp313-win32.pyd +0 -0
  107. constricter/fix/index/stubs/stubbed.py +208 -0
  108. constricter/fix/index/taken.cp313-win32.pyd +0 -0
  109. constricter/fix/index/taken.py +45 -0
  110. constricter/fix/index/tuples.cp313-win32.pyd +0 -0
  111. constricter/fix/index/tuples.py +112 -0
  112. constricter/fix/libraries/__init__.cp313-win32.pyd +0 -0
  113. constricter/fix/libraries/__init__.py +2 -0
  114. constricter/fix/libraries/library.cp313-win32.pyd +0 -0
  115. constricter/fix/libraries/library.py +420 -0
  116. constricter/fix/libraries/opened.cp313-win32.pyd +0 -0
  117. constricter/fix/libraries/opened.py +147 -0
  118. constricter/fix/libraries/overloads.cp313-win32.pyd +0 -0
  119. constricter/fix/libraries/overloads.py +960 -0
  120. constricter/fix/libraries/receivers.cp313-win32.pyd +0 -0
  121. constricter/fix/libraries/receivers.py +274 -0
  122. constricter/fix/libraries/stdlib.cp313-win32.pyd +0 -0
  123. constricter/fix/libraries/stdlib.py +752 -0
  124. constricter/fix/libraries/walks.cp313-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__.cp313-win32.pyd +0 -0
  148. constricter/fix/values/__init__.py +2 -0
  149. constricter/fix/values/aliased.cp313-win32.pyd +0 -0
  150. constricter/fix/values/aliased.py +195 -0
  151. constricter/fix/values/bodies.cp313-win32.pyd +0 -0
  152. constricter/fix/values/bodies.py +76 -0
  153. constricter/fix/values/callables.cp313-win32.pyd +0 -0
  154. constricter/fix/values/callables.py +323 -0
  155. constricter/fix/values/called.cp313-win32.pyd +0 -0
  156. constricter/fix/values/called.py +194 -0
  157. constricter/fix/values/classvars.cp313-win32.pyd +0 -0
  158. constricter/fix/values/classvars.py +629 -0
  159. constricter/fix/values/decided.cp313-win32.pyd +0 -0
  160. constricter/fix/values/decided.py +429 -0
  161. constricter/fix/values/displays.cp313-win32.pyd +0 -0
  162. constricter/fix/values/displays.py +101 -0
  163. constricter/fix/values/doubts.cp313-win32.pyd +0 -0
  164. constricter/fix/values/doubts.py +677 -0
  165. constricter/fix/values/entered.cp313-win32.pyd +0 -0
  166. constricter/fix/values/entered.py +332 -0
  167. constricter/fix/values/fills.cp313-win32.pyd +0 -0
  168. constricter/fix/values/fills.py +446 -0
  169. constricter/fix/values/guesses.cp313-win32.pyd +0 -0
  170. constricter/fix/values/guesses.py +376 -0
  171. constricter/fix/values/hinted.cp313-win32.pyd +0 -0
  172. constricter/fix/values/hinted.py +811 -0
  173. constricter/fix/values/inference.cp313-win32.pyd +0 -0
  174. constricter/fix/values/inference.py +698 -0
  175. constricter/fix/values/loops.cp313-win32.pyd +0 -0
  176. constricter/fix/values/loops.py +335 -0
  177. constricter/fix/values/members.cp313-win32.pyd +0 -0
  178. constricter/fix/values/members.py +578 -0
  179. constricter/fix/values/narrowed.cp313-win32.pyd +0 -0
  180. constricter/fix/values/narrowed.py +220 -0
  181. constricter/fix/values/operated.cp313-win32.pyd +0 -0
  182. constricter/fix/values/operated.py +202 -0
  183. constricter/fix/values/ordered.cp313-win32.pyd +0 -0
  184. constricter/fix/values/ordered.py +74 -0
  185. constricter/fix/values/returned.cp313-win32.pyd +0 -0
  186. constricter/fix/values/returned.py +479 -0
  187. constricter/fix/values/returns.cp313-win32.pyd +0 -0
  188. constricter/fix/values/returns.py +237 -0
  189. constricter/fix/values/shapes.cp313-win32.pyd +0 -0
  190. constricter/fix/values/shapes.py +654 -0
  191. constricter/fix/values/stored.cp313-win32.pyd +0 -0
  192. constricter/fix/values/stored.py +564 -0
  193. constricter/fix/values/targets.cp313-win32.pyd +0 -0
  194. constricter/fix/values/targets.py +419 -0
  195. constricter/fix/values/unchecked.cp313-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__.cp313-win32.pyd +0 -0
  208. constricter/rules/__init__.py +2 -0
  209. constricter/rules/annotations.cp313-win32.pyd +0 -0
  210. constricter/rules/annotations.py +296 -0
  211. constricter/rules/binding.cp313-win32.pyd +0 -0
  212. constricter/rules/binding.py +652 -0
  213. constricter/rules/calls.cp313-win32.pyd +0 -0
  214. constricter/rules/calls.py +460 -0
  215. constricter/rules/checker.cp313-win32.pyd +0 -0
  216. constricter/rules/checker.py +536 -0
  217. constricter/rules/declarations.cp313-win32.pyd +0 -0
  218. constricter/rules/declarations.py +730 -0
  219. constricter/rules/decorators.cp313-win32.pyd +0 -0
  220. constricter/rules/decorators.py +351 -0
  221. constricter/rules/flow.cp313-win32.pyd +0 -0
  222. constricter/rules/flow.py +502 -0
  223. constricter/rules/keys.cp313-win32.pyd +0 -0
  224. constricter/rules/keys.py +79 -0
  225. constricter/rules/late.cp313-win32.pyd +0 -0
  226. constricter/rules/late.py +969 -0
  227. constricter/rules/narrowing.cp313-win32.pyd +0 -0
  228. constricter/rules/narrowing.py +98 -0
  229. constricter/rules/parsed.cp313-win32.pyd +0 -0
  230. constricter/rules/parsed.py +136 -0
  231. constricter/rules/quoted.cp313-win32.pyd +0 -0
  232. constricter/rules/quoted.py +141 -0
  233. constricter/rules/rebinding.cp313-win32.pyd +0 -0
  234. constricter/rules/rebinding.py +169 -0
  235. constricter/rules/recorded.cp313-win32.pyd +0 -0
  236. constricter/rules/recorded.py +168 -0
  237. constricter/rules/redundant.cp313-win32.pyd +0 -0
  238. constricter/rules/redundant.py +160 -0
  239. constricter/rules/scope.cp313-win32.pyd +0 -0
  240. constricter/rules/scope.py +802 -0
  241. constricter/rules/scoped.cp313-win32.pyd +0 -0
  242. constricter/rules/scoped.py +232 -0
  243. constricter/rules/scoping.cp313-win32.pyd +0 -0
  244. constricter/rules/scoping.py +346 -0
  245. constricter/rules/settings.cp313-win32.pyd +0 -0
  246. constricter/rules/settings.py +189 -0
  247. constricter/rules/syntax.cp313-win32.pyd +0 -0
  248. constricter/rules/syntax.py +472 -0
  249. constricter/rules/tables.cp313-win32.pyd +0 -0
  250. constricter/rules/tables.py +169 -0
  251. constricter/rules/walked.cp313-win32.pyd +0 -0
  252. constricter/rules/walked.py +127 -0
  253. constricter/rules/widened.cp313-win32.pyd +0 -0
  254. constricter/rules/widened.py +322 -0
  255. constricter/trace.py +374 -0
  256. e45ce06b2fdd5c43f782__mypyc.cp313-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,169 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """A module's own tables `--fix` reads, read once for the cross-file index and the check."""
3
+
4
+ import ast
5
+ from collections import Counter
6
+ from collections.abc import Mapping
7
+ from typing import Final, NamedTuple
8
+
9
+ from constricter.fix.core.imports import rebound_names, taken_names
10
+ from constricter.fix.core.inherited import Lineage, lineage
11
+ from constricter.fix.core.signatures import AWAIT
12
+ from constricter.fix.values.targets import aliased_unions, named_tuples
13
+ from constricter.rules import walked
14
+ from constricter.rules.declarations import (
15
+ class_methods,
16
+ classes,
17
+ held,
18
+ held_class_methods,
19
+ method_returns,
20
+ partial_method_returns,
21
+ partial_returns,
22
+ returns,
23
+ self_returns,
24
+ )
25
+ from constricter.rules.decorators import Held, Pass, passes
26
+ from constricter.rules.syntax import import_bindings
27
+
28
+ _NEW_TYPES: Final = frozenset({"typing.NewType", "typing_extensions.NewType"})
29
+
30
+
31
+ class Tables(NamedTuple):
32
+ """A module's own tables (see `returns`, `classes`, `method_returns`, `class_methods`).
33
+
34
+ Its functions' declared returns (a `NewType` bound nowhere else returning itself: `Ref(name)` is
35
+ a `Ref`), and its classes' attributes and methods' returns (their classmethods' and
36
+ staticmethods' too, which an instance has as well; an `async` method's as what awaiting its call
37
+ gives, its name after `signatures.AWAIT`), the attributes each takes from the module's
38
+ other classes included (see `Lineage`); `held`, the functions other modules'
39
+ decorators may give back, and `passes`, its own such decorators; `sides`, its classes'
40
+ classmethods' and staticmethods' returns, those each takes from the module's other classes
41
+ included (see `Lineage`), and `held_sides`, those other modules' decorators may give back;
42
+ `side_calls`, `sides` as called on the class (see `_side_calls`). `partial` and
43
+ `partial_methods`: its functions' and methods' returns that are tuples with a vague part.
44
+ `tuples`: its named tuples, each with the tuple unpacking one gives (see `named_tuples`).
45
+ `unions`: its type aliases of a union, each with the union (see `aliased_unions`).
46
+ """
47
+
48
+ returns: dict[str, str]
49
+ classes: dict[str, dict[str, str]]
50
+ methods: dict[str, dict[str, str]]
51
+ held: dict[str, Held]
52
+ passes: dict[str, Pass]
53
+ sides: dict[str, dict[str, str]]
54
+ held_sides: dict[str, dict[str, Held]]
55
+ side_calls: dict[str, str]
56
+ partial: dict[str, str]
57
+ partial_methods: dict[str, dict[str, str]]
58
+ order: Lineage # its classes' ancestry, as far as the module alone sees
59
+ tuples: dict[str, str]
60
+ unions: dict[str, str]
61
+
62
+
63
+ def module_tables(tree: ast.Module) -> Tables:
64
+ """Read the module's own tables, once for the cross-file index and the check (see `parsed.keep`).
65
+
66
+ Returns:
67
+ Them.
68
+
69
+ """
70
+ own: dict[str, dict[str, str]] = class_methods(tree)
71
+ order: Lineage = lineage(tree, self_returns(tree), frozenset())
72
+ sides: dict[str, dict[str, str]] = order.flattened(own)
73
+ awaited: dict[str, dict[str, str]] = method_returns(tree, awaited=True)
74
+ made: list[str] = _new_types(tree)
75
+ rebound: frozenset[str] = frozenset(rebound_names(tree)) if made else frozenset()
76
+ return Tables(
77
+ {**returns(tree), **{name: name for name in made if name not in rebound}},
78
+ order.flattened(classes(tree)),
79
+ {
80
+ owner: {
81
+ **own.get(owner, {}),
82
+ **methods,
83
+ **{AWAIT + name: each for name, each in awaited[owner].items()},
84
+ }
85
+ for owner, methods in method_returns(tree).items()
86
+ },
87
+ held(tree),
88
+ passes(tree),
89
+ sides,
90
+ held_class_methods(tree),
91
+ _side_calls(tree, sides),
92
+ partial_returns(tree),
93
+ partial_method_returns(tree),
94
+ order,
95
+ named_tuples(tree),
96
+ aliased_unions(tree),
97
+ )
98
+
99
+
100
+ def _side_calls(tree: ast.Module, sides: Mapping[str, Mapping[str, str]]) -> dict[str, str]:
101
+ """Spell the calls to the module's classes' classmethods and staticmethods on the class itself.
102
+
103
+ `Box.make()`, for a top-level class defined once whose name nothing in the module binds as a
104
+ value (see `imports.taken_names`): there, the name is the class.
105
+
106
+ Returns:
107
+ Each call's name as written (`Box.make`), and its declared return.
108
+
109
+ """
110
+ if not any(sides.values()): # most modules: no class-side method declares a return
111
+ return {}
112
+ values: frozenset[str] = taken_names(tree)[1]
113
+ counts: Counter[str] = Counter(node.name for node in walked.classes(tree))
114
+ return {
115
+ f"{stmt.name}.{name}": annotation
116
+ for stmt in tree.body
117
+ if isinstance(stmt, ast.ClassDef) and counts[stmt.name] == 1 and stmt.name not in values
118
+ for name, annotation in sides.get(stmt.name, {}).items()
119
+ }
120
+
121
+
122
+ def _new_types(tree: ast.Module) -> list[str]:
123
+ """Name the module's `NewType`s: `Ref = NewType("Ref", str)`, at its top level.
124
+
125
+ By `typing`'s or `typing_extensions`' `NewType`, however it's imported.
126
+
127
+ Returns:
128
+ Them.
129
+
130
+ """
131
+ made: list[tuple[str, ast.Name | ast.Attribute]] = []
132
+ stmt: ast.stmt
133
+ name: str
134
+ func: ast.expr
135
+ text: str
136
+ for stmt in tree.body:
137
+ match stmt:
138
+ case ast.Assign(
139
+ targets=[ast.Name(id=name)],
140
+ value=ast.Call(
141
+ func=ast.Name() | ast.Attribute() as func,
142
+ args=[ast.Constant(value=str() as text), _],
143
+ ),
144
+ ) if text == name:
145
+ made.append((name, func))
146
+ case _:
147
+ pass
148
+ if not made: # most modules
149
+ return []
150
+ bound: dict[str, str] = {name: origin for name, origin, _ in import_bindings(tree.body)}
151
+ return [name for name, func in made if _origin(func, bound) in _NEW_TYPES]
152
+
153
+
154
+ def _origin(func: ast.Name | ast.Attribute, bound: Mapping[str, str]) -> str | None:
155
+ """Resolve a callee one or two names long through the module's imports (`bound`).
156
+
157
+ Returns:
158
+ Its dotted origin (`typing.NewType`), or `None`.
159
+
160
+ """
161
+ name: str
162
+ attr: str
163
+ match func:
164
+ case ast.Name(id=name):
165
+ return bound.get(name)
166
+ case ast.Attribute(value=ast.Name(id=name), attr=attr) if name in bound:
167
+ return f"{bound[name]}.{attr}"
168
+ case _:
169
+ return None
Binary file
@@ -0,0 +1,127 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """One walk of a whole module, shared by every pass over all of it (importing nothing of constricter's)."""
3
+
4
+ import ast
5
+ from collections.abc import Sequence
6
+ from typing import Final, TypeAlias, cast
7
+ from weakref import WeakKeyDictionary
8
+
9
+ _CTX: Final = "ctx"
10
+ # What a node's field holds, as far as the walk cares: a node, a list (of nodes, or of names), or else.
11
+ _Value: TypeAlias = ast.AST | list[ast.AST | str] | str | int | None
12
+
13
+
14
+ _Walk: TypeAlias = dict[type[ast.AST], list[ast.AST]] # a module's nodes, by their type
15
+ # Each node class's fields, but its `ctx` (only ever `Load`, `Store` or `Del`: no pass looks at them).
16
+ _FIELDS: dict[type[ast.AST], tuple[str, ...]] = {}
17
+
18
+
19
+ # Each module's walk, for as long as its tree lives: the cross-file index's walk of a file is the
20
+ # check's too, when the index kept its tree (`parsed.keep`), however many files came between.
21
+ _WALKS: Final[WeakKeyDictionary[ast.Module, "_Walk"]] = WeakKeyDictionary()
22
+
23
+
24
+ def _by_type(tree: ast.Module) -> dict[type[ast.AST], list[ast.AST]]:
25
+ """Walk a whole module once, sorting its nodes by type, for every pass that looks at all of it.
26
+
27
+ `ast.walk` was most of a check's time (on the standard library, 57%), eight passes each walking
28
+ every module whole. This walk is `ast.walk`'s, breadth first, without its per-node generators:
29
+ a generation of nodes at a time, and each class's fields looked up once.
30
+
31
+ Returns:
32
+ Each type's nodes, in `ast.walk`'s order (all but the `Load`, `Store` and `Del` markers).
33
+
34
+ """
35
+ found: dict[type[ast.AST], list[ast.AST]] | None
36
+ if (found := _WALKS.get(tree)) is not None:
37
+ return found
38
+ found = _WALKS[tree] = {}
39
+ # A generation at a time (a breadth-first queue's order): each built from the one before.
40
+ # `None` at the bottom ends it: a generation with no children adds none to follow it.
41
+ generations: list[list[ast.AST] | None] = [None, [tree]]
42
+ generation: list[ast.AST]
43
+ for generation in iter(generations.pop, None):
44
+ following: list[ast.AST] = []
45
+ node: ast.AST
46
+ for node in generation:
47
+ kind: type[ast.AST] = type(node)
48
+ found.setdefault(kind, []).append(node)
49
+ field: str
50
+ for field in _fields_of(kind):
51
+ value: _Value = cast("_Value", getattr(node, field, None))
52
+ if isinstance(value, list):
53
+ # A list, not a generator: none's resumed for each item (and it's inlined, 3.12+).
54
+ following.extend([item for item in value if isinstance(item, ast.AST)])
55
+ elif isinstance(value, ast.AST):
56
+ following.append(value)
57
+ if following:
58
+ generations.append(following)
59
+ return found
60
+
61
+
62
+ def of_type(tree: ast.Module, *kinds: type[ast.AST]) -> Sequence[ast.AST]:
63
+ """Find a module's nodes of `kinds`: a pass that wants only those needn't look at every node.
64
+
65
+ Returns:
66
+ Them, each kind's in `ast.walk`'s order, the kinds one after another.
67
+
68
+ """
69
+ table: dict[type[ast.AST], list[ast.AST]] = _by_type(tree)
70
+ return [node for kind in kinds for node in table.get(kind, [])]
71
+
72
+
73
+ def classes(tree: ast.Module) -> Sequence[ast.ClassDef]:
74
+ """Find every class a module defines, however deep.
75
+
76
+ Returns:
77
+ Them, in `ast.walk`'s order.
78
+
79
+ """
80
+ return cast("list[ast.ClassDef]", _by_type(tree).get(ast.ClassDef, []))
81
+
82
+
83
+ def children(node: ast.AST) -> list[ast.AST]:
84
+ """List a node's children, as `ast.iter_child_nodes` does but for their `ctx`, and without its generator.
85
+
86
+ Returns:
87
+ Them, in their fields' order.
88
+
89
+ """
90
+ found: list[ast.AST] = []
91
+ field: str
92
+ for field in _fields_of(type(node)):
93
+ value: _Value = cast("_Value", getattr(node, field, None))
94
+ if isinstance(value, list):
95
+ found.extend([item for item in value if isinstance(item, ast.AST)])
96
+ elif isinstance(value, ast.AST):
97
+ found.append(value)
98
+ return found
99
+
100
+
101
+ def walk(node: ast.AST) -> list[ast.AST]:
102
+ """List a node and everything under it (but `ctx` markers), for a pass that needn't their order.
103
+
104
+ Returns:
105
+ Them, depth first (not `ast.walk`'s breadth first order).
106
+
107
+ """
108
+ found: list[ast.AST] = []
109
+ waiting: list[ast.AST | None] = [None, node] # `None` at its bottom ends it
110
+ item: ast.AST
111
+ for item in iter(waiting.pop, None):
112
+ found.append(item)
113
+ waiting.extend(children(item))
114
+ return found
115
+
116
+
117
+ def _fields_of(kind: type[ast.AST]) -> tuple[str, ...]:
118
+ """Name a node class's fields but its `ctx`, worked out once per class.
119
+
120
+ Returns:
121
+ Them.
122
+
123
+ """
124
+ fields: tuple[str, ...] | None
125
+ if (fields := _FIELDS.get(kind)) is None:
126
+ fields = _FIELDS[kind] = tuple(field for field in kind._fields if field != _CTX)
127
+ return fields
@@ -0,0 +1,322 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Wider types than a value's own, written only where `fix-widen` asks, each line marked (`MARK`).
3
+
4
+ A marked annotation is `--fix`'s own: it isn't LVA005, `--coverage` counts it apart, and a later
5
+ `--fix` replaces it once the value's type is known.
6
+ """
7
+
8
+ import ast
9
+ import re
10
+ from collections import Counter
11
+ from collections.abc import Collection, Mapping, Sequence
12
+ from functools import lru_cache
13
+ from typing import Final, NamedTuple
14
+
15
+ from constricter.fix.core.known import ImportPlan, Inference
16
+ from constricter.fix.values import fills
17
+ from constricter.offences import (
18
+ EMPTY_CONTAINERS,
19
+ MARK,
20
+ MIXED_CONTAINERS,
21
+ UNKNOWN_CALLS,
22
+ UNTYPED_CALLS,
23
+ UNTYPED_PARAMETERS,
24
+ )
25
+ from constricter.rules.annotations import dotted, node_name
26
+ from constricter.rules.syntax import FUNCTION_DEFS, FunctionDef
27
+ from constricter.rules.walked import children, classes, walk
28
+
29
+ ANY: Final = "typing.Any"
30
+ _RECEIVERS: Final = frozenset({"self", "cls"})
31
+ # The decorators that leave a method's call its own `return`s.
32
+ _BINDERS: Final = frozenset({"staticmethod", "classmethod"})
33
+ # The mark, ending its line, with the space before it: what a replaced widening's fix deletes.
34
+ _MARKED: Final = re.compile(rf"[ \t]*{re.escape(MARK)}(?=[ \t]*[\r\n]*$)")
35
+
36
+
37
+ def marks(lines: Sequence[str]) -> dict[int, tuple[int, int]]:
38
+ """Find the lines a widening marked.
39
+
40
+ Returns:
41
+ Each one's number (from 1), and the mark's columns in it (UTF-8 bytes, as `ast` counts).
42
+
43
+ """
44
+ found: dict[int, tuple[int, int]] = {}
45
+ number: int
46
+ line: str
47
+ for number, line in enumerate(lines, 1):
48
+ marked: re.Match[str] | None
49
+ if (marked := _MARKED.search(line) if MARK in line else None) is not None:
50
+ start: int = len(line[: marked.start()].encode())
51
+ found[number] = (start, start + len(marked.group().encode()))
52
+ return found
53
+
54
+
55
+ @lru_cache(maxsize=64) # asked of each of a function's assignments with no fix
56
+ def untyped_parameters(function: FunctionDef) -> frozenset[str]:
57
+ """Name a function's parameters nothing types: no annotation, no default, never bound again.
58
+
59
+ A checker takes each for anything, wherever it's read. Not `*args` or `**kwargs`, nor a
60
+ method's `self` or `cls`, which is its class's.
61
+
62
+ Returns:
63
+ Them.
64
+
65
+ """
66
+ args: ast.arguments = function.args
67
+ positional: list[ast.arg] = [*args.posonlyargs, *args.args]
68
+ plain: int = len(positional) - len(args.defaults)
69
+ defaulted: set[str] = {arg.arg for arg in positional[plain:]}
70
+ defaulted.update(
71
+ arg.arg for arg, default in zip(args.kwonlyargs, args.kw_defaults, strict=True) if default
72
+ )
73
+ rebound: set[str] = {
74
+ node.id
75
+ for node in walk(function)
76
+ if isinstance(node, ast.Name) and not isinstance(node.ctx, ast.Load)
77
+ }
78
+ return frozenset(
79
+ arg.arg
80
+ for arg in (*positional, *args.kwonlyargs)
81
+ if arg.annotation is None and arg.arg not in defaulted | rebound | _RECEIVERS
82
+ )
83
+
84
+
85
+ def wider(
86
+ function: FunctionDef | None,
87
+ bound: tuple[ast.Name, ast.expr],
88
+ typed: Collection[str],
89
+ plan: ImportPlan | None,
90
+ ) -> Inference | None:
91
+ """Type a name `bound` to what comes of an untyped parameter of `function` as `Any`.
92
+
93
+ A copy of it (`x = param`), or its method's call (`y = param.read()`), all on the name's line
94
+ (the mark ends it); not one of `typed`, which something gives a type after all. What every
95
+ checker takes it for already.
96
+
97
+ Returns:
98
+ The inference, or `None`: also where the module (`plan`) can't name `Any`.
99
+
100
+ """
101
+ target: ast.Name
102
+ value: ast.expr
103
+ target, value = bound
104
+ if function is None or plan is None or value.end_lineno != target.lineno:
105
+ return None
106
+ name: str
107
+ match value:
108
+ case ast.Name(id=name) | ast.Call(func=ast.Attribute(value=ast.Name(id=name))) if (
109
+ name in untyped_parameters(function) and name not in typed
110
+ ):
111
+ spelled: str | None = plan.spell(ANY)
112
+ reason: str = f"what comes of `{name}`, a parameter no annotation types"
113
+ return None if spelled is None else Inference(spelled, reason, frozenset({UNTYPED_PARAMETERS}))
114
+ case _:
115
+ return None
116
+
117
+
118
+ def container(value: ast.expr, plan: ImportPlan, kinds: Collection[str]) -> Inference | None:
119
+ """Type a container whose elements aren't known with `Any` for them: `list[Any]`, `dict[str, Any]`.
120
+
121
+ An empty one nothing in sight fills (`[]`, `dict()`: kind `empty-containers`), or a display
122
+ whose elements' types differ or aren't known (`[1, "a"]`, `(x, y)`: `mixed-containers`); a dict
123
+ display's keys are `str` where each is a string. Only the kinds among `kinds`.
124
+
125
+ Returns:
126
+ The inference, or `None`: for any other value, or where the module (`plan`) can't name `Any`.
127
+
128
+ """
129
+ empty: str | None = fills.empty(value)
130
+ kind: str = EMPTY_CONTAINERS if empty is not None else MIXED_CONTAINERS
131
+ spelled: str | None
132
+ if (spelled := plan.spell(ANY) if kind in kinds else None) is None:
133
+ return None
134
+ keys: list[ast.expr | None]
135
+ annotation: str | None
136
+ match value:
137
+ case ast.List(elts=[_, *_]):
138
+ annotation = f"list[{spelled}]"
139
+ case ast.Set():
140
+ annotation = f"set[{spelled}]"
141
+ case ast.Tuple(elts=[_, *_]):
142
+ annotation = f"tuple[{spelled}, ...]"
143
+ case ast.Dict(keys=keys) if keys:
144
+ texts: bool = all(isinstance(key, ast.Constant) and isinstance(key.value, str) for key in keys)
145
+ annotation = f"dict[{'str' if texts else spelled}, {spelled}]"
146
+ case _:
147
+ annotation = {"list": f"list[{spelled}]", "set": f"set[{spelled}]"}.get(
148
+ empty or "",
149
+ f"dict[{spelled}, {spelled}]" if empty else None,
150
+ )
151
+ if annotation is None:
152
+ return None
153
+ reason: str = "an empty container nothing fills" if empty else "a display of mixed or unknown elements"
154
+ return Inference(annotation, reason, frozenset({kind}))
155
+
156
+
157
+ class Own(NamedTuple):
158
+ """A module's functions and classes a call may name, each defined once: what `called` looks through.
159
+
160
+ `functions`: its top-level functions, by name; `classes`: its classes, anywhere, by name.
161
+ """
162
+
163
+ functions: Mapping[str, FunctionDef]
164
+ classes: Mapping[str, ast.ClassDef]
165
+
166
+
167
+ class Caller(NamedTuple):
168
+ """The function a call is in: its class, where it reads a method's `self`, and the names it binds.
169
+
170
+ `untyped`: the other checked files' functions its module calls that declare no return, as it
171
+ spells them (see `Outside.untyped`).
172
+ """
173
+
174
+ owner: str | None
175
+ bound: Collection[str]
176
+ untyped: Collection[str] = ()
177
+
178
+
179
+ @lru_cache(maxsize=2) # asked of each scope of a module in turn
180
+ def own(tree: ast.Module) -> Own:
181
+ """Read a module's `Own`.
182
+
183
+ Returns:
184
+ It.
185
+
186
+ """
187
+ defined: list[FunctionDef | ast.ClassDef] = [
188
+ *(stmt for stmt in tree.body if isinstance(stmt, FUNCTION_DEFS)),
189
+ *classes(tree),
190
+ ]
191
+ count: Counter[str] = Counter(node.name for node in defined)
192
+ once: list[FunctionDef | ast.ClassDef] = [node for node in defined if count[node.name] == 1]
193
+ return Own(
194
+ {node.name: node for node in once if isinstance(node, FUNCTION_DEFS)},
195
+ {node.name: node for node in once if isinstance(node, ast.ClassDef)},
196
+ )
197
+
198
+
199
+ def _method(found: Own, owner: str, name: str) -> FunctionDef | None:
200
+ """Find the method a class's instance takes by `name`: its own, or the first of its bases'.
201
+
202
+ Returns:
203
+ Its definition, or `None`: where a class on the way isn't the module's, or binds the name
204
+ more than once (overloads, a property's setter) or otherwise than by a `def`.
205
+
206
+ """
207
+ waiting: list[str | None] = [
208
+ None,
209
+ owner,
210
+ ] # each class still to look in, the next one last; `None` ends it
211
+ current: str
212
+ for current in iter(waiting.pop, None):
213
+ node: ast.ClassDef | None
214
+ if (node := found.classes.get(current)) is None:
215
+ return None
216
+ bound: list[ast.AST]
217
+ if bound := _bindings(node, name):
218
+ method: ast.AST = bound[0]
219
+ return method if len(bound) == 1 and isinstance(method, FUNCTION_DEFS) else None
220
+ bases: list[str] = [base.id for base in node.bases if isinstance(base, ast.Name)]
221
+ if len(bases) != len(node.bases):
222
+ return None
223
+ waiting.extend(reversed(bases))
224
+ return None
225
+
226
+
227
+ def _bindings(node: ast.ClassDef, name: str) -> list[ast.AST]:
228
+ """Find what binds `name` in a class's body: not in its methods' bodies, nor a nested class's.
229
+
230
+ Returns:
231
+ Each `def`, class and store of it.
232
+
233
+ """
234
+ found: list[ast.AST] = []
235
+ waiting: list[ast.AST | None] = [None, *node.body] # each node still to read; `None` ends it
236
+ child: ast.AST
237
+ for child in iter(waiting.pop, None):
238
+ if isinstance(child, (*FUNCTION_DEFS, ast.ClassDef)):
239
+ found.extend([child] if child.name == name else [])
240
+ elif isinstance(child, ast.Name):
241
+ found.extend([child] if child.id == name and isinstance(child.ctx, ast.Store) else [])
242
+ else:
243
+ waiting.extend(children(child))
244
+ return found
245
+
246
+
247
+ def _undeclared(function: FunctionDef, *, awaited: bool) -> bool:
248
+ """Check whether a function's call is one a checker reading no body takes for anything.
249
+
250
+ Returns:
251
+ Whether it declares no return, by an annotation or a type comment, is decorated at most as a
252
+ static or class method, and is an `async def` just where its call is `awaited`.
253
+
254
+ """
255
+ return (
256
+ function.returns is None
257
+ and not function.type_comment
258
+ and isinstance(function, ast.AsyncFunctionDef) == awaited
259
+ and all(node_name(decorator) in _BINDERS for decorator in function.decorator_list)
260
+ )
261
+
262
+
263
+ def _imported(func: ast.expr, caller: Caller) -> bool:
264
+ """Check whether a call's function is another checked file's that declares no return.
265
+
266
+ Returns:
267
+ Whether `func` spells one of `caller.untyped`, through no name its function binds.
268
+
269
+ """
270
+ spelled: str | None = dotted(func)
271
+ return spelled is not None and spelled in caller.untyped and spelled.partition(".")[0] not in caller.bound
272
+
273
+
274
+ def called(
275
+ value: ast.expr,
276
+ caller: Caller,
277
+ found: Own,
278
+ plan: ImportPlan,
279
+ kinds: Collection[str],
280
+ ) -> Inference | None:
281
+ """Type a call of no known type as `Any`, awaited or not.
282
+
283
+ Kind `untyped-calls` where it calls a function that declares no return, which a checker
284
+ reading no body takes for anything already: a top-level function of the module's by its name,
285
+ a method of `caller`'s class (or of its bases in the module) on `self` or `cls`, or another
286
+ checked file's function (`caller.untyped`), not awaited. Kind `unknown-calls` for any other.
287
+ Only the kinds among `kinds`.
288
+
289
+ Returns:
290
+ The inference, or `None`: for any other value, or where the module (`plan`) can't name `Any`.
291
+
292
+ """
293
+ awaited: bool = isinstance(value, ast.Await)
294
+ call: ast.expr = value.value if isinstance(value, ast.Await) else value
295
+ if not isinstance(call, ast.Call):
296
+ return None
297
+ callee: FunctionDef | None = None
298
+ name: str
299
+ receiver: str
300
+ owner: str
301
+ match call.func, caller.owner:
302
+ case ast.Name(id=name), _ if name not in caller.bound:
303
+ callee = found.functions.get(name)
304
+ case ast.Attribute(value=ast.Name(id=receiver), attr=name), str() as owner if receiver in _RECEIVERS:
305
+ callee = _method(found, owner, name)
306
+ case _:
307
+ pass
308
+ untyped: bool = (
309
+ (not awaited and _imported(call.func, caller))
310
+ if callee is None
311
+ else _undeclared(callee, awaited=awaited)
312
+ )
313
+ kind: str = UNTYPED_CALLS if untyped else UNKNOWN_CALLS
314
+ spelled: str | None
315
+ if (spelled := plan.spell(ANY) if kind in kinds else None) is None:
316
+ return None
317
+ reason: str = (
318
+ f"a call of `{ast.unparse(call.func)}`, which declares no return"
319
+ if untyped
320
+ else "a call of no known type"
321
+ )
322
+ return Inference(spelled, reason, frozenset({kind}))