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,328 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """A check's rounds: every file, then again each one `--fix` changed in a way a second check sees.
3
+
4
+ Each round knows the index of what the files offer each other, the type checker's hints with
5
+ `--infer-with`, and the runs kept from the last.
6
+ """
7
+
8
+ import contextlib
9
+ from collections.abc import Callable, Mapping, Sequence
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING, Final, NamedTuple, TypeAlias, cast
12
+
13
+ from constricter import notebook
14
+ from constricter.cli import collecting, schedule, traced
15
+ from constricter.cli.options import Guesses, Mode, Options
16
+ from constricter.cli.paths import STDIN
17
+ from constricter.cli.reading import read_text
18
+ from constricter.cli.runs import CheckRun, FileRun, merged
19
+ from constricter.cli.workers import Workers, running, started
20
+ from constricter.empty import Empty
21
+ from constricter.fix.core.known import Callee, Hints, Outside
22
+ from constricter.fix.index import callers, decorated, loose, plain, project
23
+ from constricter.fix.index.stubs import installed
24
+ from constricter.offences import Fix
25
+
26
+ if TYPE_CHECKING: # slow to import, and only needed for `--infer-with`
27
+ from constricter.cli.lsp import hints
28
+
29
+ _PY: Final = ".py" # a module the index names (not a notebook)
30
+ _Hinted: TypeAlias = dict[Path, tuple[Hints, ...]] # each file's hints, a checker's each
31
+ HINT_ROUNDS: Final = 4 # with `--fix`: how many times a file is fixed, at most (see `settled`)
32
+ # The fix kinds a scope's check comes to late, once the scope is read: what was bound to such a
33
+ # name's value before had no type yet, and has one once the fix is written.
34
+ _LATE_KINDS: Final = frozenset({"optional", "filled"})
35
+
36
+
37
+ def settled(
38
+ paths: Sequence[Path],
39
+ check: Callable[[Path, Outside], FileRun],
40
+ options: Options,
41
+ session: "hints.Session | None",
42
+ ) -> list[FileRun]:
43
+ """Check every file, then again each one `--fix` changed in a way a second check would see.
44
+
45
+ Until a round changes nothing, `HINT_ROUNDS` at most: a second pass is then left nothing. With
46
+ `--likely` alone, the files a round changed are indexed again, and the others checked again too
47
+ where that changes what they know from outside: what reads a guess another file declares now is
48
+ certain.
49
+
50
+ Returns:
51
+ What each file found, a file's rounds joined.
52
+
53
+ """
54
+ runs: list[FileRun]
55
+ modules: project.Index
56
+ runs, modules = _called_again(
57
+ paths,
58
+ checked_all(paths, check, options, session),
59
+ check,
60
+ options,
61
+ session,
62
+ )
63
+ fixing: bool = options.mode is Mode.FIX
64
+ onward: bool = fixing and session is None and options.guesses is Guesses.LIKELY
65
+ again: list[int] = [
66
+ index
67
+ for index, run in enumerate(runs)
68
+ if fixing and paths[index] != STDIN and _unsettled(run, session, options.guesses)
69
+ ]
70
+ changed: list[int] = [at for at in range(len(runs)) if onward and _changed(paths[at], runs[at])]
71
+ for _ in range(HINT_ROUNDS - 1):
72
+ held: dict[int, FileRun] = {}
73
+ if changed:
74
+ modules = _reindexed(modules, [paths[i] for i in changed], options)
75
+ held = {i: run for i, run in enumerate(runs) if i not in again and paths[i] != STDIN}
76
+ if not again and not held:
77
+ break
78
+ fresh: list[tuple[int, FileRun]]
79
+ latest: project.Index
80
+ fresh, latest = _round(paths, runs, (again, held), (check, options, session), modules)
81
+ modules = latest if onward else modules
82
+ again = [index for index, run in fresh if _unsettled(run, session, options.guesses)]
83
+ changed = [index for index, run in fresh if _changed(paths[index], run)]
84
+ return runs
85
+
86
+
87
+ def _round(
88
+ paths: Sequence[Path],
89
+ runs: list[FileRun],
90
+ todo: tuple[Sequence[int], Mapping[int, FileRun]],
91
+ how: "tuple[Callable[[Path, Outside], FileRun], Options, hints.Session | None]",
92
+ modules: project.Index,
93
+ ) -> tuple[list[tuple[int, FileRun]], project.Index]:
94
+ """Check files of `paths` again, knowing `modules`: each one's new run is joined to its `runs`.
95
+
96
+ `todo`: the files to check, by index, and those held, each with its last run: checked only where
97
+ they know anything else from outside now (see `schedule.Rounds`). `how`: the check, the options
98
+ and the type checkers' session.
99
+
100
+ Returns:
101
+ Each file the round checked, with this round's run alone; and the index, with what their
102
+ functions return.
103
+
104
+ """
105
+ again: Sequence[int]
106
+ held: Mapping[int, FileRun]
107
+ again, held = todo
108
+ which: list[int] = sorted({*again, *held})
109
+ redone: list[FileRun]
110
+ latest: project.Index
111
+ redone, latest = checked_all(
112
+ [paths[i] for i in which],
113
+ how[0],
114
+ how[1],
115
+ how[2],
116
+ _Known(modules, {at: held[i] for at, i in enumerate(which) if i in held}),
117
+ )
118
+ fresh: list[tuple[int, FileRun]] = [
119
+ (index, run) for index, run in zip(which, redone, strict=True) if run is not held.get(index)
120
+ ]
121
+ index: int
122
+ run: FileRun
123
+ for index, run in fresh:
124
+ runs[index] = merged(runs[index], run)
125
+ return fresh, latest
126
+
127
+
128
+ def _changed(path: Path, run: FileRun) -> bool:
129
+ """Check whether a round of `--fix` changed a file the index has (not standard input or a notebook).
130
+
131
+ Returns:
132
+ Whether it fixed anything there.
133
+
134
+ """
135
+ return path.suffix == _PY and bool(cast("CheckRun", run).fixed)
136
+
137
+
138
+ def _reindexed(modules: project.Index, paths: Sequence[Path], options: Options) -> project.Index:
139
+ """Index again the files at `paths`, which `--fix` changed: in the run's workers, if it has them.
140
+
141
+ Returns:
142
+ The index, with them as they are now (see `project.spliced`).
143
+
144
+ """
145
+ found: list[project.Module | None] = (
146
+ started(paths, options.jobs).read() if running() else [project.read(path) for path in paths]
147
+ )
148
+ return project.spliced(modules, found)
149
+
150
+
151
+ def _unsettled(run: FileRun, session: "hints.Session | None", guesses: Guesses) -> bool:
152
+ """Check whether a round of `--fix` changed a file in a way that changes what's inferred there.
153
+
154
+ Returns:
155
+ Whether it fixed anything, with a type checker's hints: its new annotations change them.
156
+ Without, whether it made a fix the scope's check comes to late (`_LATE_KINDS`): what's bound
157
+ to such a name's value was read before its type was known. Or, with `--likely` alone, wrote a
158
+ guess: what rests on it and on no other guess is certain once it's declared.
159
+
160
+ """
161
+ checked: CheckRun = cast("CheckRun", run)
162
+ return bool(checked.fixed) and (
163
+ session is not None
164
+ or any(
165
+ isinstance(made.offence.edit, Fix)
166
+ and (made.offence.edit.kinds & _LATE_KINDS or (guesses is Guesses.LIKELY and made.offence.unsafe))
167
+ for made in checked.made
168
+ )
169
+ )
170
+
171
+
172
+ def _called_again(
173
+ paths: Sequence[Path],
174
+ first: tuple[list[FileRun], project.Index],
175
+ check: Callable[[Path, Outside], FileRun],
176
+ options: Options,
177
+ session: "hints.Session | None" = None,
178
+ ) -> tuple[list[FileRun], project.Index]:
179
+ """Check again the files whose functions' unannotated parameters every call passes one type.
180
+
181
+ Knowing those types (see `callers`), as guesses; a file among several of one module name isn't
182
+ (which its callers call can't be told). Then the other files calling those functions, knowing
183
+ what they now return. With `--fix` a file's rounds are joined; otherwise the last, knowing more,
184
+ stands.
185
+
186
+ Returns:
187
+ What each file found, and the index, with the parameters' types.
188
+
189
+ """
190
+ runs: list[FileRun]
191
+ modules: project.Index
192
+ runs, modules = first
193
+ found: dict[str, callers.Parameters] = callers.parameters(
194
+ (run.calls for run in runs if isinstance(run, CheckRun)),
195
+ modules,
196
+ )
197
+ names: list[str | None] = [project.module_name(path) if path.suffix == _PY else None for path in paths]
198
+ again: list[int]
199
+ if not (again := [at for at, name in enumerate(names) if name in found and names.count(name) == 1]):
200
+ return runs, modules
201
+ calling: list[int] = _calling(runs, found, again)
202
+ before: project.Index = callers.with_parameters(modules, found)
203
+ modules = _checked_more(paths, runs, again, (check, options, session), before)
204
+ # A caller is checked again only if a function it imports now returns something else.
205
+ calling = [
206
+ at
207
+ for at in calling
208
+ if loose.returned(modules, paths[at], {}) != loose.returned(before, paths[at], {})
209
+ ]
210
+ return runs, _checked_more(paths, runs, calling, (check, options, session), modules)
211
+
212
+
213
+ def _checked_more(
214
+ paths: Sequence[Path],
215
+ runs: list[FileRun],
216
+ which: Sequence[int],
217
+ how: "tuple[Callable[[Path, Outside], FileRun], Options, hints.Session | None]",
218
+ modules: project.Index,
219
+ ) -> project.Index:
220
+ """Check the files of `paths` at `which` again, knowing `modules`, into `runs`.
221
+
222
+ `how`: the check, the options and the type checkers' session. With `--fix` a file's rounds are
223
+ joined; otherwise the later one stands.
224
+
225
+ Returns:
226
+ The index, with what their functions now return.
227
+
228
+ """
229
+ redone: list[FileRun]
230
+ redone, modules = checked_all([paths[at] for at in which], how[0], how[1], how[2], _Known(modules))
231
+ at: int
232
+ run: FileRun
233
+ for at, run in zip(which, redone, strict=True):
234
+ runs[at] = merged(runs[at], run) if how[1].mode is Mode.FIX else run
235
+ return modules
236
+
237
+
238
+ def _calling(
239
+ runs: Sequence[FileRun],
240
+ found: Mapping[str, callers.Parameters],
241
+ again: Sequence[int],
242
+ ) -> list[int]:
243
+ """Find the files (not among `again`) calling a function whose parameters `found` types.
244
+
245
+ Returns:
246
+ Their indexes.
247
+
248
+ """
249
+ typed: set[Callee] = {(module, function) for module, functions in found.items() for function in functions}
250
+ return [
251
+ at
252
+ for at, run in enumerate(runs)
253
+ if at not in again and isinstance(run, CheckRun) and typed & run.calls.calls.keys()
254
+ ]
255
+
256
+
257
+ class _Known(NamedTuple):
258
+ """What a round of checks knows of the files: the index (`None`: index them), and their kept runs.
259
+
260
+ `kept`: see `schedule.Rounds`, by the index of each file in the round's.
261
+ """
262
+
263
+ modules: project.Index | None = None
264
+ kept: Mapping[int, FileRun] = Empty()
265
+
266
+
267
+ _FIRST: Final = _Known() # a run's first round: the files indexed, none kept
268
+
269
+
270
+ def checked_all(
271
+ paths: Sequence[Path],
272
+ check: Callable[[Path, Outside], FileRun],
273
+ options: Options,
274
+ session: "hints.Session | None" = None,
275
+ known: _Known = _FIRST,
276
+ ) -> tuple[list[FileRun], project.Index]:
277
+ """Check `paths` (`--jobs` at a time), with the `session`'s hints, knowing `known`.
278
+
279
+ Returns:
280
+ What each file found, and the index of every file's module.
281
+
282
+ """
283
+ # Before a worker starts: one forked while the servers' threads run could wait on a lock one holds.
284
+ hinted: _Hinted = {} if session is None else session.hints(_texts(paths))
285
+ if options.input.trace is not None:
286
+ hinted = traced.merged(hinted, traced.load(options.input.trace), paths)
287
+ coverage: bool = options.mode is Mode.COVERAGE # needs nothing from the other files
288
+ # A run's later rounds go to its workers, whatever their size: each knows what its files last knew.
289
+ pool: Workers | None = (
290
+ started(paths, options.jobs) if options.jobs != 1 and (len(paths) > 1 or running()) else None
291
+ )
292
+ modules: project.Index | None = known.modules
293
+ if modules is None and pool:
294
+ modules = project.Index({}, []) if coverage else pool.index()
295
+ elif modules is None:
296
+ modules = project.Index({}, []) if coverage else project.index(paths)
297
+ collecting.indexed()
298
+ if not coverage:
299
+ modules = plain.settled(
300
+ decorated.passed(installed.with_installed(modules, installed.search_path())),
301
+ options.checks.plain_bases,
302
+ )
303
+ return schedule.checked(
304
+ paths,
305
+ check,
306
+ pool,
307
+ (modules, hinted),
308
+ schedule.Rounds(merged if options.mode is Mode.FIX else None, known.kept),
309
+ )
310
+
311
+
312
+ def _texts(paths: Sequence[Path]) -> dict[Path, str]:
313
+ """Read each file to ask the type checker about (not a notebook, or standard input).
314
+
315
+ A file that can't be read is left out: checking it reports that.
316
+
317
+ Returns:
318
+ Each file's text.
319
+
320
+ """
321
+ texts: dict[Path, str] = {}
322
+ path: Path
323
+ for path in paths:
324
+ if path != STDIN and path.suffix != notebook.SUFFIX:
325
+ # UnicodeDecodeError is a ValueError; a bad encoding declaration, a SyntaxError.
326
+ with contextlib.suppress(OSError, ValueError, SyntaxError):
327
+ texts[path] = read_text(path)
328
+ return texts
@@ -0,0 +1,90 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """What checking one file gives the CLI, in each mode."""
3
+
4
+ from collections.abc import Sequence
5
+ from dataclasses import dataclass, field, replace
6
+ from typing import TypeAlias, cast
7
+
8
+ from constricter.cli.report import Result
9
+ from constricter.fix.core.known import Observed, Returns
10
+ from constricter.offences import Offence
11
+ from constricter.rules.checker import Coverage
12
+
13
+ # The lines one round of `--fix` added to a file: for each, its cell (`None` outside a notebook)
14
+ # and the line (from 1) it went before.
15
+ Added: TypeAlias = tuple[tuple[int | None, int], ...]
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class CheckRun:
20
+ """What checking one file found (and fixed, or would fix), in check/fix/diff mode."""
21
+
22
+ results: list[Result] = field(default_factory=list[Result])
23
+ baselined: int = 0
24
+ fixed: int = 0 # --fix: how many offences it fixed
25
+ text: str = "" # --diff: the diff; --fix on standard input: the fixed source
26
+ error: str = ""
27
+ returned: Returns = field(default_factory=Returns) # what its unannotated functions return
28
+ calls: Observed = field(default_factory=Observed) # what it passes checked files' functions
29
+ # --fix: the offences it fixed, each where it was before any fix, and each round's added lines.
30
+ made: list[Result] = field(default_factory=list[Result])
31
+ rounds: tuple[Added, ...] = ()
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class BaselineRun:
36
+ """One file's offences, unfiltered, for --write-baseline."""
37
+
38
+ found: list[Offence] = field(default_factory=list[Offence])
39
+ error: str = ""
40
+ returned: Returns = field(default_factory=Returns) # what its unannotated functions return
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class CoverageRun:
45
+ """One file's annotation coverage, for --coverage."""
46
+
47
+ coverage: Coverage | None = None
48
+ error: str = ""
49
+
50
+
51
+ FileRun: TypeAlias = CheckRun | BaselineRun | CoverageRun
52
+
53
+
54
+ def merged(before: FileRun, after: FileRun) -> FileRun:
55
+ """Join a file's two `--fix` rounds' `CheckRun`s: what's left is the later's, what's fixed is both's.
56
+
57
+ The later round's fixes are placed on the lines they had before the earlier ones' (see `_original`).
58
+
59
+ Returns:
60
+ The joined run.
61
+
62
+ """
63
+ first: CheckRun = cast("CheckRun", before)
64
+ later: CheckRun = cast("CheckRun", after)
65
+ made: list[Result] = [
66
+ result._replace(offence=replace(result.offence, line=_original(result.offence, first.rounds)))
67
+ for result in later.made
68
+ ]
69
+ return replace(
70
+ later,
71
+ fixed=first.fixed + later.fixed,
72
+ made=[*first.made, *made],
73
+ rounds=(*first.rounds, *later.rounds),
74
+ )
75
+
76
+
77
+ def _original(offence: Offence, rounds: Sequence[Added]) -> int:
78
+ """Find the line an offence was on before `rounds` of fixes added lines to its file.
79
+
80
+ Returns:
81
+ It; for an offence on an added line (a declaration, fixed again), the line that one went before.
82
+
83
+ """
84
+ line: int = offence.line
85
+ added: Added
86
+ for added in reversed(rounds):
87
+ before: list[int] = [at for cell, at in added if cell == offence.cell]
88
+ # The added lines above it: the first went where it says, each next one a line further down.
89
+ line -= sum(at + index < line for index, at in enumerate(before))
90
+ return line