python-constricter 0.4.1__cp311-cp311-win_amd64.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__.cp311-win_amd64.pyd +0 -0
  29. constricter/fix/__init__.py +2 -0
  30. constricter/fix/core/__init__.cp311-win_amd64.pyd +0 -0
  31. constricter/fix/core/__init__.py +2 -0
  32. constricter/fix/core/asked.cp311-win_amd64.pyd +0 -0
  33. constricter/fix/core/asked.py +56 -0
  34. constricter/fix/core/fixes.cp311-win_amd64.pyd +0 -0
  35. constricter/fix/core/fixes.py +317 -0
  36. constricter/fix/core/imports.cp311-win_amd64.pyd +0 -0
  37. constricter/fix/core/imports.py +580 -0
  38. constricter/fix/core/inherited.cp311-win_amd64.pyd +0 -0
  39. constricter/fix/core/inherited.py +311 -0
  40. constricter/fix/core/known.cp311-win_amd64.pyd +0 -0
  41. constricter/fix/core/known.py +730 -0
  42. constricter/fix/core/signatures.cp311-win_amd64.pyd +0 -0
  43. constricter/fix/core/signatures.py +132 -0
  44. constricter/fix/index/__init__.cp311-win_amd64.pyd +0 -0
  45. constricter/fix/index/__init__.py +2 -0
  46. constricter/fix/index/awaits.cp311-win_amd64.pyd +0 -0
  47. constricter/fix/index/awaits.py +90 -0
  48. constricter/fix/index/beyond.cp311-win_amd64.pyd +0 -0
  49. constricter/fix/index/beyond.py +149 -0
  50. constricter/fix/index/callers.cp311-win_amd64.pyd +0 -0
  51. constricter/fix/index/callers.py +155 -0
  52. constricter/fix/index/crossed.cp311-win_amd64.pyd +0 -0
  53. constricter/fix/index/crossed.py +327 -0
  54. constricter/fix/index/decorated.cp311-win_amd64.pyd +0 -0
  55. constricter/fix/index/decorated.py +140 -0
  56. constricter/fix/index/fixtures.cp311-win_amd64.pyd +0 -0
  57. constricter/fix/index/fixtures.py +176 -0
  58. constricter/fix/index/linked.cp311-win_amd64.pyd +0 -0
  59. constricter/fix/index/linked.py +142 -0
  60. constricter/fix/index/loose.cp311-win_amd64.pyd +0 -0
  61. constricter/fix/index/loose.py +103 -0
  62. constricter/fix/index/managed.cp311-win_amd64.pyd +0 -0
  63. constricter/fix/index/managed.py +63 -0
  64. constricter/fix/index/modules.cp311-win_amd64.pyd +0 -0
  65. constricter/fix/index/modules.py +741 -0
  66. constricter/fix/index/offers.cp311-win_amd64.pyd +0 -0
  67. constricter/fix/index/offers.py +252 -0
  68. constricter/fix/index/order.cp311-win_amd64.pyd +0 -0
  69. constricter/fix/index/order.py +155 -0
  70. constricter/fix/index/origins.cp311-win_amd64.pyd +0 -0
  71. constricter/fix/index/origins.py +238 -0
  72. constricter/fix/index/own_methods.cp311-win_amd64.pyd +0 -0
  73. constricter/fix/index/own_methods.py +91 -0
  74. constricter/fix/index/own_overloads.cp311-win_amd64.pyd +0 -0
  75. constricter/fix/index/own_overloads.py +70 -0
  76. constricter/fix/index/own_types.cp311-win_amd64.pyd +0 -0
  77. constricter/fix/index/own_types.py +330 -0
  78. constricter/fix/index/params.cp311-win_amd64.pyd +0 -0
  79. constricter/fix/index/params.py +81 -0
  80. constricter/fix/index/plain.cp311-win_amd64.pyd +0 -0
  81. constricter/fix/index/plain.py +221 -0
  82. constricter/fix/index/project.cp311-win_amd64.pyd +0 -0
  83. constricter/fix/index/project.py +797 -0
  84. constricter/fix/index/sides.cp311-win_amd64.pyd +0 -0
  85. constricter/fix/index/sides.py +160 -0
  86. constricter/fix/index/strings.cp311-win_amd64.pyd +0 -0
  87. constricter/fix/index/strings.py +46 -0
  88. constricter/fix/index/stubs/__init__.cp311-win_amd64.pyd +0 -0
  89. constricter/fix/index/stubs/__init__.py +2 -0
  90. constricter/fix/index/stubs/atoms.cp311-win_amd64.pyd +0 -0
  91. constricter/fix/index/stubs/atoms.py +425 -0
  92. constricter/fix/index/stubs/chained.cp311-win_amd64.pyd +0 -0
  93. constricter/fix/index/stubs/chained.py +69 -0
  94. constricter/fix/index/stubs/classnames.cp311-win_amd64.pyd +0 -0
  95. constricter/fix/index/stubs/classnames.py +124 -0
  96. constricter/fix/index/stubs/declared.cp311-win_amd64.pyd +0 -0
  97. constricter/fix/index/stubs/declared.py +508 -0
  98. constricter/fix/index/stubs/installed.cp311-win_amd64.pyd +0 -0
  99. constricter/fix/index/stubs/installed.py +425 -0
  100. constricter/fix/index/stubs/names.cp311-win_amd64.pyd +0 -0
  101. constricter/fix/index/stubs/names.py +200 -0
  102. constricter/fix/index/stubs/read.cp311-win_amd64.pyd +0 -0
  103. constricter/fix/index/stubs/read.py +70 -0
  104. constricter/fix/index/stubs/signatures.cp311-win_amd64.pyd +0 -0
  105. constricter/fix/index/stubs/signatures.py +656 -0
  106. constricter/fix/index/stubs/stubbed.cp311-win_amd64.pyd +0 -0
  107. constricter/fix/index/stubs/stubbed.py +208 -0
  108. constricter/fix/index/taken.cp311-win_amd64.pyd +0 -0
  109. constricter/fix/index/taken.py +45 -0
  110. constricter/fix/index/tuples.cp311-win_amd64.pyd +0 -0
  111. constricter/fix/index/tuples.py +112 -0
  112. constricter/fix/libraries/__init__.cp311-win_amd64.pyd +0 -0
  113. constricter/fix/libraries/__init__.py +2 -0
  114. constricter/fix/libraries/library.cp311-win_amd64.pyd +0 -0
  115. constricter/fix/libraries/library.py +420 -0
  116. constricter/fix/libraries/opened.cp311-win_amd64.pyd +0 -0
  117. constricter/fix/libraries/opened.py +147 -0
  118. constricter/fix/libraries/overloads.cp311-win_amd64.pyd +0 -0
  119. constricter/fix/libraries/overloads.py +960 -0
  120. constricter/fix/libraries/receivers.cp311-win_amd64.pyd +0 -0
  121. constricter/fix/libraries/receivers.py +274 -0
  122. constricter/fix/libraries/stdlib.cp311-win_amd64.pyd +0 -0
  123. constricter/fix/libraries/stdlib.py +752 -0
  124. constricter/fix/libraries/walks.cp311-win_amd64.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__.cp311-win_amd64.pyd +0 -0
  148. constricter/fix/values/__init__.py +2 -0
  149. constricter/fix/values/aliased.cp311-win_amd64.pyd +0 -0
  150. constricter/fix/values/aliased.py +195 -0
  151. constricter/fix/values/bodies.cp311-win_amd64.pyd +0 -0
  152. constricter/fix/values/bodies.py +76 -0
  153. constricter/fix/values/callables.cp311-win_amd64.pyd +0 -0
  154. constricter/fix/values/callables.py +323 -0
  155. constricter/fix/values/called.cp311-win_amd64.pyd +0 -0
  156. constricter/fix/values/called.py +194 -0
  157. constricter/fix/values/classvars.cp311-win_amd64.pyd +0 -0
  158. constricter/fix/values/classvars.py +629 -0
  159. constricter/fix/values/decided.cp311-win_amd64.pyd +0 -0
  160. constricter/fix/values/decided.py +429 -0
  161. constricter/fix/values/displays.cp311-win_amd64.pyd +0 -0
  162. constricter/fix/values/displays.py +101 -0
  163. constricter/fix/values/doubts.cp311-win_amd64.pyd +0 -0
  164. constricter/fix/values/doubts.py +677 -0
  165. constricter/fix/values/entered.cp311-win_amd64.pyd +0 -0
  166. constricter/fix/values/entered.py +332 -0
  167. constricter/fix/values/fills.cp311-win_amd64.pyd +0 -0
  168. constricter/fix/values/fills.py +446 -0
  169. constricter/fix/values/guesses.cp311-win_amd64.pyd +0 -0
  170. constricter/fix/values/guesses.py +376 -0
  171. constricter/fix/values/hinted.cp311-win_amd64.pyd +0 -0
  172. constricter/fix/values/hinted.py +811 -0
  173. constricter/fix/values/inference.cp311-win_amd64.pyd +0 -0
  174. constricter/fix/values/inference.py +698 -0
  175. constricter/fix/values/loops.cp311-win_amd64.pyd +0 -0
  176. constricter/fix/values/loops.py +335 -0
  177. constricter/fix/values/members.cp311-win_amd64.pyd +0 -0
  178. constricter/fix/values/members.py +578 -0
  179. constricter/fix/values/narrowed.cp311-win_amd64.pyd +0 -0
  180. constricter/fix/values/narrowed.py +220 -0
  181. constricter/fix/values/operated.cp311-win_amd64.pyd +0 -0
  182. constricter/fix/values/operated.py +202 -0
  183. constricter/fix/values/ordered.cp311-win_amd64.pyd +0 -0
  184. constricter/fix/values/ordered.py +74 -0
  185. constricter/fix/values/returned.cp311-win_amd64.pyd +0 -0
  186. constricter/fix/values/returned.py +479 -0
  187. constricter/fix/values/returns.cp311-win_amd64.pyd +0 -0
  188. constricter/fix/values/returns.py +237 -0
  189. constricter/fix/values/shapes.cp311-win_amd64.pyd +0 -0
  190. constricter/fix/values/shapes.py +654 -0
  191. constricter/fix/values/stored.cp311-win_amd64.pyd +0 -0
  192. constricter/fix/values/stored.py +564 -0
  193. constricter/fix/values/targets.cp311-win_amd64.pyd +0 -0
  194. constricter/fix/values/targets.py +419 -0
  195. constricter/fix/values/unchecked.cp311-win_amd64.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__.cp311-win_amd64.pyd +0 -0
  208. constricter/rules/__init__.py +2 -0
  209. constricter/rules/annotations.cp311-win_amd64.pyd +0 -0
  210. constricter/rules/annotations.py +296 -0
  211. constricter/rules/binding.cp311-win_amd64.pyd +0 -0
  212. constricter/rules/binding.py +652 -0
  213. constricter/rules/calls.cp311-win_amd64.pyd +0 -0
  214. constricter/rules/calls.py +460 -0
  215. constricter/rules/checker.cp311-win_amd64.pyd +0 -0
  216. constricter/rules/checker.py +536 -0
  217. constricter/rules/declarations.cp311-win_amd64.pyd +0 -0
  218. constricter/rules/declarations.py +730 -0
  219. constricter/rules/decorators.cp311-win_amd64.pyd +0 -0
  220. constricter/rules/decorators.py +351 -0
  221. constricter/rules/flow.cp311-win_amd64.pyd +0 -0
  222. constricter/rules/flow.py +502 -0
  223. constricter/rules/keys.cp311-win_amd64.pyd +0 -0
  224. constricter/rules/keys.py +79 -0
  225. constricter/rules/late.cp311-win_amd64.pyd +0 -0
  226. constricter/rules/late.py +969 -0
  227. constricter/rules/narrowing.cp311-win_amd64.pyd +0 -0
  228. constricter/rules/narrowing.py +98 -0
  229. constricter/rules/parsed.cp311-win_amd64.pyd +0 -0
  230. constricter/rules/parsed.py +136 -0
  231. constricter/rules/quoted.cp311-win_amd64.pyd +0 -0
  232. constricter/rules/quoted.py +141 -0
  233. constricter/rules/rebinding.cp311-win_amd64.pyd +0 -0
  234. constricter/rules/rebinding.py +169 -0
  235. constricter/rules/recorded.cp311-win_amd64.pyd +0 -0
  236. constricter/rules/recorded.py +168 -0
  237. constricter/rules/redundant.cp311-win_amd64.pyd +0 -0
  238. constricter/rules/redundant.py +160 -0
  239. constricter/rules/scope.cp311-win_amd64.pyd +0 -0
  240. constricter/rules/scope.py +802 -0
  241. constricter/rules/scoped.cp311-win_amd64.pyd +0 -0
  242. constricter/rules/scoped.py +232 -0
  243. constricter/rules/scoping.cp311-win_amd64.pyd +0 -0
  244. constricter/rules/scoping.py +346 -0
  245. constricter/rules/settings.cp311-win_amd64.pyd +0 -0
  246. constricter/rules/settings.py +189 -0
  247. constricter/rules/syntax.cp311-win_amd64.pyd +0 -0
  248. constricter/rules/syntax.py +472 -0
  249. constricter/rules/tables.cp311-win_amd64.pyd +0 -0
  250. constricter/rules/tables.py +169 -0
  251. constricter/rules/walked.cp311-win_amd64.pyd +0 -0
  252. constricter/rules/walked.py +127 -0
  253. constricter/rules/widened.cp311-win_amd64.pyd +0 -0
  254. constricter/rules/widened.py +322 -0
  255. constricter/trace.py +374 -0
  256. e45ce06b2fdd5c43f782__mypyc.cp311-win_amd64.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,799 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """The command's options: parsed from the command line over `[tool.constricter]`'s defaults."""
