python-constricter 0.4.1__cp311-cp311-win32.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. constricter/__init__.py +55 -0
  2. constricter/__main__.py +26 -0
  3. constricter/_version.py +4 -0
  4. constricter/cli/__init__.py +2 -0
  5. constricter/cli/baseline.py +107 -0
  6. constricter/cli/collecting.py +54 -0
  7. constricter/cli/command.py +547 -0
  8. constricter/cli/config.py +322 -0
  9. constricter/cli/explain.py +104 -0
  10. constricter/cli/lsp/__init__.py +2 -0
  11. constricter/cli/lsp/connection.py +475 -0
  12. constricter/cli/lsp/edits.py +188 -0
  13. constricter/cli/lsp/guard.py +133 -0
  14. constricter/cli/lsp/hints.py +391 -0
  15. constricter/cli/lsp/protocol.py +192 -0
  16. constricter/cli/lsp/remembered.py +81 -0
  17. constricter/cli/lsp/stored.py +200 -0
  18. constricter/cli/options.py +799 -0
  19. constricter/cli/paths.py +151 -0
  20. constricter/cli/reading.py +36 -0
  21. constricter/cli/report.py +397 -0
  22. constricter/cli/rounds.py +328 -0
  23. constricter/cli/runs.py +90 -0
  24. constricter/cli/schedule.py +463 -0
  25. constricter/cli/traced.py +401 -0
  26. constricter/cli/workers.py +286 -0
  27. constricter/empty.py +66 -0
  28. constricter/fix/__init__.cp311-win32.pyd +0 -0
  29. constricter/fix/__init__.py +2 -0
  30. constricter/fix/core/__init__.cp311-win32.pyd +0 -0
  31. constricter/fix/core/__init__.py +2 -0
  32. constricter/fix/core/asked.cp311-win32.pyd +0 -0
  33. constricter/fix/core/asked.py +56 -0
  34. constricter/fix/core/fixes.cp311-win32.pyd +0 -0
  35. constricter/fix/core/fixes.py +317 -0
  36. constricter/fix/core/imports.cp311-win32.pyd +0 -0
  37. constricter/fix/core/imports.py +580 -0
  38. constricter/fix/core/inherited.cp311-win32.pyd +0 -0
  39. constricter/fix/core/inherited.py +311 -0
  40. constricter/fix/core/known.cp311-win32.pyd +0 -0
  41. constricter/fix/core/known.py +730 -0
  42. constricter/fix/core/signatures.cp311-win32.pyd +0 -0
  43. constricter/fix/core/signatures.py +132 -0
  44. constricter/fix/index/__init__.cp311-win32.pyd +0 -0
  45. constricter/fix/index/__init__.py +2 -0
  46. constricter/fix/index/awaits.cp311-win32.pyd +0 -0
  47. constricter/fix/index/awaits.py +90 -0
  48. constricter/fix/index/beyond.cp311-win32.pyd +0 -0
  49. constricter/fix/index/beyond.py +149 -0
  50. constricter/fix/index/callers.cp311-win32.pyd +0 -0
  51. constricter/fix/index/callers.py +155 -0
  52. constricter/fix/index/crossed.cp311-win32.pyd +0 -0
  53. constricter/fix/index/crossed.py +327 -0
  54. constricter/fix/index/decorated.cp311-win32.pyd +0 -0
  55. constricter/fix/index/decorated.py +140 -0
  56. constricter/fix/index/fixtures.cp311-win32.pyd +0 -0
  57. constricter/fix/index/fixtures.py +176 -0
  58. constricter/fix/index/linked.cp311-win32.pyd +0 -0
  59. constricter/fix/index/linked.py +142 -0
  60. constricter/fix/index/loose.cp311-win32.pyd +0 -0
  61. constricter/fix/index/loose.py +103 -0
  62. constricter/fix/index/managed.cp311-win32.pyd +0 -0
  63. constricter/fix/index/managed.py +63 -0
  64. constricter/fix/index/modules.cp311-win32.pyd +0 -0
  65. constricter/fix/index/modules.py +741 -0
  66. constricter/fix/index/offers.cp311-win32.pyd +0 -0
  67. constricter/fix/index/offers.py +252 -0
  68. constricter/fix/index/order.cp311-win32.pyd +0 -0
  69. constricter/fix/index/order.py +155 -0
  70. constricter/fix/index/origins.cp311-win32.pyd +0 -0
  71. constricter/fix/index/origins.py +238 -0
  72. constricter/fix/index/own_methods.cp311-win32.pyd +0 -0
  73. constricter/fix/index/own_methods.py +91 -0
  74. constricter/fix/index/own_overloads.cp311-win32.pyd +0 -0
  75. constricter/fix/index/own_overloads.py +70 -0
  76. constricter/fix/index/own_types.cp311-win32.pyd +0 -0
  77. constricter/fix/index/own_types.py +330 -0
  78. constricter/fix/index/params.cp311-win32.pyd +0 -0
  79. constricter/fix/index/params.py +81 -0
  80. constricter/fix/index/plain.cp311-win32.pyd +0 -0
  81. constricter/fix/index/plain.py +221 -0
  82. constricter/fix/index/project.cp311-win32.pyd +0 -0
  83. constricter/fix/index/project.py +797 -0
  84. constricter/fix/index/sides.cp311-win32.pyd +0 -0
  85. constricter/fix/index/sides.py +160 -0
  86. constricter/fix/index/strings.cp311-win32.pyd +0 -0
  87. constricter/fix/index/strings.py +46 -0
  88. constricter/fix/index/stubs/__init__.cp311-win32.pyd +0 -0
  89. constricter/fix/index/stubs/__init__.py +2 -0
  90. constricter/fix/index/stubs/atoms.cp311-win32.pyd +0 -0
  91. constricter/fix/index/stubs/atoms.py +425 -0
  92. constricter/fix/index/stubs/chained.cp311-win32.pyd +0 -0
  93. constricter/fix/index/stubs/chained.py +69 -0
  94. constricter/fix/index/stubs/classnames.cp311-win32.pyd +0 -0
  95. constricter/fix/index/stubs/classnames.py +124 -0
  96. constricter/fix/index/stubs/declared.cp311-win32.pyd +0 -0
  97. constricter/fix/index/stubs/declared.py +508 -0
  98. constricter/fix/index/stubs/installed.cp311-win32.pyd +0 -0
  99. constricter/fix/index/stubs/installed.py +425 -0
  100. constricter/fix/index/stubs/names.cp311-win32.pyd +0 -0
  101. constricter/fix/index/stubs/names.py +200 -0
  102. constricter/fix/index/stubs/read.cp311-win32.pyd +0 -0
  103. constricter/fix/index/stubs/read.py +70 -0
  104. constricter/fix/index/stubs/signatures.cp311-win32.pyd +0 -0
  105. constricter/fix/index/stubs/signatures.py +656 -0
  106. constricter/fix/index/stubs/stubbed.cp311-win32.pyd +0 -0
  107. constricter/fix/index/stubs/stubbed.py +208 -0
  108. constricter/fix/index/taken.cp311-win32.pyd +0 -0
  109. constricter/fix/index/taken.py +45 -0
  110. constricter/fix/index/tuples.cp311-win32.pyd +0 -0
  111. constricter/fix/index/tuples.py +112 -0
  112. constricter/fix/libraries/__init__.cp311-win32.pyd +0 -0
  113. constricter/fix/libraries/__init__.py +2 -0
  114. constricter/fix/libraries/library.cp311-win32.pyd +0 -0
  115. constricter/fix/libraries/library.py +420 -0
  116. constricter/fix/libraries/opened.cp311-win32.pyd +0 -0
  117. constricter/fix/libraries/opened.py +147 -0
  118. constricter/fix/libraries/overloads.cp311-win32.pyd +0 -0
  119. constricter/fix/libraries/overloads.py +960 -0
  120. constricter/fix/libraries/receivers.cp311-win32.pyd +0 -0
  121. constricter/fix/libraries/receivers.py +274 -0
  122. constricter/fix/libraries/stdlib.cp311-win32.pyd +0 -0
  123. constricter/fix/libraries/stdlib.py +752 -0
  124. constricter/fix/libraries/walks.cp311-win32.pyd +0 -0
  125. constricter/fix/libraries/walks.py +49 -0
  126. constricter/fix/tables/aliases.json +147 -0
  127. constricter/fix/tables/attributes.json +700 -0
  128. constricter/fix/tables/awaited.json +69 -0
  129. constricter/fix/tables/awaited_overloads.json +14 -0
  130. constricter/fix/tables/bases.json +888 -0
  131. constricter/fix/tables/classes.json +1941 -0
  132. constricter/fix/tables/elements.json +156 -0
  133. constricter/fix/tables/functions.json +58 -0
  134. constricter/fix/tables/generic_attributes.json +254 -0
  135. constricter/fix/tables/method_overloads.json +461 -0
  136. constricter/fix/tables/method_signatures.json +954 -0
  137. constricter/fix/tables/methods.json +954 -0
  138. constricter/fix/tables/overloads.json +540 -0
  139. constricter/fix/tables/platform.json +37 -0
  140. constricter/fix/tables/returns.json +1362 -0
  141. constricter/fix/tables/scalar_members.json +16 -0
  142. constricter/fix/tables/scalars.json +2707 -0
  143. constricter/fix/tables/source.json +28 -0
  144. constricter/fix/tables/subscriptable.json +77 -0
  145. constricter/fix/tables/type_parameters.json +178 -0
  146. constricter/fix/tables/variables.json +2962 -0
  147. constricter/fix/values/__init__.cp311-win32.pyd +0 -0
  148. constricter/fix/values/__init__.py +2 -0
  149. constricter/fix/values/aliased.cp311-win32.pyd +0 -0
  150. constricter/fix/values/aliased.py +195 -0
  151. constricter/fix/values/bodies.cp311-win32.pyd +0 -0
  152. constricter/fix/values/bodies.py +76 -0
  153. constricter/fix/values/callables.cp311-win32.pyd +0 -0
  154. constricter/fix/values/callables.py +323 -0
  155. constricter/fix/values/called.cp311-win32.pyd +0 -0
  156. constricter/fix/values/called.py +194 -0
  157. constricter/fix/values/classvars.cp311-win32.pyd +0 -0
  158. constricter/fix/values/classvars.py +629 -0
  159. constricter/fix/values/decided.cp311-win32.pyd +0 -0
  160. constricter/fix/values/decided.py +429 -0
  161. constricter/fix/values/displays.cp311-win32.pyd +0 -0
  162. constricter/fix/values/displays.py +101 -0
  163. constricter/fix/values/doubts.cp311-win32.pyd +0 -0
  164. constricter/fix/values/doubts.py +677 -0
  165. constricter/fix/values/entered.cp311-win32.pyd +0 -0
  166. constricter/fix/values/entered.py +332 -0
  167. constricter/fix/values/fills.cp311-win32.pyd +0 -0
  168. constricter/fix/values/fills.py +446 -0
  169. constricter/fix/values/guesses.cp311-win32.pyd +0 -0
  170. constricter/fix/values/guesses.py +376 -0
  171. constricter/fix/values/hinted.cp311-win32.pyd +0 -0
  172. constricter/fix/values/hinted.py +811 -0
  173. constricter/fix/values/inference.cp311-win32.pyd +0 -0
  174. constricter/fix/values/inference.py +698 -0
  175. constricter/fix/values/loops.cp311-win32.pyd +0 -0
  176. constricter/fix/values/loops.py +335 -0
  177. constricter/fix/values/members.cp311-win32.pyd +0 -0
  178. constricter/fix/values/members.py +578 -0
  179. constricter/fix/values/narrowed.cp311-win32.pyd +0 -0
  180. constricter/fix/values/narrowed.py +220 -0
  181. constricter/fix/values/operated.cp311-win32.pyd +0 -0
  182. constricter/fix/values/operated.py +202 -0
  183. constricter/fix/values/ordered.cp311-win32.pyd +0 -0
  184. constricter/fix/values/ordered.py +74 -0
  185. constricter/fix/values/returned.cp311-win32.pyd +0 -0
  186. constricter/fix/values/returned.py +479 -0
  187. constricter/fix/values/returns.cp311-win32.pyd +0 -0
  188. constricter/fix/values/returns.py +237 -0
  189. constricter/fix/values/shapes.cp311-win32.pyd +0 -0
  190. constricter/fix/values/shapes.py +654 -0
  191. constricter/fix/values/stored.cp311-win32.pyd +0 -0
  192. constricter/fix/values/stored.py +564 -0
  193. constricter/fix/values/targets.cp311-win32.pyd +0 -0
  194. constricter/fix/values/targets.py +419 -0
  195. constricter/fix/values/unchecked.cp311-win32.pyd +0 -0
  196. constricter/fix/values/unchecked.py +278 -0
  197. constricter/jsonc.py +50 -0
  198. constricter/noqa.py +47 -0
  199. constricter/notebook.py +115 -0
  200. constricter/offences.py +436 -0
  201. constricter/overrides.py +24 -0
  202. constricter/plugins/__init__.py +2 -0
  203. constricter/plugins/flake8.py +133 -0
  204. constricter/plugins/pylint.py +186 -0
  205. constricter/py.typed +0 -0
  206. constricter/recording.py +660 -0
  207. constricter/rules/__init__.cp311-win32.pyd +0 -0
  208. constricter/rules/__init__.py +2 -0
  209. constricter/rules/annotations.cp311-win32.pyd +0 -0
  210. constricter/rules/annotations.py +296 -0
  211. constricter/rules/binding.cp311-win32.pyd +0 -0
  212. constricter/rules/binding.py +652 -0
  213. constricter/rules/calls.cp311-win32.pyd +0 -0
  214. constricter/rules/calls.py +460 -0
  215. constricter/rules/checker.cp311-win32.pyd +0 -0
  216. constricter/rules/checker.py +536 -0
  217. constricter/rules/declarations.cp311-win32.pyd +0 -0
  218. constricter/rules/declarations.py +730 -0
  219. constricter/rules/decorators.cp311-win32.pyd +0 -0
  220. constricter/rules/decorators.py +351 -0
  221. constricter/rules/flow.cp311-win32.pyd +0 -0
  222. constricter/rules/flow.py +502 -0
  223. constricter/rules/keys.cp311-win32.pyd +0 -0
  224. constricter/rules/keys.py +79 -0
  225. constricter/rules/late.cp311-win32.pyd +0 -0
  226. constricter/rules/late.py +969 -0
  227. constricter/rules/narrowing.cp311-win32.pyd +0 -0
  228. constricter/rules/narrowing.py +98 -0
  229. constricter/rules/parsed.cp311-win32.pyd +0 -0
  230. constricter/rules/parsed.py +136 -0
  231. constricter/rules/quoted.cp311-win32.pyd +0 -0
  232. constricter/rules/quoted.py +141 -0
  233. constricter/rules/rebinding.cp311-win32.pyd +0 -0
  234. constricter/rules/rebinding.py +169 -0
  235. constricter/rules/recorded.cp311-win32.pyd +0 -0
  236. constricter/rules/recorded.py +168 -0
  237. constricter/rules/redundant.cp311-win32.pyd +0 -0
  238. constricter/rules/redundant.py +160 -0
  239. constricter/rules/scope.cp311-win32.pyd +0 -0
  240. constricter/rules/scope.py +802 -0
  241. constricter/rules/scoped.cp311-win32.pyd +0 -0
  242. constricter/rules/scoped.py +232 -0
  243. constricter/rules/scoping.cp311-win32.pyd +0 -0
  244. constricter/rules/scoping.py +346 -0
  245. constricter/rules/settings.cp311-win32.pyd +0 -0
  246. constricter/rules/settings.py +189 -0
  247. constricter/rules/syntax.cp311-win32.pyd +0 -0
  248. constricter/rules/syntax.py +472 -0
  249. constricter/rules/tables.cp311-win32.pyd +0 -0
  250. constricter/rules/tables.py +169 -0
  251. constricter/rules/walked.cp311-win32.pyd +0 -0
  252. constricter/rules/walked.py +127 -0
  253. constricter/rules/widened.cp311-win32.pyd +0 -0
  254. constricter/rules/widened.py +322 -0
  255. constricter/trace.py +374 -0
  256. e45ce06b2fdd5c43f782__mypyc.cp311-win32.pyd +0 -0
  257. python_constricter-0.4.1.dist-info/METADATA +400 -0
  258. python_constricter-0.4.1.dist-info/RECORD +261 -0
  259. python_constricter-0.4.1.dist-info/WHEEL +4 -0
  260. python_constricter-0.4.1.dist-info/entry_points.txt +5 -0
  261. python_constricter-0.4.1.dist-info/licenses/LICENSE.md +21 -0
