python-constricter 0.4.1__cp311-cp311-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__.cp311-win32.pyd +0 -0
  29. constricter/fix/__init__.py +2 -0
  30. constricter/fix/core/__init__.cp311-win32.pyd +0 -0
  31. constricter/fix/core/__init__.py +2 -0
  32. constricter/fix/core/asked.cp311-win32.pyd +0 -0
  33. constricter/fix/core/asked.py +56 -0
  34. constricter/fix/core/fixes.cp311-win32.pyd +0 -0
  35. constricter/fix/core/fixes.py +317 -0
  36. constricter/fix/core/imports.cp311-win32.pyd +0 -0
  37. constricter/fix/core/imports.py +580 -0
  38. constricter/fix/core/inherited.cp311-win32.pyd +0 -0
  39. constricter/fix/core/inherited.py +311 -0
  40. constricter/fix/core/known.cp311-win32.pyd +0 -0
  41. constricter/fix/core/known.py +730 -0
  42. constricter/fix/core/signatures.cp311-win32.pyd +0 -0
  43. constricter/fix/core/signatures.py +132 -0
  44. constricter/fix/index/__init__.cp311-win32.pyd +0 -0
  45. constricter/fix/index/__init__.py +2 -0
  46. constricter/fix/index/awaits.cp311-win32.pyd +0 -0
  47. constricter/fix/index/awaits.py +90 -0
  48. constricter/fix/index/beyond.cp311-win32.pyd +0 -0
  49. constricter/fix/index/beyond.py +149 -0
  50. constricter/fix/index/callers.cp311-win32.pyd +0 -0
  51. constricter/fix/index/callers.py +155 -0
  52. constricter/fix/index/crossed.cp311-win32.pyd +0 -0
  53. constricter/fix/index/crossed.py +327 -0
  54. constricter/fix/index/decorated.cp311-win32.pyd +0 -0
  55. constricter/fix/index/decorated.py +140 -0
  56. constricter/fix/index/fixtures.cp311-win32.pyd +0 -0
  57. constricter/fix/index/fixtures.py +176 -0
  58. constricter/fix/index/linked.cp311-win32.pyd +0 -0
  59. constricter/fix/index/linked.py +142 -0
  60. constricter/fix/index/loose.cp311-win32.pyd +0 -0
  61. constricter/fix/index/loose.py +103 -0
  62. constricter/fix/index/managed.cp311-win32.pyd +0 -0
  63. constricter/fix/index/managed.py +63 -0
  64. constricter/fix/index/modules.cp311-win32.pyd +0 -0
  65. constricter/fix/index/modules.py +741 -0
  66. constricter/fix/index/offers.cp311-win32.pyd +0 -0
  67. constricter/fix/index/offers.py +252 -0
  68. constricter/fix/index/order.cp311-win32.pyd +0 -0
  69. constricter/fix/index/order.py +155 -0
  70. constricter/fix/index/origins.cp311-win32.pyd +0 -0
  71. constricter/fix/index/origins.py +238 -0
  72. constricter/fix/index/own_methods.cp311-win32.pyd +0 -0
  73. constricter/fix/index/own_methods.py +91 -0
  74. constricter/fix/index/own_overloads.cp311-win32.pyd +0 -0
  75. constricter/fix/index/own_overloads.py +70 -0
  76. constricter/fix/index/own_types.cp311-win32.pyd +0 -0
  77. constricter/fix/index/own_types.py +330 -0
  78. constricter/fix/index/params.cp311-win32.pyd +0 -0
  79. constricter/fix/index/params.py +81 -0
  80. constricter/fix/index/plain.cp311-win32.pyd +0 -0
  81. constricter/fix/index/plain.py +221 -0
  82. constricter/fix/index/project.cp311-win32.pyd +0 -0
  83. constricter/fix/index/project.py +797 -0
  84. constricter/fix/index/sides.cp311-win32.pyd +0 -0
  85. constricter/fix/index/sides.py +160 -0
  86. constricter/fix/index/strings.cp311-win32.pyd +0 -0
  87. constricter/fix/index/strings.py +46 -0
  88. constricter/fix/index/stubs/__init__.cp311-win32.pyd +0 -0
  89. constricter/fix/index/stubs/__init__.py +2 -0
  90. constricter/fix/index/stubs/atoms.cp311-win32.pyd +0 -0
  91. constricter/fix/index/stubs/atoms.py +425 -0
  92. constricter/fix/index/stubs/chained.cp311-win32.pyd +0 -0
  93. constricter/fix/index/stubs/chained.py +69 -0
  94. constricter/fix/index/stubs/classnames.cp311-win32.pyd +0 -0
  95. constricter/fix/index/stubs/classnames.py +124 -0
  96. constricter/fix/index/stubs/declared.cp311-win32.pyd +0 -0
  97. constricter/fix/index/stubs/declared.py +508 -0
  98. constricter/fix/index/stubs/installed.cp311-win32.pyd +0 -0
  99. constricter/fix/index/stubs/installed.py +425 -0
  100. constricter/fix/index/stubs/names.cp311-win32.pyd +0 -0
  101. constricter/fix/index/stubs/names.py +200 -0
  102. constricter/fix/index/stubs/read.cp311-win32.pyd +0 -0
  103. constricter/fix/index/stubs/read.py +70 -0
  104. constricter/fix/index/stubs/signatures.cp311-win32.pyd +0 -0
  105. constricter/fix/index/stubs/signatures.py +656 -0
  106. constricter/fix/index/stubs/stubbed.cp311-win32.pyd +0 -0
  107. constricter/fix/index/stubs/stubbed.py +208 -0
  108. constricter/fix/index/taken.cp311-win32.pyd +0 -0
  109. constricter/fix/index/taken.py +45 -0
  110. constricter/fix/index/tuples.cp311-win32.pyd +0 -0
  111. constricter/fix/index/tuples.py +112 -0
  112. constricter/fix/libraries/__init__.cp311-win32.pyd +0 -0
  113. constricter/fix/libraries/__init__.py +2 -0
  114. constricter/fix/libraries/library.cp311-win32.pyd +0 -0
  115. constricter/fix/libraries/library.py +420 -0
  116. constricter/fix/libraries/opened.cp311-win32.pyd +0 -0
  117. constricter/fix/libraries/opened.py +147 -0
  118. constricter/fix/libraries/overloads.cp311-win32.pyd +0 -0
  119. constricter/fix/libraries/overloads.py +960 -0
  120. constricter/fix/libraries/receivers.cp311-win32.pyd +0 -0
  121. constricter/fix/libraries/receivers.py +274 -0
  122. constricter/fix/libraries/stdlib.cp311-win32.pyd +0 -0
  123. constricter/fix/libraries/stdlib.py +752 -0
  124. constricter/fix/libraries/walks.cp311-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__.cp311-win32.pyd +0 -0
  148. constricter/fix/values/__init__.py +2 -0
  149. constricter/fix/values/aliased.cp311-win32.pyd +0 -0
  150. constricter/fix/values/aliased.py +195 -0
  151. constricter/fix/values/bodies.cp311-win32.pyd +0 -0
  152. constricter/fix/values/bodies.py +76 -0
  153. constricter/fix/values/callables.cp311-win32.pyd +0 -0
  154. constricter/fix/values/callables.py +323 -0
  155. constricter/fix/values/called.cp311-win32.pyd +0 -0
  156. constricter/fix/values/called.py +194 -0
  157. constricter/fix/values/classvars.cp311-win32.pyd +0 -0
  158. constricter/fix/values/classvars.py +629 -0
  159. constricter/fix/values/decided.cp311-win32.pyd +0 -0
  160. constricter/fix/values/decided.py +429 -0
  161. constricter/fix/values/displays.cp311-win32.pyd +0 -0
  162. constricter/fix/values/displays.py +101 -0
  163. constricter/fix/values/doubts.cp311-win32.pyd +0 -0
  164. constricter/fix/values/doubts.py +677 -0
  165. constricter/fix/values/entered.cp311-win32.pyd +0 -0
  166. constricter/fix/values/entered.py +332 -0
  167. constricter/fix/values/fills.cp311-win32.pyd +0 -0
  168. constricter/fix/values/fills.py +446 -0
  169. constricter/fix/values/guesses.cp311-win32.pyd +0 -0
  170. constricter/fix/values/guesses.py +376 -0
  171. constricter/fix/values/hinted.cp311-win32.pyd +0 -0
  172. constricter/fix/values/hinted.py +811 -0
  173. constricter/fix/values/inference.cp311-win32.pyd +0 -0
  174. constricter/fix/values/inference.py +698 -0
  175. constricter/fix/values/loops.cp311-win32.pyd +0 -0
  176. constricter/fix/values/loops.py +335 -0
  177. constricter/fix/values/members.cp311-win32.pyd +0 -0
  178. constricter/fix/values/members.py +578 -0
  179. constricter/fix/values/narrowed.cp311-win32.pyd +0 -0
  180. constricter/fix/values/narrowed.py +220 -0
  181. constricter/fix/values/operated.cp311-win32.pyd +0 -0
  182. constricter/fix/values/operated.py +202 -0
  183. constricter/fix/values/ordered.cp311-win32.pyd +0 -0
  184. constricter/fix/values/ordered.py +74 -0
  185. constricter/fix/values/returned.cp311-win32.pyd +0 -0
  186. constricter/fix/values/returned.py +479 -0
  187. constricter/fix/values/returns.cp311-win32.pyd +0 -0
  188. constricter/fix/values/returns.py +237 -0
  189. constricter/fix/values/shapes.cp311-win32.pyd +0 -0
  190. constricter/fix/values/shapes.py +654 -0
  191. constricter/fix/values/stored.cp311-win32.pyd +0 -0
  192. constricter/fix/values/stored.py +564 -0
  193. constricter/fix/values/targets.cp311-win32.pyd +0 -0
  194. constricter/fix/values/targets.py +419 -0
  195. constricter/fix/values/unchecked.cp311-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__.cp311-win32.pyd +0 -0
  208. constricter/rules/__init__.py +2 -0
  209. constricter/rules/annotations.cp311-win32.pyd +0 -0
  210. constricter/rules/annotations.py +296 -0
  211. constricter/rules/binding.cp311-win32.pyd +0 -0
  212. constricter/rules/binding.py +652 -0
  213. constricter/rules/calls.cp311-win32.pyd +0 -0
  214. constricter/rules/calls.py +460 -0
  215. constricter/rules/checker.cp311-win32.pyd +0 -0
  216. constricter/rules/checker.py +536 -0
  217. constricter/rules/declarations.cp311-win32.pyd +0 -0
  218. constricter/rules/declarations.py +730 -0
  219. constricter/rules/decorators.cp311-win32.pyd +0 -0
  220. constricter/rules/decorators.py +351 -0
  221. constricter/rules/flow.cp311-win32.pyd +0 -0
  222. constricter/rules/flow.py +502 -0
  223. constricter/rules/keys.cp311-win32.pyd +0 -0
  224. constricter/rules/keys.py +79 -0
  225. constricter/rules/late.cp311-win32.pyd +0 -0
  226. constricter/rules/late.py +969 -0
  227. constricter/rules/narrowing.cp311-win32.pyd +0 -0
  228. constricter/rules/narrowing.py +98 -0
  229. constricter/rules/parsed.cp311-win32.pyd +0 -0
  230. constricter/rules/parsed.py +136 -0
  231. constricter/rules/quoted.cp311-win32.pyd +0 -0
  232. constricter/rules/quoted.py +141 -0
  233. constricter/rules/rebinding.cp311-win32.pyd +0 -0
  234. constricter/rules/rebinding.py +169 -0
  235. constricter/rules/recorded.cp311-win32.pyd +0 -0
  236. constricter/rules/recorded.py +168 -0
  237. constricter/rules/redundant.cp311-win32.pyd +0 -0
  238. constricter/rules/redundant.py +160 -0
  239. constricter/rules/scope.cp311-win32.pyd +0 -0
  240. constricter/rules/scope.py +802 -0
  241. constricter/rules/scoped.cp311-win32.pyd +0 -0
  242. constricter/rules/scoped.py +232 -0
  243. constricter/rules/scoping.cp311-win32.pyd +0 -0
  244. constricter/rules/scoping.py +346 -0
  245. constricter/rules/settings.cp311-win32.pyd +0 -0
  246. constricter/rules/settings.py +189 -0
  247. constricter/rules/syntax.cp311-win32.pyd +0 -0
  248. constricter/rules/syntax.py +472 -0
  249. constricter/rules/tables.cp311-win32.pyd +0 -0
  250. constricter/rules/tables.py +169 -0
  251. constricter/rules/walked.cp311-win32.pyd +0 -0
  252. constricter/rules/walked.py +127 -0
  253. constricter/rules/widened.cp311-win32.pyd +0 -0
  254. constricter/rules/widened.py +322 -0
  255. constricter/trace.py +374 -0
  256. e45ce06b2fdd5c43f782__mypyc.cp311-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,564 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """What a class's methods store in its attributes, for `--fix` on `self.x` (see `returned.Table`).
3
+
4
+ Each `self.x = value` and each read of `self.x` its methods make, as the checker saw them
5
+ (`Assigned`, `Used`), are joined into the attribute's type: by every value it's assigned in the
6
+ class and the classes above and under it in the module (`attribute_types`), and, for an attribute bound
7
+ to an empty container, by what's added to it (`_filled`), there or where another file's base binds
8
+ it (`hand`). And which of a method's reads of `self` an attribute's type, once known, retypes
9
+ (`reads`, `reads_own`).
10
+ """
11
+
12
+ import ast
13
+ from collections.abc import Collection, Iterator, Mapping, Sequence
14
+ from functools import lru_cache
15
+ from typing import Final, TypeAlias
16
+ from weakref import WeakKeyDictionary
17
+
18
+ from constricter.fix.core.known import Inference, Returned
19
+ from constricter.fix.values import fills
20
+ from constricter.fix.values.inference import ASSIGNED
21
+ from constricter.rules.syntax import FunctionDef, Start, within
22
+ from constricter.rules.walked import classes, of_type
23
+
24
+ # A `return` statement as the checker saw it: its value's inference (`None`: none, or unknown), and
25
+ # what that rests on if it's a guess (`FIX_KINDS`; empty: certain). A generator has one: its type.
26
+ Recorded: TypeAlias = tuple[Inference | None, frozenset[str]]
27
+ # A `self.x = value` as the checker saw it: the attribute, and its value as a `return`'s is recorded.
28
+ Assigned: TypeAlias = tuple[str, Recorded]
29
+ # What a read of an empty container adds to it, each part as a `return`'s value is recorded (a
30
+ # `dict`'s key and value, any other's element): none for one that adds nothing, `None` for one that
31
+ # might, unseen.
32
+ Added: TypeAlias = tuple[Recorded, ...] | None
33
+ # A read of `self.x`, an attribute bound to an empty container, as the checker saw it.
34
+ Used: TypeAlias = tuple[str, Added]
35
+ # What a class's methods store in its attributes: each one's assigned values, and what's added to it.
36
+ _Stored: TypeAlias = tuple[dict[str, list[Recorded]], dict[str, list[Added]]]
37
+ _Emptied: TypeAlias = tuple[str, str] # a `self.x = []`: the attribute, and the container's kind
38
+ _Under: TypeAlias = tuple[list[Recorded], list[Added], int] # see `_under`
39
+ _FILLED: Final = "filled" # the fix kind of an empty container typed by what's added to it
40
+ SELF: Final = "self"
41
+
42
+
43
+ _NOT_METHODS: Final = frozenset({"staticmethod", "classmethod"})
44
+ _NUMBERS: Final = ("bool", "int", "float", "complex") # narrowest first: an attribute takes the widest
45
+ FUNCTIONS: Final = (ast.FunctionDef, ast.AsyncFunctionDef)
46
+ # The attributes each of a module's classes takes from another file's base that binds them to an
47
+ # empty container and does no more with them: each one's kind, by class (see `hand`).
48
+ _Handed: TypeAlias = Mapping[str, Mapping[str, str]]
49
+ _HANDED: Final[WeakKeyDictionary[ast.Module, _Handed]] = WeakKeyDictionary()
50
+
51
+
52
+ _Uses: TypeAlias = tuple[list[Start], list[str]] # attributes used: where each starts, and its name
53
+ _Listed: TypeAlias = list[tuple[Start, str]] # attributes used, each where it starts and its name
54
+
55
+
56
+ def hand(tree: ast.Module, emptied: Mapping[str, Mapping[str, str]]) -> None:
57
+ """Tell which attributes `tree`'s classes take bound empty from another file's base (see `_HANDED`).
58
+
59
+ Before the module is checked: its classes' methods' fills of one then type it for them, as
60
+ their own empty container's do.
61
+ """
62
+ _HANDED[tree] = emptied
63
+
64
+
65
+ def untouched(tree: ast.Module) -> dict[str, dict[str, str]]:
66
+ """Find the attributes each class binds to an empty container and does no more with, in the module.
67
+
68
+ Neither it nor a class of the module's under it reads or stores one again: what fills it is
69
+ another file's to say.
70
+
71
+ Returns:
72
+ Each such attribute's kind, by class; a class with none is left out.
73
+
74
+ """
75
+ found: dict[str, dict[str, str]] = {}
76
+ node: ast.ClassDef
77
+ for node in classes(tree):
78
+ stored: list[str] = _uses(tree)[2][1][within(_uses(tree)[2][0], node)]
79
+ kinds: dict[str, str] = {
80
+ attr: kind
81
+ for attr, (kind, count) in _emptied(tree, node).items()
82
+ if class_names(tree)[node.name] == 1
83
+ and stored.count(attr) == count
84
+ and not any(_read(tree, each, attr) for each in (node, *subclasses(tree, node)))
85
+ and not any(attr in _emptied(tree, each) for each in subclasses(tree, node))
86
+ }
87
+ if kinds:
88
+ found[node.name] = kinds
89
+ return found
90
+
91
+
92
+ @lru_cache(maxsize=4) # asked once per round, of the same module
93
+ def class_names(tree: ast.Module) -> Mapping[str, int]:
94
+ """Count the classes the module defines by each name.
95
+
96
+ Returns:
97
+ Each name's count.
98
+
99
+ """
100
+ counts: dict[str, int] = {}
101
+ node: ast.ClassDef
102
+ for node in classes(tree):
103
+ counts[node.name] = counts.get(node.name, 0) + 1
104
+ return counts
105
+
106
+
107
+ def attribute_types(
108
+ tree: ast.Module,
109
+ node: ast.ClassDef,
110
+ seen: tuple[Mapping[int, Sequence[Assigned]], Mapping[int, Sequence[Used]]],
111
+ ) -> Iterator[tuple[str, str, frozenset[str]]]:
112
+ """Find the class's unannotated instance attributes whose every assignment decides their type.
113
+
114
+ One counts when every place the class's code stores or deletes it (`self.x`, anywhere in the
115
+ class) is a plain `self.x = value` in one of its own methods, neither the class's body nor a
116
+ class of the module's above it binds the name (a class attribute, a method, a property) or,
117
+ above it, stores it, and every value's type is known and the same, or
118
+ numbers (`int`, then `float`: the widest). One bound to an empty container counts by what the
119
+ class's methods, and those of the module's classes under it, add to it (see `_filled`). A guess:
120
+ another file's subclass or outside code may assign it too.
121
+
122
+ `seen`: each checked function's `self.x = value`s, and its reads of `self`'s empty containers.
123
+
124
+ Yields:
125
+ Each one's name, type, and what that rests on: `assigned`, and its values' own guesses.
126
+
127
+ """
128
+ values: dict[str, list[Recorded]]
129
+ uses: dict[str, list[Added]]
130
+ values, uses = _stored(node, seen)
131
+ yield from _handed(tree, node, uses, seen)
132
+ if not values:
133
+ return
134
+ starts: list[Start]
135
+ stored: list[str]
136
+ starts, stored = _uses(tree)[2]
137
+ stores: dict[str, int] = {}
138
+ attr: str
139
+ for attr in stored[within(starts, node)]:
140
+ stores[attr] = stores.get(attr, 0) + 1
141
+ bound: frozenset[str] = _class_bound(node)
142
+ held: frozenset[str] = bound.union(
143
+ *((*_class_bound(base), *stored[within(starts, base)]) for base in superclasses(tree, node)),
144
+ )
145
+ found: list[Recorded]
146
+ for attr in sorted(_emptied(tree, node).keys() & values.keys() - bound):
147
+ filled: tuple[str, frozenset[str]] | None
148
+ if (
149
+ stores[attr] == len(values[attr])
150
+ and len(uses.get(attr, ())) == _read(tree, node, attr)
151
+ and (filled := _filled_under(tree, node, attr, (values[attr], uses.get(attr, ())), seen))
152
+ is not None
153
+ ):
154
+ yield attr, *filled
155
+ for attr, found in values.items():
156
+ types: set[str] = {value.annotation for value, _ in found if value is not None}
157
+ widest: str | None
158
+ if (
159
+ stores[attr] == len(found)
160
+ and attr not in held
161
+ and all(value is not None for value, _ in found)
162
+ and (widest := _widest(types)) is not None
163
+ ):
164
+ yield attr, widest, frozenset({ASSIGNED}).union(*(origins for _, origins in found))
165
+
166
+
167
+ def _handed(
168
+ tree: ast.Module,
169
+ node: ast.ClassDef,
170
+ uses: Mapping[str, Sequence[Added]],
171
+ seen: tuple[Mapping[int, Sequence[Assigned]], Mapping[int, Sequence[Used]]],
172
+ ) -> Iterator[tuple[str, str, frozenset[str]]]:
173
+ """Type the attributes the class takes bound empty from another file's base, by its own methods' fills.
174
+
175
+ And those of the module's classes under it, as `_filled_under` gathers them (see `_HANDED`).
176
+
177
+ Yields:
178
+ Each one's name, type, and what that rests on.
179
+
180
+ """
181
+ attr: str
182
+ kind: str
183
+ for attr, kind in sorted(_HANDED.get(tree, {}).get(node.name, {}).items()):
184
+ more: _Under | None = _under(tree, subclasses(tree, node), attr, seen)
185
+ own: Sequence[Added] = uses.get(attr, ())
186
+ filled: tuple[str, frozenset[str]] | None = (
187
+ None
188
+ if more is None or more[0] or len(own) != _read(tree, node, attr) or attr in _class_bound(node)
189
+ else _filled((kind, 0), [], [*own, *more[1]])
190
+ )
191
+ if filled is not None and _uses(tree)[2][1][within(_uses(tree)[2][0], node)].count(attr) == 0:
192
+ yield attr, *filled
193
+
194
+
195
+ def _filled_under(
196
+ tree: ast.Module,
197
+ node: ast.ClassDef,
198
+ attr: str,
199
+ own: tuple[Sequence[Recorded], Sequence[Added]],
200
+ seen: tuple[Mapping[int, Sequence[Assigned]], Mapping[int, Sequence[Used]]],
201
+ ) -> tuple[str, frozenset[str]] | None:
202
+ # `_filled`, by what the class's `own` methods store and add, and the module's classes' under it.
203
+ more: _Under | None = _under(tree, subclasses(tree, node), attr, seen)
204
+ kind: str
205
+ bare: int
206
+ kind, bare = _emptied(tree, node)[attr]
207
+ return None if more is None else _filled((kind, bare + more[2]), [*own[0], *more[0]], [*own[1], *more[1]])
208
+
209
+
210
+ @lru_cache(maxsize=1024) # asked of the same classes once per round, and of each method as it's checked
211
+ def subclasses(module: ast.Module, node: ast.ClassDef) -> tuple[ast.ClassDef, ...]:
212
+ """Find the module's classes under `node`, however far: each defined once, by a base named as it is.
213
+
214
+ Returns:
215
+ Them, in source order; none for a class whose name another of the module's has.
216
+
217
+ """
218
+ counts: Mapping[str, int] = class_names(module)
219
+ names: set[str] = {node.name} if counts[node.name] == 1 else set()
220
+ found: list[ast.ClassDef] = []
221
+ each: ast.ClassDef
222
+ for each in sorted(classes(module), key=lambda one: (one.lineno, one.col_offset)):
223
+ if (
224
+ each is not node
225
+ and counts[each.name] == 1
226
+ and any(isinstance(base, ast.Name) and base.id in names for base in each.bases)
227
+ ):
228
+ names.add(each.name)
229
+ found.append(each)
230
+ return tuple(found)
231
+
232
+
233
+ def superclasses(module: ast.Module, node: ast.ClassDef) -> list[ast.ClassDef]:
234
+ """Find the module's classes `node` is under, however far (see `subclasses`).
235
+
236
+ Returns:
237
+ Them.
238
+
239
+ """
240
+ return [each for each in classes(module) if node in subclasses(module, each)]
241
+
242
+
243
+ def _under(
244
+ tree: ast.Module,
245
+ family: Sequence[ast.ClassDef],
246
+ attr: str,
247
+ seen: tuple[Mapping[int, Sequence[Assigned]], Mapping[int, Sequence[Used]]],
248
+ ) -> _Under | None:
249
+ """Gather what the classes under one (`family`) store in its attribute `attr`, an empty container.
250
+
251
+ Each one's methods' `self.attr = value`s and reads of it, as the class's own are gathered.
252
+
253
+ Returns:
254
+ Their values, their uses, and how many of their assignments bind it empty too; or `None` if
255
+ one binds the name in its body, stores it any other way, or reads it where the checker
256
+ didn't judge the read (a nested function).
257
+
258
+ """
259
+ found: _Under = ([], [], 0)
260
+ starts: list[Start]
261
+ stored: list[str]
262
+ starts, stored = _uses(tree)[2]
263
+ node: ast.ClassDef
264
+ for node in family:
265
+ values: list[Recorded] = [
266
+ value
267
+ for method in self_methods(node)
268
+ for name, value in seen[0].get(id(method), ())
269
+ if name == attr
270
+ ]
271
+ uses: list[Added] = [
272
+ use for method in self_methods(node) for name, use in seen[1].get(id(method), ()) if name == attr
273
+ ]
274
+ if (
275
+ attr in _class_bound(node)
276
+ or stored[within(starts, node)].count(attr) != len(values)
277
+ or _read(tree, node, attr) != len(uses)
278
+ ):
279
+ return None
280
+ found[0].extend(values)
281
+ found[1].extend(uses)
282
+ return found[0], found[1], sum(_emptied(tree, node).get(attr, ("", 0))[1] for node in family)
283
+
284
+
285
+ def _stored(
286
+ node: ast.ClassDef,
287
+ seen: tuple[Mapping[int, Sequence[Assigned]], Mapping[int, Sequence[Used]]],
288
+ ) -> _Stored:
289
+ """Gather what the class's own methods store in each attribute of `self` (see `attribute_types`).
290
+
291
+ Returns:
292
+ Each attribute's assigned values, and each empty container's reads.
293
+
294
+ """
295
+ values: dict[str, list[Recorded]] = {}
296
+ uses: dict[str, list[Added]] = {}
297
+ method: FunctionDef
298
+ for method in self_methods(node):
299
+ attr: str
300
+ value: Recorded
301
+ for attr, value in seen[0].get(id(method), ()):
302
+ values.setdefault(attr, []).append(value)
303
+ use: Added
304
+ for attr, use in seen[1].get(id(method), ()):
305
+ uses.setdefault(attr, []).append(use)
306
+ return values, uses
307
+
308
+
309
+ def _filled(
310
+ emptied: tuple[str, int],
311
+ found: Sequence[Recorded],
312
+ uses: Sequence[Added],
313
+ ) -> tuple[str, frozenset[str]] | None:
314
+ """Type an attribute bound to an empty container by what its class's methods add to it.
315
+
316
+ `emptied`: its kind, and how many of its assignments (`found`) bind it empty; `uses`: every read
317
+ of it in the class, as `fills.stored` judges them. As `fills.filled` types a local: no read may
318
+ add something unseen, and what's added, or assigned to it instead, must be typed alike.
319
+
320
+ Returns:
321
+ Its type, and what that rests on (`assigned`, `filled`, and its values' own guesses); or `None`.
322
+
323
+ """
324
+ kind: str
325
+ bare: int
326
+ kind, bare = emptied
327
+ values: list[Recorded] = [value for value in found if value[0] is not None]
328
+ added: list[tuple[Recorded, ...]] = [use for use in uses if use]
329
+ parts: list[Recorded] = [*values, *(part for use in added for part in use)]
330
+ if len(values) + bare != len(found) or None in uses or any(part[0] is None for part in parts):
331
+ return None
332
+ types: set[str] = {value.annotation for value, _ in values if value is not None} | {
333
+ f"{kind}[{', '.join(part.annotation for part, _ in use if part is not None)}]" for use in added
334
+ }
335
+ if len(types) != 1:
336
+ return None
337
+ return types.pop(), frozenset({ASSIGNED, _FILLED}).union(*(origins for _, origins in parts))
338
+
339
+
340
+ @lru_cache(maxsize=4) # asked of the same module's classes, each round
341
+ def _empties(module: ast.Module) -> tuple[list[Start], list[_Emptied]]:
342
+ """Find the module's `self.x = []`s: each assignment of an empty container to an attribute of `self`.
343
+
344
+ Returns:
345
+ Where each starts, in source order; and its attribute and kind (see `fills.empty`).
346
+
347
+ """
348
+ found: list[tuple[Start, _Emptied]] = []
349
+ node: ast.AST
350
+ for node in of_type(module, ast.Assign):
351
+ kind: str | None
352
+ if isinstance(node, ast.Assign) and (kind := fills.empty(node.value)) is not None:
353
+ found.extend(
354
+ ((target.lineno, target.col_offset), (target.attr, kind))
355
+ for target in node.targets
356
+ if isinstance(target, ast.Attribute)
357
+ and isinstance(target.value, ast.Name)
358
+ and target.value.id == SELF
359
+ )
360
+ found.sort()
361
+ return [start for start, _ in found], [each for _, each in found]
362
+
363
+
364
+ @lru_cache(maxsize=1024) # asked of the same classes once per round, and of each method as it's checked
365
+ def _emptied(module: ast.Module, node: ast.ClassDef) -> Mapping[str, tuple[str, int]]:
366
+ """Find the attributes the class's code binds to an empty container, always of one kind.
367
+
368
+ Returns:
369
+ Each one's kind, and how many assignments bind it so.
370
+
371
+ """
372
+ starts: list[Start]
373
+ found: list[_Emptied]
374
+ starts, found = _empties(module)
375
+ kinds: dict[str, list[str]] = {}
376
+ attr: str
377
+ kind: str
378
+ for attr, kind in found[within(starts, node)]:
379
+ kinds.setdefault(attr, []).append(kind)
380
+ return {attr: (each[0], len(each)) for attr, each in kinds.items() if len(set(each)) == 1}
381
+
382
+
383
+ def empties(module: ast.Module, func: FunctionDef) -> Mapping[str, str]:
384
+ """Find the attributes of `self` a method's class, or one above it, binds to an empty container.
385
+
386
+ Returns:
387
+ Each one's kind; none for a function that isn't a class's own method.
388
+
389
+ """
390
+ node: ast.ClassDef | None = owners(module).get(id(func))
391
+ return {} if node is None else _class_empties(module, node)
392
+
393
+
394
+ @lru_cache(maxsize=1024) # asked of each of a class's methods as it's checked
395
+ def _class_empties(module: ast.Module, node: ast.ClassDef) -> Mapping[str, str]:
396
+ """Find the attributes of `self` a class, or one above it, binds to an empty container.
397
+
398
+ Returns:
399
+ Each one's kind.
400
+
401
+ """
402
+ # Its own, and those of the module's classes it's under: their fills are its own too.
403
+ above: list[ast.ClassDef] = [each for each in classes(module) if node in subclasses(module, each)]
404
+ handed: Mapping[str, Mapping[str, str]] = _HANDED.get(module, {})
405
+ return {
406
+ **{attr: kind for each in (*above, node) for attr, kind in handed.get(each.name, {}).items()},
407
+ **{attr: kind for each in (*above, node) for attr, (kind, _) in _emptied(module, each).items()},
408
+ }
409
+
410
+
411
+ def _read(module: ast.Module, node: ast.ClassDef, attr: str) -> int:
412
+ """Count the class's reads of `self.attr`, anywhere in it: a nested function's too.
413
+
414
+ Returns:
415
+ How many.
416
+
417
+ """
418
+ starts: list[Start]
419
+ found: list[str]
420
+ starts, found = _uses(module)[0]
421
+ return found[within(starts, node)].count(attr)
422
+
423
+
424
+ def _widest(types: set[str]) -> str | None:
425
+ """Find the one type every value's fits: the only one, or the widest of builtin numbers.
426
+
427
+ Returns:
428
+ It, or `None`.
429
+
430
+ """
431
+ if len(types) == 1:
432
+ return next(iter(types))
433
+ return max(types, key=_NUMBERS.index) if types <= set(_NUMBERS) else None
434
+
435
+
436
+ @lru_cache(maxsize=4) # asked of each function as it's checked
437
+ def owners(module: ast.Module) -> dict[int, ast.ClassDef]:
438
+ """Map each class's own methods (see `self_methods`) to it, by the method's `id()`.
439
+
440
+ Returns:
441
+ Each one's class.
442
+
443
+ """
444
+ return {id(method): node for node in classes(module) for method in self_methods(node)}
445
+
446
+
447
+ @lru_cache(maxsize=1024) # asked of the same classes once per round
448
+ def self_methods(node: ast.ClassDef) -> tuple[FunctionDef, ...]:
449
+ """Find the class's own methods whose first parameter is `self`: not a static or class method.
450
+
451
+ Returns:
452
+ Them.
453
+
454
+ """
455
+ return tuple(
456
+ stmt
457
+ for stmt in node.body
458
+ if isinstance(stmt, FUNCTIONS)
459
+ and [arg.arg for arg in (*stmt.args.posonlyargs, *stmt.args.args)][:1] == [SELF]
460
+ and not any(isinstance(d, ast.Name) and d.id in _NOT_METHODS for d in stmt.decorator_list)
461
+ )
462
+
463
+
464
+ @lru_cache(maxsize=1024) # asked of the same classes once per round
465
+ def _class_bound(node: ast.ClassDef) -> frozenset[str]:
466
+ """Find the names the class's own body binds: its class attributes, methods and nested classes.
467
+
468
+ Returns:
469
+ Them.
470
+
471
+ """
472
+ return frozenset(
473
+ name.id
474
+ for stmt in node.body
475
+ for target in _body_targets(stmt)
476
+ for name in ast.walk(target)
477
+ if isinstance(name, ast.Name)
478
+ ) | frozenset(stmt.name for stmt in node.body if isinstance(stmt, (*FUNCTIONS, ast.ClassDef)))
479
+
480
+
481
+ def _body_targets(stmt: ast.stmt) -> list[ast.expr]:
482
+ """List what a class-body statement assigns to.
483
+
484
+ Returns:
485
+ Its targets: none but for an assignment.
486
+
487
+ """
488
+ targets: list[ast.expr]
489
+ target: ast.expr
490
+ match stmt:
491
+ case ast.Assign(targets=targets):
492
+ return targets
493
+ case ast.AnnAssign(target=target) | ast.AugAssign(target=target):
494
+ return [target]
495
+ case _:
496
+ return []
497
+
498
+
499
+ def reads(module: ast.Module, node: ast.AST, attributes: Collection[str], *, anywhere: bool = False) -> bool:
500
+ """Check whether `node` (a function, or a statement) reads any of `attributes` of `self` (`self.a`).
501
+
502
+ `anywhere`: of any value (`x.a`, whatever `x` is), by its name alone, as `called` counts methods.
503
+
504
+ Returns:
505
+ Whether it does: otherwise checking it again, knowing their types, would change nothing.
506
+
507
+ """
508
+ starts: list[Start]
509
+ found: list[str]
510
+ starts, found = _uses(module)[1 if anywhere else 0]
511
+ return bool(attributes) and any(attr in attributes for attr in found[within(starts, node)])
512
+
513
+
514
+ def reads_own(tree: ast.Module, func: FunctionDef, classes_of: Mapping[int, str], found: Returned) -> bool:
515
+ """Check whether a method reads an attribute of its own class's (`self.a`) that `found` types.
516
+
517
+ `classes_of`: each method's class, by the method's `id()`.
518
+
519
+ Returns:
520
+ Whether it does: checking it again could type more.
521
+
522
+ """
523
+ owner: str | None
524
+ if (owner := classes_of.get(id(func))) is None:
525
+ return False
526
+ node: ast.ClassDef | None = owners(tree).get(id(func))
527
+ above: list[str] = (
528
+ [] if node is None else [each.name for each in classes(tree) if node in subclasses(tree, each)]
529
+ )
530
+ return reads(tree, func, {attr for name in (owner, *above) for attr in found.attributes.get(name, {})})
531
+
532
+
533
+ @lru_cache(maxsize=4) # asked of the same module's functions and classes, each round
534
+ def _uses(module: ast.Module) -> tuple[_Uses, _Uses, _Uses]:
535
+ """Find the module's attribute uses: reads of `self`'s (`self.a`), reads of any, stores to `self`'s.
536
+
537
+ A store includes a deletion (`del self.a`).
538
+
539
+ Returns:
540
+ Those three: each's starts, in source order, and its attribute's name.
541
+
542
+ """
543
+ found: tuple[_Listed, ...] = ([], [], [])
544
+ node: ast.AST
545
+ receiver: ast.expr
546
+ attr: str
547
+ for node in of_type(module, ast.Attribute):
548
+ match node:
549
+ case ast.Attribute(value=receiver, ctx=ast.Load(), attr=attr):
550
+ if isinstance(receiver, ast.Name) and receiver.id == SELF:
551
+ found[0].append(((node.lineno, node.col_offset), attr))
552
+ found[1].append(((node.lineno, node.col_offset), attr))
553
+ case ast.Attribute(value=ast.Name(id="self"), attr=attr): # a store or deletion
554
+ found[2].append(((node.lineno, node.col_offset), attr))
555
+ case _:
556
+ pass
557
+ kind: _Listed
558
+ for kind in found:
559
+ kind.sort()
560
+ of_self: _Uses
561
+ of_any: _Uses
562
+ stored: _Uses
563
+ of_self, of_any, stored = (([start for start, _ in kind], [attr for _, attr in kind]) for kind in found)
564
+ return of_self, of_any, stored