3
+
4
+ import argparse
5
+ import os
6
+ import sys
7
+ from collections.abc import Callable, Sequence
8
+ from dataclasses import dataclass, field
9
+ from enum import Enum
10
+ from pathlib import Path
11
+ from typing import Final, cast
12
+
13
+ from constricter import __version__
14
+ from constricter.cli import baseline, traced
15
+ from constricter.cli.config import (
16
+ DEFAULT_BASELINE,
17
+ config_defaults,
18
+ project_root,
19
+ unknown_codes,
20
+ unknown_fix_kinds,
21
+ unknown_widen_kinds,
22
+ widen_kinds,
23
+ )
24
+ from constricter.cli.explain import explain
25
+ from constricter.cli.lsp.protocol import SERVERS, runs
26
+ from constricter.cli.paths import STDIN, excluded
27
+ from constricter.cli.report import Format, Result
28
+ from constricter.offences import (
29
+ LEVELS,
30
+ MAX_LENGTH,
31
+ MESSAGES,
32
+ NESTING,
33
+ OPT_IN,
34
+ PLAIN_BASES,
35
+ VAGUE,
36
+ Checks,
37
+ FixPolicy,
38
+ Level,
39
+ Offence,
40
+ Tier,
41
+ )
42
+
43
+ _ALL: Final = 100 # percent
44
+
45
+
46
+ def _at_least(minimum: int) -> Callable[[str], int]:
47
+ """Make a reader of whole numbers, for `--nesting`, `--vague` and `--jobs`.
48
+
49
+ Returns:
50
+ A reader that rejects numbers below `minimum`.
51
+
52
+ """
53
+
54
+ def read(text: str) -> int:
55
+ """Read `text`.
56
+
57
+ Returns:
58
+ The whole number `text` is.
59
+
60
+ Raises:
61
+ argparse.ArgumentTypeError: It isn't one.
62
+
63
+ """
64
+ if not text.removeprefix("-").isdigit() or int(text) < minimum:
65
+ message: str = f"expected a whole number of at least {minimum}, not {text!r}"
66
+ raise argparse.ArgumentTypeError(message)
67
+ return int(text)
68
+
69
+ return read
70
+
71
+
72
+ def _percent(text: str) -> float:
73
+ """Read a percentage, 0 to 100.
74
+
75
+ Returns:
76
+ The percentage.
77
+
78
+ Raises:
79
+ argparse.ArgumentTypeError: `text` isn't one.
80
+
81
+ """
82
+ value: float
83
+ try:
84
+ value = float(text)
85
+ except ValueError:
86
+ value = -1.0
87
+ if not 0 <= value <= _ALL:
88
+ message: str = f"expected a percentage from 0 to 100, not {text!r}"
89
+ raise argparse.ArgumentTypeError(message)
90
+ return value
91
+
92
+
93
+ def _codes(text: str) -> list[str]:
94
+ """Read a comma-separated list of codes or code prefixes (`LVA001,LVA00`).
95
+
96
+ Returns:
97
+ The codes, upper-cased.
98
+
99
+ Raises:
100
+ argparse.ArgumentTypeError: One matches no code.
101
+
102
+ """
103
+ codes: list[str] = [code.strip().upper() for code in text.split(",") if code.strip()]
104
+ unknown: list[str]
105
+ if unknown := unknown_codes(codes):
106
+ message: str = f"no code starts with {', '.join(unknown)}"
107
+ raise argparse.ArgumentTypeError(message)
108
+ return codes
109
+
110
+
111
+ def _listed(text: str) -> list[str]:
112
+ """Read a comma-separated list of names (`pkg.Base,!pkg.Enum`).
113
+
114
+ Returns:
115
+ Them.
116
+
117
+ """
118
+ return [name.strip() for name in text.split(",") if name.strip()]
119
+
120
+
121
+ def _fix_kinds(text: str) -> list[str]:
122
+ """Read a comma-separated list of `--fix` mechanisms (`copy,constructor`).
123
+
124
+ Returns:
125
+ Them.
126
+
127
+ Raises:
128
+ argparse.ArgumentTypeError: One isn't a mechanism's id.
129
+
130
+ """
131
+ kinds: list[str] = [kind.strip() for kind in text.split(",") if kind.strip()]
132
+ unknown: list[str]
133
+ if unknown := unknown_fix_kinds(kinds):
134
+ message: str = f"no --fix mechanism is called {', '.join(unknown)} (see docs/FIXES.md)"
135
+ raise argparse.ArgumentTypeError(message)
136
+ return kinds
137
+
138
+
139
+ def _widen_kinds(text: str) -> list[str]:
140
+ """Read a comma-separated list of the wider types `--fix` may write (`untyped-parameters`), or `all`.
141
+
142
+ Returns:
143
+ Them.
144
+
145
+ Raises:
146
+ argparse.ArgumentTypeError: One isn't a widening's id.
147
+
148
+ """
149
+ kinds: list[str] = [kind.strip() for kind in text.split(",") if kind.strip()]
150
+ unknown: list[str]
151
+ if unknown := unknown_widen_kinds(kinds):
152
+ message: str = f"no wider type is called {', '.join(unknown)} (see docs/FIXES.md)"
153
+ raise argparse.ArgumentTypeError(message)
154
+ return widen_kinds(kinds)
155
+
156
+
157
+ def _version(text: str) -> tuple[int, int]:
158
+ """Read `--min-python`: a Python version (`3.11`).
159
+
160
+ Returns:
161
+ It.
162
+
163
+ Raises:
164
+ argparse.ArgumentTypeError: It isn't one.
165
+
166
+ """
167
+ major: str
168
+ minor: str
169
+ major, _, minor = text.partition(".")
170
+ if not (major.isdigit() and minor.isdigit()):
171
+ message: str = f"expected a Python version like 3.11, got {text!r}"
172
+ raise argparse.ArgumentTypeError(message)
173
+ return int(major), int(minor)
174
+
175
+
176
+ def _gigabytes(text: str) -> float:
177
+ """Read `--infer-memory`: a positive number of gigabytes (GiB).
178
+
179
+ Returns:
180
+ It.
181
+
182
+ Raises:
183
+ argparse.ArgumentTypeError: It isn't one.
184
+
185
+ """
186
+ try:
187
+ value: float = float(text)
188
+ except ValueError:
189
+ value = 0.0
190
+ if not value > 0:
191
+ message: str = f"expected a positive number of gigabytes, got {text!r}"
192
+ raise argparse.ArgumentTypeError(message)
193
+ return value
194
+
195
+
196
+ def _bytes(gigabytes: float | None) -> int | None:
197
+ """Turn gigabytes (GiB) into bytes.
198
+
199
+ Returns:
200
+ Them, or `None` for none.
201
+
202
+ """
203
+ return None if gigabytes is None else int(gigabytes * (1 << 30))
204
+
205
+
206
+ def _checkers(text: str) -> list[str]:
207
+ """Read `--infer-with`'s checkers: comma-separated, each once, in the order they're preferred.
208
+
209
+ Returns:
210
+ Them.
211
+
212
+ Raises:
213
+ argparse.ArgumentTypeError: One isn't a checker it knows.
214
+
215
+ """
216
+ checkers: list[str] = [checker.strip() for checker in text.split(",") if checker.strip()]
217
+ unknown: list[str]
218
+ if unknown := [checker for checker in checkers if checker not in SERVERS]:
219
+ message: str = f"unknown checker {', '.join(unknown)} (known: {', '.join(sorted(SERVERS))})"
220
+ raise argparse.ArgumentTypeError(message)
221
+ return list(dict.fromkeys(checkers))
222
+
223
+
224
+ def _parser() -> argparse.ArgumentParser:
225
+ parser: argparse.ArgumentParser = argparse.ArgumentParser(
226
+ prog="constricter",
227
+ description="Report local variables that aren't typed where they're first bound.",
228
+ )
229
+ _ = parser.add_argument(
230
+ "paths",
231
+ nargs="*",
232
+ type=Path,
233
+ help="files and directories (default: .); `-` reads standard input",
234
+ )
235
+ _ = parser.add_argument(
236
+ "--exclude",
237
+ action="append",
238
+ default=[],
239
+ metavar="GLOB",
240
+ help="skip a path, file or directory matching this glob (repeatable), e.g. 'tests/fixtures/*'",
241
+ )
242
+ _ = parser.add_argument(
243
+ "--gitignore",
244
+ action=argparse.BooleanOptionalAction,
245
+ default=True,
246
+ help="in a git checkout, skip what git ignores under a directory (--no-gitignore walks it whole)",
247
+ )
248
+ _ = parser.add_argument(
249
+ "--level",
250
+ choices=LEVELS,
251
+ default="strict",
252
+ help="which codes are errors rather than warnings (default: strict)",
253
+ )
254
+ _ = parser.add_argument(
255
+ "--max",
256
+ action="store_true",
257
+ help="the strictest check: suffocate everywhere, all scopes, the opt-in codes (command line only)",
258
+ )
259
+ _ = parser.add_argument(
260
+ "--max-fix",
261
+ action="store_true",
262
+ help="--max, --fix --unsafe-fixes, and every installed checker's hints (command line only)",
263
+ )
264
+ _ = parser.add_argument(
265
+ "--format",
266
+ type=Format,
267
+ choices=list(Format),
268
+ default=Format.TEXT,
269
+ help="output format (default: text)",
270
+ )
271
+ _ = parser.add_argument(
272
+ "--type-comments",
273
+ action="store_true",
274
+ help="count `x = 1 # type: int` as annotated",
275
+ )
276
+ _ = parser.add_argument(
277
+ "--all-scopes",
278
+ action="store_true",
279
+ help="also check module and class bodies (LVA004)",
280
+ )
281
+ _ = parser.add_argument(
282
+ "--nesting",
283
+ type=_at_least(1),
284
+ default=NESTING,
285
+ metavar="N",
286
+ help=f"report an annotation nested N deep (LVA006; default: {NESTING})",
287
+ )
288
+ _ = parser.add_argument(
289
+ "--max-length",
290
+ type=_at_least(1),
291
+ default=MAX_LENGTH,
292
+ metavar="N",
293
+ help=f"report a fixed-length tuple annotation of more than N types (LVA011; default: {MAX_LENGTH})",
294
+ )
295
+ _ = parser.add_argument(
296
+ "--vague",
297
+ type=_at_least(-1),
298
+ default=VAGUE,
299
+ metavar="LEVEL",
300
+ help=(
301
+ "how vague an annotation may be before it's LVA005 (and a fix isn't offered): -1 none, 0 one "
302
+ f"Any inside a type that says the rest, N >= 1 N+1 of them, or one alone (default: {VAGUE})"
303
+ ),
304
+ )
305
+ _ = parser.add_argument(
306
+ "--min-python",
307
+ type=_version,
308
+ metavar="VERSION",
309
+ help="the oldest Python the code runs on, whose syntax --fix writes (default: requires-python's)",
310
+ )
311
+ _ = parser.add_argument(
312
+ "--select",
313
+ type=_codes,
314
+ default=[],
315
+ metavar="CODES",
316
+ help="report only these codes or prefixes (LVA001,LVA00)",
317
+ )
318
+ _ = parser.add_argument(
319
+ "--extend-select",
320
+ type=_codes,
321
+ default=[],
322
+ metavar="CODES",
323
+ help="also report these codes (an opt-in one, like LVA012, by its full code)",
324
+ )
325
+ _ = parser.add_argument(
326
+ "--ignore",
327
+ type=_codes,
328
+ default=[],
329
+ metavar="CODES",
330
+ help="don't report these codes or prefixes",
331
+ )
332
+ _ = parser.add_argument(
333
+ "--fix",
334
+ action="store_true",
335
+ help="add the annotations a value makes unambiguous, in place",
336
+ )
337
+ _ = parser.add_argument(
338
+ "--unsafe-fixes",
339
+ action="store_true",
340
+ help="with --fix or --diff: also apply guesses (a call to a class that may be generic)",
341
+ )
342
+ _ = parser.add_argument(
343
+ "--likely",
344
+ action="store_true",
345
+ help="with --fix or --diff: also apply the guesses measured to hold (see docs/LIKELY.md)",
346
+ )
347
+ _ = parser.add_argument(
348
+ "--fix-select",
349
+ type=_fix_kinds,
350
+ default=[],
351
+ metavar="KINDS",
352
+ help="offer only fixes these mechanisms decide (literal,copy,...; default: all)",
353
+ )
354
+ _ = parser.add_argument(
355
+ "--fix-ignore",
356
+ type=_fix_kinds,
357
+ default=[],
358
+ metavar="KINDS",
359
+ help="never offer a fix one of these mechanisms decided",
360
+ )
361
+ _ = parser.add_argument(
362
+ "--unsafe-fix-select",
363
+ type=_fix_kinds,
364
+ default=[],
365
+ metavar="KINDS",
366
+ help="treat guesses from these mechanisms (constructor, narrow) as certain",
367
+ )
368
+ _ = parser.add_argument(
369
+ "--fix-widen",
370
+ type=_widen_kinds,
371
+ default=[],
372
+ metavar="KINDS",
373
+ help="also write these wider types, each marked (untyped-parameters, unions, ...; all: every one)",
374
+ )
375
+ _ = parser.add_argument(
376
+ "--fix-plain-bases",
377
+ type=_listed,
378
+ default=[],
379
+ metavar="BASES",
380
+ help="bases whose class bodies --fix annotates as a plain class's (pkg.Base,pkg,!pkg.Enum)",
381
+ )
382
+ _ = parser.add_argument(
383
+ "--infer-with",
384
+ type=_checkers,
385
+ default=[],
386
+ metavar="CHECKERS",
387
+ help="type what --fix can't with checkers' inferred types, as guesses (basedpyright,ty,pyrefly)",
388
+ )
389
+ _ = parser.add_argument(
390
+ "--infer-memory",
391
+ type=_gigabytes,
392
+ metavar="GB",
393
+ help="with --infer-with: the most memory each checker's servers use together (default: 8)",
394
+ )
395
+ _ = parser.add_argument(
396
+ "--infer-fresh",
397
+ action="store_true",
398
+ help="with --infer-with: ask the checkers again, not for hints kept from an earlier run",
399
+ )
400
+ _ = parser.add_argument(
401
+ "--infer-from",
402
+ type=Path,
403
+ metavar="FILE",
404
+ help="type what --fix can't with the types `python -m constricter.trace` recorded, as guesses",
405
+ )
406
+ _ = parser.add_argument(
407
+ "--diff",
408
+ action="store_true",
409
+ help="print what --fix would change, and change nothing",
410
+ )
411
+ _ = parser.add_argument(
412
+ "--show-fixes",
413
+ action="store_true",
414
+ help="after the report, list each fix and how the value decided it; with --fix, those made too",
415
+ )
416
+ _ = parser.add_argument(
417
+ "--statistics",
418
+ action="store_true",
419
+ help="print counts per code instead of each offence (text)",
420
+ )
421
+ _ = parser.add_argument(
422
+ "--coverage",
423
+ action="store_true",
424
+ help="print the share of first bindings that are typed, per file",
425
+ )
426
+ _ = parser.add_argument(
427
+ "--fail-under",
428
+ type=_percent,
429
+ metavar="PCT",
430
+ help="with --coverage (which it implies): exit 1 if the typed share is below PCT",
431
+ )
432
+ _ = parser.add_argument(
433
+ "--baseline",
434
+ type=Path,
435
+ metavar="FILE",
436
+ help="don't report the offences recorded in this baseline file",
437
+ )
438
+ _ = parser.add_argument(
439
+ "--write-baseline",
440
+ action="store_true",
441
+ help=f"record every offence found in the baseline file (default: {DEFAULT_BASELINE}), and exit",
442
+ )
443
+ _ = parser.add_argument(
444
+ "--explain",
445
+ choices=list(MESSAGES),
446
+ metavar="CODE",
447
+ help="explain a code and exit",
448
+ )
449
+ _ = parser.add_argument(
450
+ "--jobs",
451
+ "-j",
452
+ type=_at_least(0),
453
+ default=1,
454
+ metavar="N",
455
+ help="check N files at a time (0: one per CPU; default: 1)",
456
+ )
457
+ _ = parser.add_argument(
458
+ "--stdin-filename",
459
+ type=Path,
460
+ default=STDIN,
461
+ metavar="PATH",
462
+ help="with `-` as the path: the name to report standard input under (a `.ipynb` is a notebook)",
463
+ )
464
+ _ = parser.add_argument("--exit-zero", action="store_true", help="exit 0 even when offences are errors")
465
+ _ = parser.add_argument(
466
+ "--output-file",
467
+ type=Path,
468
+ metavar="FILE",
469
+ help="write the report to FILE, not stdout",
470
+ )
471
+ _ = parser.add_argument("--quiet", "-q", action="store_true", help="don't print the text summary line")
472
+ _ = parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
473
+ return parser
474
+
475
+
476
+ class Mode(Enum):
477
+ """What to do with what's found."""
478
+
479
+ CHECK = "check"
480
+ FIX = "fix"
481
+ DIFF = "diff"
482
+ WRITE_BASELINE = "write-baseline"
483
+ COVERAGE = "coverage"
484
+
485
+
486
+ @dataclass(frozen=True)
487
+ class Input:
488
+ """What to check: paths (`-` is standard input, reported as `stdin_name`) and globs to skip."""
489
+
490
+ paths: list[Path]
491
+ exclude: list[str]
492
+ stdin_name: Path = STDIN
493
+ trace: Path | None = None # the trace whose types `--fix` guesses with (`--infer-from`)
494
+ gitignore: bool = True # under a directory in a git checkout, only what git tracks or would
495
+
496
+ def name(self, path: Path) -> Path:
497
+ """Name `path` for reports.
498
+
499
+ Returns:
500
+ The name it's reported, levelled and baselined under.
501
+
502
+ """
503
+ return self.stdin_name if path == STDIN else path
504
+
505
+
506
+ @dataclass(frozen=True)
507
+ class Filter:
508
+ """Which offences are reported, and at which level."""
509
+
510
+ level: Level
511
+ per_path: dict[str, str]
512
+ select: list[str]
513
+ ignore: list[str]
514
+ per_file_ignores: dict[str, list[str]] = field(default_factory=dict[str, list[str]])
515
+ baseline_file: Path | None = None
516
+ entries: baseline.Entries = field(default_factory=dict[str, dict[str, int]])
517
+
518
+ def unbaselined(self, path: Path, offences: list[Offence]) -> tuple[list[Offence], int]:
519
+ """Match `path`'s offences against the baseline.
520
+
521
+ Returns:
522
+ The offences it doesn't cover, and how many it does.
523
+
524
+ """
525
+ if self.baseline_file is None:
526
+ return offences, 0
527
+ return baseline.remaining(offences, self.entries.get(baseline.key(path, self.baseline_file), {}))
528
+
529
+ def level_for(self, path: Path) -> Level:
530
+ """Decide `path`'s level.
531
+
532
+ Returns:
533
+ The level of the first `per-path-levels` glob it matches, else `--level`'s.
534
+
535
+ """
536
+ text: str = path.as_posix()
537
+ glob: str
538
+ level: str
539
+ for glob, level in self.per_path.items():
540
+ if excluded(path, [glob], text):
541
+ return LEVELS[level]
542
+ return self.level
543
+
544
+ def results(self, path: Path, offences: Sequence[Offence]) -> list[Result]:
545
+ """Filter `path`'s offences.
546
+
547
+ Returns:
548
+ Those reported at its level.
549
+
550
+ """
551
+ level: Level = self.level_for(path)
552
+ text: str = path.as_posix()
553
+ ignored: tuple[str, ...] = (
554
+ *self.ignore,
555
+ *(
556
+ code
557
+ for glob, codes in self.per_file_ignores.items()
558
+ if excluded(path, [glob], text)
559
+ for code in codes
560
+ ),
561
+ )
562
+ return [
563
+ Result(path, o, level)
564
+ for o in offences
565
+ if o.is_reported(level)
566
+ and (not self.select or o.code.startswith(tuple(self.select)))
567
+ and not o.code.startswith(ignored)
568
+ ]
569
+
570
+
571
+ @dataclass(frozen=True)
572
+ class Output:
573
+ """How the report looks, where it goes, and how the command exits."""
574
+
575
+ fmt: Format
576
+ statistics: bool
577
+ quiet: bool
578
+ show_fixes: bool = False
579
+ fail_under: float | None = None # --coverage's threshold
580
+ exit_zero: bool = False
581
+ output_file: Path | None = None
582
+
583
+
584
+ class Guesses(Enum):
585
+ """Which guesses `--fix` and `--diff` apply: none, `--likely`'s, or every one (`--unsafe-fixes`)."""
586
+
587
+ NONE = "none"
588
+ LIKELY = "likely"
589
+ ALL = "all"
590
+
591
+ @classmethod
592
+ def of(cls, *, unsafe: bool, likely: bool) -> "Guesses":
593
+ """Read the two flags.
594
+
595
+ Returns:
596
+ `ALL` with `--unsafe-fixes`, whatever `--likely` says.
597
+
598
+ """
599
+ return cls.ALL if unsafe else cls.LIKELY if likely else cls.NONE
600
+
601
+ def applies(self, offence: Offence) -> bool:
602
+ """Check whether `offence`'s fix is one to apply.
603
+
604
+ Returns:
605
+ Whether it is: a certain one always.
606
+
607
+ """
608
+ tier: Tier | None = offence.tier
609
+ return (
610
+ self is Guesses.ALL
611
+ or tier in {None, Tier.CERTAIN}
612
+ or (self is Guesses.LIKELY and tier is Tier.LIKELY)
613
+ )
614
+
615
+
616
+ @dataclass(frozen=True)
617
+ class Options: # pylint: disable=too-many-instance-attributes
618
+ """The command's parsed options."""
619
+
620
+ input: Input
621
+ checks: Checks
622
+ guesses: Guesses
623
+ filter: Filter
624
+ output: Output
625
+ mode: Mode
626
+ jobs: int
627
+ infer_with: tuple[str, ...] = () # the type checkers whose inferred types `--fix` guesses with
628
+ infer_memory: int | None = None # bytes each checker's servers may use together (`--infer-memory`)
629
+ infer_fresh: bool = False # ask the checkers again, not for hints kept from an earlier run
630
+
631
+ @classmethod
632
+ def parse(cls, argv: Sequence[str] | None) -> "Options":
633
+ """Parse `argv` over the defaults `pyproject.toml` sets; `--explain` prints and exits.
634
+
635
+ Returns:
636
+ The options.
637
+
638
+ """
639
+ parser: argparse.ArgumentParser = _parser()
640
+ try:
641
+ parser.set_defaults(**config_defaults(Path.cwd()))
642
+ except ValueError as error:
643
+ parser.error(str(error))
644
+ args: argparse.Namespace = _maximal(parser.parse_args(argv))
645
+ code: str | None
646
+ if (code := cast("str | None", args.explain)) is not None:
647
+ _ = sys.stdout.write(explain(code))
648
+ parser.exit()
649
+ paths: list[Path] = cast("list[Path]", args.paths) or [Path()]
650
+ if STDIN in paths and len(paths) > 1:
651
+ parser.error("`-` (standard input) must be the only path")
652
+ mode: Mode = _mode(parser, args)
653
+ return cls(
654
+ input=Input(
655
+ paths,
656
+ cast("list[str]", args.exclude),
657
+ cast("Path", args.stdin_filename),
658
+ _trace(parser, cast("Path | None", args.infer_from)),
659
+ gitignore=cast("bool", args.gitignore),
660
+ ),
661
+ checks=Checks(
662
+ type_comments=cast("bool", args.type_comments),
663
+ all_scopes=cast("bool", args.all_scopes),
664
+ nesting=cast("int", args.nesting),
665
+ max_length=cast("int", args.max_length),
666
+ vague=cast("int", args.vague),
667
+ narrower=tuple(
668
+ (name, tuple(wider))
669
+ for name, wider in cast("dict[str, list[str]]", getattr(args, "narrower", {})).items()
670
+ ),
671
+ final=bool(OPT_IN & {*_select(args), *cast("list[str]", args.extend_select)}),
672
+ fixes=FixPolicy(
673
+ frozenset(cast("list[str]", args.fix_select)),
674
+ frozenset(cast("list[str]", args.fix_ignore)),
675
+ frozenset(cast("list[str]", args.unsafe_fix_select)),
676
+ frozenset(cast("list[str]", args.fix_widen)),
677
+ ),
678
+ min_python=cast("tuple[int, int] | None", args.min_python),
679
+ plain_bases=(*PLAIN_BASES, *cast("list[str]", args.fix_plain_bases)),
680
+ ),
681
+ guesses=Guesses.of(unsafe=cast("bool", args.unsafe_fixes), likely=cast("bool", args.likely)),
682
+ filter=_filter(parser, args, mode),
683
+ output=Output(
684
+ fmt=cast("Format", args.format),
685
+ statistics=cast("bool", args.statistics),
686
+ quiet=cast("bool", args.quiet),
687
+ show_fixes=cast("bool", args.show_fixes),
688
+ fail_under=cast("float | None", args.fail_under),
689
+ exit_zero=cast("bool", args.exit_zero),
690
+ output_file=cast("Path | None", args.output_file),
691
+ ),
692
+ mode=mode,
693
+ # Standard input can only be read once, in this process.
694
+ jobs=1 if paths == [STDIN] else cast("int", args.jobs) or os.cpu_count() or 1,
695
+ infer_with=tuple(cast("list[str]", args.infer_with)),
696
+ infer_memory=_bytes(cast("float | None", args.infer_memory)),
697
+ infer_fresh=cast("bool", args.infer_fresh),
698
+ )
699
+
700
+
701
+ def _trace(parser: argparse.ArgumentParser, path: Path | None) -> Path | None:
702
+ """Check that `--infer-from`'s file is a trace, read here once for the run.
703
+
704
+ Returns:
705
+ The path.
706
+
707
+ """
708
+ try:
709
+ _ = None if path is None else traced.load(path)
710
+ except ValueError as error:
711
+ parser.error(str(error))
712
+ return path
713
+
714
+
715
+ def _maximal(args: argparse.Namespace) -> argparse.Namespace:
716
+ """Apply `--max-fix` (`--max`, every fix, every installed checker) and `--max`.
717
+
718
+ `--max`: `suffocate` for every path (over `per-path-levels`), all scopes, the opt-in codes.
719
+
720
+ Returns:
721
+ The arguments.
722
+
723
+ """
724
+ if cast("bool", args.max_fix):
725
+ args.max = args.fix = args.unsafe_fixes = True
726
+ chosen: list[str] = cast("list[str]", args.infer_with)
727
+ args.infer_with = [
728
+ *chosen,
729
+ *(checker for checker in SERVERS if checker not in chosen and runs(checker)),
730
+ ]
731
+ if cast("bool", args.max):
732
+ args.level = Level.SUFFOCATE.name.lower()
733
+ args.per_path_levels = {}
734
+ args.all_scopes = True
735
+ args.extend_select = [*cast("list[str]", args.extend_select), *sorted(OPT_IN)]
736
+ return args
737
+
738
+
739
+ def _filter(parser: argparse.ArgumentParser, args: argparse.Namespace, mode: Mode) -> Filter:
740
+ """Build the filter the options set, reading the baseline (the default one, if it exists).
741
+
742
+ Returns:
743
+ The filter.
744
+
745
+ """
746
+ default: Path = project_root(Path.cwd()) / DEFAULT_BASELINE
747
+ baseline_path: Path | None = cast("Path | None", args.baseline) or (
748
+ default if mode is Mode.WRITE_BASELINE or default.is_file() else None
749
+ )
750
+ entries: baseline.Entries = {}
751
+ if baseline_path is not None and mode is not Mode.WRITE_BASELINE:
752
+ try:
753
+ entries = baseline.read(baseline_path)
754
+ except ValueError as error:
755
+ parser.error(str(error))
756
+ return Filter(
757
+ level=LEVELS[cast("str", args.level)],
758
+ per_path=cast("dict[str, str]", getattr(args, "per_path_levels", {})),
759
+ select=_select(args) + cast("list[str]", args.extend_select) if _select(args) else [],
760
+ ignore=[c.upper() for c in cast("list[str]", args.ignore)],
761
+ per_file_ignores=cast("dict[str, list[str]]", getattr(args, "per_file_ignores", {})),
762
+ baseline_file=baseline_path,
763
+ entries=entries,
764
+ )
765
+
766
+
767
+ def _select(args: argparse.Namespace) -> list[str]:
768
+ """Read `--select`.
769
+
770
+ Returns:
771
+ Its codes, upper-cased.
772
+
773
+ """
774
+ return [c.upper() for c in cast("list[str]", args.select)]
775
+
776
+
777
+ def _mode(parser: argparse.ArgumentParser, args: argparse.Namespace) -> Mode:
778
+ """Decide the mode; more than one of `--fix`, `--diff` and `--write-baseline` is an error.
779
+
780
+ Returns:
781
+ What the options ask for.
782
+
783
+ """
784
+ modes: list[Mode] = [
785
+ mode
786
+ for mode, chosen in (
787
+ (Mode.FIX, cast("bool", args.fix)),
788
+ (Mode.DIFF, cast("bool", args.diff)),
789
+ (Mode.WRITE_BASELINE, cast("bool", args.write_baseline)),
790
+ (
791
+ Mode.COVERAGE,
792
+ cast("bool", args.coverage) or cast("float | None", args.fail_under) is not None,
793
+ ),
794
+ )
795
+ if chosen
796
+ ]
797
+ if len(modes) > 1:
798
+ parser.error("--fix, --diff, --write-baseline and --coverage can't be combined")
799
+ return modes[0] if modes else Mode.CHECK