python-constricter 0.4.1__cp311-cp311-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__.cp311-win32.pyd +0 -0
  29. constricter/fix/__init__.py +2 -0
  30. constricter/fix/core/__init__.cp311-win32.pyd +0 -0
  31. constricter/fix/core/__init__.py +2 -0
  32. constricter/fix/core/asked.cp311-win32.pyd +0 -0
  33. constricter/fix/core/asked.py +56 -0
  34. constricter/fix/core/fixes.cp311-win32.pyd +0 -0
  35. constricter/fix/core/fixes.py +317 -0
  36. constricter/fix/core/imports.cp311-win32.pyd +0 -0
  37. constricter/fix/core/imports.py +580 -0
  38. constricter/fix/core/inherited.cp311-win32.pyd +0 -0
  39. constricter/fix/core/inherited.py +311 -0
  40. constricter/fix/core/known.cp311-win32.pyd +0 -0
  41. constricter/fix/core/known.py +730 -0
  42. constricter/fix/core/signatures.cp311-win32.pyd +0 -0
  43. constricter/fix/core/signatures.py +132 -0
  44. constricter/fix/index/__init__.cp311-win32.pyd +0 -0
  45. constricter/fix/index/__init__.py +2 -0
  46. constricter/fix/index/awaits.cp311-win32.pyd +0 -0
  47. constricter/fix/index/awaits.py +90 -0
  48. constricter/fix/index/beyond.cp311-win32.pyd +0 -0
  49. constricter/fix/index/beyond.py +149 -0
  50. constricter/fix/index/callers.cp311-win32.pyd +0 -0
  51. constricter/fix/index/callers.py +155 -0
  52. constricter/fix/index/crossed.cp311-win32.pyd +0 -0
  53. constricter/fix/index/crossed.py +327 -0
  54. constricter/fix/index/decorated.cp311-win32.pyd +0 -0
  55. constricter/fix/index/decorated.py +140 -0
  56. constricter/fix/index/fixtures.cp311-win32.pyd +0 -0
  57. constricter/fix/index/fixtures.py +176 -0
  58. constricter/fix/index/linked.cp311-win32.pyd +0 -0
  59. constricter/fix/index/linked.py +142 -0
  60. constricter/fix/index/loose.cp311-win32.pyd +0 -0
  61. constricter/fix/index/loose.py +103 -0
  62. constricter/fix/index/managed.cp311-win32.pyd +0 -0
  63. constricter/fix/index/managed.py +63 -0
  64. constricter/fix/index/modules.cp311-win32.pyd +0 -0
  65. constricter/fix/index/modules.py +741 -0
  66. constricter/fix/index/offers.cp311-win32.pyd +0 -0
  67. constricter/fix/index/offers.py +252 -0
  68. constricter/fix/index/order.cp311-win32.pyd +0 -0
  69. constricter/fix/index/order.py +155 -0
  70. constricter/fix/index/origins.cp311-win32.pyd +0 -0
  71. constricter/fix/index/origins.py +238 -0
  72. constricter/fix/index/own_methods.cp311-win32.pyd +0 -0
  73. constricter/fix/index/own_methods.py +91 -0
  74. constricter/fix/index/own_overloads.cp311-win32.pyd +0 -0
  75. constricter/fix/index/own_overloads.py +70 -0
  76. constricter/fix/index/own_types.cp311-win32.pyd +0 -0
  77. constricter/fix/index/own_types.py +330 -0
  78. constricter/fix/index/params.cp311-win32.pyd +0 -0
  79. constricter/fix/index/params.py +81 -0
  80. constricter/fix/index/plain.cp311-win32.pyd +0 -0
  81. constricter/fix/index/plain.py +221 -0
  82. constricter/fix/index/project.cp311-win32.pyd +0 -0
  83. constricter/fix/index/project.py +797 -0
  84. constricter/fix/index/sides.cp311-win32.pyd +0 -0
  85. constricter/fix/index/sides.py +160 -0
  86. constricter/fix/index/strings.cp311-win32.pyd +0 -0
  87. constricter/fix/index/strings.py +46 -0
  88. constricter/fix/index/stubs/__init__.cp311-win32.pyd +0 -0
  89. constricter/fix/index/stubs/__init__.py +2 -0
  90. constricter/fix/index/stubs/atoms.cp311-win32.pyd +0 -0
  91. constricter/fix/index/stubs/atoms.py +425 -0
  92. constricter/fix/index/stubs/chained.cp311-win32.pyd +0 -0
  93. constricter/fix/index/stubs/chained.py +69 -0
  94. constricter/fix/index/stubs/classnames.cp311-win32.pyd +0 -0
  95. constricter/fix/index/stubs/classnames.py +124 -0
  96. constricter/fix/index/stubs/declared.cp311-win32.pyd +0 -0
  97. constricter/fix/index/stubs/declared.py +508 -0
  98. constricter/fix/index/stubs/installed.cp311-win32.pyd +0 -0
  99. constricter/fix/index/stubs/installed.py +425 -0
  100. constricter/fix/index/stubs/names.cp311-win32.pyd +0 -0
  101. constricter/fix/index/stubs/names.py +200 -0
  102. constricter/fix/index/stubs/read.cp311-win32.pyd +0 -0
  103. constricter/fix/index/stubs/read.py +70 -0
  104. constricter/fix/index/stubs/signatures.cp311-win32.pyd +0 -0
  105. constricter/fix/index/stubs/signatures.py +656 -0
  106. constricter/fix/index/stubs/stubbed.cp311-win32.pyd +0 -0
  107. constricter/fix/index/stubs/stubbed.py +208 -0
  108. constricter/fix/index/taken.cp311-win32.pyd +0 -0
  109. constricter/fix/index/taken.py +45 -0
  110. constricter/fix/index/tuples.cp311-win32.pyd +0 -0
  111. constricter/fix/index/tuples.py +112 -0
  112. constricter/fix/libraries/__init__.cp311-win32.pyd +0 -0
  113. constricter/fix/libraries/__init__.py +2 -0
  114. constricter/fix/libraries/library.cp311-win32.pyd +0 -0
  115. constricter/fix/libraries/library.py +420 -0
  116. constricter/fix/libraries/opened.cp311-win32.pyd +0 -0
  117. constricter/fix/libraries/opened.py +147 -0
  118. constricter/fix/libraries/overloads.cp311-win32.pyd +0 -0
  119. constricter/fix/libraries/overloads.py +960 -0
  120. constricter/fix/libraries/receivers.cp311-win32.pyd +0 -0
  121. constricter/fix/libraries/receivers.py +274 -0
  122. constricter/fix/libraries/stdlib.cp311-win32.pyd +0 -0
  123. constricter/fix/libraries/stdlib.py +752 -0
  124. constricter/fix/libraries/walks.cp311-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__.cp311-win32.pyd +0 -0
  148. constricter/fix/values/__init__.py +2 -0
  149. constricter/fix/values/aliased.cp311-win32.pyd +0 -0
  150. constricter/fix/values/aliased.py +195 -0
  151. constricter/fix/values/bodies.cp311-win32.pyd +0 -0
  152. constricter/fix/values/bodies.py +76 -0
  153. constricter/fix/values/callables.cp311-win32.pyd +0 -0
  154. constricter/fix/values/callables.py +323 -0
  155. constricter/fix/values/called.cp311-win32.pyd +0 -0
  156. constricter/fix/values/called.py +194 -0
  157. constricter/fix/values/classvars.cp311-win32.pyd +0 -0
  158. constricter/fix/values/classvars.py +629 -0
  159. constricter/fix/values/decided.cp311-win32.pyd +0 -0
  160. constricter/fix/values/decided.py +429 -0
  161. constricter/fix/values/displays.cp311-win32.pyd +0 -0
  162. constricter/fix/values/displays.py +101 -0
  163. constricter/fix/values/doubts.cp311-win32.pyd +0 -0
  164. constricter/fix/values/doubts.py +677 -0
  165. constricter/fix/values/entered.cp311-win32.pyd +0 -0
  166. constricter/fix/values/entered.py +332 -0
  167. constricter/fix/values/fills.cp311-win32.pyd +0 -0
  168. constricter/fix/values/fills.py +446 -0
  169. constricter/fix/values/guesses.cp311-win32.pyd +0 -0
  170. constricter/fix/values/guesses.py +376 -0
  171. constricter/fix/values/hinted.cp311-win32.pyd +0 -0
  172. constricter/fix/values/hinted.py +811 -0
  173. constricter/fix/values/inference.cp311-win32.pyd +0 -0
  174. constricter/fix/values/inference.py +698 -0
  175. constricter/fix/values/loops.cp311-win32.pyd +0 -0
  176. constricter/fix/values/loops.py +335 -0
  177. constricter/fix/values/members.cp311-win32.pyd +0 -0
  178. constricter/fix/values/members.py +578 -0
  179. constricter/fix/values/narrowed.cp311-win32.pyd +0 -0
  180. constricter/fix/values/narrowed.py +220 -0
  181. constricter/fix/values/operated.cp311-win32.pyd +0 -0
  182. constricter/fix/values/operated.py +202 -0
  183. constricter/fix/values/ordered.cp311-win32.pyd +0 -0
  184. constricter/fix/values/ordered.py +74 -0
  185. constricter/fix/values/returned.cp311-win32.pyd +0 -0
  186. constricter/fix/values/returned.py +479 -0
  187. constricter/fix/values/returns.cp311-win32.pyd +0 -0
  188. constricter/fix/values/returns.py +237 -0
  189. constricter/fix/values/shapes.cp311-win32.pyd +0 -0
  190. constricter/fix/values/shapes.py +654 -0
  191. constricter/fix/values/stored.cp311-win32.pyd +0 -0
  192. constricter/fix/values/stored.py +564 -0
  193. constricter/fix/values/targets.cp311-win32.pyd +0 -0
  194. constricter/fix/values/targets.py +419 -0
  195. constricter/fix/values/unchecked.cp311-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__.cp311-win32.pyd +0 -0
  208. constricter/rules/__init__.py +2 -0
  209. constricter/rules/annotations.cp311-win32.pyd +0 -0
  210. constricter/rules/annotations.py +296 -0
  211. constricter/rules/binding.cp311-win32.pyd +0 -0
  212. constricter/rules/binding.py +652 -0
  213. constricter/rules/calls.cp311-win32.pyd +0 -0
  214. constricter/rules/calls.py +460 -0
  215. constricter/rules/checker.cp311-win32.pyd +0 -0
  216. constricter/rules/checker.py +536 -0
  217. constricter/rules/declarations.cp311-win32.pyd +0 -0
  218. constricter/rules/declarations.py +730 -0
  219. constricter/rules/decorators.cp311-win32.pyd +0 -0
  220. constricter/rules/decorators.py +351 -0
  221. constricter/rules/flow.cp311-win32.pyd +0 -0
  222. constricter/rules/flow.py +502 -0
  223. constricter/rules/keys.cp311-win32.pyd +0 -0
  224. constricter/rules/keys.py +79 -0
  225. constricter/rules/late.cp311-win32.pyd +0 -0
  226. constricter/rules/late.py +969 -0
  227. constricter/rules/narrowing.cp311-win32.pyd +0 -0
  228. constricter/rules/narrowing.py +98 -0
  229. constricter/rules/parsed.cp311-win32.pyd +0 -0
  230. constricter/rules/parsed.py +136 -0
  231. constricter/rules/quoted.cp311-win32.pyd +0 -0
  232. constricter/rules/quoted.py +141 -0
  233. constricter/rules/rebinding.cp311-win32.pyd +0 -0
  234. constricter/rules/rebinding.py +169 -0
  235. constricter/rules/recorded.cp311-win32.pyd +0 -0
  236. constricter/rules/recorded.py +168 -0
  237. constricter/rules/redundant.cp311-win32.pyd +0 -0
  238. constricter/rules/redundant.py +160 -0
  239. constricter/rules/scope.cp311-win32.pyd +0 -0
  240. constricter/rules/scope.py +802 -0
  241. constricter/rules/scoped.cp311-win32.pyd +0 -0
  242. constricter/rules/scoped.py +232 -0
  243. constricter/rules/scoping.cp311-win32.pyd +0 -0
  244. constricter/rules/scoping.py +346 -0
  245. constricter/rules/settings.cp311-win32.pyd +0 -0
  246. constricter/rules/settings.py +189 -0
  247. constricter/rules/syntax.cp311-win32.pyd +0 -0
  248. constricter/rules/syntax.py +472 -0
  249. constricter/rules/tables.cp311-win32.pyd +0 -0
  250. constricter/rules/tables.py +169 -0
  251. constricter/rules/walked.cp311-win32.pyd +0 -0
  252. constricter/rules/walked.py +127 -0
  253. constricter/rules/widened.cp311-win32.pyd +0 -0
  254. constricter/rules/widened.py +322 -0
  255. constricter/trace.py +374 -0
  256. e45ce06b2fdd5c43f782__mypyc.cp311-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,322 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """The command's defaults, from the nearest `pyproject.toml`'s `[tool.constricter]` table."""
