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,463 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Checking files in `order.plan`'s order: each after the modules whose unannotated functions it calls.
3
+
4
+ A component (files calling each other's functions) is checked once all it comes after have settled,
5
+ knowing what their functions return; then, while their `return`s change what its files import, its
6
+ files are checked again, up to `CYCLE_ROUNDS` times, before it settles. With worker processes, a
7
+ component starts as soon as it can, rather than in lockstep with the others.
8
+ """
9
+
10
+ import pickle
11
+ from collections.abc import Callable, Mapping, Sequence
12
+ from dataclasses import dataclass, field
13
+ from pathlib import Path
14
+ from typing import TYPE_CHECKING, Final, NamedTuple, cast
15
+
16
+ from constricter.cli import collecting
17
+ from constricter.cli.runs import CoverageRun, FileRun
18
+ from constricter.cli.workers import Again, Answering, Asked, File, Told, Workers, check_share, first_done
19
+ from constricter.empty import Empty
20
+ from constricter.fix.core.known import Guarded, Hints, Outside, Passed, Returns
21
+ from constricter.fix.index import (
22
+ awaits,
23
+ beyond,
24
+ callers,
25
+ crossed,
26
+ decorated,
27
+ fixtures,
28
+ linked,
29
+ loose,
30
+ managed,
31
+ offers,
32
+ order,
33
+ own_methods,
34
+ own_types,
35
+ plain,
36
+ project,
37
+ sides,
38
+ taken,
39
+ tuples,
40
+ )
41
+ from constricter.fix.index.modules import Memo
42
+ from constricter.fix.index.stubs import stubbed
43
+
44
+ if TYPE_CHECKING:
45
+ from constricter.fix.core.signatures import ReadSignature
46
+
47
+ CYCLE_ROUNDS: Final = 3 # how many times to check again files calling each other's functions
48
+
49
+
50
+ def outside(modules: project.Index, path: Path, hinted: Mapping[Path, tuple[Hints, ...]]) -> Outside:
51
+ """Find what's known of a file from outside it: what it imports from the others, and its hints.
52
+
53
+ Returns:
54
+ It.
55
+
56
+ """
57
+ seeded: dict[str, Guarded] = {}
58
+ seeds: dict[str, Passed] = fixtures.visible(modules, path, seeded)
59
+ imported: project.Imported = crossed.imported(
60
+ modules,
61
+ path,
62
+ (seeded, fixtures.attributes(seeds)),
63
+ )
64
+ side_calls: dict[str, str]
65
+ guarded: dict[str, Guarded]
66
+ side_calls, guarded = sides.calls(modules, path, imported.guarded)
67
+ methods: stubbed.Methods = stubbed.methods(modules, path, guarded)
68
+ hints: tuple[Hints, ...] = offers.vetted(modules, path, hinted.get(path, ()))
69
+ # What only a hint can name: the classes the file imports for type checking alone.
70
+ own: offers.Own = offers.own(modules, path) if hints else offers.Own()
71
+ overloaded: dict[str, tuple[ReadSignature, ...]] = {
72
+ **stubbed.overloaded(modules, path, guarded),
73
+ **methods.signatures,
74
+ **own_methods.overloaded(modules, path, guarded),
75
+ }
76
+ return Outside(
77
+ calls={**imported.calls, **side_calls, **decorated.own(modules, path)},
78
+ # With those of the classes the overloaded methods' returns import.
79
+ classes=crossed.guarded_classes(modules, path, imported, guarded),
80
+ hints=hints,
81
+ type_vars=project.type_vars(modules, path),
82
+ returned=awaits.returned(
83
+ modules,
84
+ path,
85
+ guarded,
86
+ loose.returned(modules, path, guarded, imported.returned),
87
+ ),
88
+ guarded=guarded,
89
+ generics=imported.generics | own.generics,
90
+ callees=callers.callees(modules, path),
91
+ parameters=callers.own_parameters(modules, path),
92
+ overloaded=overloaded,
93
+ installed_classes=stubbed.classes(modules, path),
94
+ installed_parameters=methods.parameters,
95
+ installed_lineage=own_types.lineages(modules, path, guarded, methods.lineage),
96
+ installed_aliases=methods.aliases,
97
+ checking=own.guarded,
98
+ plain=plain.classes(modules, path),
99
+ members={**imported.members, **plain.members(modules, path)},
100
+ same=linked.same(modules, path, {**offers.imported(path, hints), **guarded}),
101
+ partial=imported.partial,
102
+ tuples=tuples.fields(modules, path, guarded),
103
+ fixtures=seeds,
104
+ beyond=beyond.library_bases(modules, path),
105
+ awaits=awaits.calls(modules, path, guarded),
106
+ values=linked.values(modules, path),
107
+ untyped=linked.untyped(modules, path),
108
+ unions=tuples.unions(modules, path, guarded),
109
+ managers=managed.calls(modules, path, guarded),
110
+ vouched=decorated.vouched(modules, path),
111
+ takers=taken.calls(modules, path, guarded),
112
+ emptied=beyond.emptied(modules, path),
113
+ )
114
+
115
+
116
+ class Rounds(NamedTuple):
117
+ """How a check stands to the files' earlier ones.
118
+
119
+ `merge` joins a file's two rounds' runs (`None`: the later one's stands). `kept`: the last runs
120
+ of those whose text is as it was then, by index: each is checked only if it knows anything else
121
+ from outside now, else that run is its own.
122
+ """
123
+
124
+ merge: Callable[[FileRun, FileRun], FileRun] | None = None
125
+ kept: Mapping[int, FileRun] = Empty()
126
+
127
+
128
+ class _Planned:
129
+ """What checking the files in order has found so far, and the index as it grows."""
130
+
131
+ def __init__(
132
+ self,
133
+ paths: Sequence[Path],
134
+ modules: project.Index,
135
+ hinted: Mapping[Path, tuple[Hints, ...]],
136
+ rounds: Rounds,
137
+ ) -> None:
138
+ """Plan checking `paths` with `modules` and their hints, after `rounds`."""
139
+ self.paths: Sequence[Path] = paths
140
+ self.modules: project.Index = modules
141
+ self.hinted: Mapping[Path, tuple[Hints, ...]] = hinted
142
+ self.plan: order.Plan = order.plan(modules, paths)
143
+ self.runs: dict[int, FileRun] = {}
144
+ self.earlier: Rounds = rounds
145
+ self.given: dict[int, Outside] = {} # what each file was last checked knowing
146
+ self.rounds: list[int] = [0] * len(self.plan.components)
147
+ # Each file's component.
148
+ self.of: dict[int, int] = {
149
+ at: part for part, files in enumerate(self.plan.components) for at in files
150
+ }
151
+
152
+ def start(self, component: int) -> dict[int, Outside]:
153
+ """Find what the component's files are to be checked knowing, now that it can start.
154
+
155
+ Returns:
156
+ Each file's; not a kept one's (see `checked`) that knows what it was last checked knowing.
157
+
158
+ """
159
+ given: dict[int, Outside] = {
160
+ at: outside(self.modules, self.paths[at], self.hinted) for at in self.plan.components[component]
161
+ }
162
+ self.given.update(given)
163
+ return {
164
+ at: known
165
+ for at, known in given.items()
166
+ if at not in self.earlier.kept or _WORKER.given.get(self.paths[at]) != known
167
+ }
168
+
169
+ def record(self, component: int, found: Mapping[int, FileRun]) -> dict[str, Returns]:
170
+ """Record what a round of the component's files found, and what its functions return.
171
+
172
+ Returns:
173
+ What each checked module's functions return now, by its name.
174
+
175
+ """
176
+ merge: Callable[[FileRun, FileRun], FileRun] | None = self.earlier.merge
177
+ at: int
178
+ run: FileRun
179
+ for at, run in found.items():
180
+ self.runs[at] = merge(self.runs[at], run) if merge and at in self.runs else run
181
+ returned: dict[str, Returns] = {
182
+ self.plan.names[at]: run.returned
183
+ for at, run in found.items()
184
+ if at in self.plan.names and not isinstance(run, CoverageRun)
185
+ }
186
+ self.modules = linked.with_returned(self.modules, returned)
187
+ self.rounds[component] += 1
188
+ return returned
189
+
190
+ def settled(self, component: int) -> bool:
191
+ """Check whether the component has no more rounds to run: one file, or `CYCLE_ROUNDS` done.
192
+
193
+ Returns:
194
+ Whether it is.
195
+
196
+ """
197
+ return len(self.plan.components[component]) == 1 or self.rounds[component] > CYCLE_ROUNDS
198
+
199
+ def settle(self, component: int, found: Mapping[int, FileRun]) -> dict[int, Outside]:
200
+ """Record a round (see `record`), and find what to check again, in this process.
201
+
202
+ Returns:
203
+ Those of its files to check again, each with what it's to know now; none once it's settled.
204
+
205
+ """
206
+ _ = self.record(component, found)
207
+ if self.settled(component):
208
+ return {}
209
+ again: dict[int, Outside] = {
210
+ at: outside(self.modules, self.paths[at], self.hinted) for at in self.plan.components[component]
211
+ }
212
+ again = {at: known for at, known in again.items() if known.returned != self.given[at].returned}
213
+ self.given.update(again)
214
+ return again
215
+
216
+
217
+ def checked(
218
+ paths: Sequence[Path],
219
+ check: Callable[[Path, Outside], FileRun],
220
+ pool: Workers | None,
221
+ known: tuple[project.Index, Mapping[Path, tuple[Hints, ...]]],
222
+ rounds: Rounds,
223
+ ) -> tuple[list[FileRun], project.Index]:
224
+ """Check `paths` in order (see `order.plan`), in `pool`'s workers or this process.
225
+
226
+ `known`: the index and each file's hints; `rounds`: how this check stands to earlier ones.
227
+
228
+ Returns:
229
+ What each file found, and the index, with what their functions return.
230
+
231
+ """
232
+ planned: _Planned = _Planned(paths, known[0], known[1], rounds)
233
+ if pool is None:
234
+ component: int
235
+ for component in range(len(planned.plan.components)):
236
+ _in_process(planned, check, component, planned.start(component))
237
+ else:
238
+ _Pooled(planned, check, pool).run()
239
+ runs: list[FileRun] = [planned.runs.get(at) or planned.earlier.kept[at] for at in range(len(paths))]
240
+ return runs, planned.modules
241
+
242
+
243
+ def _in_process(
244
+ planned: _Planned,
245
+ check: Callable[[Path, Outside], FileRun],
246
+ component: int,
247
+ todo: Mapping[int, Outside],
248
+ ) -> None:
249
+ """Check a component's files here (`todo`: each with what it's to know), round by round."""
250
+ if todo:
251
+ runs: list[FileRun] = check_share(check, [(planned.paths[at], given) for at, given in todo.items()])
252
+ _WORKER.given.update((planned.paths[at], given) for at, given in todo.items())
253
+ _in_process(planned, check, component, planned.settle(component, dict(zip(todo, runs, strict=True))))
254
+
255
+
256
+ @dataclass
257
+ class _Worker:
258
+ """What a worker process knows: the index it was sent, and what each of its files last knew.
259
+
260
+ A run checking alone keeps the latter here too: its files' checks, round after round.
261
+ """
262
+
263
+ modules: project.Index = field(default_factory=lambda: project.Index({}, []))
264
+ given: dict[Path, Outside] = field(default_factory=dict[Path, Outside])
265
+
266
+
267
+ @dataclass
268
+ class _News:
269
+ """What each recorded round's modules return, and how much of it each worker has been told."""
270
+
271
+ returned: list[dict[str, Returns]]
272
+ told: list[int]
273
+
274
+ def since(self, worker: int) -> dict[str, Returns]:
275
+ """Gather what a worker hasn't been told yet, which it's told now.
276
+
277
+ Returns:
278
+ Each such module's returns, the latest of each.
279
+
280
+ """
281
+ news: dict[str, Returns] = {}
282
+ last: int = self.told[worker]
283
+ each: dict[str, Returns]
284
+ for each in self.returned[last:]:
285
+ news.update(each)
286
+ self.told[worker] = len(self.returned)
287
+ return news
288
+
289
+
290
+ _WORKER: Final = _Worker()
291
+
292
+
293
+ def sent(modules: project.Index) -> bytes:
294
+ """Pickle the index for the workers: once, for them all, each then reading it at the same time.
295
+
296
+ Returns:
297
+ It, without what it keeps worked out, which each worker works out for itself.
298
+
299
+ """
300
+ return pickle.dumps(modules._replace(memo=None), pickle.HIGHEST_PROTOCOL)
301
+
302
+
303
+ def know(index: Told) -> None:
304
+ """Take the index (see `sent`), in a worker process: what it works its files' `Outside` out from."""
305
+ _WORKER.modules = cast("project.Index", pickle.loads(index))._replace(memo=Memo())
306
+
307
+
308
+ def forget() -> None:
309
+ """Forget what each file was last checked knowing, as a run starts: its workers are new."""
310
+ _WORKER.given.clear()
311
+
312
+
313
+ def check_asked(check: Callable[[Path, Outside], FileRun], asked: Asked) -> list[FileRun | None]:
314
+ """Check a worker's files, each knowing what the worker's index says of it now.
315
+
316
+ Returns:
317
+ What each found; `None` for one left unchecked, knowing what it last did (see `Again`).
318
+
319
+ """
320
+ files: Sequence[File]
321
+ returned: Mapping[str, Returns]
322
+ again: Again
323
+ files, returned, again = asked
324
+ _WORKER.modules = linked.with_returned(_WORKER.modules, returned)
325
+ found: list[FileRun | None] = []
326
+ path: Path
327
+ hints: tuple[Hints, ...]
328
+ for path, hints in files:
329
+ known: Outside = outside(_WORKER.modules, path, {path: hints} if hints else {})
330
+ last: Outside | None = _WORKER.given.get(path)
331
+ if last is not None and (
332
+ (again is Again.RETURNED and known.returned == last.returned)
333
+ or (again is Again.OUTSIDE and known == last)
334
+ ):
335
+ found.append(None)
336
+ continue
337
+ _WORKER.given[path] = known
338
+ found.append(check(path, known))
339
+ collecting.sweep()
340
+ return found
341
+
342
+
343
+ class _Pooled:
344
+ """Checking files in the workers, in `order.plan`'s order: each component once all it follows settle.
345
+
346
+ Each worker has the index, and works out what its files know from outside as it reaches them:
347
+ it's told what the checked modules' functions return since it was last asked (`told`).
348
+ """
349
+
350
+ def __init__(self, planned: _Planned, check: Callable[[Path, Outside], FileRun], pool: Workers) -> None:
351
+ """Plan the order: what each component waits for, and what follows it."""
352
+ self.planned: _Planned = planned
353
+ self.check: Callable[[Path, Outside], FileRun] = check
354
+ self.pool: Workers = pool
355
+ self.waiting: list[int] = [len(after) for after in planned.plan.after] # components still to settle
356
+ self.followers: list[list[int]] = [[] for _ in planned.plan.components]
357
+ component: int
358
+ after: frozenset[int]
359
+ for component, after in enumerate(planned.plan.after):
360
+ callee: int
361
+ for callee in after:
362
+ self.followers[callee].append(component)
363
+ self.left: list[int] = [0] * len(planned.plan.components) # each component's files being checked
364
+ self.found: dict[int, dict[int, FileRun]] = {} # what they've found so far
365
+ self.active: dict[Answering, list[int]] = {} # what each worker is checking
366
+ self.news: _News = _News([], [0] * len(pool.shares))
367
+
368
+ def run(self) -> None:
369
+ """Check every file: start what waits for nothing, then whatever each answer lets start."""
370
+ self.pool.tell(know, sent(self.planned.modules))
371
+ self.submit(
372
+ [
373
+ at
374
+ for component, count in enumerate(self.waiting)
375
+ if not count
376
+ for at in self.planned.plan.components[component]
377
+ ],
378
+ again=False,
379
+ )
380
+ done: set[Answering]
381
+ for done in iter(self._done, None):
382
+ first: list[int] = []
383
+ again: list[int] = []
384
+ future: Answering
385
+ for future in done:
386
+ at: int
387
+ run: FileRun | None
388
+ for at, run in zip(self.active.pop(future), future.result(), strict=True):
389
+ todo: tuple[list[int], bool] = self._answered(at, run)
390
+ (again if todo[1] else first).extend(todo[0])
391
+ self.submit(first, again=False)
392
+ self.submit(again, again=True)
393
+
394
+ def _done(self) -> set[Answering] | None:
395
+ """Wait for a worker's answer.
396
+
397
+ Returns:
398
+ Those that have answered; `None` once none is checking anything.
399
+
400
+ """
401
+ return first_done(self.active) if self.active else None
402
+
403
+ def submit(self, files: Sequence[int], *, again: bool) -> None:
404
+ """Start checking `files` (by index), each by its worker.
405
+
406
+ `again`: only those whose imports return something else now. Otherwise a kept file (see
407
+ `checked`) only if it knows anything else from outside now.
408
+ """
409
+ if not files:
410
+ return
411
+ hinted: Mapping[Path, tuple[Hints, ...]] = self.planned.hinted
412
+ paths: Sequence[Path] = self.planned.paths
413
+ kept: Mapping[int, FileRun] = self.planned.earlier.kept
414
+ wanted: frozenset[int] = frozenset(files)
415
+ worker: int
416
+ share: list[int]
417
+ for worker, share in enumerate(self.pool.shares):
418
+ mine: list[int] = [at for at in share if at in wanted]
419
+ each: list[int]
420
+ how: Again
421
+ for each, how in (
422
+ [(mine, Again.RETURNED)]
423
+ if again
424
+ else [
425
+ ([at for at in mine if at not in kept], Again.NO),
426
+ ([at for at in mine if at in kept], Again.OUTSIDE),
427
+ ]
428
+ ):
429
+ if each:
430
+ given: list[File] = [(paths[at], hinted.get(paths[at], ())) for at in each]
431
+ asked: Asked = (given, self.news.since(worker), how)
432
+ self.active[self.pool.ask(worker, check_asked, self.check, asked)] = each
433
+ at: int
434
+ for at in files:
435
+ self.left[self.planned.of[at]] += 1
436
+
437
+ def _answered(self, at: int, run: FileRun | None) -> tuple[list[int], bool]:
438
+ """Record what file `at` found (`None`: left unchecked); its component's round done, settle it.
439
+
440
+ Returns:
441
+ The files to check next, and whether again (the component's, where their imports return
442
+ anew) or for the first time (those of the components it let start).
443
+
444
+ """
445
+ component: int = self.planned.of[at]
446
+ if run is not None:
447
+ self.found.setdefault(component, {})[at] = run
448
+ self.left[component] -= 1
449
+ if self.left[component]:
450
+ return [], False
451
+ found: dict[int, FileRun]
452
+ # None found: every file's imports return what they did, and the component's settled.
453
+ if found := self.found.pop(component, {}):
454
+ self.news.returned.append(self.planned.record(component, found))
455
+ if not self.planned.settled(component):
456
+ return list(self.planned.plan.components[component]), True
457
+ started: list[int] = []
458
+ follower: int
459
+ for follower in self.followers[component]:
460
+ self.waiting[follower] -= 1
461
+ if not self.waiting[follower]:
462
+ started.extend(self.planned.plan.components[follower])
463
+ return started, False