python-constricter 0.4.1__cp312-cp312-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__.cp312-win32.pyd +0 -0
  29. constricter/fix/__init__.py +2 -0
  30. constricter/fix/core/__init__.cp312-win32.pyd +0 -0
  31. constricter/fix/core/__init__.py +2 -0
  32. constricter/fix/core/asked.cp312-win32.pyd +0 -0
  33. constricter/fix/core/asked.py +56 -0
  34. constricter/fix/core/fixes.cp312-win32.pyd +0 -0
  35. constricter/fix/core/fixes.py +317 -0
  36. constricter/fix/core/imports.cp312-win32.pyd +0 -0
  37. constricter/fix/core/imports.py +580 -0
  38. constricter/fix/core/inherited.cp312-win32.pyd +0 -0
  39. constricter/fix/core/inherited.py +311 -0
  40. constricter/fix/core/known.cp312-win32.pyd +0 -0
  41. constricter/fix/core/known.py +730 -0
  42. constricter/fix/core/signatures.cp312-win32.pyd +0 -0
  43. constricter/fix/core/signatures.py +132 -0
  44. constricter/fix/index/__init__.cp312-win32.pyd +0 -0
  45. constricter/fix/index/__init__.py +2 -0
  46. constricter/fix/index/awaits.cp312-win32.pyd +0 -0
  47. constricter/fix/index/awaits.py +90 -0
  48. constricter/fix/index/beyond.cp312-win32.pyd +0 -0
  49. constricter/fix/index/beyond.py +149 -0
  50. constricter/fix/index/callers.cp312-win32.pyd +0 -0
  51. constricter/fix/index/callers.py +155 -0
  52. constricter/fix/index/crossed.cp312-win32.pyd +0 -0
  53. constricter/fix/index/crossed.py +327 -0
  54. constricter/fix/index/decorated.cp312-win32.pyd +0 -0
  55. constricter/fix/index/decorated.py +140 -0
  56. constricter/fix/index/fixtures.cp312-win32.pyd +0 -0
  57. constricter/fix/index/fixtures.py +176 -0
  58. constricter/fix/index/linked.cp312-win32.pyd +0 -0
  59. constricter/fix/index/linked.py +142 -0
  60. constricter/fix/index/loose.cp312-win32.pyd +0 -0
  61. constricter/fix/index/loose.py +103 -0
  62. constricter/fix/index/managed.cp312-win32.pyd +0 -0
  63. constricter/fix/index/managed.py +63 -0
  64. constricter/fix/index/modules.cp312-win32.pyd +0 -0
  65. constricter/fix/index/modules.py +741 -0
  66. constricter/fix/index/offers.cp312-win32.pyd +0 -0
  67. constricter/fix/index/offers.py +252 -0
  68. constricter/fix/index/order.cp312-win32.pyd +0 -0
  69. constricter/fix/index/order.py +155 -0
  70. constricter/fix/index/origins.cp312-win32.pyd +0 -0
  71. constricter/fix/index/origins.py +238 -0
  72. constricter/fix/index/own_methods.cp312-win32.pyd +0 -0
  73. constricter/fix/index/own_methods.py +91 -0
  74. constricter/fix/index/own_overloads.cp312-win32.pyd +0 -0
  75. constricter/fix/index/own_overloads.py +70 -0
  76. constricter/fix/index/own_types.cp312-win32.pyd +0 -0
  77. constricter/fix/index/own_types.py +330 -0
  78. constricter/fix/index/params.cp312-win32.pyd +0 -0
  79. constricter/fix/index/params.py +81 -0
  80. constricter/fix/index/plain.cp312-win32.pyd +0 -0
  81. constricter/fix/index/plain.py +221 -0
  82. constricter/fix/index/project.cp312-win32.pyd +0 -0
  83. constricter/fix/index/project.py +797 -0
  84. constricter/fix/index/sides.cp312-win32.pyd +0 -0
  85. constricter/fix/index/sides.py +160 -0
  86. constricter/fix/index/strings.cp312-win32.pyd +0 -0
  87. constricter/fix/index/strings.py +46 -0
  88. constricter/fix/index/stubs/__init__.cp312-win32.pyd +0 -0
  89. constricter/fix/index/stubs/__init__.py +2 -0
  90. constricter/fix/index/stubs/atoms.cp312-win32.pyd +0 -0
  91. constricter/fix/index/stubs/atoms.py +425 -0
  92. constricter/fix/index/stubs/chained.cp312-win32.pyd +0 -0
  93. constricter/fix/index/stubs/chained.py +69 -0
  94. constricter/fix/index/stubs/classnames.cp312-win32.pyd +0 -0
  95. constricter/fix/index/stubs/classnames.py +124 -0
  96. constricter/fix/index/stubs/declared.cp312-win32.pyd +0 -0
  97. constricter/fix/index/stubs/declared.py +508 -0
  98. constricter/fix/index/stubs/installed.cp312-win32.pyd +0 -0
  99. constricter/fix/index/stubs/installed.py +425 -0
  100. constricter/fix/index/stubs/names.cp312-win32.pyd +0 -0
  101. constricter/fix/index/stubs/names.py +200 -0
  102. constricter/fix/index/stubs/read.cp312-win32.pyd +0 -0
  103. constricter/fix/index/stubs/read.py +70 -0
  104. constricter/fix/index/stubs/signatures.cp312-win32.pyd +0 -0
  105. constricter/fix/index/stubs/signatures.py +656 -0
  106. constricter/fix/index/stubs/stubbed.cp312-win32.pyd +0 -0
  107. constricter/fix/index/stubs/stubbed.py +208 -0
  108. constricter/fix/index/taken.cp312-win32.pyd +0 -0
  109. constricter/fix/index/taken.py +45 -0
  110. constricter/fix/index/tuples.cp312-win32.pyd +0 -0
  111. constricter/fix/index/tuples.py +112 -0
  112. constricter/fix/libraries/__init__.cp312-win32.pyd +0 -0
  113. constricter/fix/libraries/__init__.py +2 -0
  114. constricter/fix/libraries/library.cp312-win32.pyd +0 -0
  115. constricter/fix/libraries/library.py +420 -0
  116. constricter/fix/libraries/opened.cp312-win32.pyd +0 -0
  117. constricter/fix/libraries/opened.py +147 -0
  118. constricter/fix/libraries/overloads.cp312-win32.pyd +0 -0
  119. constricter/fix/libraries/overloads.py +960 -0
  120. constricter/fix/libraries/receivers.cp312-win32.pyd +0 -0
  121. constricter/fix/libraries/receivers.py +274 -0
  122. constricter/fix/libraries/stdlib.cp312-win32.pyd +0 -0
  123. constricter/fix/libraries/stdlib.py +752 -0
  124. constricter/fix/libraries/walks.cp312-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__.cp312-win32.pyd +0 -0
  148. constricter/fix/values/__init__.py +2 -0
  149. constricter/fix/values/aliased.cp312-win32.pyd +0 -0
  150. constricter/fix/values/aliased.py +195 -0
  151. constricter/fix/values/bodies.cp312-win32.pyd +0 -0
  152. constricter/fix/values/bodies.py +76 -0
  153. constricter/fix/values/callables.cp312-win32.pyd +0 -0
  154. constricter/fix/values/callables.py +323 -0
  155. constricter/fix/values/called.cp312-win32.pyd +0 -0
  156. constricter/fix/values/called.py +194 -0
  157. constricter/fix/values/classvars.cp312-win32.pyd +0 -0
  158. constricter/fix/values/classvars.py +629 -0
  159. constricter/fix/values/decided.cp312-win32.pyd +0 -0
  160. constricter/fix/values/decided.py +429 -0
  161. constricter/fix/values/displays.cp312-win32.pyd +0 -0
  162. constricter/fix/values/displays.py +101 -0
  163. constricter/fix/values/doubts.cp312-win32.pyd +0 -0
  164. constricter/fix/values/doubts.py +677 -0
  165. constricter/fix/values/entered.cp312-win32.pyd +0 -0
  166. constricter/fix/values/entered.py +332 -0
  167. constricter/fix/values/fills.cp312-win32.pyd +0 -0
  168. constricter/fix/values/fills.py +446 -0
  169. constricter/fix/values/guesses.cp312-win32.pyd +0 -0
  170. constricter/fix/values/guesses.py +376 -0
  171. constricter/fix/values/hinted.cp312-win32.pyd +0 -0
  172. constricter/fix/values/hinted.py +811 -0
  173. constricter/fix/values/inference.cp312-win32.pyd +0 -0
  174. constricter/fix/values/inference.py +698 -0
  175. constricter/fix/values/loops.cp312-win32.pyd +0 -0
  176. constricter/fix/values/loops.py +335 -0
  177. constricter/fix/values/members.cp312-win32.pyd +0 -0
  178. constricter/fix/values/members.py +578 -0
  179. constricter/fix/values/narrowed.cp312-win32.pyd +0 -0
  180. constricter/fix/values/narrowed.py +220 -0
  181. constricter/fix/values/operated.cp312-win32.pyd +0 -0
  182. constricter/fix/values/operated.py +202 -0
  183. constricter/fix/values/ordered.cp312-win32.pyd +0 -0
  184. constricter/fix/values/ordered.py +74 -0
  185. constricter/fix/values/returned.cp312-win32.pyd +0 -0
  186. constricter/fix/values/returned.py +479 -0
  187. constricter/fix/values/returns.cp312-win32.pyd +0 -0
  188. constricter/fix/values/returns.py +237 -0
  189. constricter/fix/values/shapes.cp312-win32.pyd +0 -0
  190. constricter/fix/values/shapes.py +654 -0
  191. constricter/fix/values/stored.cp312-win32.pyd +0 -0
  192. constricter/fix/values/stored.py +564 -0
  193. constricter/fix/values/targets.cp312-win32.pyd +0 -0
  194. constricter/fix/values/targets.py +419 -0
  195. constricter/fix/values/unchecked.cp312-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__.cp312-win32.pyd +0 -0
  208. constricter/rules/__init__.py +2 -0
  209. constricter/rules/annotations.cp312-win32.pyd +0 -0
  210. constricter/rules/annotations.py +296 -0
  211. constricter/rules/binding.cp312-win32.pyd +0 -0
  212. constricter/rules/binding.py +652 -0
  213. constricter/rules/calls.cp312-win32.pyd +0 -0
  214. constricter/rules/calls.py +460 -0
  215. constricter/rules/checker.cp312-win32.pyd +0 -0
  216. constricter/rules/checker.py +536 -0
  217. constricter/rules/declarations.cp312-win32.pyd +0 -0
  218. constricter/rules/declarations.py +730 -0
  219. constricter/rules/decorators.cp312-win32.pyd +0 -0
  220. constricter/rules/decorators.py +351 -0
  221. constricter/rules/flow.cp312-win32.pyd +0 -0
  222. constricter/rules/flow.py +502 -0
  223. constricter/rules/keys.cp312-win32.pyd +0 -0
  224. constricter/rules/keys.py +79 -0
  225. constricter/rules/late.cp312-win32.pyd +0 -0
  226. constricter/rules/late.py +969 -0
  227. constricter/rules/narrowing.cp312-win32.pyd +0 -0
  228. constricter/rules/narrowing.py +98 -0
  229. constricter/rules/parsed.cp312-win32.pyd +0 -0
  230. constricter/rules/parsed.py +136 -0
  231. constricter/rules/quoted.cp312-win32.pyd +0 -0
  232. constricter/rules/quoted.py +141 -0
  233. constricter/rules/rebinding.cp312-win32.pyd +0 -0
  234. constricter/rules/rebinding.py +169 -0
  235. constricter/rules/recorded.cp312-win32.pyd +0 -0
  236. constricter/rules/recorded.py +168 -0
  237. constricter/rules/redundant.cp312-win32.pyd +0 -0
  238. constricter/rules/redundant.py +160 -0
  239. constricter/rules/scope.cp312-win32.pyd +0 -0
  240. constricter/rules/scope.py +802 -0
  241. constricter/rules/scoped.cp312-win32.pyd +0 -0
  242. constricter/rules/scoped.py +232 -0
  243. constricter/rules/scoping.cp312-win32.pyd +0 -0
  244. constricter/rules/scoping.py +346 -0
  245. constricter/rules/settings.cp312-win32.pyd +0 -0
  246. constricter/rules/settings.py +189 -0
  247. constricter/rules/syntax.cp312-win32.pyd +0 -0
  248. constricter/rules/syntax.py +472 -0
  249. constricter/rules/tables.cp312-win32.pyd +0 -0
  250. constricter/rules/tables.py +169 -0
  251. constricter/rules/walked.cp312-win32.pyd +0 -0
  252. constricter/rules/walked.py +127 -0
  253. constricter/rules/widened.cp312-win32.pyd +0 -0
  254. constricter/rules/widened.py +322 -0
  255. constricter/trace.py +374 -0
  256. e45ce06b2fdd5c43f782__mypyc.cp312-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,547 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """The `constricter` command (see README)."""