3
+
4
+ import re
5
+ import tomllib
6
+ from collections.abc import Callable, Sequence
7
+ from functools import partial
8
+ from pathlib import Path
9
+ from typing import TYPE_CHECKING, Final, TypeAlias, cast
10
+
11
+ from constricter.cli.lsp.protocol import SERVERS
12
+ from constricter.jsonc import is_int
13
+ from constricter.offences import FIX_KINDS, LEVELS, MESSAGES, WIDEN_KINDS
14
+
15
+ if TYPE_CHECKING:
16
+ from datetime import date, datetime, time
17
+ from io import BufferedReader
18
+
19
+ _Toml: TypeAlias = "str | int | float | bool | datetime | date | time | list[_Toml] | dict[str, _Toml]"
20
+ Default: TypeAlias = str | int | float | bool | list[str] | dict[str, str] | dict[str, list[str]]
21
+
22
+
23
+ def unknown_codes(codes: Sequence[str]) -> list[str]:
24
+ """Check `codes` (or code prefixes) against the known codes.
25
+
26
+ Returns:
27
+ Those that match none.
28
+
29
+ """
30
+ return [code for code in codes if not any(known.startswith(code.upper()) for known in MESSAGES)]
31
+
32
+
33
+ DEFAULT_BASELINE: Final = "constricter-baseline.json"
34
+ _MIN_PYTHON: Final = "min_python"
35
+ _ALL_KINDS: Final = "all" # `fix-widen`'s: every wider type
36
+ _VERSION: Final = re.compile(r"\d+\.\d+") # `min-python`'s: `3.11`
37
+ # A lower bound among `requires-python`'s specifiers: `>=3.11`, `~=3.11`, `==3.11.*`, `>3.10`.
38
+ _LOWER_BOUND: Final = re.compile(r"(?:>=|~=|==|>)\s*(\d+)(?:\.(\d+))?")
39
+
40
+
41
+ def project_root(start: Path) -> Path:
42
+ """Find the project's root.
43
+
44
+ Returns:
45
+ The directory of the nearest `pyproject.toml`, or `start` if there's none.
46
+
47
+ """
48
+ path: Path | None = _pyproject(start)
49
+ return start if path is None else path.parent
50
+
51
+
52
+ def _pyproject(start: Path) -> Path | None:
53
+ """Find the nearest `pyproject.toml` in `start` or above it.
54
+
55
+ Returns:
56
+ Its path, or `None`.
57
+
58
+ """
59
+ directory: Path
60
+ path: Path
61
+ for directory in (start, *start.parents):
62
+ if (path := directory / "pyproject.toml").is_file():
63
+ return path
64
+ return None
65
+
66
+
67
+ def _document(path: Path) -> dict[str, _Toml]:
68
+ """Read `path`, a `pyproject.toml`.
69
+
70
+ Returns:
71
+ Its tables, as TOML parsed them.
72
+
73
+ Raises:
74
+ ValueError: The file isn't TOML.
75
+
76
+ """
77
+ file: BufferedReader
78
+ with path.open("rb") as file:
79
+ try:
80
+ return tomllib.load(file)
81
+ except tomllib.TOMLDecodeError as error:
82
+ message: str = f"{path}: {error}"
83
+ raise ValueError(message) from error
84
+
85
+
86
+ def _table(path: Path, document: dict[str, _Toml]) -> dict[str, _Toml]:
87
+ """Return the `[tool.constricter]` table of `document` (`path`'s), or an empty one.
88
+
89
+ Returns:
90
+ The table's keys and values, as TOML parsed them.
91
+
92
+ Raises:
93
+ ValueError: `tool.constricter` isn't a table.
94
+
95
+ """
96
+ table: dict[str, _Toml]
97
+ match document.get("tool"):
98
+ case {"constricter": dict() as table}:
99
+ return table
100
+ case {"constricter": _}:
101
+ message: str = f"{path}: [tool.constricter] isn't a table"
102
+ raise ValueError(message)
103
+ case _:
104
+ return {}
105
+
106
+
107
+ def _required(document: dict[str, _Toml]) -> str | None:
108
+ """Read the oldest Python a project's `requires-python` allows: its highest lower bound.
109
+
110
+ Returns:
111
+ It (`3.11`, for `>=3.11,<4`), or `None` if the project doesn't say.
112
+
113
+ """
114
+ required: str
115
+ match document.get("project"):
116
+ case {"requires-python": str() as required}:
117
+ pass
118
+ case _:
119
+ return None
120
+ found: list[tuple[str, str]] = cast("list[tuple[str, str]]", _LOWER_BOUND.findall(required))
121
+ bounds: list[tuple[int, int]] = [(int(major), int(minor or 0)) for major, minor in found]
122
+ oldest: tuple[int, int] | None = max(bounds, default=None)
123
+ return None if oldest is None else f"{oldest[0]}.{oldest[1]}"
124
+
125
+
126
+ def _level(value: _Toml) -> str | None:
127
+ """Read a level's name or number as the options take it.
128
+
129
+ Returns:
130
+ The level's name, or `None` if it isn't one.
131
+
132
+ """
133
+ text: str = str(value).lower()
134
+ return text if (isinstance(value, str) or is_int(value)) and text in LEVELS else None
135
+
136
+
137
+ def _whole(value: _Toml, minimum: int) -> int | None:
138
+ return value if is_int(value) and value >= minimum else None
139
+
140
+
141
+ def _strings(value: _Toml) -> list[str] | None:
142
+ return (
143
+ [str(item) for item in value]
144
+ if isinstance(value, list) and all(isinstance(item, str) for item in value)
145
+ else None
146
+ )
147
+
148
+
149
+ def _codes(value: _Toml) -> list[str] | None:
150
+ codes: list[str] | None = _strings(value)
151
+ return None if codes is None or unknown_codes(codes) else [code.upper() for code in codes]
152
+
153
+
154
+ def unknown_fix_kinds(kinds: Sequence[str]) -> list[str]:
155
+ """Check `kinds` against `FIX_KINDS`' ids.
156
+
157
+ Returns:
158
+ Those that aren't one.
159
+
160
+ """
161
+ return [kind for kind in kinds if kind not in FIX_KINDS]
162
+
163
+
164
+ def unknown_widen_kinds(kinds: Sequence[str]) -> list[str]:
165
+ """Check `kinds` against `WIDEN_KINDS`' ids, and `all`.
166
+
167
+ Returns:
168
+ Those that aren't one.
169
+
170
+ """
171
+ return [kind for kind in kinds if kind not in WIDEN_KINDS | {_ALL_KINDS}]
172
+
173
+
174
+ def widen_kinds(kinds: Sequence[str]) -> list[str]:
175
+ """Read `fix-widen`'s kinds, `all` as every one.
176
+
177
+ Returns:
178
+ Them.
179
+
180
+ """
181
+ return sorted(WIDEN_KINDS) if _ALL_KINDS in kinds else list(kinds)
182
+
183
+
184
+ def _widen_kinds(value: _Toml) -> list[str] | None:
185
+ kinds: list[str] | None = [value] if isinstance(value, str) else _strings(value)
186
+ return None if kinds is None or unknown_widen_kinds(kinds) else widen_kinds(kinds)
187
+
188
+
189
+ def _fix_kinds(value: _Toml) -> list[str] | None:
190
+ kinds: list[str] | None = _strings(value)
191
+ return None if kinds is None or unknown_fix_kinds(kinds) else kinds
192
+
193
+
194
+ def _flag(value: _Toml) -> bool | None:
195
+ return value if isinstance(value, bool) else None
196
+
197
+
198
+ def _version(value: _Toml) -> str | None:
199
+ """Read `min-python`: a Python version, as text (`"3.11"`).
200
+
201
+ Returns:
202
+ It, or `None` for anything else.
203
+
204
+ """
205
+ return value if isinstance(value, str) and _VERSION.fullmatch(value) else None
206
+
207
+
208
+ def _gigabytes(value: _Toml) -> float | None:
209
+ """Read `infer-memory`: a positive number of gigabytes.
210
+
211
+ Returns:
212
+ It, or `None` for anything else.
213
+
214
+ """
215
+ return (
216
+ float(value) if isinstance(value, int | float) and not isinstance(value, bool) and value > 0 else None
217
+ )
218
+
219
+
220
+ def _checkers(value: _Toml) -> list[str] | None:
221
+ """Read `infer-with`: a checker, or a list of them, each one it knows.
222
+
223
+ Returns:
224
+ Them, or `None` for anything else.
225
+
226
+ """
227
+ checkers: list[str] | None = [value] if isinstance(value, str) else _strings(value)
228
+ return checkers if checkers and all(checker in SERVERS for checker in checkers) else None
229
+
230
+
231
+ def _levels(value: _Toml) -> dict[str, str] | None:
232
+ if not isinstance(value, dict):
233
+ return None
234
+ levels: dict[str, str | None] = {glob: _level(level) for glob, level in value.items()}
235
+ return None if None in levels.values() else {glob: str(level) for glob, level in levels.items()}
236
+
237
+
238
+ def _lists(value: _Toml, read: Callable[[_Toml], list[str] | None]) -> dict[str, list[str]] | None:
239
+ """Read a table of lists, each as `read` reads one: `per-file-ignores`' codes, `narrower`'s types.
240
+
241
+ Returns:
242
+ It, or `None` if `value` isn't a table, or `read` can't read one of its values.
243
+
244
+ """
245
+ if not isinstance(value, dict):
246
+ return None
247
+ lists: dict[str, list[str] | None] = {key: read(item) for key, item in value.items()}
248
+ return None if None in lists.values() else {key: list(item or []) for key, item in lists.items()}
249
+
250
+
251
+ def _baseline(value: _Toml, root: Path) -> str | None:
252
+ """Read a baseline's or a trace's path, relative to `root` (the pyproject.toml that names it).
253
+
254
+ Returns:
255
+ The resolved path, or `None` if `value` isn't a non-empty string.
256
+
257
+ """
258
+ return str(root / value) if isinstance(value, str) and value else None
259
+
260
+
261
+ # Each key's reader: its option default, or `None` for a wrong value.
262
+ _READERS: dict[str, Callable[[_Toml], Default | None]] = {
263
+ "level": _level,
264
+ "nesting": partial(_whole, minimum=1),
265
+ "max-length": partial(_whole, minimum=1),
266
+ "vague": partial(_whole, minimum=-1),
267
+ "jobs": partial(_whole, minimum=0),
268
+ "exclude": _strings,
269
+ "select": _codes,
270
+ "ignore": _codes,
271
+ "extend-select": _codes,
272
+ "fix-select": _fix_kinds,
273
+ "fix-ignore": _fix_kinds,
274
+ "unsafe-fix-select": _fix_kinds,
275
+ "fix-widen": _widen_kinds,
276
+ "fix-plain-bases": _strings,
277
+ "type-comments": _flag,
278
+ "all-scopes": _flag,
279
+ "gitignore": _flag,
280
+ "per-path-levels": _levels,
281
+ "per-file-ignores": partial(_lists, read=_codes),
282
+ "narrower": partial(_lists, read=_strings), # a type hierarchy: each type, and those it's narrower than
283
+ "infer-with": _checkers,
284
+ "infer-memory": _gigabytes,
285
+ "min-python": _version,
286
+ }
287
+
288
+
289
+ def config_defaults(start: Path) -> dict[str, Default]:
290
+ """Return the option defaults in the nearest `pyproject.toml`'s `[tool.constricter]`.
291
+
292
+ And `min-python`'s from the project's `requires-python`, where the table doesn't set it.
293
+
294
+ Returns:
295
+ Each option's default, keyed by its `argparse` name (`type_comments`, not `type-comments`).
296
+
297
+ Raises:
298
+ ValueError: The file isn't TOML, or the table has an unknown key or a wrong value.
299
+
300
+ """
301
+ path: Path | None
302
+ if (path := _pyproject(start)) is None:
303
+ return {}
304
+ readers: dict[str, Callable[[_Toml], Default | None]] = {
305
+ **_READERS,
306
+ "baseline": partial(_baseline, root=path.parent),
307
+ "infer-from": partial(_baseline, root=path.parent),
308
+ }
309
+ defaults: dict[str, Default] = {}
310
+ key: str
311
+ value: _Toml
312
+ document: dict[str, _Toml] = _document(path)
313
+ for key, value in _table(path, document).items():
314
+ default: Default | None
315
+ if key not in readers or (default := readers[key](value)) is None:
316
+ message: str = f"{path}: [tool.constricter] has an invalid {key} = {value!r}"
317
+ raise ValueError(message)
318
+ defaults[key.replace("-", "_")] = default
319
+ required: str | None
320
+ if _MIN_PYTHON not in defaults and (required := _required(document)) is not None:
321
+ defaults[_MIN_PYTHON] = required
322
+ return defaults
@@ -0,0 +1,104 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Each code's rationale and fix, for `constricter --explain`."""
3
+
4
+ from typing import Final
5
+
6
+ from constricter.offences import (
7
+ CAN_BE_FINAL,
8
+ COMMENT_TYPED_TARGET,
9
+ LONG_TUPLE,
10
+ MESSAGES,
11
+ MISMATCHED_TYPE,
12
+ NARROWABLE_TYPE,
13
+ NESTED_TYPE,
14
+ OPT_IN,
15
+ REDUNDANT_TYPE,
16
+ UNANNOTATED,
17
+ UNANNOTATED_MEMBER,
18
+ UNTYPED_TARGET,
19
+ UNUSED_UNION_MEMBER,
20
+ VAGUE_TYPE,
21
+ Level,
22
+ Offence,
23
+ )
24
+
25
+ _WHY: Final = {
26
+ UNANNOTATED: (
27
+ "A local's type should be written where it's first bound, not left to inference.\n"
28
+ "Annotate the binding (`count: int = 0`), or declare it first (`first: int`) when unpacking,\n"
29
+ "`:=` or `with ... as` binds it. `--fix` adds the annotation when the value decides it."
30
+ ),
31
+ UNTYPED_TARGET: (
32
+ "A `for` target or `match` capture binds a local with no annotation. Declare it before the\n"
33
+ "statement (`item: str`, then `for item in items:`)."
34
+ ),
35
+ COMMENT_TYPED_TARGET: (
36
+ "A `# type:` comment is the old, Python 2 form of an annotation, which some tools ignore.\n"
37
+ "Declare the variable before the loop instead."
38
+ ),
39
+ UNANNOTATED_MEMBER: (
40
+ "With `all-scopes`, module and class variables need annotating too (`LIMIT: int = 3`). In a\n"
41
+ "dataclass, use `ClassVar[T]`: a plain annotation there makes a field. Dunder names and enum\n"
42
+ "members are exempt."
43
+ ),
44
+ VAGUE_TYPE: (
45
+ "`Any`, `object` and generics without their parameters (`list`, `dict`) say almost nothing\n"
46
+ "about the value. Name the real type: `list[str]`, a `TypedDict`, a union. `vague` lets some\n"
47
+ "through: at 0 one inside a type that says the rest (`tuple[str, Any]`), at N N+1 or one alone."
48
+ ),
49
+ NESTED_TYPE: (
50
+ "An annotation nested `nesting` deep (3 by default) is hard to read and to change. Name a part\n"
51
+ "of it with an alias (`Row: TypeAlias = tuple[int, set[str]]`, then `dict[str, list[Row]]`)."
52
+ ),
53
+ REDUNDANT_TYPE: (
54
+ "A name annotated the same way twice in one straight-line block says nothing a type checker\n"
55
+ "doesn't already know from the first one. Drop the second annotation (`x = 2`, not `x: int = 2`);\n"
56
+ "branches that never run together (an `if`'s two arms, a `try`'s body and its `except`) aren't\n"
57
+ "compared, since typing the same name the same way in each is normal, not redundant."
58
+ ),
59
+ MISMATCHED_TYPE: (
60
+ "Every value a name is bound to, over its whole lifetime in the scope, should fit its\n"
61
+ 'annotation: `count: int = 0` then `count = "done"` makes `count` mean two things. Only a\n'
62
+ "value whose type is certain is checked, against types whose every subclass is known\n"
63
+ "(builtins, and classes the module defines on such bases), so an imported class never is."
64
+ ),
65
+ NARROWABLE_TYPE: (
66
+ "An annotation wider than anything the name ever holds hides what the code really does:\n"
67
+ "`total: float = 0` only ever given `int`s is an `int`. Narrow it, or widen the values if the\n"
68
+ "annotation is the intent. Claimed only for a function's own names, when every value the name\n"
69
+ "is bound to has a certain type and no other scope writes it (`nonlocal`): a module or class\n"
70
+ "variable is state other code rebinds out of sight (`mod.X = ...`, `self.x = ...`)."
71
+ ),
72
+ UNUSED_UNION_MEMBER: (
73
+ "A union member no value ever is (`label: int | str = 3`, never a `str`) is dead weight every\n"
74
+ "reader has to consider. Drop it. Claimed under the same conditions as LVA008."
75
+ ),
76
+ LONG_TUPLE: (
77
+ "A fixed-length tuple listing more than `max-length` types (4 by default) leaves every reader\n"
78
+ "counting positions: what's `row[3]`? Name the fields instead, with a `NamedTuple` or a\n"
79
+ "dataclass. `tuple[int, ...]` (any length) isn't a list of positions, so it's never counted."
80
+ ),
81
+ CAN_BE_FINAL: (
82
+ "A local bound exactly once, by a plain `name = value` outside any loop, and never rebound\n"
83
+ "(not from a nested function either) could say so: `limit: Final = 10`. Off unless selected\n"
84
+ "by its full code (`--extend-select LVA012`): most locals are bound once."
85
+ ),
86
+ }
87
+
88
+
89
+ def explain(code: str) -> str:
90
+ """Explain `code`.
91
+
92
+ Returns:
93
+ Its message, rationale and the levels that report it.
94
+
95
+ """
96
+ offence: Offence = Offence(1, 0, "name", code)
97
+ levels: list[str] = [
98
+ f"{level.name.lower()}: {'error' if offence.is_error(level) else 'warning'}"
99
+ for level in Level
100
+ if offence.is_reported(level)
101
+ ]
102
+ message: str = MESSAGES[code].format(name="`name`", detail="T")
103
+ selected: str = " (only when selected)" if code in OPT_IN else ""
104
+ return f"{code}: {message}\n\n{_WHY[code]}\n\n{', '.join(levels)}{selected}\n"
@@ -0,0 +1,2 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """constricter.cli.lsp."""