python-constricter 0.4.1__cp313-cp313-win32.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. constricter/__init__.py +55 -0
  2. constricter/__main__.py +26 -0
  3. constricter/_version.py +4 -0
  4. constricter/cli/__init__.py +2 -0
  5. constricter/cli/baseline.py +107 -0
  6. constricter/cli/collecting.py +54 -0
  7. constricter/cli/command.py +547 -0
  8. constricter/cli/config.py +322 -0
  9. constricter/cli/explain.py +104 -0
  10. constricter/cli/lsp/__init__.py +2 -0
  11. constricter/cli/lsp/connection.py +475 -0
  12. constricter/cli/lsp/edits.py +188 -0
  13. constricter/cli/lsp/guard.py +133 -0
  14. constricter/cli/lsp/hints.py +391 -0
  15. constricter/cli/lsp/protocol.py +192 -0
  16. constricter/cli/lsp/remembered.py +81 -0
  17. constricter/cli/lsp/stored.py +200 -0
  18. constricter/cli/options.py +799 -0
  19. constricter/cli/paths.py +151 -0
  20. constricter/cli/reading.py +36 -0
  21. constricter/cli/report.py +397 -0
  22. constricter/cli/rounds.py +328 -0
  23. constricter/cli/runs.py +90 -0
  24. constricter/cli/schedule.py +463 -0
  25. constricter/cli/traced.py +401 -0
  26. constricter/cli/workers.py +286 -0
  27. constricter/empty.py +66 -0
  28. constricter/fix/__init__.cp313-win32.pyd +0 -0
  29. constricter/fix/__init__.py +2 -0
  30. constricter/fix/core/__init__.cp313-win32.pyd +0 -0
  31. constricter/fix/core/__init__.py +2 -0
  32. constricter/fix/core/asked.cp313-win32.pyd +0 -0
  33. constricter/fix/core/asked.py +56 -0
  34. constricter/fix/core/fixes.cp313-win32.pyd +0 -0
  35. constricter/fix/core/fixes.py +317 -0
  36. constricter/fix/core/imports.cp313-win32.pyd +0 -0
  37. constricter/fix/core/imports.py +580 -0
  38. constricter/fix/core/inherited.cp313-win32.pyd +0 -0
  39. constricter/fix/core/inherited.py +311 -0
  40. constricter/fix/core/known.cp313-win32.pyd +0 -0
  41. constricter/fix/core/known.py +730 -0
  42. constricter/fix/core/signatures.cp313-win32.pyd +0 -0
  43. constricter/fix/core/signatures.py +132 -0
  44. constricter/fix/index/__init__.cp313-win32.pyd +0 -0
  45. constricter/fix/index/__init__.py +2 -0
  46. constricter/fix/index/awaits.cp313-win32.pyd +0 -0
  47. constricter/fix/index/awaits.py +90 -0
  48. constricter/fix/index/beyond.cp313-win32.pyd +0 -0
  49. constricter/fix/index/beyond.py +149 -0
  50. constricter/fix/index/callers.cp313-win32.pyd +0 -0
  51. constricter/fix/index/callers.py +155 -0
  52. constricter/fix/index/crossed.cp313-win32.pyd +0 -0
  53. constricter/fix/index/crossed.py +327 -0
  54. constricter/fix/index/decorated.cp313-win32.pyd +0 -0
  55. constricter/fix/index/decorated.py +140 -0
  56. constricter/fix/index/fixtures.cp313-win32.pyd +0 -0
  57. constricter/fix/index/fixtures.py +176 -0
  58. constricter/fix/index/linked.cp313-win32.pyd +0 -0
  59. constricter/fix/index/linked.py +142 -0
  60. constricter/fix/index/loose.cp313-win32.pyd +0 -0
  61. constricter/fix/index/loose.py +103 -0
  62. constricter/fix/index/managed.cp313-win32.pyd +0 -0
  63. constricter/fix/index/managed.py +63 -0
  64. constricter/fix/index/modules.cp313-win32.pyd +0 -0
  65. constricter/fix/index/modules.py +741 -0
  66. constricter/fix/index/offers.cp313-win32.pyd +0 -0
  67. constricter/fix/index/offers.py +252 -0
  68. constricter/fix/index/order.cp313-win32.pyd +0 -0
  69. constricter/fix/index/order.py +155 -0
  70. constricter/fix/index/origins.cp313-win32.pyd +0 -0
  71. constricter/fix/index/origins.py +238 -0
  72. constricter/fix/index/own_methods.cp313-win32.pyd +0 -0
  73. constricter/fix/index/own_methods.py +91 -0
  74. constricter/fix/index/own_overloads.cp313-win32.pyd +0 -0
  75. constricter/fix/index/own_overloads.py +70 -0
  76. constricter/fix/index/own_types.cp313-win32.pyd +0 -0
  77. constricter/fix/index/own_types.py +330 -0
  78. constricter/fix/index/params.cp313-win32.pyd +0 -0
  79. constricter/fix/index/params.py +81 -0
  80. constricter/fix/index/plain.cp313-win32.pyd +0 -0
  81. constricter/fix/index/plain.py +221 -0
  82. constricter/fix/index/project.cp313-win32.pyd +0 -0
  83. constricter/fix/index/project.py +797 -0
  84. constricter/fix/index/sides.cp313-win32.pyd +0 -0
  85. constricter/fix/index/sides.py +160 -0
  86. constricter/fix/index/strings.cp313-win32.pyd +0 -0
  87. constricter/fix/index/strings.py +46 -0
  88. constricter/fix/index/stubs/__init__.cp313-win32.pyd +0 -0
  89. constricter/fix/index/stubs/__init__.py +2 -0
  90. constricter/fix/index/stubs/atoms.cp313-win32.pyd +0 -0
  91. constricter/fix/index/stubs/atoms.py +425 -0
  92. constricter/fix/index/stubs/chained.cp313-win32.pyd +0 -0
  93. constricter/fix/index/stubs/chained.py +69 -0
  94. constricter/fix/index/stubs/classnames.cp313-win32.pyd +0 -0
  95. constricter/fix/index/stubs/classnames.py +124 -0
  96. constricter/fix/index/stubs/declared.cp313-win32.pyd +0 -0
  97. constricter/fix/index/stubs/declared.py +508 -0
  98. constricter/fix/index/stubs/installed.cp313-win32.pyd +0 -0
  99. constricter/fix/index/stubs/installed.py +425 -0
  100. constricter/fix/index/stubs/names.cp313-win32.pyd +0 -0
  101. constricter/fix/index/stubs/names.py +200 -0
  102. constricter/fix/index/stubs/read.cp313-win32.pyd +0 -0
  103. constricter/fix/index/stubs/read.py +70 -0
  104. constricter/fix/index/stubs/signatures.cp313-win32.pyd +0 -0
  105. constricter/fix/index/stubs/signatures.py +656 -0
  106. constricter/fix/index/stubs/stubbed.cp313-win32.pyd +0 -0
  107. constricter/fix/index/stubs/stubbed.py +208 -0
  108. constricter/fix/index/taken.cp313-win32.pyd +0 -0
  109. constricter/fix/index/taken.py +45 -0
  110. constricter/fix/index/tuples.cp313-win32.pyd +0 -0
  111. constricter/fix/index/tuples.py +112 -0
  112. constricter/fix/libraries/__init__.cp313-win32.pyd +0 -0
  113. constricter/fix/libraries/__init__.py +2 -0
  114. constricter/fix/libraries/library.cp313-win32.pyd +0 -0
  115. constricter/fix/libraries/library.py +420 -0
  116. constricter/fix/libraries/opened.cp313-win32.pyd +0 -0
  117. constricter/fix/libraries/opened.py +147 -0
  118. constricter/fix/libraries/overloads.cp313-win32.pyd +0 -0
  119. constricter/fix/libraries/overloads.py +960 -0
  120. constricter/fix/libraries/receivers.cp313-win32.pyd +0 -0
  121. constricter/fix/libraries/receivers.py +274 -0
  122. constricter/fix/libraries/stdlib.cp313-win32.pyd +0 -0
  123. constricter/fix/libraries/stdlib.py +752 -0
  124. constricter/fix/libraries/walks.cp313-win32.pyd +0 -0
  125. constricter/fix/libraries/walks.py +49 -0
  126. constricter/fix/tables/aliases.json +147 -0
  127. constricter/fix/tables/attributes.json +700 -0
  128. constricter/fix/tables/awaited.json +69 -0
  129. constricter/fix/tables/awaited_overloads.json +14 -0
  130. constricter/fix/tables/bases.json +888 -0
  131. constricter/fix/tables/classes.json +1941 -0
  132. constricter/fix/tables/elements.json +156 -0
  133. constricter/fix/tables/functions.json +58 -0
  134. constricter/fix/tables/generic_attributes.json +254 -0
  135. constricter/fix/tables/method_overloads.json +461 -0
  136. constricter/fix/tables/method_signatures.json +954 -0
  137. constricter/fix/tables/methods.json +954 -0
  138. constricter/fix/tables/overloads.json +540 -0
  139. constricter/fix/tables/platform.json +37 -0
  140. constricter/fix/tables/returns.json +1362 -0
  141. constricter/fix/tables/scalar_members.json +16 -0
  142. constricter/fix/tables/scalars.json +2707 -0
  143. constricter/fix/tables/source.json +28 -0
  144. constricter/fix/tables/subscriptable.json +77 -0
  145. constricter/fix/tables/type_parameters.json +178 -0
  146. constricter/fix/tables/variables.json +2962 -0
  147. constricter/fix/values/__init__.cp313-win32.pyd +0 -0
  148. constricter/fix/values/__init__.py +2 -0
  149. constricter/fix/values/aliased.cp313-win32.pyd +0 -0
  150. constricter/fix/values/aliased.py +195 -0
  151. constricter/fix/values/bodies.cp313-win32.pyd +0 -0
  152. constricter/fix/values/bodies.py +76 -0
  153. constricter/fix/values/callables.cp313-win32.pyd +0 -0
  154. constricter/fix/values/callables.py +323 -0
  155. constricter/fix/values/called.cp313-win32.pyd +0 -0
  156. constricter/fix/values/called.py +194 -0
  157. constricter/fix/values/classvars.cp313-win32.pyd +0 -0
  158. constricter/fix/values/classvars.py +629 -0
  159. constricter/fix/values/decided.cp313-win32.pyd +0 -0
  160. constricter/fix/values/decided.py +429 -0
  161. constricter/fix/values/displays.cp313-win32.pyd +0 -0
  162. constricter/fix/values/displays.py +101 -0
  163. constricter/fix/values/doubts.cp313-win32.pyd +0 -0
  164. constricter/fix/values/doubts.py +677 -0
  165. constricter/fix/values/entered.cp313-win32.pyd +0 -0
  166. constricter/fix/values/entered.py +332 -0
  167. constricter/fix/values/fills.cp313-win32.pyd +0 -0
  168. constricter/fix/values/fills.py +446 -0
  169. constricter/fix/values/guesses.cp313-win32.pyd +0 -0
  170. constricter/fix/values/guesses.py +376 -0
  171. constricter/fix/values/hinted.cp313-win32.pyd +0 -0
  172. constricter/fix/values/hinted.py +811 -0
  173. constricter/fix/values/inference.cp313-win32.pyd +0 -0
  174. constricter/fix/values/inference.py +698 -0
  175. constricter/fix/values/loops.cp313-win32.pyd +0 -0
  176. constricter/fix/values/loops.py +335 -0
  177. constricter/fix/values/members.cp313-win32.pyd +0 -0
  178. constricter/fix/values/members.py +578 -0
  179. constricter/fix/values/narrowed.cp313-win32.pyd +0 -0
  180. constricter/fix/values/narrowed.py +220 -0
  181. constricter/fix/values/operated.cp313-win32.pyd +0 -0
  182. constricter/fix/values/operated.py +202 -0
  183. constricter/fix/values/ordered.cp313-win32.pyd +0 -0
  184. constricter/fix/values/ordered.py +74 -0
  185. constricter/fix/values/returned.cp313-win32.pyd +0 -0
  186. constricter/fix/values/returned.py +479 -0
  187. constricter/fix/values/returns.cp313-win32.pyd +0 -0
  188. constricter/fix/values/returns.py +237 -0
  189. constricter/fix/values/shapes.cp313-win32.pyd +0 -0
  190. constricter/fix/values/shapes.py +654 -0
  191. constricter/fix/values/stored.cp313-win32.pyd +0 -0
  192. constricter/fix/values/stored.py +564 -0
  193. constricter/fix/values/targets.cp313-win32.pyd +0 -0
  194. constricter/fix/values/targets.py +419 -0
  195. constricter/fix/values/unchecked.cp313-win32.pyd +0 -0
  196. constricter/fix/values/unchecked.py +278 -0
  197. constricter/jsonc.py +50 -0
  198. constricter/noqa.py +47 -0
  199. constricter/notebook.py +115 -0
  200. constricter/offences.py +436 -0
  201. constricter/overrides.py +24 -0
  202. constricter/plugins/__init__.py +2 -0
  203. constricter/plugins/flake8.py +133 -0
  204. constricter/plugins/pylint.py +186 -0
  205. constricter/py.typed +0 -0
  206. constricter/recording.py +660 -0
  207. constricter/rules/__init__.cp313-win32.pyd +0 -0
  208. constricter/rules/__init__.py +2 -0
  209. constricter/rules/annotations.cp313-win32.pyd +0 -0
  210. constricter/rules/annotations.py +296 -0
  211. constricter/rules/binding.cp313-win32.pyd +0 -0
  212. constricter/rules/binding.py +652 -0
  213. constricter/rules/calls.cp313-win32.pyd +0 -0
  214. constricter/rules/calls.py +460 -0
  215. constricter/rules/checker.cp313-win32.pyd +0 -0
  216. constricter/rules/checker.py +536 -0
  217. constricter/rules/declarations.cp313-win32.pyd +0 -0
  218. constricter/rules/declarations.py +730 -0
  219. constricter/rules/decorators.cp313-win32.pyd +0 -0
  220. constricter/rules/decorators.py +351 -0
  221. constricter/rules/flow.cp313-win32.pyd +0 -0
  222. constricter/rules/flow.py +502 -0
  223. constricter/rules/keys.cp313-win32.pyd +0 -0
  224. constricter/rules/keys.py +79 -0
  225. constricter/rules/late.cp313-win32.pyd +0 -0
  226. constricter/rules/late.py +969 -0
  227. constricter/rules/narrowing.cp313-win32.pyd +0 -0
  228. constricter/rules/narrowing.py +98 -0
  229. constricter/rules/parsed.cp313-win32.pyd +0 -0
  230. constricter/rules/parsed.py +136 -0
  231. constricter/rules/quoted.cp313-win32.pyd +0 -0
  232. constricter/rules/quoted.py +141 -0
  233. constricter/rules/rebinding.cp313-win32.pyd +0 -0
  234. constricter/rules/rebinding.py +169 -0
  235. constricter/rules/recorded.cp313-win32.pyd +0 -0
  236. constricter/rules/recorded.py +168 -0
  237. constricter/rules/redundant.cp313-win32.pyd +0 -0
  238. constricter/rules/redundant.py +160 -0
  239. constricter/rules/scope.cp313-win32.pyd +0 -0
  240. constricter/rules/scope.py +802 -0
  241. constricter/rules/scoped.cp313-win32.pyd +0 -0
  242. constricter/rules/scoped.py +232 -0
  243. constricter/rules/scoping.cp313-win32.pyd +0 -0
  244. constricter/rules/scoping.py +346 -0
  245. constricter/rules/settings.cp313-win32.pyd +0 -0
  246. constricter/rules/settings.py +189 -0
  247. constricter/rules/syntax.cp313-win32.pyd +0 -0
  248. constricter/rules/syntax.py +472 -0
  249. constricter/rules/tables.cp313-win32.pyd +0 -0
  250. constricter/rules/tables.py +169 -0
  251. constricter/rules/walked.cp313-win32.pyd +0 -0
  252. constricter/rules/walked.py +127 -0
  253. constricter/rules/widened.cp313-win32.pyd +0 -0
  254. constricter/rules/widened.py +322 -0
  255. constricter/trace.py +374 -0
  256. e45ce06b2fdd5c43f782__mypyc.cp313-win32.pyd +0 -0
  257. python_constricter-0.4.1.dist-info/METADATA +400 -0
  258. python_constricter-0.4.1.dist-info/RECORD +261 -0
  259. python_constricter-0.4.1.dist-info/WHEEL +4 -0
  260. python_constricter-0.4.1.dist-info/entry_points.txt +5 -0
  261. python_constricter-0.4.1.dist-info/licenses/LICENSE.md +21 -0
