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,752 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Standard-library functions and classes whose types `--fix` knows, and how a module names them.
3
+
4
+ The tables are generated from typeshed's stubs into `tables/` when the package is built, one JSON
5
+ file each (see `stdlib_tables/generate.py`): `RETURNS` holds functions returning the same builtin type
6
+ whatever their arguments; `OVERLOADS` those whose arguments decide it (`str` in, `str` out;
7
+ `bytes` in, `bytes` out), so they're typed only when those are known; `CLASSES` holds
8
+ non-generic classes, and functions returning one; `library_member` types those classes' methods
9
+ and attributes. A call is matched by the module and name it resolves to through the module's imports
10
+ (`origins`), not by how it's spelled, so `import os as o` then `o.getpid()`, or `from os import
11
+ getpid`, are the same call, and a `getpid` from anywhere else isn't.
12
+ """
13
+
14
+ import ast
15
+ import builtins
16
+ import json
17
+ from collections.abc import Iterable, Mapping
18
+ from functools import cache, lru_cache
19
+ from pathlib import Path
20
+ from types import MappingProxyType
21
+ from typing import Final, NamedTuple, TypeAlias, cast
22
+
23
+ from constricter.fix.core.known import ImportPlan, Inference, Known
24
+ from constricter.fix.core.signatures import Variant
25
+ from constricter.rules.syntax import import_bindings
26
+ from constricter.rules.walked import classes
27
+
28
+
29
+ def _table(name: str) -> object:
30
+ """Read one of the tables (`tables/<name>.json`), plain JSON as generated.
31
+
32
+ Returns:
33
+ Its entries.
34
+
35
+ """
36
+ # Not through `jsonc`: its comment stripping took 20 ms of every run's start, for nothing.
37
+ return cast(
38
+ "object",
39
+ json.loads(Path(__file__).parents[1].joinpath("tables", f"{name}.json").read_bytes()),
40
+ )
41
+
42
+
43
+ # `os.environ`'s own method with a fixed type: a variable's, which the tables' functions don't hold.
44
+ _ENVIRON: Final = {"os.environ.copy": "dict[str, str]"}
45
+ # `tarfile.open` is `TarFile.open` (typeshed's `open = TarFile.open`): an alias the tables don't follow.
46
+ _ALIASED: Final = {"tarfile.open": "tarfile.TarFile"}
47
+ RETURNS: Final = {**cast("dict[str, str]", _table("returns")), **_ENVIRON, **_ALIASED}
48
+ # Functions whose arguments decide their type: each signature, as each configuration reads them
49
+ # (see `constricter.fix.libraries.overloads`).
50
+ OVERLOADS: Final = cast("dict[str, list[Variant]]", _table("overloads"))
51
+ # Classes, and functions (constructors, classmethods) returning one: typed by that class's dotted
52
+ # path, spelled (and imported, if it must be) the way the module can.
53
+ CLASSES: Final = cast("dict[str, str]", _table("classes"))
54
+ # The builtin iterators no Python can subscript at run time.
55
+ _UNSUBSCRIPTABLE: Final = frozenset({"zip", "map", "filter", "reversed"})
56
+ # The `pathlib` classes: each one's `/` joins another part onto it.
57
+ _PATHS: Final = frozenset(
58
+ f"pathlib.{name}"
59
+ for name in ("Path", "PosixPath", "PurePath", "PurePosixPath", "PureWindowsPath", "WindowsPath")
60
+ )
61
+ _ALIASES: Final = cast("Mapping[str, str]", _table("aliases")) # a class's other public paths, to its own
62
+ # Each class's methods' returns and attributes' types apart from its public ancestors' (`_BASES`),
63
+ # `None` where it hides one of theirs: `_member` resolves the rest through them.
64
+ _Own: TypeAlias = Mapping[str, Mapping[str, str | None]]
65
+ _METHODS: Final = cast("_Own", _table("methods"))
66
+ _ATTRIBUTES: Final = cast("_Own", _table("attributes"))
67
+ # What awaiting a call of each `async def` gives: a function's by its path, a method's by its
68
+ # class's path and its name (a class's own path: its instance).
69
+ _AWAITED: Final = cast("Mapping[str, str]", _table("awaited"))
70
+ # The `async def`s whose arguments decide what awaiting their call gives, as `OVERLOADS` holds a
71
+ # function's; `signatures.AWAIT` before a path names one's entry to `overloads.chosen`.
72
+ AWAITED_OVERLOADS: Final = cast("dict[str, list[Variant]]", _table("awaited_overloads"))
73
+ # The generic classes awaiting an instance of gives its last type argument.
74
+ _AWAITABLE: Final = frozenset({"asyncio.Future", "asyncio.Task"})
75
+ # Each class's public ancestors in the tables, nearest first, comma-separated.
76
+ _BASES: Final = cast("Mapping[str, str]", _table("bases"))
77
+ # Classes' methods whose arguments decide their type: each one's entry in `method_signatures` (its
78
+ # signatures without `self`, as `OVERLOADS`', under the class defining it: `module.Class.method`).
79
+ _METHOD_OVERLOADS: Final = cast("_Own", _table("method_overloads")) # by name, as `_METHODS`
80
+ # Each generic class's type parameters, in order, comma-separated: an instance's type binds them.
81
+ # One ending `=` has a default (PEP 696): a class all of whose have one may be written bare.
82
+ _TYPE_PARAMETERS: Final = cast("Mapping[str, str]", _table("type_parameters"))
83
+ # The generic classes every Python can subscript at run time (`itertools.chain[str]`, not
84
+ # `itertools.count[int]`), as a module's own annotations are evaluated.
85
+ _SUBSCRIPTABLE: Final = cast("Mapping[str, str]", _table("subscriptable"))
86
+ # Each generic class's own attributes and properties, as templates naming its type parameters
87
+ # (`re.Match`'s `string`: `AnyStr`), which its instance's type arguments bind; and a non-generic
88
+ # class's own that `_ATTRIBUTES` can't hold (`ast.Module`'s `body`: `list[ast.stmt]`).
89
+ _GENERIC_ATTRIBUTES: Final = cast("_Own", _table("generic_attributes"))
90
+ # What iterating each class's instance gives, as such a template (`io.TextIOWrapper`'s `str`).
91
+ _ELEMENTS: Final = cast("Mapping[str, str]", _table("elements"))
92
+
93
+
94
+ @cache
95
+ def method_signatures() -> dict[str, list[Variant]]:
96
+ """Read the `method_signatures` table, the first time a call needs it.
97
+
98
+ Returns:
99
+ Each method's signatures (see `OVERLOADS`), by its entry.
100
+
101
+ """
102
+ return cast("dict[str, list[Variant]]", _table("method_signatures"))
103
+
104
+
105
+ @cache
106
+ def _scalars() -> Mapping[str, str]:
107
+ return cast("Mapping[str, str]", _table("scalars"))
108
+
109
+
110
+ @cache
111
+ def _scalar_members() -> Mapping[str, frozenset[str]]:
112
+ return {
113
+ scalar: frozenset(names)
114
+ for scalar, names in cast("Mapping[str, list[str]]", _table("scalar_members")).items()
115
+ }
116
+
117
+
118
+ def scalar_verdicts(path: str) -> str | None:
119
+ """Look up which builtin scalars a standard-library class or alias takes (`typing.SupportsIndex`).
120
+
121
+ Read the first time an installed package's overloads need it (see `constricter.fix.index.stubs.stubbed`).
122
+
123
+ Returns:
124
+ Its verdict per `overloads.SCALARS` type, in order; or `None` if the tables don't have it.
125
+
126
+ """
127
+ return _scalars().get(path)
128
+
129
+
130
+ def scalar_members(scalar: str) -> frozenset[str] | None:
131
+ """Name what an argument of builtin scalar type `scalar` has, for a protocol to be checked against.
132
+
133
+ Returns:
134
+ Its members, or `None` if the tables don't have them.
135
+
136
+ """
137
+ return _scalar_members().get(scalar)
138
+
139
+
140
+ # An environment lookup: `os.environ.get(k)` is `str | None`, with a `str` default it's `str`
141
+ # (`os.environ`'s generic `Mapping.get` decides it by the arguments).
142
+ ENVIRONMENT: Final = "os.environ.get"
143
+ _KIND: Final = "stdlib" # the fix kind of what the tables type
144
+ _BUILTIN_NAMES: Final = frozenset({*dir(builtins), "None"})
145
+ _DOT: Final = "."
146
+ _ENTER: Final = "__enter__"
147
+ _SELF: Final = "Self" # in a template: the receiver's own type
148
+ ANY: Final = "Any" # a function's whole return in `RETURNS`, declared `typing.Any` (`json.loads`)
149
+ KNOWN: Final = frozenset({*RETURNS, *OVERLOADS, ENVIRONMENT, *CLASSES}) # every function the tables type
150
+ # Capitalised functions the tables don't type (`xml.etree.ElementTree.Comment`): no constructors.
151
+ FUNCTIONS: Final = frozenset(cast("Mapping[str, str]", _table("functions")))
152
+ # Module-level variables' types (`sys.path`: `list[str]`): builtin annotations, or classes' paths.
153
+ VARIABLES: Final = cast("dict[str, str]", _table("variables"))
154
+ _TABLE_MODULES: Final = frozenset(
155
+ name.rsplit(".", count)[0]
156
+ for name in (*KNOWN, *_ALIASES, *_METHODS, *_ATTRIBUTES, *_BASES, *VARIABLES)
157
+ for count in range(1, name.count(".") + 1)
158
+ )
159
+
160
+
161
+ def origins(tree: ast.Module) -> dict[str, str]:
162
+ """Map each top-level name the module imports from the standard library to what it is.
163
+
164
+ `import os` binds `os` to `os` (and `import os.path` binds `os` too); `import os.path as p`
165
+ binds `p` to `os.path`; `from os import getpid as pid` binds `pid` to `os.getpid`. Only the
166
+ modules the tables name are kept, and a relative import names no module here.
167
+
168
+ Returns:
169
+ Each bound name, mapped to its dotted origin.
170
+
171
+ """
172
+ return imported(tree.body)
173
+
174
+
175
+ def imported(body: Iterable[ast.stmt]) -> dict[str, str]:
176
+ """Map the names the imports among `body` bind to what they are (see `origins`).
177
+
178
+ Returns:
179
+ Each bound name, mapped to its dotted origin.
180
+
181
+ """
182
+ return {name: origin for name, origin, module in import_bindings(body) if module in _TABLE_MODULES}
183
+
184
+
185
+ def resolved(func: ast.expr, bound: Mapping[str, str]) -> str | None:
186
+ """Resolve a callee (`o.path.join`, `getpid`) through the module's imports (`bound`, see `origins`).
187
+
188
+ Returns:
189
+ Its dotted origin (`os.path.join`), or `None` if its first name isn't one the module imports.
190
+
191
+ """
192
+ name: str
193
+ owner: ast.expr
194
+ attr: str
195
+ match func:
196
+ case ast.Name(id=name) if name in bound:
197
+ return bound[name]
198
+ case ast.Attribute(value=owner, attr=attr):
199
+ found: str | None = resolved(owner, bound)
200
+ return None if found is None else f"{found}.{attr}"
201
+ case _:
202
+ return None
203
+
204
+
205
+ def library_member(
206
+ receiver: str,
207
+ name: str,
208
+ call: ast.Call | None,
209
+ known: Known,
210
+ *,
211
+ inherited: bool = False,
212
+ ) -> Inference | None:
213
+ """Type a standard-library class's attribute or property, or (`call`) its method's return.
214
+
215
+ `receiver` is the annotation of what it's looked up on, as the module spells it
216
+ (`ArgumentParser`, `argparse.ArgumentParser`), resolved through its imports and the ones `--fix`
217
+ is adding to it; a class the member gives is spelled (and imported, if it must be) as the
218
+ module can. The arguments of a call don't matter: the tables hold only what they can't change.
219
+ `inherited`: whether it's looked up for a class under `receiver`, where a member that is
220
+ `receiver` itself may be its `Self` (`Path.resolve()`), the inheriting class.
221
+
222
+ Returns:
223
+ Its inference, or `None` if the class or its member isn't in the tables, or a class it gives
224
+ can't be named.
225
+
226
+ """
227
+ path: str | None = _class_path(receiver, known)
228
+ found: str | None = (
229
+ None if path is None else _member(_ATTRIBUTES if call is None else _METHODS, path, name)
230
+ )
231
+ if found is not None and inherited and found == path:
232
+ return None
233
+ plan: ImportPlan | None = known.names.plan
234
+ if found is not None and is_class(found):
235
+ found = None if plan is None else plan.spell(found)
236
+ what: str = "annotation" if call is None else "return type"
237
+ return (
238
+ None
239
+ if found is None
240
+ else Inference(found, f"`{path}.{name}`'s {what} in typeshed", frozenset({_KIND}))
241
+ )
242
+
243
+
244
+ def awaited_call(func: ast.expr, known: Known) -> Inference | None:
245
+ """Type what awaiting a call of a standard-library `async def` gives (`await asyncio.start_server(...)`).
246
+
247
+ Returns:
248
+ Its inference, or `None` if `func` isn't one the tables hold, or its class can't be named.
249
+
250
+ """
251
+ path: str | None = resolved(func, known.names.stdlib)
252
+ return None if path is None else _awaited(path, path, known)
253
+
254
+
255
+ def awaited_member(receiver: str, name: str, known: Known) -> Inference | None:
256
+ """Type what awaiting a call of a standard-library class's `async def` method gives.
257
+
258
+ `await reader.readline()`, on a receiver typed `asyncio.StreamReader`; and what `async with`
259
+ binds, by `__aenter__`: the receiver's own type, where that returns `Self`.
260
+
261
+ Returns:
262
+ Its inference, or `None` if the class or its method isn't in the tables, or a class it gives
263
+ can't be named.
264
+
265
+ """
266
+ path: str | None = _class_path(receiver, known)
267
+ return None if path is None else _awaited(f"{path}.{name}", path, known, receiver)
268
+
269
+
270
+ def _awaited(entry: str, path: str, known: Known, receiver: str | None = None) -> Inference | None:
271
+ found: str | None = _AWAITED.get(entry)
272
+ plan: ImportPlan | None = known.names.plan
273
+ if found is not None and found == path and receiver is not None:
274
+ found = receiver # its `Self`: the instance, as the module wrote its type
275
+ elif found is not None and is_class(found):
276
+ found = None if plan is None else plan.spell(found)
277
+ return (
278
+ None if found is None else Inference(found, f"`{entry}`'s return type, awaited", frozenset({_KIND}))
279
+ )
280
+
281
+
282
+ def awaited_value(annotation: str, known: Known) -> str | None:
283
+ """Find what awaiting a value typed `annotation` gives: `T`, of an `asyncio.Future[T]` or a `Task[T]`.
284
+
285
+ Returns:
286
+ It, as the annotation writes it; `None` for any other type.
287
+
288
+ """
289
+ path: str | None
290
+ args: list[ast.expr]
291
+ path, args = _receiver(annotation, known)
292
+ return ast.unparse(args[-1]) if path in _AWAITABLE and args else None
293
+
294
+
295
+ def bases(tree: ast.Module, bound: Mapping[str, str]) -> dict[str, str]:
296
+ """Spell the standard-library classes the module's classes inherit from (`unittest.TestCase`).
297
+
298
+ Those the tables hold whole; and a generic one given all its arguments
299
+ (`collections.OrderedDict[str, int]`), which decide its members' types: written with builtin
300
+ types, dotted names and the module's own classes alone, not a type variable, which means
301
+ nothing outside its class.
302
+
303
+ Returns:
304
+ Each, as the module's imports (`bound`, see `origins`) name it where it's a base, with its
305
+ path in the tables (`""`: a generic one's, whose members its arguments type).
306
+
307
+ """
308
+ found: dict[str, str] = {}
309
+ plain: frozenset[str] = _BUILTIN_NAMES | {node.name for node in classes(tree)}
310
+ base: ast.expr
311
+ for base in (base for node in classes(tree) for base in node.bases):
312
+ head: ast.expr = base.value if isinstance(base, ast.Subscript) else base
313
+ path: str = resolved(head, bound) or ""
314
+ path = _ALIASES.get(path, path)
315
+ given: int = 0
316
+ if isinstance(base, ast.Subscript) and all(
317
+ part.id in plain for part in ast.walk(base.slice) if isinstance(part, ast.Name)
318
+ ):
319
+ given = len(base.slice.elts) if isinstance(base.slice, ast.Tuple) else 1
320
+ if given and len(_TYPE_PARAMETERS.get(path, "").split(",")) == given and path in _TYPE_PARAMETERS:
321
+ found[ast.unparse(base)] = ""
322
+ if not given and CLASSES.get(path) == path:
323
+ found[ast.unparse(base)] = path
324
+ return found
325
+
326
+
327
+ def members(path: str) -> frozenset[str]:
328
+ """Name every member the tables hold of the class at `path`, its public ancestors' included.
329
+
330
+ Returns:
331
+ Them.
332
+
333
+ """
334
+ own: set[str] = set()
335
+ table: _Own
336
+ for table in (_METHODS, _ATTRIBUTES, _METHOD_OVERLOADS, _GENERIC_ATTRIBUTES):
337
+ own.update(table.get(path, {}))
338
+ return frozenset(own).union(*(members(ancestor) for ancestor in _ancestors(path)))
339
+
340
+
341
+ def attributes(path: str) -> dict[str, str]:
342
+ """Type the attributes the tables hold of the class at `path`, its public ancestors' included.
343
+
344
+ Returns:
345
+ Each one's type, by name: the class's own, where an ancestor has one of its name.
346
+
347
+ """
348
+ found: dict[str, str] = {}
349
+ ancestor: str
350
+ for ancestor in _ancestors(path):
351
+ found = attributes(ancestor) | found
352
+ own: Mapping[str, str | None] = _ATTRIBUTES.get(path, {})
353
+ return found | {name: typed for name, typed in own.items() if typed is not None}
354
+
355
+
356
+ def lines(path: str) -> frozenset[str]:
357
+ """Name the class at `path` and every public ancestor the tables give it.
358
+
359
+ Returns:
360
+ Their paths: two classes sharing one have an order between them the tables don't say.
361
+
362
+ """
363
+ return frozenset({path}).union(*(lines(ancestor) for ancestor in _ancestors(path)))
364
+
365
+
366
+ def enters_itself(receiver: str, known: Known) -> bool:
367
+ """Check whether `receiver` is a standard-library class whose `__enter__` returns the instance itself.
368
+
369
+ `receiver` is an annotation as the module spells it; its type arguments (`Popen[bytes]`) are the
370
+ instance's, which `with` then gives as it is.
371
+
372
+ Returns:
373
+ Whether it is.
374
+
375
+ """
376
+ path: str | None = _receiver(receiver, known)[0]
377
+ return path is not None and _member(_METHODS, path, _ENTER) == path
378
+
379
+
380
+ def _member(table: "_Own", path: str, name: str) -> str | None:
381
+ """Look up a class's member in a table: its own entry, else its nearest public ancestor's.
382
+
383
+ Returns:
384
+ Its entry (an attribute's type, a method's return, a `method_signatures` entry), or `None`.
385
+
386
+ """
387
+ own: Mapping[str, str | None] = table.get(path, {})
388
+ if name in own:
389
+ return own[name]
390
+ ancestor: str
391
+ for ancestor in _ancestors(path):
392
+ found: str | None
393
+ if (found := _member(table, ancestor, name)) is not None:
394
+ return found
395
+ return None
396
+
397
+
398
+ @lru_cache(maxsize=1024)
399
+ def _ancestors(path: str) -> tuple[str, ...]:
400
+ return tuple(_BASES[path].split(",")) if path in _BASES else ()
401
+
402
+
403
+ class Method(NamedTuple):
404
+ """A standard-library method whose arguments decide its type, on a receiver of a known type.
405
+
406
+ `entry`: its `method_signatures` entry; `instance`: the receiver's type arguments, if they're
407
+ builtins (`Pattern[str]`'s `str`), for a signature declaring `self`'s; `types`: its class's type
408
+ parameters, bound to the receiver's type arguments as the module spells them. For a receiver
409
+ typed through an installed class's alias (`npt.NDArray[np.float64]`): `templates`, the class's
410
+ type parameters as templates naming the alias's, and `matched`, the receiver's type as the class
411
+ it stands for, which a method's `self` is matched against (see `signatures.Expansion`).
412
+ """
413
+
414
+ entry: str
415
+ instance: list[str] | None
416
+ types: dict[str, str]
417
+ templates: Mapping[str, str] = MappingProxyType({})
418
+ matched: str | None = None
419
+
420
+
421
+ def for_receiver(method: Method, receiver: str) -> Method:
422
+ """Bind a method's `Self` to `receiver`: a class under the one it was found on has it as its own.
423
+
424
+ Returns:
425
+ The method, its `Self` (if it binds one) the receiver's type.
426
+
427
+ """
428
+ return method._replace(types={**method.types, _SELF: receiver}) if _SELF in method.types else method
429
+
430
+
431
+ def overloaded_method(receiver: str, name: str, known: Known) -> Method | None:
432
+ """Find a standard-library class's method whose arguments decide its type (see `method_signatures`).
433
+
434
+ Returns:
435
+ It, or `None` if the receiver isn't such a class or the method isn't such a method.
436
+
437
+ """
438
+ path: str | None
439
+ args: list[ast.expr]
440
+ path, args = _receiver(receiver, known)
441
+ entry: str | None = None if path is None else _member(_METHOD_OVERLOADS, path, name)
442
+ if path is None or entry is None:
443
+ return None
444
+ texts: list[str] = [ast.unparse(arg) for arg in args]
445
+ builtin: bool = all(
446
+ isinstance(node, ast.Name) and node.id in _BUILTIN_NAMES and known.is_builtin(node.id)
447
+ for arg in args
448
+ for node in ast.walk(arg)
449
+ if isinstance(node, ast.Name | ast.Attribute)
450
+ )
451
+ return Method(entry, texts if args and builtin else None, _bound(path, texts, receiver))
452
+
453
+
454
+ def operand(annotation: str, known: Known) -> str | None:
455
+ """Name an operand's type as an operator's `takes` does: a builtin's name, or a class's path.
456
+
457
+ Returns:
458
+ It, or `None` for any other type: a union, a subscript, a class the tables don't have.
459
+
460
+ """
461
+ root: ast.expr = _parsed(annotation)
462
+ if isinstance(root, ast.Name) and root.id in _BUILTIN_NAMES:
463
+ return root.id if known.is_builtin(root.id) else None
464
+ return _path(root, known) if isinstance(root, ast.Name | ast.Attribute) else None
465
+
466
+
467
+ def inherits(path: str, ancestor: str) -> bool:
468
+ """Check whether the class at `path` is under `ancestor`, as far as the tables know its bases.
469
+
470
+ Returns:
471
+ Whether it is.
472
+
473
+ """
474
+ return any(base == ancestor or inherits(base, ancestor) for base in _ancestors(path))
475
+
476
+
477
+ def generic_attribute(receiver: str, name: str, known: Known) -> tuple[str, str, dict[str, str]] | None:
478
+ """Find a class's attribute or property held as a template, on a receiver of a known type.
479
+
480
+ A generic class's own (`string`, on an `re.Match[str]`), or one a class with no type parameters
481
+ has or inherits (`parameters`, on an `inspect.Signature`).
482
+
483
+ Returns:
484
+ Its class's path, its template (see `_GENERIC_ATTRIBUTES`), and the class's type parameters
485
+ bound to the receiver's type arguments as the module spells them; or `None`.
486
+
487
+ """
488
+ path: str | None
489
+ args: list[ast.expr]
490
+ path, args = _receiver(receiver, known)
491
+ template: str | None = None
492
+ if path is not None:
493
+ template = (
494
+ _GENERIC_ATTRIBUTES.get(path, {}).get(name)
495
+ if path in _TYPE_PARAMETERS
496
+ else _member(_GENERIC_ATTRIBUTES, path, name)
497
+ )
498
+ if path is None or template is None:
499
+ return None
500
+ return path, template, _bound(path, [ast.unparse(arg) for arg in args], receiver)
501
+
502
+
503
+ def element(receiver: str, known: Known) -> tuple[str, str, dict[str, str]] | None:
504
+ """Find what iterating a standard-library class's instance gives (a `for` loop over a file: its lines).
505
+
506
+ Returns:
507
+ Its class's path, the element's template (see `_ELEMENTS`), and the class's type parameters
508
+ bound to the receiver's type arguments as the module spells them; or `None`.
509
+
510
+ """
511
+ path: str | None
512
+ args: list[ast.expr]
513
+ path, args = _receiver(receiver, known)
514
+ template: str | None = None if path is None else _ELEMENTS.get(path)
515
+ if path is None or template is None:
516
+ return None
517
+ return path, template, _bound(path, [ast.unparse(arg) for arg in args], receiver)
518
+
519
+
520
+ def _receiver(receiver: str, known: Known) -> tuple[str | None, list[ast.expr]]:
521
+ """Resolve a receiver's annotation to its class's path, and its type arguments (`re.Match[str]`'s `str`).
522
+
523
+ Returns:
524
+ The path (`None` if it's no standard-library class the module can name), and the arguments.
525
+
526
+ """
527
+ root: ast.expr = _parsed(receiver)
528
+ args: list[ast.expr] = []
529
+ if isinstance(root, ast.Subscript):
530
+ args = list(root.slice.elts) if isinstance(root.slice, ast.Tuple) else [root.slice]
531
+ root = root.value
532
+ return _path(root, known), args
533
+
534
+
535
+ def _bound(path: str, texts: list[str], receiver: str) -> dict[str, str]:
536
+ """Bind a generic class's type parameters to an instance's type arguments, if it gives them all.
537
+
538
+ And `Self` to `receiver`, the instance's type as the module spells it: not a generic class
539
+ named without its arguments.
540
+
541
+ Returns:
542
+ Each parameter's argument, by name; none if they don't match.
543
+
544
+ """
545
+ params: list[str] = [param.rstrip("=") for param in _TYPE_PARAMETERS.get(path, "").split(",") if param]
546
+ if len(params) != len(texts):
547
+ return {}
548
+ return {**dict(zip(params, texts, strict=True)), _SELF: receiver}
549
+
550
+
551
+ def generics(bound: Mapping[str, str]) -> frozenset[str]:
552
+ """Spell the standard library's generic classes as a module's imports (`bound`, see `origins`) name them.
553
+
554
+ Returns:
555
+ Each spelling (`StreamHandler`, `logging.StreamHandler`): written bare, one is missing its
556
+ type arguments.
557
+
558
+ """
559
+ return frozenset(
560
+ f"{name}{path.removeprefix(origin)}"
561
+ for name, origin in bound.items()
562
+ for path in _generic_paths(origin)
563
+ )
564
+
565
+
566
+ def held_whole(path: str) -> str | None:
567
+ """Find the class at `path` (or the one `path` is another name of), if the tables hold it whole.
568
+
569
+ Returns:
570
+ Its own path; `None` for anything else, a generic class included (see `bases`).
571
+
572
+ """
573
+ own: str = _ALIASES.get(path, path)
574
+ return own if CLASSES.get(own) == own else None
575
+
576
+
577
+ def class_argument(node: ast.expr, known: Known) -> str | None:
578
+ """Resolve a name or dotted path passed as an argument (`csv.Error`, `sqlite.OperationalError`).
579
+
580
+ Returns:
581
+ The standard-library class's own path, if the tables hold it whole and the module imports the
582
+ name it starts with; `None` otherwise.
583
+
584
+ """
585
+ path: str | None = resolved(node, known.names.stdlib)
586
+ return None if path is None else held_whole(path)
587
+
588
+
589
+ def defines_class(path: str) -> bool:
590
+ """Check whether `path` is a class the tables know: a plain one, or a generic one.
591
+
592
+ Returns:
593
+ Whether it is (`decimal.Decimal`, `operator.itemgetter`; not `os.path`, nor `logging.getLogger`).
594
+
595
+ """
596
+ return CLASSES.get(path) == path or path in _TYPE_PARAMETERS
597
+
598
+
599
+ def needs_arguments(path: str) -> bool:
600
+ """Check whether `path` is a generic class of the standard library's that is missing arguments, bare.
601
+
602
+ Returns:
603
+ Whether it is (`operator.itemgetter`; not `io.BufferedReader`, whose parameter has a default).
604
+
605
+ """
606
+ return path in _generic_paths(path)
607
+
608
+
609
+ @lru_cache(maxsize=1024)
610
+ def _generic_paths(origin: str) -> tuple[str, ...]:
611
+ """Find the generic classes an import's origin names (itself, or those in its module) that need arguments.
612
+
613
+ Returns:
614
+ Their paths.
615
+
616
+ """
617
+ return tuple(
618
+ path
619
+ for path, params in _TYPE_PARAMETERS.items()
620
+ if (path == origin or path.startswith(f"{origin}."))
621
+ and not all(param.endswith("=") for param in params.split(","))
622
+ )
623
+
624
+
625
+ def evaluable(annotation: str, known: Known) -> bool:
626
+ """Check that an annotation subscripts no standard-library class that can't be at run time.
627
+
628
+ Nor a builtin iterator that can't (`zip[tuple[int, str]]`).
629
+
630
+ Returns:
631
+ Whether it can be evaluated (as a module's annotations are) without that `TypeError`.
632
+
633
+ """
634
+ node: ast.AST
635
+ for node in ast.walk(_parsed(annotation)):
636
+ if not isinstance(node, ast.Subscript):
637
+ continue
638
+ if isinstance(node.value, ast.Name) and node.value.id in _UNSUBSCRIPTABLE:
639
+ return False
640
+ path: str | None = _path(node.value, known)
641
+ if path in _TYPE_PARAMETERS and path not in _SUBSCRIPTABLE:
642
+ return False
643
+ return True
644
+
645
+
646
+ def joins_path(left: str, right: str | None, known: Known) -> bool:
647
+ """Check whether `left / right` joins a path: a `pathlib` class's `/` gives its own class back.
648
+
649
+ Returns:
650
+ Whether `left` is a `pathlib` path class's annotation, and `right` a `str`'s or the same.
651
+
652
+ """
653
+ return right in {"str", left} and is_path(left, known)
654
+
655
+
656
+ def is_path(receiver: str, known: Known) -> bool:
657
+ """Check whether `receiver` is a `pathlib` path class's annotation.
658
+
659
+ Returns:
660
+ Whether it is.
661
+
662
+ """
663
+ return _class_path(receiver, known) in _PATHS
664
+
665
+
666
+ def _class_path(receiver: str, known: Known) -> str | None:
667
+ """Resolve a receiver's annotation to a class's path in the tables (its own, not an alias).
668
+
669
+ Returns:
670
+ The path, or `None` if the annotation doesn't name a standard-library class the module imports
671
+ (or `--fix` is importing).
672
+
673
+ """
674
+ return _path(_parsed(receiver), known)
675
+
676
+
677
+ def _path(root: ast.expr, known: Known) -> str | None:
678
+ """Resolve a class's name or dotted path to its path in the tables (see `_class_path`).
679
+
680
+ Returns:
681
+ The path, or `None`.
682
+
683
+ """
684
+ path: str | None = resolved(root, known.names.stdlib)
685
+ plan: ImportPlan | None = known.names.plan
686
+ if path is None and plan is not None and plan.added:
687
+ path = resolved(root, _added(tuple(plan.added.values())))
688
+ if path is None and plan is not None and plan.guarded:
689
+ path = _guarded(root, plan)
690
+ if path is None and plan is not None and plan.checking.bound:
691
+ path = resolved(root, plan.checking.bound)
692
+ # A class's own path, where the module binds no name it starts with: a library base out of its
693
+ # sight, as the index names it (see `constricter.fix.index.beyond`).
694
+ if path is None and isinstance(root, ast.Attribute): # a dotted path alone can be a class's own
695
+ written: str = ast.unparse(root)
696
+ if held_whole(written) and (plan is None or written.partition(".")[0] not in plan.taken):
697
+ path = written
698
+ return None if path is None else _ALIASES.get(path, path)
699
+
700
+
701
+ def _guarded(root: ast.expr, plan: ImportPlan) -> str | None:
702
+ """Resolve a class's name or dotted path through an import for type checking (`ImportPlan.guarded`).
703
+
704
+ One the module has, or `--fix` is adding for another file's type: the next run would resolve it
705
+ by the import, so this one does.
706
+
707
+ Returns:
708
+ Its dotted origin, or `None` if its first name isn't such an import's.
709
+
710
+ """
711
+ first: str | None = next(
712
+ (node.id for node in ast.walk(root) if isinstance(node, ast.Name) and node.id in plan.guarded),
713
+ None,
714
+ )
715
+ if first is None:
716
+ return None
717
+ return resolved(root, {first: ".".join(part for part in plan.guarded[first].origin if part)})
718
+
719
+
720
+ @lru_cache(maxsize=256)
721
+ def _added(statements: tuple[str, ...]) -> Mapping[str, str]:
722
+ """Map the names the imports `--fix` is adding bind (see `imported`), read once for all its lookups.
723
+
724
+ Returns:
725
+ Each bound name, mapped to its dotted origin: shared, so only read it.
726
+
727
+ """
728
+ return imported(ast.parse("\n".join(statements)).body)
729
+
730
+
731
+ @lru_cache(maxsize=4096)
732
+ def _parsed(annotation: str) -> ast.expr:
733
+ """Parse an annotation, once for all its lookups.
734
+
735
+ `annotation` is always `ast.unparse`'s own output, so it's always valid Python. The tree is
736
+ shared: only read it.
737
+
738
+ Returns:
739
+ Its expression.
740
+
741
+ """
742
+ return ast.parse(annotation, mode="eval").body
743
+
744
+
745
+ def is_class(annotation: str) -> bool:
746
+ """Check whether a table's annotation is a class's dotted path (`io.BytesIO`), not a builtin one.
747
+
748
+ Returns:
749
+ Whether it is.
750
+
751
+ """
752
+ return _DOT in annotation and all(part.isidentifier() for part in annotation.split(_DOT))