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,741 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """What each checked file offers other files' `--fix`, read once: the cross-file index (`index`).
3
+
4
+ `read` takes a file's module name, its top-level functions' declared return types (as
5
+ `declarations.returns` picks them), its classes' attributes and methods' returns (as `classes` and
6
+ `method_returns` do), its generic classes and type variables, and what each top-level name refers
7
+ to; `constricter.fix.index.project` looks things up in it for each file.
8
+ """
9
+
10
+ import ast
11
+ import bisect
12
+ import itertools
13
+ import re
14
+ from collections import Counter
15
+ from collections.abc import Iterable, Iterator, Mapping, Sequence
16
+ from dataclasses import dataclass, field, replace
17
+ from functools import lru_cache
18
+ from pathlib import Path
19
+ from typing import Final, NamedTuple, TypeAlias, cast
20
+
21
+ from constricter.empty import Empty
22
+ from constricter.fix.core.imports import taken_names
23
+ from constricter.fix.core.known import Origin, Passed, Returns, Takers, pickled, unpickled
24
+ from constricter.fix.core.signatures import AWAIT
25
+ from constricter.fix.index.origins import guarded_origins, origins_of, skipped, starred
26
+ from constricter.fix.index.params import Param, open_functions
27
+ from constricter.fix.index.stubs.declared import Declarations, Signature, declarations, kinds, overloads
28
+ from constricter.fix.values import classvars, entered, fills
29
+ from constricter.fix.values.bodies import yields_itself
30
+ from constricter.fix.values.returned import held_back, unannotated
31
+ from constricter.fix.values.stored import untouched
32
+ from constricter.overrides import override
33
+ from constricter.rules import parsed
34
+ from constricter.rules.annotations import dotted, is_composite, node_name
35
+ from constricter.rules.declarations import awaited_returns, defined_type_vars, generic_classes
36
+ from constricter.rules.decorators import Held, Pass, is_fixture, passing
37
+ from constricter.rules.keys import key, optional
38
+ from constricter.rules.syntax import child_statements, top_level
39
+ from constricter.rules.tables import Tables, module_tables
40
+ from constricter.rules.walked import classes as walked_classes
41
+ from constricter.rules.walked import of_type
42
+
43
+ _PACKAGE: Final = "__init__"
44
+ _DOT: Final = "."
45
+ _TYPE_ALIAS: Final = "TypeAlias" # the annotation, and the node class of `type X = ...` (Python 3.12+)
46
+ # What a type checker decides an `if` by, taking one arm alone: `sys.version_info`, `TYPE_CHECKING`.
47
+ _DECIDED: Final = frozenset({"version_info", "platform", "TYPE_CHECKING"})
48
+ SUFFIX: Final = ".py"
49
+ CONFTEST: Final = "conftest" # the module pytest reads a directory's fixtures from
50
+ _UNNAMED: Final = frozenset({"__call__", "__enter__"}) # members a statement takes without naming them
51
+ _GET: Final = "get"
52
+ _CLASS: Final = "class" # `project.CLASS`: the kind of name `classes_under` spells, with an alias
53
+ _ANNOTATION: Final = 200 # the longest string read for the names a type in it is written with
54
+ _DOTTED: Final = re.compile(r"[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)+")
55
+ STUB: Final = ".pyi"
56
+
57
+
58
+ @dataclass(frozen=True, slots=True, kw_only=True)
59
+ class Module: # pylint: disable=too-many-instance-attributes
60
+ """What one file offers and uses: its name, functions' return types, names' origins, and classes'.
61
+
62
+ `classes` and `methods`: each class's attributes and its methods' returns (see `Classes`), those
63
+ it takes from the file's other classes too (see `Lineage`).
64
+ """
65
+
66
+ name: str
67
+ returns: dict[str, str]
68
+ names: dict[str, Origin]
69
+ classes: Mapping[str, Mapping[str, str]] = field(default_factory=dict[str, Mapping[str, str]])
70
+ methods: Mapping[str, Mapping[str, str]] = field(default_factory=dict[str, Mapping[str, str]])
71
+ type_vars: frozenset[str] = frozenset() # its module-level type variables
72
+ # What it imports under a top-level `if` or `try` (`if TYPE_CHECKING:`), for `type_vars` alone.
73
+ guarded: Mapping[str, Origin] = field(default_factory=dict[str, Origin])
74
+ # The modules it imports everything from, a top-level `if` or `try`'s too (see `origins.starred`).
75
+ stars: tuple[str, ...] = ()
76
+ unannotated: frozenset[str] = frozenset() # its functions a `return` could type (`returned`)
77
+ # Those a `return` could type but for their decorators, with them spelled: the index may vouch
78
+ # for them (see `decorated.passed`), and they join `unannotated`.
79
+ decorated: Mapping[str, tuple[str, ...]] = field(default_factory=dict[str, tuple[str, ...]])
80
+ called: frozenset[str] = frozenset() # what it calls through its top-level names (`f`, `u.f`)
81
+ passed: frozenset[str] = frozenset() # what it passes as an argument through them (`np.float64`)
82
+ method_calls: frozenset[str] = frozenset() # the methods it calls on anything (`astype` in `a.astype(x)`)
83
+ attributes: frozenset[str] = frozenset() # every attribute it reads or calls, of anything (`x` in `a.x`)
84
+ returned: Returns = field(default_factory=Returns) # what they return, once it's checked
85
+ generics: frozenset[str] = frozenset() # its generic classes, which a type mustn't write bare
86
+ installed: bool = False # an installed package's, read for its types alone (see `installed`)
87
+ # Its plain top-level functions with a parameter left unannotated (see `open_functions`), and
88
+ # what every call passes each such parameter, once they're all seen (see `callers`). Plain
89
+ # `dict`s: the CLI's worker processes send modules back, pickled.
90
+ open: Mapping[str, tuple[Param, ...]] = field(default_factory=dict[str, tuple[Param, ...]])
91
+ # See `Seeds`
92
+ parameters: Mapping[str, Mapping[str, Passed]] = field(
93
+ default_factory=dict[str, Mapping[str, Passed]],
94
+ )
95
+ declared: Declarations | None = None # an installed module's, for its overloads (see `declared`)
96
+ # Its functions declaring a return under decorators other modules may vouch for, each with those
97
+ # Decorators as written, and its own decorators that give a function back (see `decorators`).
98
+ held: Mapping[str, Held] = field(default_factory=dict[str, Held])
99
+ passes: Mapping[str, Pass] = field(default_factory=dict[str, Pass])
100
+ vouched: frozenset[str] = frozenset() # those of `held` they vouched for, now among `returns`
101
+ # Its type aliases, and whether each takes type arguments (see `_aliases`)
102
+ aliases: Mapping[str, bool] = field(default_factory=dict[str, bool])
103
+ # The names its top level assigns twice or more (see `_assigned`): variables, to a type checker.
104
+ rebound: frozenset[str] = frozenset()
105
+ # Its top-level names something in it binds as a value too (a local `m`, under `import pkg.m as m`).
106
+ shadowed: frozenset[str] = frozenset()
107
+ # Its classes' bases as written, their variables typed by their values, and which of them are
108
+ # Plain, once the index settles it (see `constricter.fix.values.classvars`, `plain.settled`).
109
+ bases: Mapping[str, tuple[str, ...]] = field(default_factory=dict[str, tuple[str, ...]])
110
+ # The names each of its classes' bodies binds
111
+ bound: Mapping[str, frozenset[str]] = field(default_factory=dict[str, frozenset[str]])
112
+ members: Mapping[str, Mapping[str, str]] = field(default_factory=dict[str, Mapping[str, str]])
113
+ # Its classes' variables bound to a bare name, with that name (`classvars.named`): typed as the
114
+ # Class it names, among `members`, where the index finds one (`plain.settled`).
115
+ named: Mapping[str, Mapping[str, str]] = field(default_factory=dict[str, Mapping[str, str]])
116
+ plain: frozenset[str] = frozenset()
117
+ # Its classes' classmethods' and staticmethods' declared returns (see `declarations.class_methods`);
118
+ # Those held back as `held` are, and those of them the index vouched for, now among `sides`.
119
+ sides: Mapping[str, Mapping[str, str]] = field(default_factory=dict[str, Mapping[str, str]])
120
+ held_sides: Mapping[str, Mapping[str, Held]] = field(default_factory=dict[str, Mapping[str, Held]])
121
+ vouched_sides: frozenset[tuple[str, str]] = frozenset()
122
+ # Its functions' and classes' methods' declared returns that are tuples with a vague part, which
123
+ # Only an unpacking can use (see `known.Partial`): the methods as `methods` has them.
124
+ partial: Mapping[str, str] = field(default_factory=dict[str, str])
125
+ partial_methods: Mapping[str, Mapping[str, str]] = field(default_factory=dict[str, Mapping[str, str]])
126
+ # Its named tuples' fields (see `targets.named_tuples`)
127
+ tuples: Mapping[str, str] = field(default_factory=dict[str, str])
128
+ # Its type aliases of a union (see `targets.aliased_unions`)
129
+ unions: Mapping[str, str] = field(default_factory=dict[str, str])
130
+ # Its top-level pytest fixtures, and whether each is a generator (its value is what it yields).
131
+ fixtures: Mapping[str, bool] = field(default_factory=dict[str, bool])
132
+ # A checked file's functions defined with `@overload`, each with its overloads' signatures.
133
+ overloads: Mapping[str, tuple[Signature, ...]] = field(default_factory=dict[str, tuple[Signature, ...]])
134
+ folder: str = "" # a checked file's directory: where pytest looks for the `conftest.py`s above it
135
+ # Its top-level names bound by assignment to a value: not a type alias, nor a type variable.
136
+ assigned: frozenset[str] = frozenset()
137
+ # Its classes' methods a `return` could type (as `unannotated`), by name, whatever the class.
138
+ loose: frozenset[str] = frozenset()
139
+ # Its classes' methods declared to return a bare `Self` (see `declarations.self_returns`).
140
+ selfish: Mapping[str, frozenset[str]] = field(default_factory=dict[str, frozenset[str]])
141
+ # What awaiting each of its `async def`s' calls gives (`awaited_returns`)
142
+ awaits: Mapping[str, str] = field(default_factory=dict[str, str])
143
+ # The dotted names a checked file writes (`u.helper`, `pkg.m.Row`, in a string too), by their
144
+ # First name: of all a module it imports has, what it can mean (see `written_under`).
145
+ written: Mapping[str, tuple[str, ...]] = field(default_factory=dict[str, tuple[str, ...]])
146
+ # Its top-level functions `@contextmanager` makes managers: what each declares it yields, `""`
147
+ # For one declaring nothing, which its `yield`s may type (see `constricter.fix.values.entered`).
148
+ managers: Mapping[str, str] = field(default_factory=dict[str, str])
149
+ # Its top-level `async def`s declaring no return, which their `return`s may type (see `returned`).
150
+ unawaited: frozenset[str] = frozenset()
151
+ # Its functions that declare a parameter a builtin container (see `constricter.fix.values.fills.takers`).
152
+ takers: Mapping[str, Takers] = field(default_factory=dict[str, Takers])
153
+ # A checked file's type aliases, type variables and protocols, read as a stub's (see `declared.kinds`).
154
+ kinds: Declarations | None = None
155
+ # The attributes its classes bind to an empty container and do no more with (see `stored.untouched`).
156
+ emptied: Mapping[str, Mapping[str, str]] = field(default_factory=dict[str, Mapping[str, str]])
157
+
158
+ @override
159
+ def __getstate__(self) -> dict[str, object]:
160
+ """Pickle it by its fields (see `pickled`).
161
+
162
+ Returns:
163
+ Them.
164
+
165
+ """
166
+ return pickled(self)
167
+
168
+ def __setstate__(self, state: dict[str, object]) -> None:
169
+ """Unpickle it (see `unpickled`)."""
170
+ unpickled(self, state)
171
+
172
+ @property
173
+ def typing(self) -> Declarations | None:
174
+ """What its names stand for in a signature: an installed module's declarations, or its `kinds`."""
175
+ return self.declared or self.kinds
176
+
177
+ def reads(self, name: str | None) -> bool:
178
+ """Check whether a signature's name is read by what the module declares it: an alias, a type variable.
179
+
180
+ Returns:
181
+ Whether it is: any of an installed module's; a checked file's alias, type variable or protocol.
182
+
183
+ """
184
+ found: Declarations | None = self.kinds
185
+ return self.installed or (
186
+ found is not None
187
+ and any(name in each for each in (found.aliases, found.variables, found.protocols))
188
+ )
189
+
190
+
191
+ # The classes a module names by its own imports: each one's spelling, its module and its name
192
+ # there; and the generic ones it spells through a module.
193
+ _Spelled: TypeAlias = tuple[list[tuple[str, str, str]], list[str]]
194
+ _ByClass: TypeAlias = dict[str, frozenset[str]] # each class's methods of a kind, by its name
195
+ _Under: TypeAlias = list[tuple[str, Origin]] # classes' spellings after a package's name, and what each is
196
+
197
+
198
+ @dataclass
199
+ class Memo:
200
+ """What's worked out once of an index's classes, for every file that asks.
201
+
202
+ Which classes its modules define, what they're named by and their bases don't change as the
203
+ files are checked (what their functions return does): what follows from those alone is kept
204
+ here, by the index that has it and each one made from it with the same modules.
205
+ """
206
+
207
+ # A package's and its submodules' classes or aliases: each one's spelling after the package's
208
+ # Name, and what it is, by the package and the kind.
209
+ under: dict[tuple[str, str], _Under] = field(default_factory=dict)
210
+ # What a module names (see `_Spelled`), by its name and folder.
211
+ spelled: dict[tuple[str, str], _Spelled] = field(default_factory=dict)
212
+ # A class's line of bases (see `own_types.lineages`), by its path.
213
+ lines: dict[str, tuple[str, ...]] = field(default_factory=dict)
214
+ # The members a class declares a type for (attributes, methods' returns), by its module and name.
215
+ members: dict[tuple[str, str], frozenset[str]] = field(default_factory=dict)
216
+ # The methods a module's classes overload, by class (see `own_methods`), by its name and folder.
217
+ overloaded: dict[tuple[str, str], "_ByClass"] = field(default_factory=dict)
218
+
219
+
220
+ class Index(NamedTuple):
221
+ """Every checked file's module, and their names sorted for a module/submodule lookup."""
222
+
223
+ modules: dict[str, Module]
224
+ names: list[str] # modules, sorted by name
225
+ repeated: frozenset[str] = frozenset() # the names more than one file has: `modules` has the last
226
+ # Each checked `conftest.py` outside any package, by its directory: several share the one name.
227
+ conftests: Mapping[str, Module] = Empty()
228
+ memo: Memo | None = None # `None`: nothing is kept (an index made by hand)
229
+
230
+
231
+ @lru_cache(maxsize=65536) # asked of each checked file a dozen times: its folders are looked at once
232
+ def module_name(path: Path) -> str:
233
+ """Name `path`'s module: its package folders (those with an `__init__.py`), then it.
234
+
235
+ Returns:
236
+ The dotted module name.
237
+
238
+ """
239
+ packages: list[Path] = list(
240
+ itertools.takewhile(
241
+ lambda folder: (folder / f"{_PACKAGE}{SUFFIX}").is_file(),
242
+ path.resolve().parents,
243
+ ),
244
+ )
245
+ return ".".join(
246
+ [*(folder.name for folder in reversed(packages)), *([] if path.stem == _PACKAGE else [path.stem])],
247
+ )
248
+
249
+
250
+ def index(paths: Sequence[Path]) -> Index:
251
+ """Read each `.py` file in `paths` (one that can't be read or parsed is left out).
252
+
253
+ Returns:
254
+ Each module's name, mapped to what it offers and uses.
255
+
256
+ """
257
+ return indexed(read(path) for path in paths)
258
+
259
+
260
+ def indexed(found: Iterable[Module | None]) -> Index:
261
+ """Index modules already read (`read`'s, in any process): `None`s, for files it couldn't, left out.
262
+
263
+ Returns:
264
+ Each module's name, mapped to what it offers and uses.
265
+
266
+ """
267
+ counts: Counter[str] = Counter()
268
+ modules: dict[str, Module] = {}
269
+ conftests: dict[str, Module] = {}
270
+ module: Module | None
271
+ for module in found:
272
+ if module is not None:
273
+ counts[module.name] += 1
274
+ modules[module.name] = module
275
+ if module.name == CONFTEST and module.folder:
276
+ conftests[module.folder] = module
277
+ repeated: frozenset[str] = frozenset(name for name, count in counts.items() if count > 1)
278
+ return Index(modules, sorted(modules), repeated, conftests, Memo())
279
+
280
+
281
+ def spliced(catalog: Index, found: Iterable[Module | None]) -> Index:
282
+ """Put modules read again (`read`'s, of files `--fix` changed) in place of the ones `catalog` has.
283
+
284
+ What the index learned of each from the others (`returned`, `parameters`) is kept; a name several
285
+ files have keeps the module `catalog` has, but in `conftests`. What's kept worked out goes: a
286
+ class's members may now declare their types.
287
+
288
+ Returns:
289
+ The index, with them.
290
+
291
+ """
292
+ modules: dict[str, Module] = dict(catalog.modules)
293
+ conftests: dict[str, Module] = dict(catalog.conftests)
294
+ again: Module
295
+ for again in (each for each in found if each is not None):
296
+ local: bool = again.name == CONFTEST and again.folder in conftests
297
+ before: Module | None
298
+ if (before := conftests[again.folder] if local else modules.get(again.name)) is None:
299
+ continue
300
+ module: Module = replace(again, returned=before.returned, parameters=before.parameters)
301
+ if local:
302
+ conftests[module.folder] = module
303
+ if modules.get(module.name) is before if local else module.name not in catalog.repeated:
304
+ modules[module.name] = module
305
+ return catalog._replace(modules=modules, conftests=conftests, memo=Memo())
306
+
307
+
308
+ def read(path: Path, name: str | None = None) -> Module | None:
309
+ """Read what one `.py` file offers and uses.
310
+
311
+ With `name`, an installed package's module (see `installed`): a stub (`.pyi`) too, named that,
312
+ and not kept for a check.
313
+
314
+ Returns:
315
+ Its module, or `None` if it isn't a `.py` file, or can't be read or parsed.
316
+
317
+ """
318
+ if path.suffix not in ({SUFFIX} if name is None else {SUFFIX, STUB}) or not path.is_file():
319
+ return None
320
+ source: str | None
321
+ if (source := _source(path)) is None:
322
+ return None
323
+ try:
324
+ tree: ast.Module = parsed.parse(source, str(path))
325
+ except (SyntaxError, ValueError): # a null byte is a ValueError
326
+ return None
327
+ own: Tables = module_tables(tree)
328
+ assigned: Counter[str] = _assigned(tree.body)
329
+ rebound: frozenset[str] = frozenset(bound for bound, count in assigned.items() if count > 1)
330
+ if name is None:
331
+ parsed.keep(source, (tree, own)) # for the check to take, rather than parse it and read it again
332
+ named: str = name or module_name(path)
333
+ names: dict[str, Origin] = origins_of(tree, named, is_package=path.stem == _PACKAGE)
334
+ names.update(skipped(tree, names)) # `pa = pytest.importorskip("pyarrow")`, as `import pyarrow as pa`
335
+ return Module(
336
+ name=named,
337
+ returns=own.returns,
338
+ names=names,
339
+ classes=own.classes,
340
+ methods=own.order.flattened(own.methods),
341
+ type_vars=defined_type_vars(tree),
342
+ guarded=guarded_origins(tree, named, is_package=path.stem == _PACKAGE),
343
+ stars=starred(tree, named, is_package=path.stem == _PACKAGE),
344
+ unannotated=unannotated(tree.body, passing(tree, defined_type_vars(tree))),
345
+ decorated=held_back(tree.body),
346
+ called=_called(tree, names),
347
+ generics=generic_classes(tree),
348
+ loose=frozenset[str]().union(*(_loose(node.body) for node in walked_classes(tree))),
349
+ selfish=dict(own.order.selfish),
350
+ awaits=awaited_returns(tree),
351
+ written={} if name is not None else _written(tree),
352
+ passed=frozenset() if name is not None else _passed(tree, names),
353
+ method_calls=frozenset() if name is not None else _method_calls(tree),
354
+ attributes=frozenset() if name is not None else _attributes(tree),
355
+ installed=name is not None,
356
+ open=open_functions(tree),
357
+ declared=None if name is None else declarations(tree),
358
+ held=own.held,
359
+ passes=own.passes,
360
+ aliases={alias: generic for alias, generic in _aliases(tree).items() if alias not in rebound},
361
+ rebound=rebound,
362
+ assigned=frozenset(assigned).difference(_aliases(tree).keys(), defined_type_vars(tree)),
363
+ shadowed=frozenset(names) & taken_names(tree)[1] if name is None else frozenset(),
364
+ bases={} if name is not None else classvars.bases(tree),
365
+ bound=dict(own.order.bound),
366
+ members={} if name is not None else classvars.members(tree),
367
+ named={} if name is not None else classvars.named(tree),
368
+ sides=own.sides,
369
+ held_sides=own.held_sides,
370
+ partial=own.partial,
371
+ partial_methods=own.order.flattened(own.partial_methods),
372
+ tuples=own.tuples,
373
+ unions=own.unions,
374
+ fixtures=_fixtures(tree),
375
+ overloads={} if name is not None else overloads(tree),
376
+ folder="" if name is not None else str(path.resolve().parent),
377
+ managers={} if name is not None else _managers(tree),
378
+ takers={} if name is not None else fills.takers(tree),
379
+ emptied={} if name is not None else untouched(tree),
380
+ kinds=None if name is not None else kinds(tree, _aliases(tree).keys() - rebound),
381
+ unawaited=frozenset(
382
+ stmt.name
383
+ for stmt in tree.body
384
+ if name is None and isinstance(stmt, ast.AsyncFunctionDef) and stmt.returns is None
385
+ ),
386
+ )
387
+
388
+
389
+ def _loose(body: Sequence[ast.stmt]) -> frozenset[str]:
390
+ """Name a class's methods whose `return`s could type their calls, as the files calling them name them.
391
+
392
+ Returns:
393
+ Each one's name, an `async def`'s after `AWAIT` (see `_attributes`).
394
+
395
+ """
396
+ return unannotated(body) | {
397
+ f"{AWAIT}{stmt.name}"
398
+ for stmt in body
399
+ if isinstance(stmt, ast.AsyncFunctionDef) and stmt.returns is None
400
+ }
401
+
402
+
403
+ def _managers(tree: ast.Module) -> dict[str, str]:
404
+ """Find a module's top-level functions `@contextmanager` makes context managers.
405
+
406
+ Returns:
407
+ Each one's name, and what it declares it yields (`""`: nothing, see `entered.undeclared`).
408
+
409
+ """
410
+ declared: dict[str, str] = entered.managers(tree)
411
+ return {
412
+ **dict.fromkeys(entered.undeclared(tree), ""),
413
+ **{name: yielded for name, yielded in declared.items() if _DOT not in name},
414
+ }
415
+
416
+
417
+ def _written(tree: ast.Module) -> dict[str, tuple[str, ...]]:
418
+ """Find the dotted names a module writes: each attribute chain on a name, and those its strings hold.
419
+
420
+ A string's (an annotation in quotes, a `cast`'s target) and a type comment's count: a type may
421
+ be written in either.
422
+
423
+ Returns:
424
+ Them, sorted, by their first name; each chain's own beginnings too (`pkg.m`, of `pkg.m.Row`).
425
+
426
+ """
427
+ found: set[str] = set()
428
+ node: ast.AST
429
+ for node in of_type(tree, ast.Attribute):
430
+ text: str | None
431
+ if (text := dotted(cast("ast.Attribute", node))) is not None:
432
+ found.add(text)
433
+ texts: list[str] = [
434
+ node.value
435
+ for node in cast("list[ast.Constant]", of_type(tree, ast.Constant))
436
+ if isinstance(node.value, str) and len(node.value) <= _ANNOTATION
437
+ ]
438
+ texts.extend(
439
+ cast("str | None", getattr(node, "type_comment", None)) or ""
440
+ for kind in (ast.FunctionDef, ast.AsyncFunctionDef, ast.Assign, ast.For, ast.With, ast.arg)
441
+ for node in of_type(tree, kind)
442
+ )
443
+ chain: str
444
+ for chain in cast("list[str]", _DOTTED.findall("\n".join(texts))):
445
+ parts: list[str] = chain.split(".")
446
+ found.update(".".join(parts[:count]) for count in range(2, len(parts) + 1))
447
+ grouped: dict[str, list[str]] = {}
448
+ for chain in sorted(found):
449
+ grouped.setdefault(chain.partition(".")[0], []).append(chain)
450
+ return {root: tuple(chains) for root, chains in grouped.items()}
451
+
452
+
453
+ def exported(module: Module) -> Mapping[str, Origin]:
454
+ """Name what a module imports from others, which a file may import from it in turn.
455
+
456
+ What it imports for type checking alone too: a package's `__init__` that imports its names only
457
+ when they're asked for (pydantic's) has them there.
458
+
459
+ Returns:
460
+ Each, by its name there.
461
+
462
+ """
463
+ return {**module.guarded, **module.names} if module.guarded else module.names
464
+
465
+
466
+ def takes(module: Module, name: str) -> bool:
467
+ """Check whether `module` binds `name` by an import from another module: one to follow there.
468
+
469
+ Returns:
470
+ Whether it does.
471
+
472
+ """
473
+ return module.names.get(name, (module.name, None))[0] != module.name
474
+
475
+
476
+ def written_under(target: Module, local: str) -> Iterator[tuple[str, str, str]]:
477
+ """Find the names `target` writes under `local`, a module it imports (`u.helper`, `pkg.util.helper`).
478
+
479
+ Yields:
480
+ Each one, the submodule it's written through (`""`, or `.util`), and its last name.
481
+
482
+ """
483
+ prefix: str = f"{local}."
484
+ chain: str
485
+ for chain in target.written.get(local.partition(".")[0], ()):
486
+ if chain.startswith(prefix):
487
+ sub: str
488
+ name: str
489
+ sub, _, name = chain.removeprefix(local).rpartition(".")
490
+ yield chain, sub, name
491
+
492
+
493
+ def submodules(catalog: Index, prefix: str) -> Iterator[Module]:
494
+ """Find the module named `prefix`, and every module dotted under it (`pkg.util` under `pkg`).
495
+
496
+ A module's identifier characters all sort after `.`, so the names in `[prefix, prefix + "/")` are
497
+ exactly `prefix` itself and those starting with `prefix + "."` (`/` is the character after `.`).
498
+
499
+ Yields:
500
+ Each such module, by name.
501
+
502
+ """
503
+ start: int = bisect.bisect_left(catalog.names, prefix)
504
+ stop: int = bisect.bisect_left(catalog.names, f"{prefix}/")
505
+ name: str
506
+ for name in catalog.names[start:stop]:
507
+ yield catalog.modules[name]
508
+
509
+
510
+ def classes_under(catalog: Index, local: str, package: str, kind: str) -> Iterator[tuple[str, Origin]]:
511
+ """Spell the classes (or, by `kind`, the aliases) of `package` and its submodules as a file may name them.
512
+
513
+ One importing it as `local`: every one, written in the file or not, since a type written for it
514
+ may name any (`m.Row`, by `import m`).
515
+
516
+ Returns:
517
+ Each spelling (`m.Row`, `pkg.m.Row`), and the module and name it is.
518
+
519
+ """
520
+ found: _Under | None
521
+ if (found := None if catalog.memo is None else catalog.memo.under.get((package, kind))) is None:
522
+ found = [
523
+ (f"{other.name.removeprefix(package)}.{name}", (other.name, name))
524
+ for other in submodules(catalog, package)
525
+ for name in (other.classes if kind == _CLASS else other.aliases)
526
+ ]
527
+ if catalog.memo is not None:
528
+ catalog.memo.under[package, kind] = found
529
+ return ((f"{local}{suffix}", origin) for suffix, origin in found)
530
+
531
+
532
+ def _fixtures(tree: ast.Module) -> dict[str, bool]:
533
+ """Find the module's top-level pytest fixtures.
534
+
535
+ Returns:
536
+ Each one's name, and whether it's a generator.
537
+
538
+ """
539
+ return {
540
+ stmt.name: yields_itself(stmt)
541
+ for stmt in tree.body
542
+ if isinstance(stmt, ast.FunctionDef) and is_fixture(stmt)
543
+ }
544
+
545
+
546
+ def _aliases(tree: ast.Module) -> dict[str, bool]:
547
+ """Find a module's type aliases, at its top level or under an `if` or `try` there.
548
+
549
+ A name annotated `TypeAlias`, bound to a subscript or a union (`Json = dict[str, "Json"] | str`),
550
+ or a `type` statement's.
551
+
552
+ Returns:
553
+ Each one's name, and whether it takes type arguments: its value names one of the module's type
554
+ variables, or it has type parameters of its own.
555
+
556
+ """
557
+ type_vars: frozenset[str] = defined_type_vars(tree)
558
+ found: dict[str, bool] = {}
559
+ stmt: ast.stmt
560
+ name: str
561
+ value: ast.expr
562
+ annotation: ast.expr
563
+ for stmt in top_level(tree.body):
564
+ match stmt:
565
+ case ast.AnnAssign(
566
+ target=ast.Name(id=name),
567
+ annotation=annotation,
568
+ value=ast.expr() as value,
569
+ ) if node_name(annotation) == _TYPE_ALIAS:
570
+ found[name] = _names_any(value, type_vars)
571
+ case ast.Assign(targets=[ast.Name(id=name)], value=value) if is_composite(value):
572
+ found[name] = _names_any(value, type_vars)
573
+ case _ if type(stmt).__name__ == _TYPE_ALIAS:
574
+ named: ast.Name = cast("ast.Name", getattr(stmt, "name", None))
575
+ found[named.id] = bool(cast("object", getattr(stmt, "type_params", ())))
576
+ case _:
577
+ pass
578
+ return found
579
+
580
+
581
+ def _assigned(body: Sequence[ast.stmt]) -> Counter[str]:
582
+ """Count how many times a run of a module's top level may assign each name, as a type checker reads it.
583
+
584
+ Under its `if`s and `try`s too: both arms of an `if`, but one of an `if` a checker decides
585
+ (`sys.version_info >= (3, 10)`, `TYPE_CHECKING`), and never the arm a constant rules out
586
+ (`elif False:`). A name assigned twice is a variable to it, not an alias (`if MYPY: X = A` /
587
+ `else: X = B`).
588
+
589
+ Returns:
590
+ Each name's count.
591
+
592
+ """
593
+ counts: Counter[str] = Counter()
594
+ stmt: ast.stmt
595
+ name: str
596
+ test: ast.expr
597
+ constant: bool
598
+ for stmt in body:
599
+ match stmt:
600
+ case (
601
+ ast.Assign(targets=[ast.Name(id=name)])
602
+ | ast.AnnAssign(target=ast.Name(id=name), value=ast.expr())
603
+ ):
604
+ counts[name] += 1
605
+ case ast.If(test=ast.Constant(value=bool() as constant)):
606
+ counts += _assigned(stmt.body if constant else stmt.orelse)
607
+ case ast.If(test=test):
608
+ arms: tuple[Counter[str], Counter[str]] = (_assigned(stmt.body), _assigned(stmt.orelse))
609
+ counts += (arms[0] | arms[1]) if _names_any(test, _DECIDED) else (arms[0] + arms[1])
610
+ case ast.Try() | ast.TryStar():
611
+ counts += _assigned(child_statements(stmt))
612
+ case _:
613
+ pass
614
+ return counts
615
+
616
+
617
+ def _names_any(value: ast.expr, names: frozenset[str]) -> bool:
618
+ return any(node_name(node) in names for node in ast.walk(value))
619
+
620
+
621
+ def _called(tree: ast.Module, names: Mapping[str, Origin]) -> frozenset[str]:
622
+ """Find what the module calls through its top-level names: `f()`, `u.f()`, `pkg.util.f()`.
623
+
624
+ Returns:
625
+ Each callee, as written.
626
+
627
+ """
628
+ callees: Iterator[str | None] = (
629
+ dotted(node.func) for node in cast("list[ast.Call]", of_type(tree, ast.Call))
630
+ )
631
+ return frozenset(callee for callee in callees if callee is not None and callee.partition(".")[0] in names)
632
+
633
+
634
+ def _passed(tree: ast.Module, names: Mapping[str, Origin]) -> frozenset[str]:
635
+ """Find what the module passes as an argument through its top-level names: `np.float64`, `Row`.
636
+
637
+ Returns:
638
+ Each, as written.
639
+
640
+ """
641
+ arguments: Iterator[str | None] = (
642
+ dotted(argument)
643
+ for node in cast("list[ast.Call]", of_type(tree, ast.Call))
644
+ for argument in (*node.args, *(keyword.value for keyword in node.keywords))
645
+ )
646
+ return frozenset(name for name in arguments if name is not None and name.partition(".")[0] in names)
647
+
648
+
649
+ def _method_calls(tree: ast.Module) -> frozenset[str]:
650
+ """Name the methods the module calls on anything: `astype` in `a.astype(x)`, and a `with`'s `__enter__`.
651
+
652
+ Returns:
653
+ Them.
654
+
655
+ """
656
+ entering: bool = any(
657
+ item.optional_vars is not None
658
+ for node in cast("list[ast.With]", of_type(tree, ast.With))
659
+ for item in node.items
660
+ )
661
+ return frozenset(
662
+ node.func.attr
663
+ for node in cast("list[ast.Call]", of_type(tree, ast.Call))
664
+ if isinstance(node.func, ast.Attribute)
665
+ ) | (frozenset({entered.ENTER}) if entering else frozenset[str]())
666
+
667
+
668
+ def _attributes(tree: ast.Module) -> frozenset[str]:
669
+ """Name the attributes the module takes of anything: `x` in `a.x`, `astype` in `a.astype(x)`.
670
+
671
+ And those its statements take without naming them: a call's `__call__`, a `with`'s `__enter__`;
672
+ and the literal keys it reads of anything (`d["x"]`, `d.get("x")`), as a `TypedDict`'s are held
673
+ (see `keys.key`, `keys.optional`), and the methods it awaits a call of, as an `async def`'s are held.
674
+
675
+ Returns:
676
+ Them: all of another file's class's members it can use.
677
+
678
+ """
679
+ named: frozenset[str] = frozenset(
680
+ node.attr for node in cast("list[ast.Attribute]", of_type(tree, ast.Attribute))
681
+ )
682
+ indexes: list[ast.expr] = [
683
+ node.slice for node in cast("list[ast.Subscript]", of_type(tree, ast.Subscript))
684
+ ]
685
+ indexes.extend(
686
+ node.args[0]
687
+ for node in cast("list[ast.Call]", of_type(tree, ast.Call))
688
+ if node.args and isinstance(node.func, ast.Attribute) and node.func.attr == _GET
689
+ )
690
+ awaited: frozenset[str] = frozenset(
691
+ AWAIT + node.value.func.attr
692
+ for node in cast("list[ast.Await]", of_type(tree, ast.Await))
693
+ if isinstance(node.value, ast.Call) and isinstance(node.value.func, ast.Attribute)
694
+ )
695
+ keys: frozenset[str] = frozenset(
696
+ spelled(index.value)
697
+ for index in indexes
698
+ if isinstance(index, ast.Constant) and isinstance(index.value, str)
699
+ for spelled in (key, optional)
700
+ )
701
+ return named | keys | awaited | _quoted(tree) | _UNNAMED
702
+
703
+
704
+ def _quoted(tree: ast.Module) -> frozenset[str]:
705
+ """Name the attributes the module's quoted annotations take: `Study` in `study: "optuna.Study"`.
706
+
707
+ Returns:
708
+ Them.
709
+
710
+ """
711
+ functions: list[ast.FunctionDef | ast.AsyncFunctionDef] = cast(
712
+ "list[ast.FunctionDef | ast.AsyncFunctionDef]",
713
+ of_type(tree, ast.FunctionDef, ast.AsyncFunctionDef),
714
+ )
715
+ annotations: list[ast.expr | None] = [
716
+ *(node.annotation for node in cast("list[ast.arg]", of_type(tree, ast.arg))),
717
+ *(node.annotation for node in cast("list[ast.AnnAssign]", of_type(tree, ast.AnnAssign))),
718
+ *(node.returns for node in functions),
719
+ ]
720
+ return frozenset(
721
+ part
722
+ for annotation in annotations
723
+ if annotation is not None
724
+ for node in ast.walk(annotation)
725
+ if isinstance(node, ast.Constant) and isinstance(node.value, str)
726
+ for dotted in cast("list[str]", _DOTTED.findall(node.value))
727
+ for part in dotted.split(".")[1:]
728
+ )
729
+
730
+
731
+ def _source(path: Path) -> str | None:
732
+ """Read a module's text.
733
+
734
+ Returns:
735
+ It, or `None` if it can't be read, or decoded (`SyntaxError`: an unknown encoding).
736
+
737
+ """
738
+ try:
739
+ return parsed.text(path.read_bytes())
740
+ except (OSError, SyntaxError, ValueError): # UnicodeDecodeError is a ValueError
741
+ return None