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,429 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Builtins and operators `--fix` types by their operands' types, as typeshed's `builtins.pyi` has them.
3
+
4
+ `abs`, `round`, `divmod` and `sum` of builtin numbers; `min` and `max` of values of one type (or of
5
+ numbers, the wider one), or of something's elements; `next` of what yields a known type; `dict` of a
6
+ mapping, of pairs or of keywords, and `dict.fromkeys`; `enumerate`, `zip`, `map` and `reversed`, of
7
+ what they yield; the builtin classes' classmethods with a fixed return (`bytes.fromhex`); and `-n`,
8
+ `+n` and `~n`. `infer` types a value as
9
+ `constricter.fix.values.inference` does, and `loop` what iterating it gives.
10
+ """
11
+
12
+ import ast
13
+ from collections.abc import Callable, Mapping, Sequence
14
+ from typing import Final, NamedTuple, TypeAlias
15
+
16
+ from constricter.fix.core.known import ImportPlan, Inference, Known
17
+ from constricter.fix.values.shapes import Infer, is_none, or_none, typed
18
+ from constricter.fix.values.targets import dict_parts, iterator_call
19
+ from constricter.rules.annotations import dotted
20
+ from constricter.rules.flow import members
21
+
22
+ _KIND: Final = "builtin" # the fix kind
23
+ _INT: Final = "int"
24
+ _FLOAT: Final = "float"
25
+ _COMPLEX: Final = "complex"
26
+ _INTEGERS: Final = frozenset({"bool", _INT})
27
+ _REALS: Final = _INTEGERS | {_FLOAT}
28
+ _KEY: Final = "key" # `min`'s and `max`'s keyword that only orders
29
+ _DEFAULT: Final = "default" # theirs for an empty iterable
30
+ _START: Final = "start" # `sum`'s
31
+ # What `sum` doesn't add as another class's instances: the builtin numbers (see `_sum`), the text
32
+ # Types it refuses, and `None`.
33
+ _LIST: Final = "list"
34
+ _TUPLE: Final = "tuple"
35
+ _UNSUMMED: Final = _REALS | {"str", "bytes", "None"}
36
+ _PAIR: Final = 2 # `divmod`'s arguments, and the most `round` and `next` take
37
+ _ANY_LENGTH: Final = "..." # a `tuple[T, ...]`'s second part
38
+ _ENUMERATE: Final = "enumerate"
39
+ _ZIP: Final = "zip"
40
+ _ZIPPED: Final = 5 # the most iterables typeshed's `zip` overloads type part by part
41
+ _ITERATOR: Final = "collections.abc.Iterator" # what `iter` gives
42
+
43
+
44
+ class _Call(NamedTuple):
45
+ """One call to a builtin: its name, positional arguments and keywords, and how to type their parts."""
46
+
47
+ name: str
48
+ args: Sequence[ast.expr]
49
+ keywords: Mapping[str, ast.expr]
50
+ infer: Infer
51
+ loop: Infer
52
+ plan: ImportPlan | None # how the module names a library class, one it's still to import too
53
+
54
+
55
+ _Rule: TypeAlias = Callable[[_Call], Inference | None]
56
+
57
+
58
+ def _made(call: _Call, annotation: str, *parts: Inference) -> Inference:
59
+ """Write a call's inference, resting on its `parts`'.
60
+
61
+ Returns:
62
+ It.
63
+
64
+ """
65
+ return Inference(
66
+ annotation,
67
+ f"`{call.name}` of {parts[0].reason}",
68
+ frozenset({_KIND}).union(*(part.kinds for part in parts)),
69
+ tuple(read for part in parts for read in part.reads),
70
+ )
71
+
72
+
73
+ def _numbers(call: _Call, count: int) -> list[Inference] | None:
74
+ """Type a call's arguments, `count` of them and no keyword, each a builtin number.
75
+
76
+ Returns:
77
+ Their inferences, or `None` if it's called otherwise or one isn't an `int`, `bool` or `float`.
78
+
79
+ """
80
+ if call.keywords or len(call.args) != count:
81
+ return None
82
+ found: list[Inference | None] = [call.infer(arg) for arg in call.args]
83
+ numbers: list[Inference] = [part for part in found if part is not None and part.annotation in _REALS]
84
+ return numbers if len(numbers) == count else None
85
+
86
+
87
+ def _wider(parts: Sequence[Inference]) -> str:
88
+ """Name the type arithmetic on builtin numbers gives.
89
+
90
+ Returns:
91
+ `float` if any of them is one, else `int`.
92
+
93
+ """
94
+ return _FLOAT if any(part.annotation == _FLOAT for part in parts) else _INT
95
+
96
+
97
+ def _abs(call: _Call) -> Inference | None:
98
+ """Type `abs(x)`: an `int`'s is an `int`, a `float`'s or a `complex`'s a `float`.
99
+
100
+ Returns:
101
+ The inference, or `None`.
102
+
103
+ """
104
+ found: Inference | None = call.infer(call.args[0]) if len(call.args) == 1 and not call.keywords else None
105
+ if found is None or found.annotation not in _REALS | {_COMPLEX}:
106
+ return None
107
+ return _made(call, _INT if found.annotation in _INTEGERS else _FLOAT, found)
108
+
109
+
110
+ def _round(call: _Call) -> Inference | None:
111
+ """Type `round(x)`, an `int`, and `round(x, digits)`, a number of `x`'s type.
112
+
113
+ Returns:
114
+ The inference, or `None`.
115
+
116
+ """
117
+ digits: bool = len(call.args) == _PAIR and not is_none(call.args[1])
118
+ found: list[Inference] | None = _numbers(call._replace(args=call.args[:1]), 1)
119
+ if found is None or len(call.args) > _PAIR or call.keywords:
120
+ return None
121
+ return _made(call, _wider(found) if digits else _INT, *found)
122
+
123
+
124
+ def _divmod(call: _Call) -> Inference | None:
125
+ """Type `divmod(a, b)` of builtin numbers: a pair of `int`s, or of `float`s if either is one.
126
+
127
+ Returns:
128
+ The inference, or `None`.
129
+
130
+ """
131
+ found: list[Inference] | None = _numbers(call, _PAIR)
132
+ return None if found is None else _made(call, f"tuple[{_wider(found)}, {_wider(found)}]", *found)
133
+
134
+
135
+ def _sum(call: _Call) -> Inference | None:
136
+ """Type `sum(xs)` of builtin numbers, with a start of them or none: an `int`, or a `float`.
137
+
138
+ Of another one type `T`: with no start, a `T | int` (the `0` it starts from, for no elements);
139
+ with a start of `T`, or an empty display of `T`'s builtin class (`sum(lists, [])`), a `T`.
140
+
141
+ Returns:
142
+ The inference, or `None`.
143
+
144
+ """
145
+ starts: list[ast.expr] = [*call.args[1:], *call.keywords.values()]
146
+ if not call.args or len(starts) > 1 or call.keywords.keys() - {_START}:
147
+ return None
148
+ element: Inference | None = call.loop(call.args[0])
149
+ parts: list[Inference | None] = [element, *(call.infer(start) for start in starts)]
150
+ found: list[Inference] = [part for part in parts if part is not None and part.annotation in _REALS]
151
+ if len(found) == len(parts):
152
+ return _made(call, _wider(found), *found)
153
+ if element is None or element.annotation in _UNSUMMED or len(members(element.annotation) or ()) != 1:
154
+ return None
155
+ if not starts:
156
+ return _made(call, f"{element.annotation} | {_INT}", element)
157
+ start: Inference | None = parts[1]
158
+ same: bool = start is not None and start.annotation == element.annotation
159
+ emptied: bool = _empty_of(starts[0], element.annotation)
160
+ return _made(call, element.annotation, element) if same or emptied else None
161
+
162
+
163
+ def _empty_of(start: ast.expr, annotation: str) -> bool:
164
+ """Check whether `start` is an empty `list` or `tuple` display, and `annotation` that builtin's type.
165
+
166
+ A `tuple` of any length (`tuple[T, ...]`): a fixed one's sum is longer than it.
167
+
168
+ Returns:
169
+ Whether it is: adding it to one changes nothing.
170
+
171
+ """
172
+ head: str = annotation.partition("[")[0]
173
+ return (isinstance(start, ast.List) and not start.elts and head == _LIST) or (
174
+ isinstance(start, ast.Tuple) and not start.elts and head == _TUPLE and annotation.endswith(", ...]")
175
+ )
176
+
177
+
178
+ def _extreme(call: _Call) -> Inference | None:
179
+ """Type `min` and `max`: of several values of one type, that type; of one argument, its elements'.
180
+
181
+ Of an `int` and a `float`, a `float`: either fits one.
182
+
183
+ With a `default`, one of the elements' type, or `None` (`T | None`).
184
+
185
+ Returns:
186
+ The inference, or `None`.
187
+
188
+ """
189
+ if not call.args or call.keywords.keys() - {_KEY, _DEFAULT}:
190
+ return None
191
+ default: ast.expr | None = call.keywords.get(_DEFAULT)
192
+ if len(call.args) > 1:
193
+ parts: list[Inference | None] = [typed(arg, call.infer) for arg in call.args]
194
+ found: list[Inference] = [part for part in parts if part is not None]
195
+ if len(found) != len(parts) or default is not None:
196
+ return None
197
+ if len({part.annotation for part in found}) > 1:
198
+ # Numbers of two types: the wider one holds whichever it is.
199
+ mixed: bool = all(part.annotation in _REALS for part in found)
200
+ return _made(call, _wider(found), *found) if mixed else None
201
+ single: bool = len(members(found[0].annotation) or ()) == 1
202
+ return _made(call, found[0].annotation, *found) if single else None
203
+ element: Inference | None = call.loop(call.args[0])
204
+ return None if element is None else _defaulted(call, element, default)
205
+
206
+
207
+ def _next(call: _Call) -> Inference | None:
208
+ """Type `next(it)`: what `it` yields; with a default, of that type, or `None` (`T | None`).
209
+
210
+ Returns:
211
+ The inference, or `None`.
212
+
213
+ """
214
+ if call.keywords or not 1 <= len(call.args) <= _PAIR:
215
+ return None
216
+ element: Inference | None = call.loop(call.args[0])
217
+ return None if element is None else _defaulted(call, element, (*call.args, None)[1])
218
+
219
+
220
+ def _defaulted(call: _Call, element: Inference, default: ast.expr | None) -> Inference | None:
221
+ """Type a call that gives an `element`, or else its `default` (if any).
222
+
223
+ Returns:
224
+ The element's type, a default of it, or with `None` for a `None` one; `None` for any other.
225
+
226
+ """
227
+ if default is None:
228
+ return _made(call, element.annotation, element)
229
+ if is_none(default):
230
+ union: str | None = or_none(element.annotation)
231
+ return None if union is None else _made(call, union, element)
232
+ other: Inference | None = call.infer(default)
233
+ same: bool = other is not None and other.annotation == element.annotation
234
+ return _made(call, element.annotation, element, other) if other is not None and same else None
235
+
236
+
237
+ def _dict(call: _Call) -> Inference | None:
238
+ """Type `dict(d)`, by `d`'s keys and values; `dict(pairs)`, by their parts; `dict(a=1)`, a `dict[str, T]`.
239
+
240
+ Returns:
241
+ The inference, or `None` for both arguments and keywords, or parts that don't agree.
242
+
243
+ """
244
+ if call.keywords and not call.args:
245
+ values: list[Inference | None] = [call.infer(value) for value in call.keywords.values()]
246
+ found: list[Inference] = [value for value in values if value is not None]
247
+ same: bool = len({value.annotation for value in found}) == 1 and len(found) == len(values)
248
+ return _made(call, f"dict[str, {found[0].annotation}]", *found) if same else None
249
+ if call.keywords or len(call.args) != 1:
250
+ return None
251
+ copied: Inference | None = typed(call.args[0], call.infer)
252
+ parts: tuple[str, str] | None = None if copied is None else dict_parts(copied.annotation)
253
+ if copied is not None and parts is not None:
254
+ return _made(call, f"dict[{parts[0]}, {parts[1]}]", copied)
255
+ item: Inference | None = call.loop(call.args[0])
256
+ pair: tuple[str, str] | None = None if item is None else _pair(item.annotation)
257
+ return None if item is None or pair is None else _made(call, f"dict[{pair[0]}, {pair[1]}]", item)
258
+
259
+
260
+ def _pair(annotation: str) -> tuple[str, str] | None:
261
+ """Read a `tuple[K, V]`'s two parts.
262
+
263
+ Returns:
264
+ Them, as text; `None` for any other annotation, a `tuple[T, ...]` included.
265
+
266
+ """
267
+ key: ast.expr
268
+ value: ast.expr
269
+ # `annotation` is always `ast.unparse`'s own output, so it's always valid Python to parse back.
270
+ match ast.parse(annotation, mode="eval").body:
271
+ case ast.Subscript(value=ast.Name(id="tuple" | "Tuple"), slice=ast.Tuple(elts=[key, value])) if (
272
+ ast.unparse(value) != _ANY_LENGTH
273
+ ):
274
+ return ast.unparse(key), ast.unparse(value)
275
+ case _:
276
+ return None
277
+
278
+
279
+ def _fromkeys(call: _Call) -> Inference | None:
280
+ """Type `dict.fromkeys(keys, value)`: a `dict` of the keys' elements to the value's type.
281
+
282
+ Returns:
283
+ The inference, or `None` without a value (each is then `None`, or anything), or a part unknown.
284
+
285
+ """
286
+ if call.keywords or len(call.args) != _PAIR:
287
+ return None
288
+ key: Inference | None = call.loop(call.args[0])
289
+ value: Inference | None = call.infer(call.args[1])
290
+ if key is None or value is None:
291
+ return None
292
+ return _made(call, f"dict[{key.annotation}, {value.annotation}]", key, value)
293
+
294
+
295
+ def _fixed(call: _Call) -> Inference:
296
+ """Type a builtin class's classmethod whose return is fixed, whatever it's given.
297
+
298
+ Returns:
299
+ The inference.
300
+
301
+ """
302
+ return Inference(_FIXED[call.name], f"`{call.name}`'s fixed return type", frozenset({_KIND}))
303
+
304
+
305
+ # The builtin classes' classmethods and staticmethods whose return type is fixed, as called on the class.
306
+ _FIXED: Final = {
307
+ "bytearray.fromhex": "bytearray",
308
+ "bytes.fromhex": "bytes",
309
+ "bytes.maketrans": "bytes",
310
+ "float.fromhex": "float",
311
+ "int.from_bytes": "int",
312
+ }
313
+
314
+
315
+ def _iterating(call: _Call) -> Inference | None:
316
+ """Type `enumerate(xs)`, `zip(xs, ys)`, `map(f, xs)`, `filter(f, xs)` or `reversed(xs)`: the iterator.
317
+
318
+ Of what it yields, as a loop over it is typed (`call.loop`): `zip[tuple[str, int]]`, `map[str]`,
319
+ `filter[str]`; `enumerate[T]` by the `T` it counts.
320
+
321
+ Returns:
322
+ The inference, or `None` where a loop's target has none, or `zip` takes more than typeshed
323
+ has overloads for.
324
+
325
+ """
326
+ if not call.args or (call.name == _ZIP and len(call.args) > _ZIPPED):
327
+ return None
328
+ whole: ast.Call = ast.copy_location(
329
+ ast.Call(
330
+ ast.Name(call.name, ast.Load()),
331
+ [*call.args],
332
+ [ast.keyword(arg=name, value=value) for name, value in call.keywords.items()],
333
+ ),
334
+ call.args[0],
335
+ )
336
+ if iterator_call(whole) is None: # a keyword that may change what it yields
337
+ return None
338
+ element: Inference | None = call.loop(call.args[0] if call.name == _ENUMERATE else whole)
339
+ if element is None or (call.name == _ENUMERATE and call.loop(whole) is None):
340
+ return None
341
+ return _made(call, f"{call.name}[{element.annotation}]", element)
342
+
343
+
344
+ def _iter(call: _Call) -> Inference | None:
345
+ """Type `iter(xs)`: an `Iterator` of what a loop over `xs` binds.
346
+
347
+ Returns:
348
+ The inference, or `None`: with a sentinel, where a loop's target has no type, or where the
349
+ module can't name `Iterator`.
350
+
351
+ """
352
+ element: Inference | None = None if call.keywords or len(call.args) != 1 else call.loop(call.args[0])
353
+ spelled: str | None = None if element is None or call.plan is None else call.plan.spell(_ITERATOR)
354
+ return (
355
+ None
356
+ if element is None or spelled is None
357
+ else _made(call, f"{spelled}[{element.annotation}]", element)
358
+ )
359
+
360
+
361
+ _CALLS: Final[Mapping[str, _Rule]] = {
362
+ "abs": _abs,
363
+ **dict.fromkeys(("enumerate", "filter", "map", "reversed", "zip"), _iterating),
364
+ "dict": _dict,
365
+ "dict.fromkeys": _fromkeys,
366
+ "divmod": _divmod,
367
+ "iter": _iter,
368
+ "max": _extreme,
369
+ "min": _extreme,
370
+ "next": _next,
371
+ "round": _round,
372
+ "sum": _sum,
373
+ **dict.fromkeys(_FIXED, _fixed),
374
+ }
375
+
376
+
377
+ def decides(func: ast.expr, known: Known) -> bool:
378
+ """Check whether `func` is a builtin, or a builtin class's method, this module types the calls of.
379
+
380
+ Returns:
381
+ Whether it is, and the module doesn't bind the name itself.
382
+
383
+ """
384
+ spelled: str | None = dotted(func)
385
+ return spelled is not None and spelled in _CALLS and known.is_builtin(spelled.partition(".")[0])
386
+
387
+
388
+ def builtin(value: ast.expr, known: Known, infer: Infer, loop: Infer) -> Inference | None:
389
+ """Infer a call to a builtin its arguments' types decide (see the module docstring).
390
+
391
+ Returns:
392
+ The inference, or `None` for any other call, one that unpacks its arguments, a module that
393
+ binds the name itself, or arguments that don't decide it.
394
+
395
+ """
396
+ func: ast.expr
397
+ args: list[ast.expr]
398
+ keywords: list[ast.keyword]
399
+ match value:
400
+ case ast.Call(func=func, args=args, keywords=keywords) if (
401
+ decides(func, known)
402
+ and not any(isinstance(arg, ast.Starred) for arg in args)
403
+ and all(keyword.arg for keyword in keywords)
404
+ ):
405
+ named: dict[str, ast.expr] = {keyword.arg or "": keyword.value for keyword in keywords}
406
+ name: str = ast.unparse(func)
407
+ return _CALLS[name](_Call(name, args, named, infer, loop, known.names.plan))
408
+ case _:
409
+ return None
410
+
411
+
412
+ def unary(value: ast.UnaryOp, infer: Infer) -> Inference | None:
413
+ """Infer `-x` and `+x` of a builtin number (its own type, a `bool`'s an `int`), and `~n` of an `int`.
414
+
415
+ Not `not x`, a `bool` whatever `x` is.
416
+
417
+ Returns:
418
+ The inference, or `None` for any other operand.
419
+
420
+ """
421
+ found: Inference | None = infer(value.operand)
422
+ signed: bool = isinstance(value.op, ast.USub | ast.UAdd)
423
+ if found is None or found.annotation not in (_REALS | {_COMPLEX} if signed else _INTEGERS):
424
+ return None
425
+ return Inference(
426
+ _INT if found.annotation in _INTEGERS else found.annotation,
427
+ "arithmetic on builtin types",
428
+ found.kinds | {"arithmetic"},
429
+ )
@@ -0,0 +1,101 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """A list, set, tuple or dict display's type, from its elements' types.
3
+
4
+ Where a list's, set's or dict's differ, they're joined into a union (`[1, "a"]` is a
5
+ `list[int | str]`): a guess, since a checker joins them too, but not always to the same union (mypy
6
+ to their common base).
7
+ """
8
+
9
+ import ast
10
+ import re
11
+ from collections.abc import Sequence
12
+ from typing import Final
13
+
14
+ from constricter.fix.core.known import Inference
15
+
16
+ JOINED: Final = "joined" # the fix kind (and guessing mechanism) of a display's joined element types
17
+ _MOST_JOINED: Final = 3 # the most types a display's elements are joined from
18
+ _PLAIN: Final = re.compile(r"[A-Za-z_][\w.]*") # a type that's a name alone
19
+ _UNION_BAR: Final = " | "
20
+ _NONE: Final = "None"
21
+
22
+
23
+ def element_type(
24
+ elements: Sequence[ast.expr | None],
25
+ parts: Sequence[Inference | None],
26
+ ) -> tuple[str | None, bool]:
27
+ """Type a display's `elements` as one: the type they all have (`parts`), or their types joined.
28
+
29
+ Returns:
30
+ The type (`None` if there's none to write), and whether it's joined from several.
31
+
32
+ """
33
+ uniform: str | None = _uniform(parts)
34
+ return (uniform, False) if uniform is not None else (_joint(elements, parts), True)
35
+
36
+
37
+ def tuple_type(parts: Sequence[Inference | None], max_length: int) -> str | None:
38
+ """Type a tuple display from its elements' types.
39
+
40
+ One type per element (`tuple[int, str]`), up to `max_length` of them; a longer one (LVA011's)
41
+ is `tuple[T, ...]` when every element is a `T`, and nothing when they differ: its fields need
42
+ names, not a list of types. One that unpacks (`max_length` below 0) is always the longer kind.
43
+
44
+ Returns:
45
+ The annotation, or `None` if an element's type isn't known or a long tuple's differ.
46
+
47
+ """
48
+ known_parts: list[Inference] = [part for part in parts if part is not None]
49
+ if len(known_parts) != len(parts):
50
+ return None
51
+ if len(parts) <= max_length:
52
+ return f"tuple[{', '.join(part.annotation for part in known_parts)}]"
53
+ element: str | None = _uniform(parts)
54
+ return None if element is None else f"tuple[{element}, ...]"
55
+
56
+
57
+ def _uniform(parts: Sequence[Inference | None]) -> str | None:
58
+ """Find the one type every element has.
59
+
60
+ Returns:
61
+ That type, or `None` if they differ or any is unknown.
62
+
63
+ """
64
+ types: set[str | None] = {None if part is None else part.annotation for part in parts}
65
+ return next(iter(types)) if len(types) == 1 else None
66
+
67
+
68
+ def _joint(elements: Sequence[ast.expr | None], parts: Sequence[Inference | None]) -> str | None:
69
+ """Join a display's `elements`' types (`parts`) where they differ: `int | str`, `None` last.
70
+
71
+ Up to `_MOST_JOINED` of them, each a plain name (a builtin's, a class's): a union of unions or
72
+ of containers is better left to the author. A literal `None` among them is `None`.
73
+
74
+ Returns:
75
+ The union, or `None` if any is unknown, or they're too many or not plain.
76
+
77
+ """
78
+ found: list[str | None] = [
79
+ _NONE
80
+ if isinstance(element, ast.Constant) and element.value is None
81
+ else None
82
+ if part is None
83
+ else part.annotation
84
+ for element, part in zip(elements, parts, strict=True)
85
+ ]
86
+ return None if None in found else union(list(dict.fromkeys(each for each in found if each is not None)))
87
+
88
+
89
+ def union(types: Sequence[str]) -> str | None:
90
+ """Join several types as one: `int | str`, `None` last.
91
+
92
+ Up to `_MOST_JOINED` of them, each a plain name (a builtin's, a class's): a union of unions or of
93
+ containers is better left to the author.
94
+
95
+ Returns:
96
+ The union, or `None` if they're too many, too few, or not plain.
97
+
98
+ """
99
+ if not 1 < len(types) <= _MOST_JOINED or not all(_PLAIN.fullmatch(each) for each in types):
100
+ return None
101
+ return _UNION_BAR.join(sorted(types, key=lambda each: each == _NONE))