@@ -0,0 +1,160 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Cross-module `--fix` for a classmethod or staticmethod called on its class: `Row.make()`.
3
+
4
+ A class another checked file defines, found through the calling file's imports and the re-exports
5
+ between them (`MultiIndex.from_arrays(...)`, `pd.MultiIndex.from_arrays(...)`); the method's declared
6
+ return types the call where the file can write it (see `project.portable`). And one of the file's
7
+ own classes that takes the method from such a base (`Sub.make()`): a `Self` return is that class.
8
+ """
9
+
10
+ from collections.abc import Callable, Iterator, Mapping
11
+ from pathlib import Path
12
+ from typing import Final
13
+
14
+ from constricter.fix.core.known import Guarded, Origin
15
+ from constricter.fix.index import project
16
+ from constricter.fix.index.modules import SUFFIX, Index, Module, module_name
17
+
18
+ _DOT: Final = "."
19
+ _DEPTH: Final = 8 # how many of the file's own classes a line of bases is followed through
20
+ _HOPS: Final = 5 # how many re-exports a call is followed through
21
+
22
+
23
+ def calls(
24
+ catalog: Index,
25
+ path: Path,
26
+ guarded: Mapping[str, Guarded],
27
+ ) -> tuple[dict[str, str], dict[str, Guarded]]:
28
+ """Type the calls the file at `path` makes to other checked files' classes' class-side methods.
29
+
30
+ `guarded`: the names its other imported types need imported for type checking (see `Guarded`).
31
+
32
+ Returns:
33
+ Each call's name as written (`Row.make`, `m.Row.make`), mapped to its type; and `guarded`,
34
+ with the names these types need too. No call for a file `catalog` doesn't have.
35
+
36
+ """
37
+ needed: dict[str, Guarded] = dict(guarded)
38
+ found: dict[str, str] = {}
39
+ target: Module | None
40
+ if path.suffix != SUFFIX or (target := catalog.modules.get(module_name(path))) is None:
41
+ return found, needed
42
+ spelled: str
43
+ for spelled in target.called:
44
+ receiver: str
45
+ method: str
46
+ receiver, _, method = spelled.rpartition(_DOT)
47
+ base: str | None = _base(target, receiver, method, _DEPTH)
48
+ defined: tuple[Module, str] | None = _class_named(catalog.modules, target, base or receiver)
49
+ if defined is None or method not in defined[0].sides.get(defined[1], {}):
50
+ continue
51
+ if base is not None and method in defined[0].selfish.get(defined[1], ()):
52
+ found[spelled] = receiver # its `Self`: the file's own class
53
+ continue
54
+ declared: dict[str, str] = {spelled: defined[0].sides[defined[1]][method]}
55
+ found.update(project.portable(catalog.modules, (target, defined), base or receiver, declared, needed))
56
+ return found, needed
57
+
58
+
59
+ def _base(target: Module, receiver: str, method: str, depth: int) -> str | None:
60
+ """Find the base another module defines that one of `target`'s own classes takes `method` from.
61
+
62
+ Its first base, through the file's own classes, that the file doesn't define: `None` for a
63
+ class that isn't the file's, one that binds `method` itself, or a name the file binds as a
64
+ value too.
65
+
66
+ Returns:
67
+ The base, as the file writes it.
68
+
69
+ """
70
+ if (
71
+ receiver not in target.bases
72
+ or receiver in target.shadowed
73
+ or method in target.bound.get(receiver, frozenset[str]())
74
+ ):
75
+ return None
76
+ written: str
77
+ for written in target.bases[receiver] if depth else ():
78
+ if written not in target.bases:
79
+ return written
80
+ if method in target.bound.get(written, frozenset[str]()):
81
+ return None # the file's own: its tables have it
82
+ found: str | None
83
+ if (found := _base(target, written, method, depth - 1)) is not None:
84
+ return found
85
+ return None
86
+
87
+
88
+ def _class_named(modules: Mapping[str, Module], target: Module, spelled: str) -> tuple[Module, str] | None:
89
+ """Find the class another module defines that `target` writes as `spelled` (`Row`, `m.Row`, `pkg.Row`).
90
+
91
+ Returns:
92
+ The module defining it and its name there, through imports and re-exports; or `None`.
93
+
94
+ """
95
+ origin: Origin | None = named(target, spelled)
96
+ return None if origin is None else project.definition(modules, origin, project.CLASS)
97
+
98
+
99
+ def named(target: Module, spelled: str) -> Origin | None:
100
+ """Resolve what `target` writes as `spelled` (`Row`, `m.Row`, `pkg.m.load`) through its imports.
101
+
102
+ Returns:
103
+ The module it's an attribute of, and its name there; `None` for a name the file doesn't
104
+ import, binds as a value too, or imports from itself.
105
+
106
+ """
107
+ first: str
108
+ rest: str
109
+ first, _, rest = spelled.partition(_DOT)
110
+ origin: Origin | None = target.names.get(first)
111
+ if origin is None or origin[0] == target.name or first in target.shadowed:
112
+ return None
113
+ path: list[str] = [origin[0], *([origin[1]] if origin[1] else []), *(rest.split(_DOT) if rest else [])]
114
+ return _DOT.join(path[:-1]), path[-1]
115
+
116
+
117
+ def called(
118
+ modules: Mapping[str, Module],
119
+ target: Module,
120
+ has: Callable[[Module, str], bool],
121
+ ) -> Iterator[tuple[str, tuple[Module, str]]]:
122
+ """Find the calls `target` makes to what another module defines, as `has` says of a module's name.
123
+
124
+ Through its imports, and the re-exports between the modules (`_HOPS` of them).
125
+
126
+ Yields:
127
+ Each call's name as written (`load`, `client.load`), and the module defining what it calls
128
+ and its name there.
129
+
130
+ """
131
+ spelled: str
132
+ for spelled in target.called:
133
+ origin: Origin | None = named(target, spelled)
134
+ defined: tuple[Module, str] | None = (
135
+ None if origin is None else _followed(modules, origin, has, _HOPS)
136
+ )
137
+ if defined is not None:
138
+ yield spelled, defined
139
+
140
+
141
+ def _followed(
142
+ modules: Mapping[str, Module],
143
+ origin: Origin,
144
+ has: Callable[[Module, str], bool],
145
+ hops: int,
146
+ ) -> tuple[Module, str] | None:
147
+ """Follow `origin` (through re-exports) to the module that `has` it.
148
+
149
+ Returns:
150
+ That module and the name, or `None`.
151
+
152
+ """
153
+ module: Module | None = modules.get(origin[0])
154
+ name: str | None = origin[1]
155
+ if module is None or name is None or not hops:
156
+ return None
157
+ if has(module, name):
158
+ return module, name
159
+ onward: Origin | None = module.names.get(name)
160
+ return _followed(modules, onward, has, hops - 1) if onward and onward[0] != module.name else None
@@ -0,0 +1,46 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """The strings an annotation leaves inside it (an `Annotated`'s metadata), and the names in them."""
3
+
4
+ import ast
5
+ from functools import lru_cache
6
+ from typing import Final
7
+
8
+ from constricter.rules.annotations import node_name
9
+
10
+ _LITERAL: Final = "Literal"
11
+
12
+
13
+ @lru_cache(maxsize=4096)
14
+ def quoted(annotation: str) -> frozenset[str]:
15
+ """Find the names in the strings left inside an annotation: `meta` in `Annotated[int, 'meta']`.
16
+
17
+ A quoted type is read as its text before it gets here (see `annotations.written`). Not a
18
+ `Literal`'s strings, nor a whole annotation's own quotes (see `roots`).
19
+
20
+ Returns:
21
+ The names.
22
+
23
+ """
24
+ tree: ast.expr = ast.parse(annotation, mode="eval").body
25
+ found: set[str] = set()
26
+ waiting: list[ast.AST | None] = [None, tree] # `None` at its bottom ends it
27
+ node: ast.AST
28
+ for node in iter(waiting.pop, None):
29
+ if isinstance(node, ast.Constant) and isinstance(node.value, str) and node is not tree:
30
+ found.update(_names_in(node.value))
31
+ elif not (isinstance(node, ast.Subscript) and node_name(node.value) == _LITERAL):
32
+ waiting.extend(ast.iter_child_nodes(node))
33
+ return frozenset(found)
34
+
35
+
36
+ def _names_in(text: str) -> set[str]:
37
+ """Read the names a quoted part of an annotation is written with.
38
+
39
+ Returns:
40
+ Them; none if it isn't an expression.
41
+
42
+ """
43
+ try:
44
+ return {node.id for node in ast.walk(ast.parse(text, mode="eval")) if isinstance(node, ast.Name)}
45
+ except SyntaxError:
46
+ return set()
@@ -0,0 +1,2 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """constricter.fix.index.stubs."""
@@ -0,0 +1,425 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """What an installed package's annotations are made of, and which arguments each part takes.
3
+
4
+ An annotation is read (by `constricter.fix.index.stubs.signatures`) into its union's members (`Atom`s):
5
+ `None`, a `Literal`'s values, a type variable (with its bound's members), a `type[...]`, a class, or something
6
+ unread. Each takes a builtin scalar or container argument, or a class passed as one, by the verdicts
7
+ here: a standard-library class by the `scalars` table, an installed protocol by its members, and no
8
+ other installed class.
9
+ """
10
+
11
+ import ast
12
+ from collections.abc import Iterator, Mapping, Sequence
13
+ from functools import lru_cache
14
+ from types import EllipsisType
15
+ from typing import Final, NamedTuple, TypeAlias
16
+
17
+ from constricter.fix.core.known import Origin
18
+ from constricter.fix.core.signatures import Constant
19
+ from constricter.fix.index.modules import Module
20
+ from constricter.fix.index.stubs.declared import Alias, Declarations, Protocol, Variable
21
+ from constricter.fix.libraries import stdlib
22
+ from constricter.fix.libraries.overloads import CONTAINERS, SCALARS
23
+
24
+ YES: Final = "y"
25
+ NO: Final = "n"
26
+ MAYBE: Final = "?"
27
+ NONE: Final = "None"
28
+ LITERAL_STRING: Final = "LiteralString"
29
+ VAR: Final = "var"
30
+ TYPE: Final = "type"
31
+ CLASS: Final = "class"
32
+ UNKNOWN_ATOM: Final = "unknown"
33
+ STR: Final = "str" # `project.definition`'s kind for a function `Declarations` has
34
+ BUILTINS_MODULE: Final = "builtins"
35
+ TYPING: Final = frozenset({"typing", "typing_extensions"})
36
+ ANYTHING: Final = frozenset(
37
+ {"typing.Any", "typing_extensions.Any", "_typeshed.Incomplete", "builtins.object"},
38
+ )
39
+ TYPES: Final = frozenset({"builtins.type", "typing.Type", "typing_extensions.Type"})
40
+ REFUSING: Final = frozenset({"Callable", "Type"}) # no scalar is a callable or a class
41
+ PARAM_SPEC: Final = "*" # a `ParamSpec`'s or `TypeVarTuple`'s bound (see `declared`)
42
+ BUILTIN_TYPES: Final = (*SCALARS, *CONTAINERS) # the `scalars` table's columns
43
+ ELEMENT_OF: Final = frozenset({"list", "set", "frozenset"}) # one type argument: their elements'
44
+ TUPLE: Final = "tuple"
45
+ HOMOGENEOUS: Final = 2 # `tuple[int, ...]`'s arguments
46
+
47
+
48
+ class Scope(NamedTuple):
49
+ """Where an annotation is read: its module, and the type parameters in scope.
50
+
51
+ Each parameter is its bound's text (`None`: unbounded; `"*"`: a `ParamSpec`'s), or an
52
+ argument an alias was subscripted with: its annotation and the scope that's read in.
53
+ """
54
+
55
+ module: Module
56
+ params: Mapping[str, "str | tuple[ast.expr, Scope] | None"]
57
+ owner: str # the module whose function is read: only its type variables are bound
58
+
59
+
60
+ class Atom(NamedTuple):
61
+ """One member of an annotation's union.
62
+
63
+ `kind`: `none`, `literal` (`values`), `any`, `var` (a type variable `name`, bounded by `bound`
64
+ read in `scope`), `type` (a `type[...]`, whose own atoms are `inner`), `class` (`origin`, and
65
+ the installed module defining it, if one does), or `unknown`.
66
+ """
67
+
68
+ kind: str
69
+ values: tuple[Constant, ...] = ()
70
+ name: str = ""
71
+ bound: str | None = None
72
+ scope: Scope | None = None
73
+ inner: tuple["Atom", ...] = ()
74
+ origin: Origin = ("", None)
75
+ module: Module | None = None
76
+ subscripted: bool = False
77
+ args: tuple[ast.expr, ...] = () # a class's type arguments, read in `scope`
78
+ constrained: bool = False # a type variable's `bound` is its constraints: it binds none of them
79
+
80
+
81
+ UNKNOWN: Final = Atom(UNKNOWN_ATOM)
82
+
83
+
84
+ # Whether an atom takes a container argument, and what that container's elements must fit, if it says.
85
+ Taken: TypeAlias = tuple[str, tuple[Atom, ...] | None]
86
+
87
+
88
+ def atom_path(atom: Atom) -> str:
89
+ """Name a class atom by its dotted path (`typing.SupportsIndex`).
90
+
91
+ Returns:
92
+ It.
93
+
94
+ """
95
+ return f"{atom.origin[0]}.{atom.origin[1]}"
96
+
97
+
98
+ def owned(atom: Atom, owner: str) -> bool:
99
+ """Check that a type variable is the function's own: one of its type parameters, or its module's.
100
+
101
+ Returns:
102
+ Whether it is.
103
+
104
+ """
105
+ return atom.scope is not None and atom.scope.module.name == owner
106
+
107
+
108
+ def argument_of(atom: Atom) -> tuple[ast.expr, Scope]:
109
+ """Read back the argument an alias's parameter was bound to (an `arg` atom).
110
+
111
+ Returns:
112
+ Its annotation, and the scope it's read in.
113
+
114
+ """
115
+ scope: Scope | None = atom.scope
116
+ assert scope is not None # an `arg` atom always has one # ruff: ignore[assert]
117
+ return parse_text(atom.name), scope
118
+
119
+
120
+ def bound_alias(alias: Alias, where: Scope, args: Sequence[ast.expr], caller: Scope) -> Scope:
121
+ """Scope an alias's value: its type parameters bound to the arguments it's subscripted with.
122
+
123
+ Its own (`type X[T] = ...`), or else the type variables its value names, in order; one without
124
+ an argument stays unbound (any type).
125
+
126
+ Returns:
127
+ The scope.
128
+
129
+ """
130
+ params: tuple[str, ...] = alias.params or free_variables(alias.value, where)
131
+ bound: dict[str, str | tuple[ast.expr, Scope] | None] = dict.fromkeys(params)
132
+ name: str
133
+ arg: ast.expr
134
+ for name, arg in zip(params, args, strict=False):
135
+ bound[name] = (arg, caller)
136
+ return Scope(where.module, bound, caller.owner)
137
+
138
+
139
+ def free_variables(value: str, scope: Scope) -> tuple[str, ...]:
140
+ """Name the type variables an old-style alias's value names (its parameters), in order.
141
+
142
+ Returns:
143
+ Them.
144
+
145
+ """
146
+ declared: Declarations | None = scope.module.typing
147
+ variables: Mapping[str, Variable] = {} if declared is None else declared.variables
148
+ return tuple(
149
+ dict.fromkeys(
150
+ node.id
151
+ for node in ast.walk(parse_text(value))
152
+ if isinstance(node, ast.Name) and (node.id in variables or node.id in scope.module.type_vars)
153
+ ),
154
+ )
155
+
156
+
157
+ def verdict_of(atoms: Sequence[Atom], scalar: str, *, constant: bool) -> str:
158
+ """Decide whether an annotation's atoms take an argument of type `scalar` (a literal one if `constant`).
159
+
160
+ Returns:
161
+ `y` if one certainly does, `n` if none does, else `?`.
162
+
163
+ """
164
+ return combined({scalar_verdict(atom, scalar, constant=constant) for atom in atoms})
165
+
166
+
167
+ def combined(verdicts: set[str]) -> str:
168
+ """Combine the verdicts of a union's members: `y` if one takes it, `n` if none does, else `?`.
169
+
170
+ Returns:
171
+ The verdict.
172
+
173
+ """
174
+ if YES in verdicts:
175
+ return YES
176
+ return NO if verdicts == {NO} else MAYBE
177
+
178
+
179
+ def scalar_verdict(atom: Atom, scalar: str, *, constant: bool) -> str:
180
+ """Decide whether one atom takes an argument of type `scalar`.
181
+
182
+ Returns:
183
+ The verdict.
184
+
185
+ """
186
+ match atom.kind:
187
+ case "none":
188
+ return YES if scalar == NONE else NO
189
+ case "literal":
190
+ types: set[str] = {type(value).__name__ if value is not None else NONE for value in atom.values}
191
+ matches: bool = scalar in types or (scalar == LITERAL_STRING and STR in types)
192
+ return NO if constant or not matches else MAYBE
193
+ case "var":
194
+ return bound_verdict(atom, scalar)
195
+ case "type":
196
+ return NO
197
+ case "class":
198
+ return class_takes(atom, scalar)
199
+ case _:
200
+ return MAYBE
201
+
202
+
203
+ def bound_verdict(atom: Atom, scalar: str) -> str:
204
+ """Decide whether a type variable takes an argument of type `scalar`, by its bound (its `inner` atoms).
205
+
206
+ Returns:
207
+ The verdict (`y` without one).
208
+
209
+ """
210
+ if atom.bound is None:
211
+ return YES
212
+ return MAYBE if atom.bound == PARAM_SPEC else verdict_of(atom.inner, scalar, constant=False)
213
+
214
+
215
+ def class_takes(atom: Atom, scalar: str) -> str:
216
+ """Decide whether a class takes an argument of type `scalar`.
217
+
218
+ A standard-library one by the `scalars` table; an installed protocol by its members (a generic
219
+ one's parameters unread); no other installed class.
220
+
221
+ Returns:
222
+ The verdict.
223
+
224
+ """
225
+ path: str = atom_path(atom)
226
+ if atom.module is None:
227
+ return external_takes(path, scalar)
228
+ declared: Declarations | None = atom.module.typing
229
+ protocol: Protocol | None = None if declared is None else declared.protocols.get(path.rpartition(".")[2])
230
+ members: frozenset[str] | None = stdlib.scalar_members(scalar)
231
+ if protocol is None or (members is not None and not protocol.members <= members):
232
+ return NO
233
+ return (
234
+ MAYBE
235
+ if members is None or atom.subscripted or path.rpartition(".")[2] in atom.module.generics
236
+ else YES
237
+ )
238
+
239
+
240
+ def element_of(container: str, args: Sequence[ast.expr]) -> ast.expr | None:
241
+ """Find what a builtin container's type arguments say its elements are (`list[int]`, `tuple[int, ...]`).
242
+
243
+ Returns:
244
+ Their annotation, or `None` for any other (`tuple[int, str]`, a `dict`'s, none at all).
245
+
246
+ """
247
+ if container in ELEMENT_OF and len(args) == 1:
248
+ return args[0]
249
+ ellipsis: bool = (
250
+ len(args) == HOMOGENEOUS
251
+ and isinstance(args[1], ast.Constant)
252
+ and isinstance(args[1].value, EllipsisType)
253
+ )
254
+ return args[0] if container == TUPLE and ellipsis else None
255
+
256
+
257
+ @lru_cache(maxsize=8192) # asked of each parameter's classes, for each scalar
258
+ def external_takes(path: str, scalar: str) -> str:
259
+ """Decide whether a class outside the installed packages (`typing.SupportsIndex`) takes a `scalar`.
260
+
261
+ Returns:
262
+ The verdict: the `scalars` table's, or `?` where it hasn't one.
263
+
264
+ """
265
+ module: str
266
+ name: str
267
+ module, _, name = path.rpartition(".")
268
+ verdicts: str | None = stdlib.scalar_verdicts(path)
269
+ if path in ANYTHING:
270
+ verdicts = YES * len(BUILTIN_TYPES)
271
+ elif module in TYPING and name == LITERAL_STRING:
272
+ verdicts = "".join(YES if each == LITERAL_STRING else NO for each in BUILTIN_TYPES)
273
+ elif module in TYPING and name in REFUSING:
274
+ verdicts = NO * len(BUILTIN_TYPES)
275
+ return MAYBE if verdicts is None else verdicts[BUILTIN_TYPES.index(scalar)]
276
+
277
+
278
+ def class_verdict(atom: Atom) -> str:
279
+ """Decide whether one atom takes a class passed as an argument (`dtype=np.float64`).
280
+
281
+ A class is an instance of `type` alone: `type[...]`, `type` and `object` take it; `None`, a
282
+ literal, any other class, or a protocol with an attribute or property (which a class has as a
283
+ descriptor, not a value) don't.
284
+
285
+ Returns:
286
+ The verdict.
287
+
288
+ """
289
+ found: str = MAYBE
290
+ match atom.kind:
291
+ case "none" | "literal":
292
+ found = NO
293
+ case "type": # a `type[T]` takes any class (bounds unchecked: a call outside one is an error)
294
+ found = YES if all(inner.kind == VAR or is_anything(inner) for inner in atom.inner) else MAYBE
295
+ case "var" if atom.bound is None:
296
+ found = YES
297
+ case "var" if atom.bound != PARAM_SPEC:
298
+ found = combined({class_verdict(inner) for inner in atom.inner})
299
+ case "class":
300
+ found = class_takes_class(atom)
301
+ case _:
302
+ pass
303
+ return found
304
+
305
+
306
+ def is_anything(atom: Atom) -> bool:
307
+ """Check whether an atom is `Any`, `object` or `typeshed`'s `Incomplete`.
308
+
309
+ Returns:
310
+ Whether it is.
311
+
312
+ """
313
+ return atom.kind == CLASS and atom_path(atom) in ANYTHING
314
+
315
+
316
+ def class_takes_class(atom: Atom) -> str:
317
+ """Decide whether a class atom takes a class passed as an argument (see `class_verdict`).
318
+
319
+ Returns:
320
+ The verdict.
321
+
322
+ """
323
+ origin: Origin = atom.origin
324
+ if atom_path(atom) in ANYTHING | TYPES:
325
+ return YES
326
+ if atom.module is None:
327
+ return NO if origin[0] == BUILTINS_MODULE else MAYBE
328
+ declared: Declarations | None = atom.module.typing
329
+ protocol: Protocol | None = None if declared is None else declared.protocols.get(origin[1] or "")
330
+ return NO if protocol is None or protocol.properties else MAYBE
331
+
332
+
333
+ def scalar_binds(atoms: Sequence[Atom], scalar: str, owner: str) -> tuple[str, str] | None:
334
+ """Find the type variable an argument of type `scalar` binds: the one member that takes it.
335
+
336
+ Returns:
337
+ Its name, and the argument's type (a literal's widened to `str`); or `None`.
338
+
339
+ """
340
+ taking: list[Atom] = [atom for atom in atoms if scalar_verdict(atom, scalar, constant=False) != NO]
341
+ if len(taking) != 1 or taking[0].kind != VAR or taking[0].constrained or not owned(taking[0], owner):
342
+ return None
343
+ if scalar_verdict(taking[0], scalar, constant=False) != YES:
344
+ return None
345
+ return taking[0].name, STR if scalar == LITERAL_STRING else scalar
346
+
347
+
348
+ def class_binds(atoms: Sequence[Atom], owner: str) -> str | None:
349
+ """Find the type variable a class argument binds: a `type[T]`, all else refusing a class.
350
+
351
+ Returns:
352
+ Its name, or `None`.
353
+
354
+ """
355
+ types: list[Atom] = [atom for atom in atoms if atom.kind == TYPE]
356
+ if len(types) != 1 or any(class_verdict(atom) != NO for atom in atoms if atom.kind != TYPE):
357
+ return None
358
+ inner: tuple[Atom, ...] = types[0].inner
359
+ variable: Atom | None = inner[0] if len(inner) == 1 and inner[0].kind == VAR else None
360
+ return None if variable is None or variable.constrained or not owned(variable, owner) else variable.name
361
+
362
+
363
+ def class_bounded(atoms: Sequence[Atom]) -> bool:
364
+ """Check whether the type variable a class argument binds (see `class_binds`) is bounded.
365
+
366
+ Returns:
367
+ Whether a `type[T]` among `atoms` has a `T` with a bound.
368
+
369
+ """
370
+ return any(
371
+ inner.kind == VAR and inner.bound not in {None, PARAM_SPEC}
372
+ for atom in atoms
373
+ if atom.kind == TYPE
374
+ for inner in atom.inner
375
+ )
376
+
377
+
378
+ def literal_values(args: Sequence[ast.expr]) -> Iterator[Constant]:
379
+ """Read a `Literal[...]`'s values (a name, which isn't one, left out).
380
+
381
+ Yields:
382
+ Each.
383
+
384
+ """
385
+ arg: ast.expr
386
+ for arg in args:
387
+ if isinstance(arg, ast.Constant) and isinstance(
388
+ arg.value,
389
+ bool | int | float | complex | str | bytes | None,
390
+ ):
391
+ yield arg.value
392
+
393
+
394
+ def joined(separator: str, parts: Sequence[str | None]) -> str | None:
395
+ """Join a template's parts, none of which may be missing.
396
+
397
+ Returns:
398
+ The text, or `None` if a part is.
399
+
400
+ """
401
+ return None if None in parts else separator.join(part for part in parts if part is not None)
402
+
403
+
404
+ @lru_cache(maxsize=8192) # a package's signatures name the same few thousand types over and over
405
+ def parse_text(text: str) -> ast.expr:
406
+ """Parse an annotation's text; one that doesn't parse (`"int["`) names nothing.
407
+
408
+ Returns:
409
+ Its tree, shared by everyone who asks: to read, never to change.
410
+
411
+ """
412
+ try:
413
+ return ast.parse(text, mode="eval").body
414
+ except SyntaxError:
415
+ return ast.Constant(value=...) # read as nothing it names
416
+
417
+
418
+ def private_name(name: str | None) -> bool:
419
+ """Check whether a dotted name (`numpy._typing`) has a private part, or is missing.
420
+
421
+ Returns:
422
+ Whether it has, or is.
423
+
424
+ """
425
+ return name is None or any(part.startswith("_") for part in name.split("."))