@@ -0,0 +1,656 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """An installed function's signatures, read from its stub as the standard library's tables hold theirs.
3
+
4
+ A stub's overloads (see `constricter.fix.index.stubs.declared`) are resolved across the installed modules the
5
+ index holds, into the signatures of `constricter.fix.libraries.overloads`: which argument types each
6
+ parameter certainly takes or refuses, the type variables they bind, and the return as a template
7
+ naming classes by their public dotted paths. What they take is worked out from the parameter's
8
+ annotation, through aliases (a generic one's parameters bound by its arguments), type variables'
9
+ bounds, unions and `Literal`s: a builtin scalar by the `scalars` table for a standard-library
10
+ class (`SupportsIndex`), by its members for an installed protocol, and never by any other
11
+ installed class; a class passed as an argument (`dtype=np.float64`) by `type[...]`, which binds
12
+ its type variable to it.
13
+ """
14
+
15
+ import ast
16
+ import builtins
17
+ from collections.abc import Iterator, Mapping, Sequence
18
+ from typing import Final, cast
19
+
20
+ from constricter.fix.core.known import Origin
21
+ from constricter.fix.core.signatures import (
22
+ CLASS_BINDS,
23
+ CLASS_BOUNDED,
24
+ CLASS_VERDICT,
25
+ CONTAINER_BINDS,
26
+ CONTAINER_VERDICTS,
27
+ ELEMENT_VERDICTS,
28
+ Accepts,
29
+ Constant,
30
+ Expansion,
31
+ Parameter,
32
+ ReadSignature,
33
+ )
34
+ from constricter.fix.index import own_types, project
35
+ from constricter.fix.index.modules import Module
36
+ from constricter.fix.index.stubs import classnames
37
+ from constricter.fix.index.stubs.atoms import (
38
+ ANYTHING,
39
+ BUILTINS_MODULE,
40
+ CLASS,
41
+ MAYBE,
42
+ NO,
43
+ NONE,
44
+ PARAM_SPEC,
45
+ TYPE,
46
+ TYPES,
47
+ TYPING,
48
+ UNKNOWN,
49
+ VAR,
50
+ YES,
51
+ Atom,
52
+ Scope,
53
+ Taken,
54
+ argument_of,
55
+ atom_path,
56
+ bound_alias,
57
+ class_binds,
58
+ class_bounded,
59
+ class_verdict,
60
+ combined,
61
+ element_of,
62
+ free_variables,
63
+ joined,
64
+ literal_values,
65
+ owned,
66
+ parse_text,
67
+ private_name,
68
+ scalar_binds,
69
+ scalar_verdict,
70
+ verdict_of,
71
+ )
72
+ from constricter.fix.index.stubs.declared import Alias, Class, Declarations, Signature, Variable
73
+ from constricter.fix.index.stubs.names import ALIAS_DEPTH, ARG, Resolved, Resolver
74
+ from constricter.fix.index.stubs.read import MEMO, Asked, Expanded
75
+ from constricter.fix.libraries.overloads import CONTAINERS, SCALARS, substituted
76
+
77
+ # What an atom is (see `Atom`).
78
+ _NONE_ATOM: Final = "none"
79
+ _LITERAL_ATOM: Final = "literal"
80
+ _OPTIONAL: Final = "Optional"
81
+ _LITERAL: Final = "Literal"
82
+ _ANNOTATED: Final = "Annotated"
83
+ _UNIONS: Final = frozenset({"Union", "Optional"}) # `_OPTIONAL` too
84
+ _TYPE_VARIABLE: Final = "t" # `Accepts`' key for an unbounded type variable
85
+ _SELF: Final = "Self"
86
+ _ANY: Final = "typing.Any" # a pattern's anything
87
+ _RENAMED: Final = "_alias" # an alias's type parameter, renamed apart (see `Expansion`)
88
+
89
+
90
+ def read_all(
91
+ reader: "Reader",
92
+ module: Module,
93
+ written: tuple[Signature, ...],
94
+ variables: frozenset[str],
95
+ params: Mapping[str, str | None],
96
+ ) -> tuple[ReadSignature, ...]:
97
+ """Read a function's (or method's) signatures, `params` its class's type parameters.
98
+
99
+ A parameter every signature declares the same way takes whatever a call passes it, unless it
100
+ names a type variable (the module's, the class's in `variables`, or the signature's own).
101
+
102
+ Returns:
103
+ Them.
104
+
105
+ """
106
+ declared: Declarations | None = module.declared # none: a checked file's (see `read_signatures`)
107
+ shared: set[tuple[str, str, bool, str | None]] = set(written[0].params)
108
+ signature: Signature
109
+ for signature in written[1:]:
110
+ shared &= set(signature.params)
111
+ named: frozenset[str] = frozenset(
112
+ {
113
+ *(() if declared is None else declared.variables),
114
+ *module.type_vars,
115
+ *variables,
116
+ *(param for each in written for param, _ in each.type_params),
117
+ },
118
+ )
119
+ shared = {param for param in shared if not _names_any(param[3], named)}
120
+ return tuple(reader.signature(module, signature, shared, params) for signature in written)
121
+
122
+
123
+ def _names(text: str) -> frozenset[str]:
124
+ """Name the bare names a template or pattern uses (its type variables').
125
+
126
+ Returns:
127
+ Them.
128
+
129
+ """
130
+ return frozenset(node.id for node in ast.walk(parse_text(text)) if isinstance(node, ast.Name))
131
+
132
+
133
+ def _names_any(annotation: str | None, names: frozenset[str]) -> bool:
134
+ """Check whether an annotation names any of `names`.
135
+
136
+ Returns:
137
+ Whether it does.
138
+
139
+ """
140
+ if annotation is None:
141
+ return False
142
+ node: ast.AST
143
+ for node in ast.walk(parse_text(annotation)):
144
+ if isinstance(node, ast.Name) and node.id in names:
145
+ return True
146
+ if isinstance(node, ast.Constant) and isinstance(node.value, str) and _names_any(node.value, names):
147
+ return True # a forward reference
148
+ return False
149
+
150
+
151
+ class Reader(Resolver):
152
+ """Reads one function's signatures (see the module docstring)."""
153
+
154
+ def __init__(self, modules: Mapping[str, Module]) -> None:
155
+ """Read in `modules`."""
156
+ super().__init__(modules)
157
+ self.matching: bool = False # writing a pattern to match a receiver against (see `pattern`)
158
+
159
+ def signature(
160
+ self,
161
+ module: Module,
162
+ signature: Signature,
163
+ shared: set[tuple[str, str, bool, str | None]],
164
+ types: Mapping[str, str | None],
165
+ ) -> ReadSignature:
166
+ """Read one signature: what each parameter takes, and its return as a template.
167
+
168
+ A parameter every signature declares the same way, naming no type variable, takes whatever a
169
+ call passes it (as the tables' do): only those that tell the signatures apart, or bind a
170
+ type variable, are read.
171
+
172
+ Returns:
173
+ It.
174
+
175
+ """
176
+ scope: Scope = Scope(module, {**types, **dict(signature.type_params)}, module.name)
177
+ params: tuple[Parameter, ...] = tuple(
178
+ (
179
+ param[0],
180
+ param[1],
181
+ param[2],
182
+ None if param in shared or param[3] is None else self._accepted(param[3], scope),
183
+ )
184
+ for param in signature.params
185
+ )
186
+ returns: str | None = (
187
+ None
188
+ if signature.returns is None
189
+ else self._template(ast.parse(signature.returns, mode="eval").body, scope, 0)
190
+ )
191
+ if signature.self_type is None:
192
+ return ReadSignature(params, returns, None)
193
+ # A method declaring its `self` (`self: NDArray[ScalarT]`) is for such instances alone.
194
+ receiver: str | None = self.pattern(parse_text(signature.self_type), scope)
195
+ # Each type variable `self`'s pattern names, bounded (its constraints, for a constrained one).
196
+ variables: Mapping[str, Variable] = cast("Declarations", module.declared).variables
197
+ bounds: list[tuple[str, str]] = []
198
+ name: str
199
+ for name in sorted(_names(receiver or "")):
200
+ bound: str | tuple[ast.expr, Scope] | None = (
201
+ scope.params[name]
202
+ if name in scope.params
203
+ else variables[name].bound
204
+ if name in variables
205
+ else None
206
+ )
207
+ pattern: str | None = (
208
+ self.pattern(parse_text(bound), scope)
209
+ if isinstance(bound, str) and bound != PARAM_SPEC
210
+ else None
211
+ )
212
+ if pattern is not None:
213
+ bounds.append((name, pattern))
214
+ return ReadSignature(
215
+ params,
216
+ returns,
217
+ None if receiver is not None else [MAYBE],
218
+ receiver,
219
+ tuple(bounds),
220
+ )
221
+
222
+ def pattern(self, expr: ast.expr, scope: Scope) -> str | None:
223
+ """Write an annotation as a pattern for a receiver's type to match (see `ReadSignature.receiver`).
224
+
225
+ As a template, but every class by where it's defined (`builtins.tuple`), aliases written out,
226
+ and anything (`Any`, `object`, a type variable from elsewhere) as `typing.Any`.
227
+
228
+ Returns:
229
+ It, or `None` if it names something that can't be matched (a `Literal`).
230
+
231
+ """
232
+ self.matching = True
233
+ try:
234
+ return self._template(expr, scope, 0)
235
+ finally:
236
+ self.matching = False
237
+
238
+ def _accepted(self, text: str, scope: Scope) -> Accepts:
239
+ """Work out what a parameter annotated `text` takes, once for each module and scope it's read in.
240
+
241
+ A package's signatures write the same few hundred annotations over and over (`Axis`,
242
+ `bool`, `str | None`).
243
+
244
+ Returns:
245
+ It (see `_accepts`), shared by every parameter written alike: to read, never to change.
246
+
247
+ """
248
+ # A signature's scope holds bounds' texts alone (see `signature`): no alias's argument.
249
+ bounds: Mapping[str, str | None] = cast("Mapping[str, str | None]", scope.params)
250
+ if not scope.module.installed: # a checked file's: by the index's classes, which the memo outlives
251
+ return self._accepts(parse_text(text), scope)
252
+ memo: dict[Asked, Accepts] = MEMO.of(self.modules).accepts
253
+ key: Asked
254
+ if (key := (scope.module.name, scope.owner, text, tuple(sorted(bounds.items())))) not in memo:
255
+ memo[key] = self._accepts(parse_text(text), scope)
256
+ return memo[key]
257
+
258
+ def _accepts(self, annotation: ast.expr, scope: Scope) -> Accepts:
259
+ """Work out what a parameter takes (see `stdlib.Accepts`).
260
+
261
+ Returns:
262
+ It.
263
+
264
+ """
265
+ atoms: list[Atom] = list(self._atoms(annotation, scope, 0))
266
+ literals: list[Constant] = [value for atom in atoms for value in atom.values]
267
+ values: str = "".join(verdict_of(atoms, scalar, constant=False) for scalar in SCALARS)
268
+ found: Accepts = {"v": values, CLASS_VERDICT: combined({class_verdict(atom) for atom in atoms})}
269
+ if literals:
270
+ found["lit"] = literals
271
+ constants: str = "".join(verdict_of(atoms, scalar, constant=True) for scalar in SCALARS)
272
+ if constants != values:
273
+ found["c"] = constants
274
+ binds: dict[str, list[str]] = {}
275
+ scalar: str
276
+ for scalar in SCALARS:
277
+ bound: tuple[str, str] | None
278
+ if (bound := scalar_binds(atoms, scalar, scope.owner)) is not None:
279
+ binds[scalar] = [bound[0], bound[1]]
280
+ if binds:
281
+ found["var"] = binds
282
+ if (
283
+ len(atoms) == 1
284
+ and atoms[0].kind == VAR
285
+ and atoms[0].bound is None
286
+ and owned(atoms[0], scope.owner)
287
+ ):
288
+ found[_TYPE_VARIABLE] = atoms[0].name
289
+ variable: str | None
290
+ if (variable := class_binds(atoms, scope.owner)) is not None:
291
+ found[CLASS_BINDS] = variable
292
+ if class_bounded(atoms):
293
+ found[CLASS_BOUNDED] = True
294
+ self._containers(atoms, scope, found)
295
+ if not scope.module.installed: # a checked file's: by the checked files' classes it names
296
+ own_types.accept(self.modules, atoms, lambda arg, where: self._atoms(arg, where, 1), found)
297
+ return found
298
+
299
+ def _containers(self, atoms: Sequence[Atom], scope: Scope, found: Accepts) -> None:
300
+ """Add what a parameter takes of each builtin container argument to `found` (see `Accepts`).
301
+
302
+ A container every member refuses, or one member takes (its elements' types too, where that
303
+ member says: `tuple[int, ...]`); and the bounded type variable it binds, where the parameter
304
+ is only that (`ShapeT`, bound to `tuple[int, ...]`).
305
+ """
306
+ verdicts: dict[str, str] = {}
307
+ elements: dict[str, str] = {}
308
+ container: str
309
+ for container in CONTAINERS:
310
+ each: list[Taken] = [self._container(atom, container, 0) for atom in atoms]
311
+ verdict: str
312
+ if (verdict := combined({taken for taken, _ in each})) != MAYBE:
313
+ verdicts[container] = verdict
314
+ taking: list[tuple[Atom, ...] | None] = [inner for taken, inner in each if taken != NO]
315
+ if verdict == YES and len(taking) == 1 and taking[0] is not None:
316
+ elements[container] = "".join(
317
+ verdict_of(taking[0], scalar, constant=False) for scalar in SCALARS
318
+ )
319
+ if verdicts:
320
+ found[CONTAINER_VERDICTS] = verdicts
321
+ if elements:
322
+ found[ELEMENT_VERDICTS] = elements
323
+ only: Atom | None = atoms[0] if len(atoms) == 1 and atoms[0].kind == VAR else None
324
+ if (
325
+ only is not None
326
+ and only.bound not in {None, PARAM_SPEC}
327
+ and not only.constrained
328
+ and owned(only, scope.owner)
329
+ ):
330
+ found[CONTAINER_BINDS] = only.name
331
+
332
+ def _container(self, atom: Atom, container: str, hops: int) -> Taken:
333
+ """Decide whether one atom takes a builtin `container` argument, and what its elements must be.
334
+
335
+ Returns:
336
+ The verdict, and the atoms its elements' type must fit where the atom says (`list[int]`,
337
+ `tuple[int, ...]`); `None` where it doesn't.
338
+
339
+ """
340
+ verdict: str = scalar_verdict(atom, container, constant=False)
341
+ elements: tuple[Atom, ...] | None = None
342
+ if atom.kind == VAR and atom.bound not in {None, PARAM_SPEC} and hops < ALIAS_DEPTH:
343
+ each: list[Taken] = [self._container(inner, container, hops + 1) for inner in atom.inner]
344
+ taking: list[tuple[Atom, ...] | None] = [inner for taken, inner in each if taken != NO]
345
+ verdict = combined({taken for taken, _ in each})
346
+ elements = taking[0] if len(taking) == 1 else None
347
+ elif atom.kind == CLASS and atom.module is None and atom_path(atom) == f"builtins.{container}":
348
+ verdict = YES
349
+ of: ast.expr | None = element_of(container, atom.args)
350
+ if atom.args and (of is None or atom.scope is None):
351
+ verdict = MAYBE # `tuple[int, str]`, `dict[str, int]`: their elements aren't read
352
+ elif of is not None and atom.scope is not None:
353
+ elements = tuple(self._atoms(of, atom.scope, hops + 1))
354
+ return verdict, elements
355
+
356
+ def _atoms(self, expr: ast.expr, scope: Scope, hops: int) -> Iterator[Atom]:
357
+ """Split an annotation into its union's members, through aliases, `Optional`, `Union`, strings.
358
+
359
+ Yields:
360
+ Each member.
361
+
362
+ """
363
+ text: str
364
+ left: ast.expr
365
+ right: ast.expr
366
+ if hops > ALIAS_DEPTH:
367
+ yield UNKNOWN
368
+ return
369
+ match expr:
370
+ case ast.Constant(value=None):
371
+ yield Atom(_NONE_ATOM)
372
+ case ast.Constant(value=str() as text):
373
+ yield from self._atoms(parse_text(text), scope, hops + 1)
374
+ case ast.BinOp(left=left, op=ast.BitOr(), right=right):
375
+ yield from self._atoms(left, scope, hops + 1)
376
+ yield from self._atoms(right, scope, hops + 1)
377
+ case ast.Subscript():
378
+ yield from self._subscript_atoms(expr, scope, hops)
379
+ case ast.Name() | ast.Attribute():
380
+ yield from self._named_atoms(expr, scope, hops, ())
381
+ case _:
382
+ yield UNKNOWN
383
+
384
+ def _named_atoms(
385
+ self,
386
+ expr: ast.Name | ast.Attribute,
387
+ scope: Scope,
388
+ hops: int,
389
+ args: Sequence[ast.expr],
390
+ ) -> Iterator[Atom]:
391
+ """Read a name an annotation uses, subscripted with `args` (none if it isn't).
392
+
393
+ Yields:
394
+ Its atoms: an alias's, bound by `args`; else the one it is.
395
+
396
+ """
397
+ resolved: Resolved | None
398
+ if (resolved := self.resolve(expr, scope)) is None:
399
+ yield UNKNOWN
400
+ elif not isinstance(resolved, Atom):
401
+ alias: Alias
402
+ where: Scope
403
+ alias, where, _ = resolved
404
+ yield from self._atoms(parse_text(alias.value), bound_alias(alias, where, args, scope), hops + 1)
405
+ elif resolved.kind == ARG:
406
+ yield from self._atoms(*argument_of(resolved), hops + 1)
407
+ elif resolved.kind == VAR and resolved.scope is not None and resolved.bound not in {None, PARAM_SPEC}:
408
+ bound: ast.expr = parse_text(resolved.bound or "")
409
+ yield resolved._replace(inner=tuple(self._atoms(bound, resolved.scope, hops + 1)))
410
+ else:
411
+ yield resolved._replace(subscripted=bool(args), args=tuple(args), scope=scope)
412
+
413
+ def _subscript_atoms(self, expr: ast.Subscript, scope: Scope, hops: int) -> Iterator[Atom]:
414
+ args: list[ast.expr] = list(expr.slice.elts) if isinstance(expr.slice, ast.Tuple) else [expr.slice]
415
+ base: ast.expr = expr.value
416
+ if not isinstance(base, ast.Name | ast.Attribute):
417
+ yield UNKNOWN
418
+ return
419
+ path: str | None = self._external(base, scope)
420
+ if path is not None and path.rpartition(".")[0] in TYPING:
421
+ name: str = path.rpartition(".")[2]
422
+ if name in _UNIONS:
423
+ members: list[ast.expr] = [*args, *([ast.Constant(None)] if name == _OPTIONAL else [])]
424
+ yield from (atom for member in members for atom in self._atoms(member, scope, hops + 1))
425
+ return
426
+ if name == _LITERAL:
427
+ yield Atom(_LITERAL_ATOM, tuple(literal_values(args)))
428
+ return
429
+ if name == _ANNOTATED:
430
+ yield from self._atoms(args[0], scope, hops + 1)
431
+ return
432
+ if path in TYPES:
433
+ yield Atom(TYPE, inner=tuple(self._atoms(args[0], scope, hops + 1)))
434
+ return
435
+ yield from self._named_atoms(base, scope, hops, args)
436
+
437
+ def _external(self, expr: ast.Name | ast.Attribute, scope: Scope) -> str | None:
438
+ """Name what an annotation's name refers to outside the installed modules (`typing.Union`).
439
+
440
+ Returns:
441
+ Its dotted path, or `None` if it's an installed module's, or unknown.
442
+
443
+ """
444
+ resolved: Resolved | None = self.resolve(expr, scope)
445
+ if not isinstance(resolved, Atom) or resolved.kind != CLASS or resolved.module is not None:
446
+ return None
447
+ return atom_path(resolved)
448
+
449
+ def method(self, module: Module, klass: Class, name: str, hops: int) -> tuple[ReadSignature, ...] | None:
450
+ """Read a class's method (its own, else its nearest base's that has it) as a function's are.
451
+
452
+ Returns:
453
+ Its signatures, or `None` if neither it nor a base it can follow declares it.
454
+
455
+ """
456
+ written: tuple[Signature, ...] | None
457
+ if (written := klass.methods.get(name)) is not None:
458
+ return read_all(
459
+ self,
460
+ module,
461
+ written,
462
+ frozenset(name for name, _ in klass.params),
463
+ dict(klass.params),
464
+ )
465
+ base: str
466
+ for base in klass.bases if hops < ALIAS_DEPTH else ():
467
+ resolved: Resolved | None = self.resolved_base(base, module)
468
+ if (
469
+ isinstance(resolved, Atom)
470
+ and resolved.module is not None
471
+ and resolved.module.declared is not None
472
+ ):
473
+ inherited: Class | None = resolved.module.declared.classes.get(resolved.origin[1] or "")
474
+ read: tuple[ReadSignature, ...] | None = (
475
+ None if inherited is None else self.method(resolved.module, inherited, name, hops + 1)
476
+ )
477
+ if read is not None:
478
+ return read
479
+ return None
480
+
481
+ def expansion(self, origin: Origin) -> Expanded | None:
482
+ """Expand an installed alias of a generic class (`NDArray`), as a receiver's type (see `Expansion`).
483
+
484
+ Returns:
485
+ The class, the expansion, and the installed classes its pattern names; or `None` for an
486
+ alias of anything else (a union, a protocol, another alias).
487
+
488
+ """
489
+ module: Module = self.modules[origin[0]]
490
+ alias: Alias = cast("Declarations", module.declared).aliases[origin[1] or ""]
491
+ value: ast.expr = parse_text(alias.value)
492
+ own: Scope = Scope(module, {}, module.name)
493
+ base: ast.Name | ast.Attribute
494
+ index: ast.expr
495
+ match value:
496
+ case ast.Subscript(value=ast.Name() | ast.Attribute() as base, slice=index):
497
+ pass
498
+ case _:
499
+ return None
500
+ head: Resolved | None = self.resolve(base, own)
501
+ if not isinstance(head, Atom) or head.kind != CLASS or head.module is None:
502
+ return None
503
+ klass: Class | None = cast("Declarations", head.module.declared).classes.get(head.origin[1] or "")
504
+ params: tuple[str, ...] = alias.params or free_variables(alias.value, own)
505
+ scope: Scope = Scope(module, dict.fromkeys(params), module.name)
506
+ receiver: str | None = self.pattern(value, scope)
507
+ if klass is None or receiver is None:
508
+ return None
509
+ renamed: dict[str, str] = {param: f"{_RENAMED}{place}" for place, param in enumerate(params)}
510
+ args: list[ast.expr] = list(index.elts) if isinstance(index, ast.Tuple) else [index]
511
+ return (
512
+ head.origin,
513
+ Expansion(
514
+ tuple(renamed.values()),
515
+ substituted(receiver, renamed),
516
+ self._templates(klass, args, scope, renamed),
517
+ ),
518
+ self._named(receiver),
519
+ )
520
+
521
+ def _templates(
522
+ self,
523
+ klass: Class,
524
+ args: Sequence[ast.expr],
525
+ scope: Scope,
526
+ renamed: Mapping[str, str],
527
+ ) -> tuple[tuple[str, str], ...]:
528
+ """Write what an alias passes each of its class's type parameters, where it can be written.
529
+
530
+ Returns:
531
+ Each parameter with its template, the alias's own parameters `renamed`.
532
+
533
+ """
534
+ found: list[tuple[str, str]] = []
535
+ param: str
536
+ arg: ast.expr
537
+ for (param, _), arg in zip(klass.params, args, strict=False): # a class's own subscript fits
538
+ template: str | None
539
+ if (template := self._template(arg, scope, 0)) is not None:
540
+ found.append((param, substituted(template, renamed)))
541
+ return tuple(found)
542
+
543
+ def _named(self, pattern: str) -> tuple[tuple[str, Origin], ...]:
544
+ """Find the installed classes a pattern names.
545
+
546
+ Returns:
547
+ Each by its path there, and where it's defined.
548
+
549
+ """
550
+ found: dict[str, Origin] = {}
551
+ node: ast.AST
552
+ for node in ast.walk(parse_text(pattern)):
553
+ path: str = ast.unparse(node) if isinstance(node, ast.Attribute) else ""
554
+ where: Origin = (path.rpartition(".")[0], path.rpartition(".")[2])
555
+ if path and classnames.declares(self.modules, where):
556
+ found[path] = where
557
+ return tuple(found.items())
558
+
559
+ def _template(self, expr: ast.expr, scope: Scope, hops: int) -> str | None:
560
+ """Write an annotation as a template: classes by their public dotted paths, type variables bare.
561
+
562
+ A private alias is written as what it stands for, its parameters bound by its arguments.
563
+
564
+ Returns:
565
+ The template, or `None` if it names something `--fix` can't write (`Any`, a `Literal`, a
566
+ class with no public path, a type variable from elsewhere).
567
+
568
+ """
569
+ text: str
570
+ left: ast.expr
571
+ right: ast.expr
572
+ base: ast.Name | ast.Attribute
573
+ found: str | None = None
574
+ match expr:
575
+ case _ if hops > ALIAS_DEPTH:
576
+ pass
577
+ case ast.Constant(value=None):
578
+ found = NONE
579
+ case ast.Constant(value=builtins.Ellipsis):
580
+ found = "..."
581
+ case ast.Constant(value=str() as text):
582
+ found = self._template(parse_text(text), scope, hops + 1)
583
+ case ast.BinOp(left=left, op=ast.BitOr(), right=right):
584
+ found = joined(" | ", [self._template(side, scope, hops + 1) for side in (left, right)])
585
+ case ast.Subscript(value=ast.Name() | ast.Attribute() as base):
586
+ args: list[ast.expr] = (
587
+ list(expr.slice.elts) if isinstance(expr.slice, ast.Tuple) else [expr.slice]
588
+ )
589
+ found = self._named_template(base, scope, hops, args)
590
+ case ast.Name() | ast.Attribute():
591
+ found = self._named_template(expr, scope, hops, [])
592
+ case _:
593
+ pass
594
+ return found
595
+
596
+ def _named_template(
597
+ self,
598
+ expr: ast.Name | ast.Attribute,
599
+ scope: Scope,
600
+ hops: int,
601
+ args: Sequence[ast.expr],
602
+ ) -> str | None:
603
+ """Write a name, subscripted with `args` (none if it isn't), as a template.
604
+
605
+ Returns:
606
+ It, or `None`.
607
+
608
+ """
609
+ resolved: Resolved | None = self.resolve(expr, scope)
610
+ written: str | None
611
+ if resolved is None:
612
+ return None
613
+ if not isinstance(resolved, Atom):
614
+ alias: Alias
615
+ where: Scope
616
+ origin: Origin
617
+ alias, where, origin = resolved
618
+ public: Origin = project.public_origin(self.modules, origin)
619
+ if not self.matching and not private_name(public[0]) and not private_name(public[1]):
620
+ written = f"{public[0]}.{public[1]}"
621
+ elif alias.params or free_variables(alias.value, where) or not args:
622
+ return self._template(
623
+ parse_text(alias.value),
624
+ bound_alias(alias, where, args, scope),
625
+ hops + 1,
626
+ )
627
+ else: # another name for a generic class (`_dtype = dtype`): its arguments are the class's
628
+ written = self._template(parse_text(alias.value), where, hops + 1)
629
+ elif resolved.kind == ARG:
630
+ return self._template(*argument_of(resolved), hops + 1)
631
+ elif resolved.kind == VAR:
632
+ written = resolved.name if owned(resolved, scope.owner) else _ANY if self.matching else None
633
+ else:
634
+ written = self._class_path(resolved)
635
+ if written is None or not args:
636
+ return written
637
+ texts: list[str | None] = [self._template(arg, scope, hops + 1) for arg in args]
638
+ return None if None in texts else f"{written}[{', '.join(t for t in texts if t is not None)}]"
639
+
640
+ def _class_path(self, atom: Atom) -> str | None:
641
+ """Write a class as a template names it: a builtin bare, any other by its public dotted path.
642
+
643
+ Returns:
644
+ It, or `None` for `typing`'s forms, `object`, `type`, or a class with no public path.
645
+
646
+ """
647
+ origin: Origin = atom.origin
648
+ if self.matching:
649
+ return _ANY if atom_path(atom) in ANYTHING else None if origin[0] in TYPING else atom_path(atom)
650
+ if atom.module is not None:
651
+ origin = project.public_origin(self.modules, origin)
652
+ elif origin[0] == BUILTINS_MODULE:
653
+ return origin[1] if origin[1] not in {"object", "type"} else None
654
+ elif origin[0] in TYPING:
655
+ return _SELF if origin[1] == _SELF else None # a method's `Self`: the receiver's type
656
+ return None if private_name(origin[0]) or private_name(origin[1]) else f"{origin[0]}.{origin[1]}"