3
+
4
+ import codecs
5
+ import contextlib
6
+ import difflib
7
+ import importlib
8
+ import json
9
+ import sys
10
+ from collections.abc import Callable, Sequence
11
+ from dataclasses import replace
12
+ from functools import partial
13
+ from pathlib import Path
14
+ from typing import TYPE_CHECKING, Final, NamedTuple, TextIO, TypeAlias, cast
15
+
16
+ from constricter import notebook
17
+ from constricter.cli import baseline, collecting, schedule
18
+ from constricter.cli.lsp.protocol import HintError
19
+ from constricter.cli.options import Mode, Options, Output
20
+ from constricter.cli.paths import STDIN, python_files
21
+ from constricter.cli.reading import encoding_of, read_text
22
+ from constricter.cli.report import Format, Result, fix_reasons, render, statistics
23
+ from constricter.cli.rounds import checked_all, settled
24
+ from constricter.cli.runs import Added, BaselineRun, CheckRun, CoverageRun, FileRun
25
+ from constricter.cli.workers import kept
26
+ from constricter.fix.core import fixes
27
+ from constricter.fix.core.known import Outside, Returns
28
+ from constricter.noqa import lines, unsuppressed
29
+ from constricter.offences import (
30
+ DEFAULT_CHECKS,
31
+ Checks,
32
+ Edit,
33
+ Offence,
34
+ )
35
+ from constricter.rules import parsed
36
+ from constricter.rules.checker import Checked, Coverage, annotation_coverage, checked_source
37
+
38
+ if TYPE_CHECKING: # slow to import, and only needed for `--infer-with`
39
+ from constricter.cli.lsp import hints
40
+
41
+ EXIT_CLEAN: Final = 0
42
+ EXIT_FOUND: Final = 1
43
+ EXIT_ERROR: Final = 2
44
+ _ALL: Final = 100 # percent
45
+
46
+
47
+ def _source(raw: str, name: Path) -> tuple[str, list[notebook.Line]]:
48
+ """Extract the Python in `raw`: a notebook's code cells, joined.
49
+
50
+ Returns:
51
+ The source, and each line's cell if it's a notebook.
52
+
53
+ """
54
+ return notebook.parse(raw, str(name)) if name.suffix == notebook.SUFFIX else (raw, [])
55
+
56
+
57
+ def checked_text(
58
+ raw: str,
59
+ name: Path,
60
+ checks: Checks = DEFAULT_CHECKS,
61
+ *,
62
+ outside: Outside | None = None,
63
+ ) -> Checked:
64
+ """Check `raw`, the text of `name`: the offences no `# noqa` suppresses.
65
+
66
+ A notebook's code cells are checked as one module, and each offence placed in its cell.
67
+ Raises `ValueError` for a `.ipynb` that isn't a notebook.
68
+
69
+ Returns:
70
+ The unsuppressed offences, a notebook's placed in their cells, and what its functions return
71
+ (see `Checked`).
72
+
73
+ """
74
+ source: str
75
+ where: list[notebook.Line]
76
+ source, where = _source(raw, name)
77
+ checked: Checked = checked_source(source, str(name), checks, outside=outside)
78
+ offences: list[Offence] = unsuppressed(checked.offences, lines(source))
79
+ return checked._replace(offences=[_placed(o, where) for o in offences] if where else offences)
80
+
81
+
82
+ def _placed(offence: Offence, where: list[notebook.Line]) -> Offence:
83
+ """Place an offence from a notebook's joined module in its cell, its declaration's line too.
84
+
85
+ Returns:
86
+ The offence, its `line` (and a `Edit.DECLARE` fix's statement line) counted in its `cell`; a
87
+ fix that would add an import isn't offered.
88
+
89
+ """
90
+ line: notebook.Line = where[offence.line - 1]
91
+ placed: Offence = replace(offence, line=line.line, cell=line.cell)
92
+ # A notebook's cells have no import block.
93
+ if offence.edit is not None and (offence.edit.imports or offence.edit.guarded):
94
+ return replace(placed, edit=None)
95
+ if offence.edit is not None and offence.edit.edit is Edit.DECLARE:
96
+ statement: int = where[offence.edit.span[0] - 1].line
97
+ placed = replace(placed, edit=offence.edit._replace(span=(statement, offence.edit.span[1])))
98
+ return placed
99
+
100
+
101
+ # One changed part of a fixed file (the file, or a notebook's cell): its label, old and new lines.
102
+ _Change: TypeAlias = tuple[str, list[str], list[str]]
103
+ # One JSON value a coverage report holds, and one file's row of them.
104
+ _Scalar: TypeAlias = str | int | float
105
+ _Row: TypeAlias = dict[str, _Scalar]
106
+
107
+
108
+ class Fixed(NamedTuple):
109
+ """`raw`, fixed: its new text, and each changed part (the file, or a notebook's cell)."""
110
+
111
+ text: str
112
+ changes: list[_Change] # each part's label and old and new lines
113
+
114
+
115
+ def _fixed(raw: str, name: Path, offences: Sequence[Offence]) -> Fixed:
116
+ """Add each fixable offence's annotation to `raw`, the text of `name`.
117
+
118
+ Returns:
119
+ The new text, and each changed part.
120
+
121
+ """
122
+ if name.suffix == notebook.SUFFIX:
123
+ text: str
124
+ cells: list[notebook.Cell]
125
+ text, cells = notebook.fix(raw, offences)
126
+ return Fixed(text, [(f"{name}:cell {c.number}", c.old, c.new) for c in cells])
127
+ old: list[str] = lines(raw)
128
+ new: list[str] = fixes.apply(old, offences)
129
+ return Fixed("".join(new), [(str(name), old, new)] if new != old else [])
130
+
131
+
132
+ def _diff(raw: str, name: Path, offences: Sequence[Offence]) -> str:
133
+ return "".join(
134
+ "".join(difflib.unified_diff(old, new, label, label))
135
+ for label, old, new in _fixed(raw, name, offences).changes
136
+ )
137
+
138
+
139
+ def fix_file(path: Path, offences: Sequence[Offence]) -> int:
140
+ """Add each fixable offence's annotation to `path` (a notebook's, in its cells), in its encoding.
141
+
142
+ Raises `UnicodeEncodeError`, leaving the file as it was, when an annotation can't be written in
143
+ the file's encoding (a PEP 263 declaration's).
144
+
145
+ Returns:
146
+ How many offences were fixed.
147
+
148
+ """
149
+ count: int
150
+ if not (count := sum(1 for o in offences if o.edit is not None)):
151
+ return 0
152
+ data: bytes = path.read_bytes()
153
+ encoding: str = encoding_of(path, data)
154
+ _ = path.write_bytes(_fixed(data.decode(encoding), path, offences).text.encode(encoding))
155
+ return count
156
+
157
+
158
+ def _shown_lines(raw: str, name: Path) -> dict[tuple[int | None, int], str]:
159
+ """Map each line of `raw` (a notebook's, in its cells) to its text, as offences are placed.
160
+
161
+ Returns:
162
+ Each line's text, by its cell (`None` outside a notebook) and line number.
163
+
164
+ """
165
+ source: str
166
+ where: list[notebook.Line]
167
+ source, where = _source(raw, name)
168
+ return {
169
+ (where[index].cell if where else None, where[index].line if where else index + 1): text
170
+ for index, text in enumerate(source.splitlines())
171
+ }
172
+
173
+
174
+ def _checked(path: Path, name: Path, checks: Checks, outside: Outside) -> tuple[str, Checked]:
175
+ """Read `path` and check it as `name`; raises what reading or parsing it does.
176
+
177
+ Returns:
178
+ Its text, and what checking it found.
179
+
180
+ """
181
+ raw: str = read_text(path)
182
+ return raw, checked_text(raw, name, checks, outside=outside)
183
+
184
+
185
+ def _read_checked(
186
+ path: Path,
187
+ name: Path,
188
+ checks: Checks,
189
+ outside: Outside,
190
+ ) -> tuple[str, Checked, str]:
191
+ """Read and check `path`, turning a read or parse error into a message instead of raising.
192
+
193
+ Returns:
194
+ Its text and what checking it found (nothing on error), and the error message (empty on success).
195
+
196
+ """
197
+ raw: str
198
+ checked: Checked
199
+ try:
200
+ raw, checked = _checked(path, name, checks, outside)
201
+ except (OSError, ValueError, SyntaxError) as error: # UnicodeDecodeError is a ValueError
202
+ return "", Checked([], Returns()), f"{name}: error: {error}"
203
+ return raw, checked, ""
204
+
205
+
206
+ def _results(raw: str, name: Path, offences: Sequence[Offence], options: Options) -> list[Result]:
207
+ """Turn the offences in `raw`, the text of `name`, into the results to report.
208
+
209
+ Returns:
210
+ Each one the options don't filter out, with its source line and its fix as a text edit.
211
+
212
+ """
213
+ shown: dict[tuple[int | None, int], str] = _shown_lines(raw, name)
214
+ # A notebook's cells have no file lines for an edit to point at.
215
+ text: list[str] = [] if name.suffix == notebook.SUFFIX else lines(raw)
216
+ return [
217
+ r._replace(
218
+ source=shown.get((r.offence.cell, r.offence.line), ""),
219
+ replacements=fixes.replacements(text, r.offence) if text else (),
220
+ )
221
+ for r in options.filter.results(name, offences)
222
+ ]
223
+
224
+
225
+ def _check_path(path: Path, outside: Outside, options: Options) -> CheckRun:
226
+ """Check (and fix, or diff) one file, given what's known of it from outside it.
227
+
228
+ Returns:
229
+ What it found; a file that can't be read or parsed is an error.
230
+
231
+ """
232
+ name: Path = options.input.name(path)
233
+ raw: str
234
+ checked: Checked
235
+ error: str
236
+ raw, checked, error = _read_checked(path, name, options.checks, outside)
237
+ if error:
238
+ return CheckRun(error=error)
239
+ return replace(
240
+ _handled(path, (raw, name), checked.offences, options),
241
+ returned=checked.returned,
242
+ calls=checked.calls,
243
+ )
244
+
245
+
246
+ def _handled(path: Path, text: tuple[str, Path], offences: list[Offence], options: Options) -> CheckRun:
247
+ """Report, fix or diff the offences found in `path` (`text`: its text, and its name).
248
+
249
+ Returns:
250
+ What was found, and fixed.
251
+
252
+ """
253
+ raw: str
254
+ name: Path
255
+ raw, name = text
256
+ baselined: int
257
+ offences, baselined = options.filter.unbaselined(name, offences)
258
+ results: list[Result] = _results(raw, name, offences, options)
259
+ fixing: list[Offence] = [
260
+ r.offence for r in results if r.offence.edit is not None and options.guesses.applies(r.offence)
261
+ ]
262
+ if options.mode is Mode.DIFF:
263
+ return CheckRun(text=_diff(raw, name, fixing))
264
+ if options.mode is not Mode.FIX:
265
+ return CheckRun(results, baselined)
266
+ left: list[Result] = [r for r in results if r.offence not in fixing]
267
+ if path == STDIN: # the fixed source goes to stdout
268
+ return CheckRun(left, baselined, len(fixing), _fixed(raw, name, fixing).text)
269
+ fixed: int
270
+ try:
271
+ fixed = fix_file(path, fixing)
272
+ except UnicodeEncodeError as failure: # the file is left as it was, its offences unfixed
273
+ # Its canonical name: PyPy reports `latin1` where CPython says `latin-1`.
274
+ encoding: str = codecs.lookup(failure.encoding).name
275
+ message: str = f"an annotation can't be written in its encoding, {encoding}; left as it was"
276
+ return CheckRun(results, baselined, error=f"{name}: error: {message}")
277
+ if fixed:
278
+ _ = parsed.take(raw) # the tree of the text it had: no check will ask for it again
279
+ return CheckRun(
280
+ left,
281
+ baselined,
282
+ fixed,
283
+ made=[r._replace(replacements=(), fixed=True) for r in results if r.offence in fixing],
284
+ rounds=(_added(raw, name, fixing),),
285
+ )
286
+
287
+
288
+ def _added(raw: str, name: Path, fixing: Sequence[Offence]) -> Added:
289
+ """Find the lines fixing `fixing` adds to `raw`, the text of `name` (see `fixes.inserted`).
290
+
291
+ Returns:
292
+ Each one's cell (`None` outside a notebook) and the line it goes before; a notebook's cells
293
+ get declarations alone.
294
+
295
+ """
296
+ if name.suffix != notebook.SUFFIX:
297
+ return tuple((None, line) for line in fixes.inserted(lines(raw), fixing))
298
+ declared: list[tuple[int, int]] = [
299
+ (o.cell or 0, o.edit.span[0]) for o in fixing if o.edit is not None and o.edit.edit is Edit.DECLARE
300
+ ]
301
+ return tuple(sorted(declared))
302
+
303
+
304
+ def _baseline_path(path: Path, outside: Outside, options: Options) -> BaselineRun:
305
+ """Check one file, unfiltered, for --write-baseline.
306
+
307
+ Returns:
308
+ Every offence found; a file that can't be read or parsed is an error.
309
+
310
+ """
311
+ name: Path = options.input.name(path)
312
+ checked: Checked
313
+ error: str
314
+ _, checked, error = _read_checked(path, name, options.checks, outside)
315
+ return BaselineRun(error=error) if error else BaselineRun(checked.offences, returned=checked.returned)
316
+
317
+
318
+ def _cover_path(path: Path, _outside: Outside, options: Options) -> CoverageRun:
319
+ """Count one file's typed first bindings.
320
+
321
+ Returns:
322
+ The counts; a file that can't be read or parsed is an error.
323
+
324
+ """
325
+ name: Path = options.input.name(path)
326
+ try:
327
+ return CoverageRun(annotation_coverage(_source(read_text(path), name)[0], options.checks))
328
+ except (OSError, ValueError, SyntaxError) as error:
329
+ return CoverageRun(error=f"{name}: error: {error}")
330
+
331
+
332
+ def _check_all(options: Options) -> tuple[list[Path], list[FileRun]]:
333
+ """Check every file (see `_check_rounds`), its rounds by one set of worker processes.
334
+
335
+ Returns:
336
+ The names, and what each file found.
337
+
338
+ """
339
+ schedule.forget()
340
+ with kept():
341
+ return _check_rounds(options)
342
+
343
+
344
+ def _check_rounds(options: Options) -> tuple[list[Path], list[FileRun]]:
345
+ """Check every file (`--jobs` at a time), in order.
346
+
347
+ With `--infer-with`, the type checker's server runs throughout: it's asked for every file's
348
+ hints first, and with `--fix`, each file a round changed is asked again and checked again, as its
349
+ new annotations change what the checker infers (see `settled`). A baseline records offences,
350
+ and coverage counts annotations: hints change neither, only what `--fix` offers.
351
+
352
+ Returns:
353
+ The names, and what each file found.
354
+
355
+ """
356
+ paths: list[Path] = list(
357
+ python_files(options.input.paths, options.input.exclude, tracked=options.input.gitignore),
358
+ )
359
+ check: Callable[[Path, Outside], FileRun]
360
+ if options.mode is Mode.COVERAGE:
361
+ check = partial(_cover_path, options=options)
362
+ elif options.mode is Mode.WRITE_BASELINE:
363
+ check = partial(_baseline_path, options=options)
364
+ else:
365
+ check = partial(_check_path, options=options)
366
+ names: list[Path] = [options.input.name(path) for path in paths]
367
+ if options.mode in {Mode.COVERAGE, Mode.WRITE_BASELINE}:
368
+ return names, checked_all(paths, check, options)[0]
369
+ if not options.infer_with:
370
+ return names, settled(paths, check, options, None)
371
+ session: hints.Session
372
+ # Imported only for `--infer-with`: it's slow to import, for every run's start.
373
+ with cast("type[hints.Session]", importlib.import_module("constricter.cli.lsp.hints").Session)(
374
+ options.infer_with,
375
+ Path.cwd(),
376
+ options.jobs,
377
+ options.infer_memory,
378
+ kept=not options.infer_fresh,
379
+ ) as session:
380
+ runs: list[FileRun] = settled(paths, check, options, session)
381
+ checker: str
382
+ path: Path
383
+ for checker, path in session.abandoned:
384
+ name: Path = options.input.name(path)
385
+ _ = sys.stderr.write(f"constricter: warning: {checker} hung on {name} twice: no hints for it\n")
386
+ return names, runs
387
+
388
+
389
+ def _report(options: Options, runs: Sequence[FileRun], files: int) -> int:
390
+ """Print the results (`runs` is check/fix/diff mode's: every other mode has its own printing).
391
+
392
+ Returns:
393
+ The exit status.
394
+
395
+ """
396
+ checked: Sequence[CheckRun] = cast("Sequence[CheckRun]", runs)
397
+ results: list[Result] = [result for run in checked for result in run.results]
398
+ output: Output = options.output
399
+ text: bool = output.fmt in {Format.TEXT, Format.FULL}
400
+ # With `--show-fixes`, the fixes `--fix` made too: after the rest, as text or JSON.
401
+ shown: list[Result] = [*results, *(made for run in checked if output.show_fixes for made in run.made)]
402
+ line: str
403
+ for line in (
404
+ statistics(results)
405
+ if text and output.statistics
406
+ else render(output.fmt, shown if output.fmt is Format.JSON else results)
407
+ ):
408
+ _ = sys.stdout.write(f"{line}\n")
409
+ for line in fix_reasons(shown) if text and output.show_fixes else ():
410
+ _ = sys.stdout.write(f"{line}\n")
411
+ errors: int = sum(r.offence.is_error(r.level) for r in results)
412
+ if text and not output.quiet:
413
+ parts: list[str] = [
414
+ f"Found {errors} error(s) and {len(results) - errors} warning(s) in {files} file(s)",
415
+ ]
416
+ if options.mode is Mode.FIX:
417
+ parts.append(f"fixed {sum(run.fixed for run in checked)}")
418
+ guesses: int
419
+ if guesses := sum(r.offence.unsafe for r in results):
420
+ likely: int = sum(r.offence.likely for r in results)
421
+ parts.append(
422
+ f"{guesses} more with --unsafe-fixes" + f" ({likely} with --likely)" * bool(likely),
423
+ )
424
+ if options.filter.baseline_file is not None:
425
+ parts.append(f"{sum(run.baselined for run in checked)} baselined")
426
+ _ = sys.stdout.write("; ".join(parts) + ".\n")
427
+ return EXIT_FOUND if errors else EXIT_CLEAN
428
+
429
+
430
+ def _coverage(options: Options, paths: Sequence[Path], runs: Sequence[FileRun]) -> int:
431
+ """Print each file's and the total annotation coverage (`runs` is --coverage mode's).
432
+
433
+ Returns:
434
+ The exit status.
435
+
436
+ """
437
+ covered: Sequence[CoverageRun] = cast("Sequence[CoverageRun]", runs)
438
+ counted: list[tuple[Path, Coverage]] = [
439
+ (path, run.coverage) for path, run in zip(paths, covered, strict=True) if run.coverage is not None
440
+ ]
441
+ total: Coverage = Coverage(
442
+ sum(c.typed for _, c in counted),
443
+ sum(c.total for _, c in counted),
444
+ sum(c.widened for _, c in counted),
445
+ )
446
+ if options.output.fmt is Format.JSON:
447
+ report: dict[str, _Scalar | list[_Row]] = {
448
+ **_counts(total),
449
+ "files": [{"path": str(path), **_counts(c)} for path, c in counted],
450
+ }
451
+ _ = sys.stdout.write(json.dumps(report, indent=2) + "\n")
452
+ else:
453
+ path: Path
454
+ c: Coverage
455
+ for path, c in counted:
456
+ _ = sys.stdout.write(f"{path}: {_typed(c)}\n")
457
+ _ = sys.stdout.write(f"Total: {_typed(total)} in {len(counted)} file(s).\n")
458
+ threshold: float | None = options.output.fail_under
459
+ return EXIT_FOUND if threshold is not None and total.percent < threshold else EXIT_CLEAN
460
+
461
+
462
+ def _counts(found: Coverage) -> _Row:
463
+ """Name a coverage's counts, for `--format=json`.
464
+
465
+ Returns:
466
+ Them, by name.
467
+
468
+ """
469
+ return {
470
+ "typed": found.typed,
471
+ "total": found.total,
472
+ "widened": found.widened,
473
+ "percent": round(found.percent, 1),
474
+ }
475
+
476
+
477
+ def _typed(found: Coverage) -> str:
478
+ """Write a coverage as text: `3/4 typed (75.0%)`, then what a widening annotated, if anything.
479
+
480
+ Returns:
481
+ It.
482
+
483
+ """
484
+ wider: str = f", {found.widened} widened" if found.widened else ""
485
+ return f"{found.typed}/{found.total} typed ({found.percent:.1f}%){wider}"
486
+
487
+
488
+ def _run(options: Options) -> int:
489
+ """Check, fix, diff, count or baseline, printing what that finds.
490
+
491
+ Returns:
492
+ The exit status.
493
+
494
+ """
495
+ names: list[Path]
496
+ runs: list[FileRun]
497
+ with collecting.by_hand():
498
+ try:
499
+ names, runs = _check_all(options)
500
+ except HintError as error:
501
+ _ = sys.stderr.write(f"constricter: error: {error}\n")
502
+ return EXIT_ERROR
503
+ _ = sys.stderr.write("".join(f"{run.error}\n" for run in runs if run.error))
504
+ failed: bool = any(run.error for run in runs)
505
+ status: int
506
+ if options.mode is Mode.WRITE_BASELINE and options.filter.baseline_file is not None:
507
+ file: Path = options.filter.baseline_file
508
+ baselined: Sequence[BaselineRun] = cast("Sequence[BaselineRun]", runs)
509
+ found: dict[str, list[Offence]] = {
510
+ baseline.key(n, file): run.found for n, run in zip(names, baselined, strict=True)
511
+ }
512
+ _ = sys.stdout.write(f"Wrote {baseline.write(file, found)} offence(s) to {file}.\n")
513
+ status = EXIT_CLEAN
514
+ elif options.mode is Mode.COVERAGE:
515
+ status = _coverage(options, names, runs)
516
+ elif options.mode is Mode.DIFF:
517
+ checked: Sequence[CheckRun] = cast("Sequence[CheckRun]", runs)
518
+ diffs: str = "".join(run.text for run in checked)
519
+ _ = sys.stdout.write(diffs)
520
+ status = EXIT_FOUND if diffs else EXIT_CLEAN
521
+ elif options.mode is Mode.FIX and options.input.paths == [STDIN] and runs and not failed:
522
+ fixed: CheckRun = cast("CheckRun", runs[0])
523
+ _ = sys.stdout.write(fixed.text) # standard input, fixed, is the whole output
524
+ status = EXIT_FOUND if any(r.offence.is_error(r.level) for r in fixed.results) else EXIT_CLEAN
525
+ else:
526
+ status = _report(options, runs, len(names))
527
+ return EXIT_ERROR if failed else status
528
+
529
+
530
+ def main(argv: Sequence[str] | None = None) -> int:
531
+ """Run the command.
532
+
533
+ Returns:
534
+ Its exit status.
535
+
536
+ """
537
+ options: Options = Options.parse(argv)
538
+ file: Path | None = options.output.output_file
539
+ stream: TextIO
540
+ with (
541
+ file.open("w", encoding="utf-8", newline="\n")
542
+ if file
543
+ else contextlib.nullcontext(sys.stdout) as stream,
544
+ contextlib.redirect_stdout(stream),
545
+ ):
546
+ status: int = _run(options)
547
+ return EXIT_CLEAN if options.output.exit_zero and status == EXIT_FOUND else status