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,811 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """`--infer-with`: a type checker's inferred type (an inlay hint) as a fix, when the file can use it.
3
+
4
+ Always a guess: a hint is the type the checker gave the value where the name is bound, so it can be
5
+ too narrow for a later binding, or too wide for what the code means. It's judged here as text, as
6
+ the checker printed it:
7
+
8
+ - a class object printed `<class 'Point'>` (ty's way) is `type[Point]`, a type variable printed with
9
+ its scope (`Model@create_model`) the variable, and an intersection with a truthiness
10
+ (`str & ~AlwaysFalsy`) its other member;
11
+ - an unpacked tuple (`tuple[str, *tuple[str, ...]]`, Python 3.11's syntax) is kept where the
12
+ project's oldest Python parses it (`min-python`); else written with `Unpack` where the module
13
+ imports it (one level deeper, to LVA006), and dropped where it doesn't;
14
+ - a `Literal` is widened to its values' types (`Literal[1, 2]` is `int`, `Literal[Color.RED]` is
15
+ `Color`), and `LiteralString` to `str`, then a union's repeated members dropped;
16
+ - anything that isn't an annotation (`Module("os")`, a callable's signature, `Self@C`) is dropped,
17
+ as is a vague one (`Any`, `list[Unknown]`), a bare `None`, one with `Never` or `NoReturn` in it
18
+ (an empty display's `list[Never]`, or code the checker takes not to run), one nested as deep as
19
+ LVA006 reports, or a tuple as long as LVA011 does;
20
+ - a special form alone (`type[Generic]`) is dropped, as is a union inside the hint
21
+ (`dict[str, int | bytes]`), and a `Literal` in a class's own arguments, which may be bound to it
22
+ (a union for a name its function doesn't narrow, a name it returns as `Self` and one bound again
23
+ are `constricter.rules`' to drop);
24
+ - `TypeAlias`, hinted for an alias's assignment, declares a module's alias written as a subscript or
25
+ a union (see `type_alias`), and is dropped anywhere else;
26
+ - a generator's, coroutine's or async generator's class as `types` has it at run time (ty's
27
+ `GeneratorType[str, None, None]`) is `collections.abc`'s, as every checker types the expression
28
+ that makes one (`Generator[str, None, None]`);
29
+ - every name in it must be one the file can use: a builtin, a name the module binds at its top
30
+ level (before the binding, in a module body, where the annotation is evaluated), a class or
31
+ alias it imports under `if TYPE_CHECKING:` or the hint's own edits import (`Offered`: imported
32
+ for type checking alone, as the checker says from where; see `constricter.fix.index.offers` for which
33
+ are taken), or a class the checker prints by its bare name from a module `ImportPlan.spell` can
34
+ import (`Callable`, `Iterator`, `Path`, `deque`, ...). Anything else, it can't be sure what the
35
+ name means. Nor where the file binds a name to another thing than the checker prints it for: one
36
+ of those well-known classes bound to its own class or a module (`class Match`, `import datetime`),
37
+ a builtin that takes no type arguments given some (numpy's `bool[bool]`, printed bare), a dotted
38
+ name with a private part (`np._NBitIntP`), or a class a function defines (`Parser.read.Dialect`).
39
+
40
+ A hint with edits is judged as it shows first (`Thing[str]`), then as they'd write it
41
+ (`things.Thing[str]`, through a module the file imports).
42
+ """
43
+
44
+ import ast
45
+ import builtins
46
+ import re
47
+ import sys
48
+ from functools import lru_cache
49
+ from typing import Final, TypeAlias
50
+
51
+ from constricter.fix.core.known import Hints, ImportPlan, Inference, Known, Offered
52
+ from constricter.fix.libraries import stdlib
53
+ from constricter.rules.annotations import ABSTRACT, depth, dotted, length, vague_fits
54
+
55
+ KIND: Final = "checker" # the fix kind, and what the guess rests on
56
+ TRACED: Final = "traced" # the same, for a traced run's types (`--infer-from`)
57
+ KINDS: Final = frozenset({KIND, TRACED})
58
+ _LITERAL: Final = "Literal"
59
+ _NONE: Final = "None"
60
+ _BUILTINS: Final = frozenset(dir(builtins))
61
+ ALIAS: Final = "TypeAlias" # what a checker hints an alias's assignment as
62
+ _ALIAS_ORIGINS: Final = ("typing.TypeAlias", "typing_extensions.TypeAlias")
63
+ _CLASS: Final = "type["
64
+ _UNPACK: Final = "Unpack" # what an unpacked tuple (`*tuple[str, ...]`) is written with
65
+ _UNPACK_ORIGINS: Final = frozenset({"typing.Unpack", "typing_extensions.Unpack"})
66
+ # `collections.abc`'s classes, which a checker prints bare (not `Set`: that's `AbstractSet` to it).
67
+ _ABSTRACT: Final = sorted(ABSTRACT | {"Hashable", "MappingView", "Sized"})
68
+ # Classes a checker prints by their bare names, and where each is from.
69
+ WELL_KNOWN: Final = {
70
+ **{name: f"collections.abc.{name}" for name in _ABSTRACT},
71
+ **{
72
+ name: f"collections.{name}" for name in ("ChainMap", "Counter", "OrderedDict", "defaultdict", "deque")
73
+ },
74
+ **{name: f"re.{name}" for name in ("Match", "Pattern")},
75
+ **{name: f"pathlib.{name}" for name in ("Path", "PosixPath", "PurePath", "WindowsPath")},
76
+ **{
77
+ name: f"io.{name}"
78
+ for name in ("BufferedRandom", "BufferedReader", "BufferedWriter", "BytesIO", "FileIO", "StringIO")
79
+ },
80
+ **{name: f"datetime.{name}" for name in ("date", "datetime", "time", "timedelta", "timezone")},
81
+ **{name: f"subprocess.{name}" for name in ("CompletedProcess", "Popen")},
82
+ "ArgumentParser": "argparse.ArgumentParser",
83
+ "Decimal": "decimal.Decimal",
84
+ "Fraction": "fractions.Fraction",
85
+ "Logger": "logging.Logger",
86
+ "Namespace": "argparse.Namespace",
87
+ "UUID": "uuid.UUID",
88
+ "stat_result": "os.stat_result",
89
+ }
90
+ # Nodes an annotation is made of: names, attributes, subscripts and their parts, and unions.
91
+ _ANNOTATION_NODES: Final = (
92
+ ast.Name,
93
+ ast.Attribute,
94
+ ast.Subscript,
95
+ ast.Tuple,
96
+ ast.List,
97
+ ast.Constant,
98
+ ast.BinOp,
99
+ ast.BitOr,
100
+ ast.Load,
101
+ ast.Starred, # `tuple[str, *tuple[str, ...]]`
102
+ )
103
+ _WIDER: Final = {"LiteralString": "str"}
104
+ # `typing`'s special forms, which a checker prints for the form itself (`type[Generic]`, `Annotated`).
105
+ _FORMS: Final = frozenset(
106
+ {
107
+ "Annotated",
108
+ "ClassVar",
109
+ "Concatenate",
110
+ "Final",
111
+ "Generic",
112
+ "Literal",
113
+ "NotRequired",
114
+ "Optional",
115
+ "Protocol",
116
+ "ReadOnly",
117
+ "Required",
118
+ "TypeGuard",
119
+ "TypeIs",
120
+ "Union",
121
+ "Unpack",
122
+ },
123
+ )
124
+ # ty prints a class object as `<class 'Point'>`: that's `type[Point]`.
125
+ _CLASS_OBJECT: Final = re.compile(r"<class '(\w+)'>")
126
+ # ty prints a type variable with the function or class that declares it: `Model@create_model`.
127
+ _SCOPED: Final = re.compile(r"\b(?!Self@)(\w+)@\w+")
128
+ # What ty intersects a type with where a truth test narrowed it: `str & ~AlwaysFalsy`.
129
+ _TRUTHINESS: Final = frozenset({"AlwaysFalsy", "AlwaysTruthy"})
130
+ _DISCARD: Final = "_"
131
+ # Each name a hint's edits import: its statement, module and name there (see `_origins`).
132
+ _Origins: TypeAlias = dict[str, tuple[str, str, str]]
133
+ _STDLIB: Final = sys.stdlib_module_names | {"_typeshed"} # and the module its stubs alone have
134
+ _RELATIVE: Final = "."
135
+ _DOT: Final = "."
136
+ # How one unbound name of a hint can be written: through a guarded import, or `ImportPlan.spell`.
137
+ _OWN: Final = "own" # the module imports it under `if TYPE_CHECKING:`
138
+ _OFFERED: Final = "offered" # the hint's edits import it
139
+ _KNOWN: Final = "known" # `WELL_KNOWN` says where it's from
140
+ # The builtins that take type arguments: another of a builtin's names given some is another class.
141
+ _GENERIC_BUILTINS: Final = frozenset(
142
+ {
143
+ "BaseExceptionGroup",
144
+ "ExceptionGroup",
145
+ "classmethod",
146
+ "dict",
147
+ "enumerate",
148
+ "filter",
149
+ "frozenset",
150
+ "list",
151
+ "map",
152
+ "memoryview",
153
+ "reversed",
154
+ "set",
155
+ "slice",
156
+ "staticmethod",
157
+ "tuple",
158
+ "type",
159
+ "zip",
160
+ },
161
+ )
162
+ # `types`' classes for what a generator, a coroutine and an async generator make, and how checkers
163
+ # type those: as `collections.abc`'s classes.
164
+ _RUNTIME: Final = {
165
+ "GeneratorType": "Generator",
166
+ "CoroutineType": "Coroutine",
167
+ "AsyncGeneratorType": "AsyncGenerator",
168
+ }
169
+ _TYPES: Final = "types"
170
+ _BOTTOM: Final = frozenset({"Never", "NoReturn"}) # the type of no value: never a variable's
171
+
172
+
173
+ def hinted(
174
+ text: str,
175
+ known: Known,
176
+ *,
177
+ nesting: int,
178
+ before: int | None,
179
+ offered: Offered | None = None,
180
+ ) -> str | None:
181
+ """Judge a checker's hint `text` as a fix for a binding (in a module body, on line `before`).
182
+
183
+ `offered`: what the hint's edits would write, judged if `text` is no fix.
184
+
185
+ Returns:
186
+ The annotation, spelled as the file can (an import added if it must be), or `None`.
187
+
188
+ """
189
+ imports: tuple[str, ...] = () if offered is None else offered.imports
190
+ candidates: list[str] = [text]
191
+ if offered is not None and not _renamed(text, offered.text, known.names.plan, _origins(imports)):
192
+ candidates.append(offered.text)
193
+ candidate: str
194
+ for candidate in dict.fromkeys(candidates):
195
+ annotation: str | None
196
+ if (annotation := _spelled(candidate, known, imports, nesting=nesting, before=before)) is not None:
197
+ return annotation
198
+ return None
199
+
200
+
201
+ def inference(annotation: str, found: Hints) -> Inference:
202
+ """Make the fix a hint of `found` gives, as `hinted` wrote it.
203
+
204
+ Returns:
205
+ It.
206
+
207
+ """
208
+ reason: str = (
209
+ "what a traced run bound it to" if found.kind == TRACED else f"{found.checker}'s inferred type"
210
+ )
211
+ return Inference(annotation, reason, frozenset({found.kind}))
212
+
213
+
214
+ def type_alias(known: Known, before: int) -> str | None:
215
+ """Name `TypeAlias`, to declare the alias a module body binds on line `before`.
216
+
217
+ As an import the module has names it (`typing`'s, or `typing_extensions`'s), if it runs by then,
218
+ the name itself before one through its module (`TypeAlias`, then `typing.TypeAlias`); else by a
219
+ new import from `typing`, where every Python the module runs on has it there (`aliased`).
220
+
221
+ Returns:
222
+ The name, or `None` if the module can't name it there.
223
+
224
+ """
225
+ plan: ImportPlan | None
226
+ if (plan := known.names.plan) is None:
227
+ return None
228
+ name: str | None
229
+ named: list[str]
230
+ if not (named := [name for origin in _ALIAS_ORIGINS if (name := plan.named(origin)) is not None]):
231
+ return plan.spell(_ALIAS_ORIGINS[0]) if known.names.aliased else None
232
+ bound: list[str] = [each for each in named if plan.defined.get(each.partition(".")[0], before) < before]
233
+ return min(bound, key=lambda each: _DOT in each) if bound else None
234
+
235
+
236
+ def renames(name: str, annotation: str, *, local: bool) -> bool:
237
+ """Check whether `annotation`, a hint's, says `name` is another name for a class: a `type[C]`.
238
+
239
+ An alias annotations are written with, which declared (a variable) it would no longer be. A
240
+ module's name is taken for one, and a function's (`local`) if it's written as a class's is
241
+ (`Pair = tuple[int, str]`, not `cls = type(self)`).
242
+
243
+ Returns:
244
+ Whether it does.
245
+
246
+ """
247
+ return annotation.startswith(_CLASS) and not (local and name[:1].islower())
248
+
249
+
250
+ def _renamed(shown: str, written: str, plan: ImportPlan | None, origins: _Origins) -> bool:
251
+ """Check whether a hint's edit writes a generic `shown` under one name under another.
252
+
253
+ ty shows a generic alias (`NDArray[bool]`) and writes the class it stands for with the alias's
254
+ own arguments (`np.ndarray[np.bool]`), which aren't the class's. A name the file or the edit's
255
+ imports bind to the one shown (`from shapes import Shape as S`) is the same one.
256
+
257
+ Returns:
258
+ Whether it does.
259
+
260
+ """
261
+ try:
262
+ pairs: list[tuple[ast.expr, ast.expr]] = list(zip(_heads(shown), _heads(written), strict=False))
263
+ except SyntaxError:
264
+ return False
265
+ was: ast.expr
266
+ now: ast.expr
267
+ for was, now in pairs:
268
+ name: str = now.attr if isinstance(now, ast.Attribute) else ast.unparse(now)
269
+ if name in origins and isinstance(now, ast.Name):
270
+ name = origins[name][2]
271
+ elif isinstance(now, ast.Name):
272
+ name = (name if plan is None else plan.bound.get(name, name)).rpartition(_DOT)[2]
273
+ if name != (was.attr if isinstance(was, ast.Attribute) else ast.unparse(was)):
274
+ return True
275
+ return False
276
+
277
+
278
+ def _heads(text: str) -> list[ast.expr]:
279
+ """List what a hint's `text` subscripts (`dict` and `list` in `dict[str, list[int]]`), outermost first.
280
+
281
+ Returns:
282
+ Them.
283
+
284
+ """
285
+ root: ast.expr = ast.parse(_plain(text), mode="eval").body
286
+ return [node.value for node in ast.walk(root) if isinstance(node, ast.Subscript)]
287
+
288
+
289
+ def _spelled(
290
+ text: str,
291
+ known: Known,
292
+ imports: tuple[str, ...],
293
+ *,
294
+ nesting: int,
295
+ before: int | None,
296
+ ) -> str | None:
297
+ """Write a hint's `text` as an annotation the file can hold, with `imports` for its names.
298
+
299
+ Returns:
300
+ The annotation, or `None` if it isn't one, says too little, or names what the file can't.
301
+
302
+ """
303
+ plan: ImportPlan | None = known.names.plan
304
+ try:
305
+ parsed: ast.expr = ast.parse(_plain(text), mode="eval").body
306
+ except SyntaxError:
307
+ return None
308
+ root: ast.expr | None = _widened(parsed)
309
+ if plan is not None and root is not None and not known.names.starred:
310
+ root = _unstarred(root, plan)
311
+ if plan is None or root is None or not _annotation(root) or not _fits(root, nesting, known):
312
+ return None
313
+ origins: _Origins = _origins(imports)
314
+ root = _abstract(root, plan, origins)
315
+ lone: set[str] = _lone(root)
316
+ # `TypeAlias` is `type_alias`'s to write. A special form alone isn't a type.
317
+ if _aliases(root) or lone & _FORMS or _misnamed(root, plan) or _bottom(root):
318
+ return None
319
+ names: list[str] = sorted(
320
+ {
321
+ node.id
322
+ for node in ast.walk(root)
323
+ if isinstance(node, ast.Name) and not _usable(node.id, plan, before)
324
+ },
325
+ )
326
+ ways: dict[str, str | None] = {name: _way(name, plan, origins, lone=name in lone) for name in names}
327
+ if None in ways.values():
328
+ return None
329
+ spelled: dict[str, str] = {}
330
+ name: str
331
+ for name in names:
332
+ found: str | None
333
+ if (found := _named(name, ways[name], plan, origins)) is None:
334
+ return None
335
+ spelled[name] = found
336
+ # The annotation holds no strings (its only constants are `None` and `...`): a name is a word
337
+ # that isn't an attribute's.
338
+ return re.sub(
339
+ r"(?<![\w.])(\w+)",
340
+ lambda word: spelled.get(word[1], word[1]),
341
+ ast.unparse(root),
342
+ )
343
+
344
+
345
+ def _unstarred(root: ast.expr, plan: ImportPlan) -> ast.expr | None:
346
+ """Write an annotation's unpacked tuples as every Python 3 parses them: `Unpack[tuple[str, ...]]`.
347
+
348
+ `*tuple[str, ...]` in a subscript is Python 3.11's syntax: for a module that may run on an
349
+ older one (see `LibraryNames.starred`).
350
+
351
+ Returns:
352
+ The annotation; `None` for one that unpacks, where the module doesn't import `Unpack`.
353
+
354
+ """
355
+ nodes: list[ast.AST] = list(ast.walk(root))
356
+ if not any(isinstance(node, ast.Starred) for node in nodes):
357
+ return root
358
+ if plan.bound.get(_UNPACK) not in _UNPACK_ORIGINS:
359
+ return None
360
+ lone: list[ast.expr] = [] # each tuple of one unpacked element: `tuple[*Ts]`'s, its one argument
361
+ node: ast.AST
362
+ for node in nodes:
363
+ if isinstance(node, ast.Tuple | ast.List):
364
+ if isinstance(node, ast.Tuple) and len(node.elts) == 1 and isinstance(node.elts[0], ast.Starred):
365
+ lone.append(node)
366
+ node.elts = [
367
+ ast.Subscript(ast.Name(_UNPACK), each.value) if isinstance(each, ast.Starred) else each
368
+ for each in node.elts
369
+ ]
370
+ for node in nodes:
371
+ if isinstance(node, ast.Subscript) and isinstance(node.slice, ast.Tuple) and node.slice in lone:
372
+ node.slice = node.slice.elts[0]
373
+ return root
374
+
375
+
376
+ def _abstract(root: ast.expr, plan: ImportPlan, origins: _Origins) -> ast.expr:
377
+ """Write `types`' generator, coroutine and async generator classes as `collections.abc`'s.
378
+
379
+ Where the name is `types`' (`GeneratorType` that the module or the hint's edits import from it,
380
+ `types.GeneratorType` through the module): see `_RUNTIME`.
381
+
382
+ Returns:
383
+ The annotation.
384
+
385
+ """
386
+ node: ast.AST
387
+ abstract: str | None
388
+ for node in ast.walk(root):
389
+ if isinstance(node, ast.Subscript) and (abstract := _runtime(node.value, plan, origins)) is not None:
390
+ node.value = ast.Name(abstract)
391
+ return root if (abstract := _runtime(root, plan, origins)) is None else ast.Name(abstract)
392
+
393
+
394
+ def _runtime(node: ast.expr, plan: ImportPlan, origins: _Origins) -> str | None:
395
+ """Name the `collections.abc` class for `types`' class `node` names, if it names one of `_RUNTIME`'s.
396
+
397
+ Returns:
398
+ It, or `None`.
399
+
400
+ """
401
+ name: str
402
+ module: str
403
+ match node:
404
+ case ast.Name(id=name) if name in _RUNTIME and _origin(name, plan, origins) == f"{_TYPES}.{name}":
405
+ return _RUNTIME[name]
406
+ case ast.Attribute(value=ast.Name(id=module), attr=name) if (
407
+ name in _RUNTIME and plan.bound.get(module) == _TYPES
408
+ ):
409
+ return _RUNTIME[name]
410
+ case _:
411
+ return None
412
+
413
+
414
+ def _bottom(root: ast.expr) -> bool:
415
+ """Check whether an annotation has `Never` or `NoReturn` in it, however it's spelled.
416
+
417
+ Returns:
418
+ Whether it does.
419
+
420
+ """
421
+ return any(
422
+ (isinstance(node, ast.Name) and node.id in _BOTTOM)
423
+ or (isinstance(node, ast.Attribute) and node.attr in _BOTTOM)
424
+ for node in ast.walk(root)
425
+ )
426
+
427
+
428
+ def _misnamed(root: ast.expr, plan: ImportPlan) -> bool:
429
+ """Check whether an annotation names what the file binds to another thing than the checker means.
430
+
431
+ A module, written bare (`import socket`, then `socket`: the checker's is a class), a well-known
432
+ class's name (`Match`) the module binds to its own class, a builtin it doesn't rebind given type
433
+ arguments it takes none of (numpy's `bool`, printed bare), a private part of a dotted name
434
+ (`np._NBitIntP`), or a class a function defines, named through it (`Parser.read.Dialect`).
435
+
436
+ Returns:
437
+ Whether it does.
438
+
439
+ """
440
+ node: ast.AST
441
+ name: str
442
+ # A dotted name's root (`datetime` in `datetime.date`) is a module's, rightly.
443
+ roots: set[int] = {id(node.value) for node in ast.walk(root) if isinstance(node, ast.Attribute)}
444
+ for node in ast.walk(root):
445
+ match node:
446
+ case ast.Name(id=name) if id(node) not in roots and (
447
+ plan.bound.get(name) == name
448
+ or (name in WELL_KNOWN and name in plan.defined and not _imported(name, plan))
449
+ ):
450
+ return True
451
+ case ast.Subscript(value=ast.Name(id=name)) if (
452
+ name in _BUILTINS and name not in plan.defined and name not in _GENERIC_BUILTINS
453
+ ):
454
+ return True
455
+ case ast.Attribute():
456
+ parts: list[str] = _dotted(node)
457
+ if parts and (
458
+ any(part.startswith("_") for part in parts[1:])
459
+ or (parts[0] not in plan.bound and any(part[:1].islower() for part in parts[1:-1]))
460
+ ):
461
+ return True
462
+ case _:
463
+ pass
464
+ return False
465
+
466
+
467
+ def _origin(name: str, plan: ImportPlan, origins: _Origins) -> str | None:
468
+ """Find what a hint's bare `name` is: what the module's import binds, else the hint's edits'.
469
+
470
+ Returns:
471
+ Its qualified name; `None` where the module binds it otherwise, or nothing says.
472
+
473
+ """
474
+ if name in plan.bound:
475
+ return plan.bound[name]
476
+ if name in plan.defined or name not in origins:
477
+ return None
478
+ return f"{origins[name][1]}.{origins[name][2]}"
479
+
480
+
481
+ def _imported(name: str, plan: ImportPlan) -> bool:
482
+ """Check whether the module binds `name` by importing a class of that name (`from re import Match`).
483
+
484
+ Returns:
485
+ Whether it does: not by `import name` (a module), nor under another name, nor otherwise.
486
+
487
+ """
488
+ bound: str | None = plan.bound.get(name)
489
+ return bound is not None and _DOT in bound and bound.rpartition(_DOT)[2] == name
490
+
491
+
492
+ def _dotted(node: ast.expr) -> list[str]:
493
+ """Split a dotted name into its parts: `np.ma.MaskedArray`'s `np`, `ma` and `MaskedArray`.
494
+
495
+ Returns:
496
+ Them, the root first; empty for an attribute of anything but a name.
497
+
498
+ """
499
+ spelled: str | None = dotted(node)
500
+ return [] if spelled is None else spelled.split(_DOT)
501
+
502
+
503
+ def _plain(text: str) -> str:
504
+ """Write a hint's text as Python reads it: ty's class objects and scoped type variables as names.
505
+
506
+ Returns:
507
+ It: `<class 'Point'>` as `type[Point]`, `Model@create_model` as `Model`.
508
+
509
+ """
510
+ return _SCOPED.sub(r"\1", _CLASS_OBJECT.sub(r"type[\1]", text))
511
+
512
+
513
+ def _lone(root: ast.expr) -> set[str]:
514
+ """Name the names an annotation writes without type arguments (`Box` in `list[Box]`, not `list`).
515
+
516
+ Returns:
517
+ Them.
518
+
519
+ """
520
+ subscripted: set[int] = {id(node.value) for node in ast.walk(root) if isinstance(node, ast.Subscript)}
521
+ return {node.id for node in ast.walk(root) if isinstance(node, ast.Name) and id(node) not in subscripted}
522
+
523
+
524
+ def _aliases(root: ast.expr) -> bool:
525
+ """Check whether an annotation is (or has in it) `TypeAlias`, however it's spelled.
526
+
527
+ Returns:
528
+ Whether it does.
529
+
530
+ """
531
+ return any(
532
+ (isinstance(node, ast.Name) and node.id == ALIAS)
533
+ or (isinstance(node, ast.Attribute) and node.attr == ALIAS)
534
+ for node in ast.walk(root)
535
+ )
536
+
537
+
538
+ @lru_cache(maxsize=1024)
539
+ def _origins(imports: tuple[str, ...]) -> _Origins:
540
+ """Read the `from` imports a hint's edits add (see `Offered`), each a statement binding one name.
541
+
542
+ An `import m` is passed over: it names no class.
543
+
544
+ Returns:
545
+ Each name bound, with its statement, the module (a relative one with its dots) and its name there.
546
+
547
+ """
548
+ found: _Origins = {}
549
+ statement: str
550
+ for statement in imports:
551
+ node: ast.stmt = ast.parse(statement).body[0]
552
+ if isinstance(node, ast.ImportFrom):
553
+ alias: ast.alias = node.names[0]
554
+ module: str = _RELATIVE * node.level + (node.module or "")
555
+ found[alias.asname or alias.name] = (statement, module, alias.name)
556
+ return found
557
+
558
+
559
+ def _way(name: str, plan: ImportPlan, origins: _Origins, *, lone: bool) -> str | None:
560
+ """Choose how a name the module doesn't bind where annotations run can be written.
561
+
562
+ `lone`: whether the annotation writes it without type arguments somewhere.
563
+
564
+ Returns:
565
+ `_OWN`, `_OFFERED` or `_KNOWN`; `None` if nothing says what the name means. From the standard
566
+ library, a hint's edits must import a class its tables know, from a public module, and never a
567
+ generic one written bare (a checker prints one so when it doesn't know its arguments).
568
+
569
+ """
570
+ if name == _DISCARD:
571
+ return None
572
+ if name in plan.guarded and plan.guarded[name].statement is None and name not in plan.values:
573
+ return _OWN
574
+ module: str
575
+ attribute: str
576
+ _, module, attribute = origins.get(name, ("", "", ""))
577
+ # A standard-library class a checker prints bare is imported to run, as `--fix`'s own are.
578
+ standard: bool = name not in origins or module.partition(_RELATIVE)[0] in _STDLIB
579
+ if name in WELL_KNOWN and standard:
580
+ return _KNOWN
581
+ if name not in origins:
582
+ return None
583
+ path: str = f"{module}.{attribute}"
584
+ writable: bool = (
585
+ not any(part.startswith("_") for part in module.split(_RELATIVE))
586
+ and stdlib.defines_class(path)
587
+ and not (lone and stdlib.needs_arguments(path))
588
+ )
589
+ return _OFFERED if writable or not standard else None
590
+
591
+
592
+ def _named(name: str, way: str | None, plan: ImportPlan, origins: _Origins) -> str | None:
593
+ """Write `name` the `way` chosen for it, adding the import it takes.
594
+
595
+ Returns:
596
+ The name as the file is to write it, or `None` if the import can't bind it.
597
+
598
+ """
599
+ if way == _KNOWN:
600
+ return plan.spell(WELL_KNOWN[name])
601
+ if way == _OWN:
602
+ return name
603
+ statement: str
604
+ module: str
605
+ attribute: str
606
+ statement, module, attribute = origins[name]
607
+ # Through an import that runs, if the module has one of what the hint's would bind.
608
+ found: str | None
609
+ if not module.startswith(_RELATIVE) and (found := plan.named(f"{module}.{attribute}")) is not None:
610
+ return found
611
+ return name if plan.guard(name, (module, attribute), statement) else None
612
+
613
+
614
+ def _annotation(root: ast.expr) -> bool:
615
+ """Check that an expression is an annotation, and says something: not a bare `None`.
616
+
617
+ Names, attributes, subscripts and unions, whose only constants are `None` and `...`: not a call
618
+ (`Module("os")`), a signature, or a string.
619
+
620
+ Returns:
621
+ Whether it is.
622
+
623
+ """
624
+ return (
625
+ all(isinstance(node, _ANNOTATION_NODES) for node in ast.walk(root))
626
+ and all(node.value in {None, Ellipsis} for node in ast.walk(root) if isinstance(node, ast.Constant))
627
+ and not (isinstance(root, ast.Constant) and root.value is None)
628
+ and not _mixes(root)
629
+ )
630
+
631
+
632
+ def _mixes(root: ast.expr) -> bool:
633
+ """Check whether an annotation has a union inside it (`dict[str, int | bytes]`): a mixed container's.
634
+
635
+ Its elements are read as one member or another, which no test of the name narrows.
636
+
637
+ Returns:
638
+ Whether it does.
639
+
640
+ """
641
+ chain: set[int] = _chain(root)
642
+ return any(isinstance(node, ast.BinOp) and id(node) not in chain for node in ast.walk(root))
643
+
644
+
645
+ def _chain(root: ast.expr) -> set[int]:
646
+ """Find the nodes joining an annotation's outermost union: `A | B` in `A | B | C`.
647
+
648
+ Returns:
649
+ Their identities.
650
+
651
+ """
652
+ return {id(root)} | _chain(root.left) if isinstance(root, ast.BinOp) else set()
653
+
654
+
655
+ def _fits(root: ast.expr, nesting: int, known: Known) -> bool:
656
+ """Check the rules accept an annotation: not vague (LVA005), too deep (LVA006) or long (LVA011).
657
+
658
+ Returns:
659
+ Whether it is.
660
+
661
+ """
662
+ return (
663
+ vague_fits(root, known.limits.vague)
664
+ and depth(root) < nesting
665
+ and length(root) <= known.limits.max_length
666
+ )
667
+
668
+
669
+ def _usable(name: str, plan: ImportPlan, before: int | None) -> bool:
670
+ """Check whether the annotation can use `name` as it is: the module binds it, or it's a builtin.
671
+
672
+ In a module body (`before`, the binding's line), the module must bind it earlier: the
673
+ annotation is evaluated there.
674
+
675
+ Returns:
676
+ Whether it can.
677
+
678
+ """
679
+ if name == _DISCARD: # gettext's, or a throwaway: never a class the checker means
680
+ return False
681
+ if name in plan.defined:
682
+ return before is None or plan.defined[name] < before
683
+ return name in _BUILTINS
684
+
685
+
686
+ def _widened(node: ast.expr) -> ast.expr | None:
687
+ """Widen every `Literal` in an annotation to its values' types, and a union's repeats away.
688
+
689
+ Returns:
690
+ The annotation, or `None` if a `Literal` holds something that isn't a plain value.
691
+
692
+ """
693
+ name: str
694
+ value: ast.expr
695
+ inner: ast.expr
696
+ elements: list[ast.expr]
697
+ match node:
698
+ case ast.Subscript(value=ast.Name(id=name), slice=inner) if name == _LITERAL:
699
+ members: list[ast.expr] = inner.elts if isinstance(inner, ast.Tuple) else [inner]
700
+ return _union([_literal_type(member) for member in members])
701
+ case ast.BinOp(op=ast.BitOr() | ast.BitAnd()):
702
+ return _joined(node)
703
+ case ast.Subscript(value=value, slice=inner):
704
+ widened: ast.expr | None = _widened(inner)
705
+ # A class's own argument may be bound to the literals (`Reader[Literal["frame"]]`).
706
+ # Compared as written: a node built here has no `ctx` before Python 3.13, so its dump differs.
707
+ same: bool = widened is not None and (_holds(value) or ast.unparse(widened) == ast.unparse(inner))
708
+ return ast.Subscript(value, widened) if widened is not None and same else None
709
+ case ast.Tuple(elts=elements) | ast.List(elts=elements):
710
+ parts: list[ast.expr | None] = [_widened(element) for element in elements]
711
+ kept: list[ast.expr] = [part for part in parts if part is not None]
712
+ rebuilt: ast.expr = ast.Tuple(kept) if isinstance(node, ast.Tuple) else ast.List(kept)
713
+ return rebuilt if len(kept) == len(parts) else None
714
+ case ast.Name(id=name):
715
+ return ast.Name(_WIDER.get(name, name))
716
+ case _:
717
+ return node
718
+
719
+
720
+ def _holds(head: ast.expr) -> bool:
721
+ """Check whether a subscript's head is a container of any type: a builtin, or `collections.abc`'s.
722
+
723
+ Returns:
724
+ Whether it is.
725
+
726
+ """
727
+ return isinstance(head, ast.Name) and (head.id in _BUILTINS or head.id in WELL_KNOWN)
728
+
729
+
730
+ def _joined(node: ast.BinOp) -> ast.expr | None:
731
+ """Widen a union's members; or read an intersection with a truthiness as its one other member.
732
+
733
+ Returns:
734
+ The annotation, or `None` for a member that widens to none, or any other intersection.
735
+
736
+ """
737
+ if isinstance(node.op, ast.BitOr):
738
+ return _union([_widened(member) for member in _members(node)])
739
+ types: list[ast.expr] = [part for part in _members(node, ast.BitAnd) if not _truthiness(part)]
740
+ return _widened(types[0]) if len(types) == 1 else None
741
+
742
+
743
+ def _literal_type(member: ast.expr) -> ast.expr | None:
744
+ """Type one `Literal` value: `1` is `int`, `-1` too, `"a"` is `str`, `Color.RED` is `Color`.
745
+
746
+ Returns:
747
+ Its type, or `None` for anything else.
748
+
749
+ """
750
+ owner: ast.expr
751
+ value: bool | int | str | bytes
752
+ match member:
753
+ case ast.Constant(value=None):
754
+ return member
755
+ case ast.Constant(value=bool() | int() | str() | bytes() as value):
756
+ return ast.Name(type(value).__name__)
757
+ case ast.UnaryOp(op=ast.USub(), operand=ast.Constant(value=int())):
758
+ return ast.Name("int")
759
+ case ast.Attribute(value=owner):
760
+ return owner
761
+ case _:
762
+ return None
763
+
764
+
765
+ def _members(node: ast.expr, joined: type[ast.operator] = ast.BitOr) -> list[ast.expr]:
766
+ """Flatten a union (`A | B | C`) into its members; or, `joined` by `&`, an intersection.
767
+
768
+ Returns:
769
+ Them, in order.
770
+
771
+ """
772
+ if isinstance(node, ast.BinOp) and isinstance(node.op, joined):
773
+ return [*_members(node.left, joined), *_members(node.right, joined)]
774
+ return [node]
775
+
776
+
777
+ def _truthiness(node: ast.expr) -> bool:
778
+ """Check whether an intersection's member only says its value is true, or false (`~AlwaysFalsy`).
779
+
780
+ Returns:
781
+ Whether it does.
782
+
783
+ """
784
+ name: str
785
+ match node:
786
+ case ast.UnaryOp(op=ast.Invert(), operand=ast.Name(id=name)):
787
+ return name in _TRUTHINESS
788
+ case _:
789
+ return False
790
+
791
+
792
+ def _union(members: list[ast.expr | None]) -> ast.expr | None:
793
+ """Join members into a union, each once, in order (`None` last, as it's conventionally written).
794
+
795
+ Returns:
796
+ The union (a single member alone), or `None` if any member is.
797
+
798
+ """
799
+ seen: dict[str, ast.expr] = {}
800
+ member: ast.expr | None
801
+ for member in members:
802
+ if member is None:
803
+ return None
804
+ part: ast.expr
805
+ for part in _members(member):
806
+ _ = seen.setdefault(ast.unparse(part), part)
807
+ ordered: list[ast.expr] = sorted(seen.values(), key=lambda part: ast.unparse(part) == _NONE)
808
+ union: ast.expr = ordered[0]
809
+ for member in ordered[1:]:
810
+ union = ast.BinOp(union, ast.BitOr(), member)
811
+ return union