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,425 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Installed packages' declared types, for `--fix` to type calls into them as it does checked files'.
3
+
4
+ `with_installed` adds to the cross-file index the modules the checked files import from installed
5
+ packages that declare their types: a stub package (`numpy-stubs`), a `py.typed` package's stubs
6
+ (`.pyi`) or source, or a lone stub module. Each is found on the search path (`search_path`: the
7
+ running interpreter's, and the active virtual environment's) as the import system would, and read
8
+ as `modules.read` reads a checked file, but never checked or fixed; the modules they import from
9
+ their own package (re-exports) are read too, up to `LIMIT` modules in all. Each module read is
10
+ cached (`cached`) under the user's cache directory, by its file's path, size and modification time
11
+ and constricter's version and reading code: a package upgraded, or a changed constricter, reads it
12
+ again. Each version and reading code has its own part of the cache, swept as it's written to, a day
13
+ apart at most (`swept`): what's unread for a month goes, and what's over its cap.
14
+ """
15
+
16
+ import contextlib
17
+ import hashlib
18
+ import os
19
+ import pickle # the cache: written and read by constricter alone, in the user's own directory
20
+ import sys
21
+ import sysconfig
22
+ import tempfile
23
+ import time
24
+ from collections.abc import Iterable, Mapping, Sequence
25
+ from dataclasses import replace
26
+ from functools import lru_cache
27
+ from pathlib import Path
28
+ from typing import Final, cast
29
+
30
+ from constricter import __version__
31
+ from constricter.fix.index.modules import STUB, SUFFIX, Index, Memo, Module, read
32
+
33
+ LIMIT: Final = 2000 # the most installed modules one run reads
34
+ _HOPS: Final = 5 # re-exports followed to a type variable
35
+ _STUBS: Final = "-stubs"
36
+ _TYPED: Final = "py.typed"
37
+ _PACKAGE: Final = "__init__"
38
+ _UNDERSCORE: Final = "_"
39
+ _VIRTUAL_ENV: Final = "VIRTUAL_ENV"
40
+ _STDLIB: Final = sys.stdlib_module_names
41
+ _CACHE_HOME: Final = "XDG_CACHE_HOME"
42
+ _LOCAL_APP_DATA: Final = "LOCALAPPDATA" # Windows'
43
+ _CACHE: Final = "constricter"
44
+ _ENTRIES: Final = "*.pickle"
45
+ _SWEPT: Final = ".swept" # in a build's part of the cache: touched when that build last swept it
46
+ _DAY: Final = 86400.0 # the seconds between a build's sweeps
47
+ _KEPT: Final = 30 * _DAY # the seconds an entry is kept unread, and another build's part unswept
48
+ _CAP: Final = 256 << 20 # the bytes a build's entries may take: past it, the longest unread go
49
+
50
+
51
+ def search_path() -> tuple[Path, ...]:
52
+ """List the directories installed packages are found in: the interpreter's, then the active environment's.
53
+
54
+ The environment in `VIRTUAL_ENV`, where the checked code runs, even when constricter runs in
55
+ another (pre-commit's, pipx's).
56
+
57
+ Returns:
58
+ Them, the standard library's own left out.
59
+
60
+ """
61
+ stdlib: set[Path] = {Path(sysconfig.get_paths()[key]).resolve() for key in ("stdlib", "platstdlib")}
62
+ active: str | None = os.environ.get(_VIRTUAL_ENV)
63
+ extra: list[str] = (
64
+ [] if active is None else [str(p) for p in sorted(Path(active).glob("lib/python3*/site-packages"))]
65
+ )
66
+ extra += [] if active is None else [str(Path(active) / "Lib" / "site-packages")] # Windows
67
+ found: dict[Path, None] = {}
68
+ entry: str
69
+ for entry in (*sys.path, *extra):
70
+ path: Path = Path(entry or ".").resolve()
71
+ if path.is_dir() and path not in stdlib:
72
+ found[path] = None
73
+ return tuple(found)
74
+
75
+
76
+ def with_installed(catalog: Index, search: Sequence[Path]) -> Index:
77
+ """Add the installed modules the checked files import to `catalog` (see the module docstring).
78
+
79
+ Returns:
80
+ The index, with them.
81
+
82
+ """
83
+ wanted: list[str] = _imported(catalog.modules.values()) # grows as it's walked
84
+ found: dict[str, Module] = {}
85
+ seen: set[str] = set(catalog.modules)
86
+ name: str
87
+ for name in wanted:
88
+ if len(found) >= LIMIT:
89
+ break
90
+ path: Path | None
91
+ module: Module | None
92
+ if (
93
+ name in seen
94
+ or (path := locate(name, tuple(search))) is None
95
+ or (module := cached(path, name)) is None
96
+ ):
97
+ seen.add(name)
98
+ continue
99
+ seen.add(name)
100
+ found[name] = module
101
+ top: str = name.partition(".")[0].lstrip(_UNDERSCORE)
102
+ # Walked as it grows: the modules this one re-exports from, its package's private twin's
103
+ # too (`pytest`'s, from `_pytest`).
104
+ wanted.extend( # ruff: ignore[loop-iterator-mutation]
105
+ other for other in _imported([module]) if other.partition(".")[0].lstrip(_UNDERSCORE) == top
106
+ )
107
+ if not found:
108
+ return catalog
109
+ every: dict[str, Module] = {**catalog.modules, **found}
110
+ every.update((name, _generics(every, module)) for name, module in found.items())
111
+ # The checked files' repeated names kept; what was worked out of the modules there were, not.
112
+ return catalog._replace(modules=every, names=sorted(every), memo=Memo())
113
+
114
+
115
+ def _generics(modules: Mapping[str, Module], module: Module) -> Module:
116
+ """Drop the classes a subscripted base made look generic whose bases pass no type variable.
117
+
118
+ `class float64(floating[_64Bit])` binds every parameter it inherits: it's written bare.
119
+
120
+ Returns:
121
+ The module, its `generics` corrected.
122
+
123
+ """
124
+ bases: Mapping[str, frozenset[str]] = {} if module.declared is None else module.declared.bases
125
+ bound: frozenset[str] = frozenset(
126
+ name
127
+ for name, names in bases.items()
128
+ if name in module.generics and not any(_is_type_var(modules, module, found) for found in names)
129
+ )
130
+ return replace(module, generics=module.generics - bound) if bound else module
131
+
132
+
133
+ def _is_type_var(modules: Mapping[str, Module], module: Module, name: str, hops: int = _HOPS) -> bool:
134
+ """Check whether `name` is a type variable in `module`: its own, or one it imports from an indexed module.
135
+
136
+ Returns:
137
+ Whether it is (unknown: no).
138
+
139
+ """
140
+ if name in module.type_vars or (module.declared is not None and name in module.declared.variables):
141
+ return True
142
+ origin: tuple[str, str | None] | None = module.names.get(name) or module.guarded.get(name)
143
+ onward: Module | None = None if origin is None or origin[1] is None else modules.get(origin[0])
144
+ return (
145
+ hops > 0
146
+ and onward is not None
147
+ and onward is not module
148
+ and origin is not None
149
+ and _is_type_var(modules, onward, origin[1] or "", hops - 1)
150
+ )
151
+
152
+
153
+ def cache_directory() -> Path:
154
+ """Find where installed modules' reads are cached: `$XDG_CACHE_HOME`, `%LOCALAPPDATA%`, or `~/.cache`.
155
+
156
+ Returns:
157
+ Its `constricter` directory (not made yet).
158
+
159
+ """
160
+ base: str | None = os.environ.get(_CACHE_HOME) or os.environ.get(_LOCAL_APP_DATA)
161
+ return (Path(base) if base else Path.home() / ".cache") / _CACHE / "installed"
162
+
163
+
164
+ def cached(path: Path, name: str) -> Module | None:
165
+ """Read an installed module (see `modules.read`), from the cache if it's there and still current.
166
+
167
+ A cache that can't be read or written (a read-only home, a corrupt entry) is passed over: the
168
+ module is read from its file.
169
+
170
+ Returns:
171
+ Its module, or `None` if it can't be read or parsed.
172
+
173
+ """
174
+ try:
175
+ stat: os.stat_result = path.stat()
176
+ except OSError:
177
+ return None
178
+ key: str = f"{name}\0{path.resolve()}\0{stat.st_size}\0{stat.st_mtime_ns}"
179
+ entry: Path = _own() / f"{hashlib.sha256(key.encode()).hexdigest()}.pickle"
180
+ loaded: Module | None
181
+ if (loaded := _load(entry)) is not None:
182
+ return loaded
183
+ module: Module | None
184
+ if (module := read(path, name)) is not None:
185
+ _store(entry, module)
186
+ return module
187
+
188
+
189
+ @lru_cache(maxsize=1)
190
+ def _code() -> str:
191
+ """Fingerprint the code that reads a module and shapes what's cached (`constricter.fix`'s).
192
+
193
+ A development build keeps its version as its modules' fields change: an entry pickled by another
194
+ unpickles with them shifted.
195
+
196
+ Returns:
197
+ Its sources' digest.
198
+
199
+ """
200
+ sources: list[Path] = sorted(Path(__file__).parents[1].rglob(f"*{SUFFIX}"))
201
+ return hashlib.sha256(b"\0".join(source.read_bytes() for source in sources)).hexdigest()
202
+
203
+
204
+ def _own() -> Path:
205
+ """Find this build's part of the cache: another version's or build's entries unpickle wrong.
206
+
207
+ Returns:
208
+ Its directory, named by the version and `_code` (not made yet).
209
+
210
+ """
211
+ return cache_directory() / hashlib.sha256(f"{__version__}\0{_code()}".encode()).hexdigest()[:16]
212
+
213
+
214
+ def _load(entry: Path) -> Module | None:
215
+ """Read a cache entry, and mark one read after half its keep as read now.
216
+
217
+ Returns:
218
+ Its module, or `None` if there's none, or it can't be read.
219
+
220
+ """
221
+ with contextlib.suppress(OSError, pickle.UnpicklingError, EOFError, AttributeError, TypeError):
222
+ module: Module | None = _module(cast("object", pickle.loads(entry.read_bytes())))
223
+ if _age(entry, time.time()) > _KEPT / 2:
224
+ os.utime(entry)
225
+ return module
226
+ return None
227
+
228
+
229
+ def _age(path: Path, now: float) -> float:
230
+ """Find how long ago `path` was written or marked read.
231
+
232
+ Returns:
233
+ The seconds; none for one that can't be read, which is then kept.
234
+
235
+ """
236
+ try:
237
+ return now - path.stat().st_mtime
238
+ except OSError:
239
+ return 0.0
240
+
241
+
242
+ def _dropped(part: Path) -> None:
243
+ """Remove a build's part of the cache: its entries, what a stopped write left, and its directory."""
244
+ with contextlib.suppress(OSError):
245
+ each: Path
246
+ for each in (*part.glob(_ENTRIES), *part.glob("*.tmp"), part / _SWEPT):
247
+ each.unlink(missing_ok=True)
248
+ part.rmdir()
249
+
250
+
251
+ @lru_cache(maxsize=4) # once a process for a cache, as it first writes there
252
+ def swept(own: Path) -> None:
253
+ """Sweep the cache, a day or more after this build last did.
254
+
255
+ The entries no build's part holds (the cache's, before each had one) go, and another build's
256
+ part unswept for `_KEPT`: one still used sweeps, and marks its own. Of this build's entries,
257
+ those unread for `_KEPT`, then the longest unread until the rest fit `_CAP`.
258
+ """
259
+ now: float = time.time()
260
+ if (own / _SWEPT).exists() and _age(own / _SWEPT, now) < _DAY:
261
+ return
262
+ with contextlib.suppress(OSError):
263
+ (own / _SWEPT).touch()
264
+ other: Path
265
+ for other in own.parent.iterdir():
266
+ if other.match(_ENTRIES):
267
+ other.unlink(missing_ok=True)
268
+ elif other != own and len(other.name) == len(own.name) and _age(other / _SWEPT, now) > _KEPT:
269
+ _dropped(other)
270
+ held: list[tuple[float, int, Path]] = sorted(
271
+ (stat.st_mtime, stat.st_size, entry) for entry in own.glob(_ENTRIES) for stat in (entry.stat(),)
272
+ )
273
+ size: int = sum(taken for _, taken, _ in held)
274
+ written: float
275
+ taken: int
276
+ entry: Path
277
+ for written, taken, entry in held:
278
+ if now - written <= _KEPT and size <= _CAP:
279
+ break
280
+ entry.unlink(missing_ok=True)
281
+ size -= taken
282
+
283
+
284
+ def _module(found: object) -> Module | None:
285
+ return found if isinstance(found, Module) else None
286
+
287
+
288
+ def _store(entry: Path, module: Module) -> None:
289
+ """Write a module's read to its cache entry, whole or not at all (another run may be reading it)."""
290
+ with contextlib.suppress(OSError):
291
+ entry.parent.mkdir(parents=True, exist_ok=True)
292
+ handle: int
293
+ temporary: str
294
+ handle, temporary = tempfile.mkstemp(dir=entry.parent, suffix=".tmp")
295
+ os.close(handle)
296
+ _ = Path(temporary).write_bytes(pickle.dumps(module))
297
+ _ = Path(temporary).replace(entry)
298
+ swept(entry.parent)
299
+
300
+
301
+ def _imported(found: Iterable[Module]) -> list[str]:
302
+ """List the modules `found` import from outside the standard library, and those they may name.
303
+
304
+ `from pkg import util` may name the module `pkg.util` as well as something `pkg` defines; a call
305
+ through an imported module (`pkg.util.f()` after `import pkg.util`) names each module on its way.
306
+
307
+ Returns:
308
+ Their names, in order, each once.
309
+
310
+ """
311
+ names: dict[str, None] = {}
312
+ module: Module
313
+ for module in found:
314
+ origin: tuple[str, str | None]
315
+ for origin in (*module.names.values(), *module.guarded.values()):
316
+ if origin[0].partition(".")[0] not in _STDLIB and origin[0] != module.name:
317
+ names[origin[0]] = None
318
+ if origin[1] is not None:
319
+ names[f"{origin[0]}.{origin[1]}"] = None
320
+ callee: str
321
+ for callee in module.called:
322
+ head: str
323
+ parts: list[str]
324
+ head, *parts = callee.split(".")
325
+ through: tuple[str, str | None] = module.names[head]
326
+ if through[1] is None and through[0].partition(".")[0] not in _STDLIB:
327
+ names.update(
328
+ dict.fromkeys(".".join([through[0], *parts[:end]]) for end in range(1, len(parts))),
329
+ )
330
+ return list(names)
331
+
332
+
333
+ @lru_cache(maxsize=4096)
334
+ def locate(name: str, search: tuple[Path, ...]) -> Path | None:
335
+ """Find the file an installed module `name` declares its types in, as the import system would.
336
+
337
+ In the first directory of `search` that has its package: its stub package (`pkg-stubs`) first,
338
+ then the package itself if it's typed (`py.typed`), a stub (`.pyi`) before its source; or a lone
339
+ stub module (`mod.pyi`).
340
+
341
+ Returns:
342
+ The file, or `None` if it isn't installed, or doesn't declare its types.
343
+
344
+ """
345
+ top: str
346
+ rest: list[str]
347
+ top, *rest = name.split(".")
348
+ directory: Path
349
+ for directory in search:
350
+ root: Path | None
351
+ stub: bool
352
+ here: bool
353
+ root, stub, here = _package(directory, top)
354
+ if root is not None:
355
+ return _module_file(root.joinpath(*rest))
356
+ if not rest and stub:
357
+ return directory / f"{top}{STUB}"
358
+ if here:
359
+ return None # installed here, untyped: an earlier directory shadows any later one
360
+ return None
361
+
362
+
363
+ def unseen(name: str) -> Module | None:
364
+ """Read an installed module that declares no types, for its classes' bases alone.
365
+
366
+ Never part of the index: nothing it declares types a call, as no type checker reads it. What
367
+ its classes inherit from still says where a class under one ends (see `constricter.fix.index.beyond`).
368
+
369
+ Returns:
370
+ Its module, read from its source as `cached` reads one; `None` for the standard library's, or
371
+ one that isn't installed.
372
+
373
+ """
374
+ if not name or name.partition(".")[0] in _STDLIB:
375
+ return None
376
+ return _unseen(name, _searched(tuple(sys.path), os.environ.get(_VIRTUAL_ENV)))
377
+
378
+
379
+ @lru_cache(maxsize=4)
380
+ def _searched(_path: tuple[str, ...], _active: str | None) -> tuple[Path, ...]: # `search_path`, for both
381
+ return search_path()
382
+
383
+
384
+ @lru_cache(maxsize=1024) # asked for each class under one of its classes
385
+ def _unseen(name: str, search: tuple[Path, ...]) -> Module | None:
386
+ paths: Iterable[Path | None] = (
387
+ _module_file(directory.joinpath(*name.split("."))) for directory in search
388
+ )
389
+ path: Path | None = next((path for path in paths if path is not None), None)
390
+ return None if path is None else cached(path, name)
391
+
392
+
393
+ @lru_cache(maxsize=4096) # asked for each of a package's modules: the same answer for them all
394
+ def _package(directory: Path, top: str) -> tuple[Path | None, bool, bool]:
395
+ """Find what `directory` has of the top-level package or module `top`.
396
+
397
+ Returns:
398
+ Where its types are declared (its stub package, else the package itself if it's typed), or
399
+ `None`; whether it has a lone stub module for it; and whether it's installed there at all.
400
+
401
+ """
402
+ roots: list[Path] = [directory / f"{top}{_STUBS}"]
403
+ if (directory / top / _TYPED).is_file():
404
+ roots.append(directory / top)
405
+ return (
406
+ next((root for root in roots if root.is_dir()), None),
407
+ (directory / f"{top}{STUB}").is_file(),
408
+ (directory / top).is_dir() or (directory / f"{top}{SUFFIX}").is_file(),
409
+ )
410
+
411
+
412
+ def _module_file(base: Path) -> Path | None:
413
+ """Find a module's file under a package's root: a package's `__init__`, or a module, stub first.
414
+
415
+ Returns:
416
+ It, or `None`.
417
+
418
+ """
419
+ candidates: tuple[Path, ...] = (
420
+ base / f"{_PACKAGE}{STUB}",
421
+ base / f"{_PACKAGE}{SUFFIX}",
422
+ base.with_name(f"{base.name}{STUB}"),
423
+ base.with_name(f"{base.name}{SUFFIX}"),
424
+ )
425
+ return next((path for path in candidates if path.is_file()), None)
@@ -0,0 +1,200 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """What a name in a stub refers to, across the installed modules the index holds (`Resolver`).
3
+
4
+ An alias, a class or a type variable, where it's declared, a class's bases in order, and what a
5
+ call's name refers to through its module's imports; `stub_signatures.Reader` reads signatures with
6
+ it.
7
+ """
8
+
9
+ import ast
10
+ import builtins
11
+ from collections.abc import Iterator, Mapping
12
+ from typing import Final, TypeAlias
13
+
14
+ from constricter.fix.core.known import Origin
15
+ from constricter.fix.index import project
16
+ from constricter.fix.index.modules import Module
17
+ from constricter.fix.index.stubs import classnames
18
+ from constricter.fix.index.stubs.atoms import (
19
+ BUILTINS_MODULE,
20
+ CLASS,
21
+ VAR,
22
+ Atom,
23
+ Scope,
24
+ atom_path,
25
+ parse_text,
26
+ )
27
+ from constricter.fix.index.stubs.declared import Alias, Class, Declarations, Variable
28
+
29
+ ARG: Final = "arg"
30
+ _BUILTINS: Final = frozenset(dir(builtins))
31
+ ALIAS_DEPTH: Final = 20 # aliases followed before giving up
32
+ _UNFOLLOWED: Final = "?" # a lineage's base that can't be followed
33
+
34
+
35
+ Resolved: TypeAlias = Atom | tuple[Alias, Scope, Origin]
36
+
37
+
38
+ def call_origin(modules: Mapping[str, Module], target: Module, callee: str) -> Origin | None:
39
+ """Find what a call's name refers to: `f`, `np.empty`, `pkg.util.f` through the module's imports.
40
+
41
+ A package's attribute, re-exported from anywhere (`numpy.empty`), not just what it defines.
42
+
43
+ Returns:
44
+ Its origin, or `None`.
45
+
46
+ """
47
+ head: str
48
+ parts: list[str]
49
+ head, *parts = callee.split(".")
50
+ origin: Origin | None = classnames.as_module(modules, target.names.get(head))
51
+ part: str
52
+ for part in parts:
53
+ if origin is None or origin[1] is not None:
54
+ return None
55
+ submodule: str = f"{origin[0]}.{part}"
56
+ origin = (submodule, None) if submodule in modules else (origin[0], part)
57
+ return origin
58
+
59
+
60
+ class Resolver:
61
+ """Resolves a stub's names across `modules` (see the module docstring)."""
62
+
63
+ def __init__(self, modules: Mapping[str, Module]) -> None:
64
+ """Resolve in `modules`."""
65
+ self.modules: Mapping[str, Module] = modules
66
+
67
+ def resolve(self, expr: ast.Name | ast.Attribute, scope: Scope) -> Resolved | None:
68
+ """Resolve a name an annotation uses: a type parameter, a class, an alias, a type variable.
69
+
70
+ Returns:
71
+ It as an atom (`arg`: a parameter an alias's argument binds), or an alias with the scope
72
+ its value is read in; or `None` if it's none of them.
73
+
74
+ """
75
+ origin: Origin | None
76
+ if isinstance(expr, ast.Name) and expr.id in scope.params:
77
+ param: str | tuple[ast.expr, Scope] | None = scope.params[expr.id]
78
+ if isinstance(param, tuple):
79
+ return Atom(ARG, name=ast.unparse(param[0]), scope=param[1])
80
+ return Atom(VAR, name=expr.id, bound=param, scope=scope)
81
+ if (origin := self._origin(expr, scope.module)) is None or origin[1] is None:
82
+ return None
83
+ origin = project.canonical_origin(self.modules, origin)
84
+ defining: Module | None = self.modules.get(origin[0])
85
+ if defining is None or not defining.reads(origin[1]):
86
+ return Atom(CLASS, origin=origin)
87
+ return self._declared(defining, origin)
88
+
89
+ def resolved_base(self, base: str, module: Module) -> Resolved | None:
90
+ """Resolve a base `module`'s class names, as written there (`Base`, `pkg.Base[T]`).
91
+
92
+ Returns:
93
+ What it refers to, or `None` for a base that isn't a name.
94
+
95
+ """
96
+ parsed: ast.expr = parse_text(base)
97
+ parsed = parsed.value if isinstance(parsed, ast.Subscript) else parsed
98
+ return (
99
+ self.resolve(parsed, Scope(module, {}, module.name))
100
+ if isinstance(parsed, ast.Name | ast.Attribute)
101
+ else None
102
+ )
103
+
104
+ def lineage(self, origin: Origin, hops: int) -> Iterator[str]:
105
+ """Walk an installed class and its ancestors, depth first (see `Methods.lineage`).
106
+
107
+ Yields:
108
+ Where each is defined (`numpy.float64`), then `?` if a base can't be followed.
109
+
110
+ """
111
+ yield f"{origin[0]}.{origin[1]}"
112
+ module: Module = self.modules[origin[0]]
113
+ klass: Class | None
114
+ # No class: a checked file's protocol, or one its module defines twice (attrs's `AttrsInstance_`).
115
+ if module.declared is None or (klass := module.declared.classes.get(origin[1] or "")) is None:
116
+ yield _UNFOLLOWED
117
+ return
118
+ base: str
119
+ for base in klass.bases:
120
+ resolved: Resolved | None = self.resolved_base(base, module)
121
+ if not isinstance(resolved, Atom) or resolved.kind != CLASS:
122
+ yield _UNFOLLOWED
123
+ elif resolved.module is None:
124
+ yield atom_path(resolved)
125
+ elif hops < ALIAS_DEPTH:
126
+ yield from self.lineage(resolved.origin, hops + 1)
127
+ else: # a cycle of bases
128
+ yield _UNFOLLOWED
129
+
130
+ def is_class(self, target: Module, name: str) -> bool:
131
+ """Check whether a dotted name the module writes (`np.float64`) is an installed class or its alias.
132
+
133
+ Returns:
134
+ Whether it is.
135
+
136
+ """
137
+ origin: Origin | None = call_origin(self.modules, target, name)
138
+ resolved: Resolved | None = (
139
+ None
140
+ if origin is None or origin[1] is None
141
+ else self._declared_of(project.canonical_origin(self.modules, origin))
142
+ )
143
+ if resolved is not None and not isinstance(resolved, Atom): # an alias: of a class?
144
+ value: ast.expr = parse_text(resolved[0].value)
145
+ base: ast.expr = value.value if isinstance(value, ast.Subscript) else value
146
+ resolved = self.resolve(base, resolved[1]) if isinstance(base, ast.Name | ast.Attribute) else None
147
+ return isinstance(resolved, Atom) and resolved.kind == CLASS and resolved.module is not None
148
+
149
+ def _declared_of(self, origin: Origin) -> Resolved | None:
150
+ defining: Module | None = self.modules.get(origin[0])
151
+ return None if defining is None or not defining.reads(origin[1]) else self._declared(defining, origin)
152
+
153
+ @staticmethod
154
+ def _declared(module: Module, origin: Origin) -> Resolved | None:
155
+ """Read what an installed module defines under `origin`'s name: an alias, a type variable, a class.
156
+
157
+ Returns:
158
+ It, or `None` for anything else (a function, a variable).
159
+
160
+ """
161
+ name: str = origin[1] or ""
162
+ declared: Declarations | None = module.typing
163
+ own: Scope = Scope(module, {}, module.name)
164
+ if declared is not None and name in declared.aliases:
165
+ return declared.aliases[name], own, origin
166
+ variable: Variable | None
167
+ if (variable := None if declared is None else declared.variables.get(name)) is not None:
168
+ return Atom(VAR, name=name, bound=variable.bound, scope=own, constrained=variable.constrained)
169
+ if (
170
+ name in module.classes
171
+ or name in module.generics
172
+ or (declared is not None and name in declared.protocols)
173
+ ):
174
+ return Atom(CLASS, origin=origin, module=module)
175
+ return None
176
+
177
+ def _origin(self, expr: ast.Name | ast.Attribute, module: Module) -> Origin | None:
178
+ """Find what a (dotted) name refers to in `module`: an import, its own, or a builtin.
179
+
180
+ Returns:
181
+ Its origin (a module's: `(name, None)`), or `None`.
182
+
183
+ """
184
+ if isinstance(expr, ast.Name):
185
+ declared: Declarations | None = module.typing
186
+ return (
187
+ ((module.name, expr.id) if declared is not None and expr.id in declared.aliases else None)
188
+ or module.names.get(expr.id)
189
+ or module.guarded.get(expr.id)
190
+ or ((BUILTINS_MODULE, expr.id) if expr.id in _BUILTINS else None)
191
+ )
192
+ if not isinstance(expr.value, ast.Name | ast.Attribute):
193
+ return None
194
+ base: Origin | None
195
+ if (base := self._origin(expr.value, module)) is None:
196
+ return None
197
+ if base[1] is None: # a module's attribute, or its submodule
198
+ submodule: str = f"{base[0]}.{expr.attr}"
199
+ return (submodule, None) if submodule in self.modules else (base[0], expr.attr)
200
+ return None