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,446 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """`--fix` for an empty container filled later: `x = []`, then only `x.append(v)`, is `list[T]`.
3
+
4
+ A guess (`--unsafe-fixes`): only this function's own uses are seen, and something else could still
5
+ add to it. So every use of the name must be one of a few that can't: a fill whose value's type is
6
+ known (`append`, `insert`, `add`, `setdefault`, `x[k] = v`; `extend` and `update`, by their one
7
+ argument's elements), a pass to a checked file's function whose parameter declares the container
8
+ it takes (`add(names, "a")`, `collect(into=names)`: that type), a read (`x[k]`, `x.get(k)`,
9
+ iterating it, `len(x)`, `", ".join(x)`, `x + more`, `[*x]`, returning it, alone or in a tuple),
10
+ one that only shrinks or reorders it
11
+ (`pop`, `sort`, `clear`), or a local bound to it (`alias = x`) whose own uses all only read it.
12
+ Anything else (passing it to another function, a nested function that sees it) leaves it alone.
13
+
14
+ An instance attribute bound to one (`self.items = []`) is judged the same way, each read of it in its
15
+ class's methods a use (`stored`): `constricter.fix.values.returned` types it from them all.
16
+ """
17
+
18
+ import ast
19
+ from collections import Counter
20
+ from collections.abc import Iterator, Mapping, Sequence
21
+ from typing import Final, NamedTuple
22
+
23
+ from constricter.fix.core.known import Inference, Known, Takers
24
+ from constricter.fix.values.inference import inference
25
+ from constricter.fix.values.loops import looped
26
+ from constricter.fix.values.shapes import displayed
27
+ from constricter.fix.values.targets import dict_parts
28
+ from constricter.rules.annotations import dotted, is_vague
29
+ from constricter.rules.syntax import NESTED_SCOPES, own_nodes
30
+ from constricter.rules.walked import walk
31
+
32
+ _LIST: Final = "list"
33
+ _DICT: Final = "dict"
34
+ _SET: Final = "set"
35
+ # What adds an element, by container: the method, and which argument is the element (the key, for a
36
+ # `dict`, is argument 0).
37
+ _ADDERS: Final = {
38
+ _LIST: {"append": 0, "insert": 1},
39
+ _SET: {"add": 0},
40
+ _DICT: {"setdefault": 1},
41
+ }
42
+ # What adds every element of its one argument, by container (a `dict`'s: another's entries).
43
+ _SPREADERS: Final = {_LIST: "extend", _SET: "update", _DICT: "update"}
44
+ # Methods that only read, shrink or reorder.
45
+ _READERS: Final = frozenset(
46
+ {"pop", "get", "items", "keys", "values", "copy", "sort", "reverse", "clear", "count", "index"}
47
+ | {"remove", "discard", "popitem"},
48
+ )
49
+ # Builtins a container can be passed to without being changed.
50
+ _PURE: Final = frozenset(
51
+ {"len", "sorted", "list", "tuple", "set", "frozenset", "enumerate", "reversed", "any", "all"}
52
+ | {"sum", "min", "max", "zip", "iter", "bool", "str", "repr", "dict"},
53
+ )
54
+ _JOIN: Final = "join"
55
+
56
+
57
+ class Fill(NamedTuple):
58
+ """One addition: its key (a `dict`'s), and its value.
59
+
60
+ `spread`: the value isn't what's added, but holds it: its elements (`extend`), or a `dict`'s
61
+ entries (`update`).
62
+ """
63
+
64
+ key: ast.expr | None
65
+ value: ast.expr
66
+ spread: bool = False
67
+
68
+
69
+ class Given(NamedTuple):
70
+ """One pass of the container to a call, as an argument of its own: at a position, or by a keyword."""
71
+
72
+ call: ast.Call
73
+ at: int | str
74
+
75
+
76
+ def takers(tree: ast.Module) -> dict[str, Takers]:
77
+ """Find the module's top-level functions that declare a parameter a builtin container (`list[str]`).
78
+
79
+ A plain `def` or `async def`, defined once and undecorated, without `*args`: an argument's
80
+ place is its parameter's.
81
+
82
+ Returns:
83
+ Each one's such parameters (see `Takers`), by its name; none for a function with none.
84
+
85
+ """
86
+ counts: Counter[str] = Counter(
87
+ stmt.name
88
+ for stmt in tree.body
89
+ if isinstance(stmt, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef)
90
+ )
91
+ found: dict[str, Takers] = {}
92
+ stmt: ast.stmt
93
+ for stmt in tree.body:
94
+ if (
95
+ not isinstance(stmt, ast.FunctionDef | ast.AsyncFunctionDef)
96
+ or counts[stmt.name] != 1
97
+ or stmt.decorator_list
98
+ or stmt.args.vararg is not None
99
+ ):
100
+ continue
101
+ positional: list[ast.arg] = [*stmt.args.posonlyargs, *stmt.args.args]
102
+ taken: dict[str, tuple[int | None, bool, str]] = {}
103
+ at: int
104
+ arg: ast.arg
105
+ for at, arg in enumerate([*positional, *stmt.args.kwonlyargs]):
106
+ text: str | None
107
+ if (text := _container(arg.annotation)) is not None:
108
+ place: int | None = at if at < len(positional) else None
109
+ taken[arg.arg] = (place, at >= len(stmt.args.posonlyargs), text)
110
+ if taken:
111
+ found[stmt.name] = taken
112
+ return found
113
+
114
+
115
+ def _container(annotation: ast.expr | None) -> str | None:
116
+ """Read a parameter's annotation that is a builtin container of known elements: `list[str]`.
117
+
118
+ Returns:
119
+ It as text, or `None` for any other annotation, a vague one, or none.
120
+
121
+ """
122
+ head: str
123
+ match annotation:
124
+ case ast.Subscript(value=ast.Name(id=head)) if head in _ADDERS and not is_vague(annotation):
125
+ return ast.unparse(annotation)
126
+ case _:
127
+ return None
128
+
129
+
130
+ def empty(value: ast.expr) -> str | None:
131
+ """Recognise an empty container: `[]`, `{}`, `list()`, `dict()`, `set()`.
132
+
133
+ Returns:
134
+ Its kind (`list`, `dict`, `set`), or `None` for anything else.
135
+
136
+ """
137
+ name: str
138
+ match value:
139
+ case ast.List(elts=[]):
140
+ return _LIST
141
+ case ast.Dict(keys=[]):
142
+ return _DICT
143
+ case ast.Call(func=ast.Name(id=name), args=[], keywords=[]) if name in {_LIST, _DICT, _SET}:
144
+ return name
145
+ case _:
146
+ return None
147
+
148
+
149
+ class Uses(NamedTuple):
150
+ """A function body's names as `filled` judges them, read once for all its empty containers.
151
+
152
+ `names`: each name's uses (not its bindings) outside nested scopes; `parents`: each node's parent,
153
+ by `id()`; `nested`: every name a nested function, lambda or class uses.
154
+ """
155
+
156
+ names: Mapping[str, list[ast.Name]]
157
+ parents: Mapping[int, ast.AST]
158
+ nested: frozenset[str]
159
+
160
+
161
+ def uses(body: Sequence[ast.stmt]) -> Uses:
162
+ """Read a function body's names for `filled`.
163
+
164
+ Returns:
165
+ Them.
166
+
167
+ """
168
+ names: dict[str, list[ast.Name]] = {}
169
+ parents: dict[int, ast.AST] = {}
170
+ nested: set[str] = set()
171
+ node: ast.AST
172
+ for node in own_nodes(body, parents):
173
+ if isinstance(node, NESTED_SCOPES):
174
+ nested.update(inner.id for inner in walk(node) if isinstance(inner, ast.Name))
175
+ elif isinstance(node, ast.Name) and not isinstance(node.ctx, ast.Store):
176
+ names.setdefault(node.id, []).append(node)
177
+ return Uses(names, parents, frozenset(nested))
178
+
179
+
180
+ def filled(
181
+ found: Uses,
182
+ name: str,
183
+ kind: str,
184
+ known: Known,
185
+ declared: Mapping[str, str],
186
+ ) -> Inference | None:
187
+ """Type an empty container `name` of `kind` from what its function's body (`found`) adds to it.
188
+
189
+ Returns:
190
+ `list[T]`, `set[T]` or `dict[K, V]`, or `None` if a use could add something unseen (a nested
191
+ scope sees it, out of this function's sight), nothing is added, or what's added isn't typed
192
+ alike.
193
+
194
+ """
195
+ if name in found.nested:
196
+ return None
197
+ fills: list[Fill] = []
198
+ taken: list[str | None] = []
199
+ node: ast.Name
200
+ for node in found.names.get(name, []):
201
+ fill: Fill | Given | bool
202
+ if (fill := _use(node, kind, found.parents)) is False and not _only_read(node, kind, found):
203
+ return None
204
+ if isinstance(fill, Fill):
205
+ fills.append(fill)
206
+ elif isinstance(fill, Given):
207
+ taken.append(_taken(fill, kind, known, declared))
208
+ typed: Inference | None = _typed(fills, kind, known, declared) if fills else None
209
+ if (fills and typed is None) or None in taken or not (fills or taken):
210
+ return None
211
+ types: set[str | None] = {*taken, *([] if typed is None else [typed.annotation])}
212
+ passed: Inference = Inference(str(next(iter(types))), "what it's passed as", frozenset({"filled"}))
213
+ return None if len(types) != 1 else typed or passed
214
+
215
+
216
+ def _taken(given: Given, kind: str, known: Known, declared: Mapping[str, str]) -> str | None:
217
+ """Type the container by the parameter a call it's passed to takes it as (see `Indirect.takers`).
218
+
219
+ Returns:
220
+ The parameter's declared type, a container of its kind; `None` for a callee that declares
221
+ none there, one the scope binds itself, or a call that unpacks arguments before it.
222
+
223
+ """
224
+ callee: str = dotted(given.call.func) or ""
225
+ unpacked: bool = any(isinstance(arg, ast.Starred) for arg in given.call.args)
226
+ if unpacked or callee.partition(".")[0] in declared:
227
+ return None
228
+ text: str | None = next(
229
+ (
230
+ text
231
+ for name, (position, named, text) in known.indirect.takers.get(callee, {}).items()
232
+ if (position == given.at) or (named and name == given.at)
233
+ ),
234
+ None,
235
+ )
236
+ return text if text is not None and text.partition("[")[0] == kind else None
237
+
238
+
239
+ def added_names(found: Uses, name: str, kind: str) -> frozenset[str]:
240
+ """Name what the body (`found`) adds to container `name` that is a bare name: `d`, in `xs.append(d)`.
241
+
242
+ One in a display added too (`xs.append((key, d))`). Where the function tests one
243
+ (`isinstance(d, C)`), what's added is narrowed there: its declared type isn't the elements'.
244
+
245
+ Returns:
246
+ The names.
247
+
248
+ """
249
+ fills: list[Fill] = [fill for fill in additions(found, name, kind) if not fill.spread]
250
+ return frozenset(
251
+ part.id for fill in fills for given in (fill.key, fill.value) for part in displayed(given)
252
+ )
253
+
254
+
255
+ def additions(found: Uses, name: str, kind: str) -> list[Fill]:
256
+ """List what the body (`found`) adds to container `name` of `kind`: its fills, in order.
257
+
258
+ Returns:
259
+ Them.
260
+
261
+ """
262
+ return [
263
+ fill
264
+ for fill in (_use(node, kind, found.parents) for node in found.names.get(name, []))
265
+ if isinstance(fill, Fill)
266
+ ]
267
+
268
+
269
+ def stored(body: Sequence[ast.stmt], kinds: Mapping[str, str]) -> Iterator[tuple[str, Fill | bool]]:
270
+ """Judge each read of `self`'s attributes bound to an empty container, in one method's body.
271
+
272
+ `kinds`: each such attribute's kind. A nested function's or lambda's reads aren't judged.
273
+
274
+ Yields:
275
+ The attribute, and its use as `filled` judges a name's: a fill, `True` for one that adds
276
+ nothing, `False` for one that might add something unseen.
277
+
278
+ """
279
+ parents: dict[int, ast.AST] = {}
280
+ found: Uses | None = None # read only for an attribute a local is bound to
281
+ node: ast.AST
282
+ attr: str
283
+ for node in own_nodes(body, parents):
284
+ match node:
285
+ case ast.Attribute(value=ast.Name(id="self"), attr=attr, ctx=ast.Load()) if attr in kinds:
286
+ judged: Fill | Given | bool = _use(node, kinds[attr], parents)
287
+ use: Fill | bool = False if isinstance(judged, Given) else judged # passed on: unseen
288
+ if use is False and isinstance(parents.get(id(node)), ast.Assign):
289
+ found = found or uses(body)
290
+ use = _only_read(node, kinds[attr], found._replace(parents={**found.parents, **parents}))
291
+ yield attr, use
292
+ case _:
293
+ pass
294
+
295
+
296
+ def _only_read(node: ast.expr, kind: str, found: Uses) -> bool:
297
+ """Check whether a read binding the container to a local (`alias = x`) adds nothing to it.
298
+
299
+ Returns:
300
+ Whether every use of that local, in no nested scope, only reads it.
301
+
302
+ """
303
+ name: str
304
+ value: ast.expr
305
+ match found.parents.get(id(node)):
306
+ case ast.Assign(targets=[ast.Name(id=name)], value=value) if (
307
+ value is node and name not in found.nested
308
+ ):
309
+ return all(_use(use, kind, found.parents) is True for use in found.names.get(name, []))
310
+ case _:
311
+ return False
312
+
313
+
314
+ def _use(node: ast.expr, kind: str, parents: Mapping[int, ast.AST]) -> Fill | Given | bool:
315
+ """Judge one read of the container.
316
+
317
+ Returns:
318
+ A fill (what it adds), a pass to a call (whose parameter may say what it takes), `True` for
319
+ a use that adds nothing, `False` for one that might add something unseen.
320
+
321
+ """
322
+ parent: ast.AST | None = parents.get(id(node))
323
+ grandparent: ast.AST | None = None if parent is None else parents.get(id(parent))
324
+ attr: str
325
+ keyword: str
326
+ match parent:
327
+ case ast.Attribute(attr=attr) if isinstance(grandparent, ast.Call) and grandparent.func is parent:
328
+ return _method(attr, grandparent, kind)
329
+ case ast.Subscript(ctx=ast.Store()):
330
+ return _stored(parent, grandparent, kind)
331
+ case _ if _reads(parent, grandparent):
332
+ return True
333
+ case ast.Call() if any(arg is node for arg in parent.args):
334
+ return Given(parent, next(at for at, arg in enumerate(parent.args) if arg is node))
335
+ case ast.keyword(arg=str() as keyword) if isinstance(grandparent, ast.Call):
336
+ return Given(grandparent, keyword)
337
+ case _:
338
+ return False
339
+
340
+
341
+ def _reads(parent: ast.AST | None, grandparent: ast.AST | None) -> bool:
342
+ """Judge a use by the node it's in (and the one that's in): one that can't add to the container.
343
+
344
+ Returns:
345
+ Whether it only reads it: indexed, compared, returned (in a tuple too), iterated, tested,
346
+ formatted, an operand (`x + more`), unpacked (`[*x]`), or passed to a builtin that doesn't
347
+ change it (`len`, `sorted`, ...) or to a `join`.
348
+
349
+ """
350
+ func: str
351
+ attr: str
352
+ match parent:
353
+ case ast.Subscript() | ast.Compare() | ast.Return() | ast.For() | ast.comprehension():
354
+ return True
355
+ case (
356
+ ast.If()
357
+ | ast.While()
358
+ | ast.IfExp()
359
+ | ast.BoolOp()
360
+ | ast.UnaryOp(op=ast.Not())
361
+ | ast.FormattedValue()
362
+ | ast.BinOp()
363
+ | ast.Starred()
364
+ | ast.YieldFrom()
365
+ ):
366
+ return True
367
+ case ast.Call(func=ast.Name(id=func)):
368
+ return func in _PURE
369
+ case ast.Call(func=ast.Attribute(attr=attr)):
370
+ return attr == _JOIN
371
+ case ast.Tuple():
372
+ return isinstance(grandparent, ast.Return)
373
+ case _:
374
+ return False
375
+
376
+
377
+ def _method(attr: str, call: ast.Call, kind: str) -> Fill | bool:
378
+ """Judge a method called on the container.
379
+
380
+ Returns:
381
+ A fill for an adder or a spreader (with the arguments it needs), `True` for a reader, else
382
+ `False`.
383
+
384
+ """
385
+ position: int | None = _ADDERS[kind].get(attr)
386
+ if position is not None and not call.keywords and len(call.args) == position + 1:
387
+ return Fill(call.args[0] if kind == _DICT else None, call.args[position])
388
+ if attr == _SPREADERS[kind] and not call.keywords and len(call.args) == 1:
389
+ return Fill(None, call.args[0], spread=True)
390
+ return attr in _READERS
391
+
392
+
393
+ def _stored(target: ast.Subscript, statement: ast.AST | None, kind: str) -> Fill | bool:
394
+ """Judge `x[k] = v`: a `dict`'s fill (key and value), a `list`'s element replaced.
395
+
396
+ Returns:
397
+ The fill, or `False` for any other store (`x[k] += v`, `x[1:2] = ...`, a set's).
398
+
399
+ """
400
+ if (
401
+ isinstance(statement, ast.Assign)
402
+ and statement.targets == [target]
403
+ and kind != _SET
404
+ and not isinstance(target.slice, ast.Slice)
405
+ ):
406
+ return Fill(target.slice if kind == _DICT else None, statement.value)
407
+ return False
408
+
409
+
410
+ def _typed(fills: Sequence[Fill], kind: str, known: Known, declared: Mapping[str, str]) -> Inference | None:
411
+ """Type the container from its fills, if they agree.
412
+
413
+ Returns:
414
+ The inference, or `None` if a key or value isn't typed, or they differ.
415
+
416
+ """
417
+ each: list[list[Inference | None]] = [added(fill, kind, known, declared) for fill in fills]
418
+ rows: list[list[Inference]] = [[part for part in row if part is not None] for row in each]
419
+ if [len(row) for row in rows] != [len(row) for row in each]:
420
+ return None
421
+ parts: list[Inference] = [part for row in rows for part in row]
422
+ # A `dict`'s keys, then its values; any other container's elements.
423
+ types: list[set[str]] = [{row[at].annotation for row in rows} for at in range(len(rows[0]))]
424
+ if any(len(found) != 1 for found in types):
425
+ return None
426
+ annotation: str = f"{kind}[{', '.join(next(iter(found)) for found in types)}]"
427
+ reason: str = f"what the function adds to it ({len(fills)} {'fill' if len(fills) == 1 else 'fills'})"
428
+ return Inference(annotation, reason, frozenset({"filled"}).union(*(part.kinds for part in parts)))
429
+
430
+
431
+ def added(fill: Fill, kind: str, known: Known, declared: Mapping[str, str]) -> list[Inference | None]:
432
+ """Type what one fill adds: a `dict`'s key and value, any other container's element.
433
+
434
+ Returns:
435
+ Each one's inference (`None`: unknown).
436
+
437
+ """
438
+ if not fill.spread:
439
+ return [inference(part, known, declared) for part in (fill.key, fill.value) if part is not None]
440
+ if kind != _DICT:
441
+ return [looped(fill.value, known, declared, inference)]
442
+ found: Inference | None = inference(fill.value, known, declared)
443
+ types: tuple[str, str] | None = None if found is None else dict_parts(found.annotation)
444
+ if found is None or types is None:
445
+ return [None, None]
446
+ return [found._replace(annotation=types[0]), found._replace(annotation=types[1])]