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,98 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Value flow over a module's scopes, and its findings (LVA008, LVA009, LVA010) as offences with fixes."""
3
+
4
+ import ast
5
+ from collections.abc import Sequence
6
+ from functools import lru_cache
7
+ from typing import Final
8
+
9
+ from constricter.offences import NARROW, Edit, Fix, FixPolicy, Offence
10
+ from constricter.rules.annotations import node_name
11
+ from constricter.rules.flow import Finding, Kind
12
+ from constricter.rules.scope import Scope
13
+ from constricter.rules.walked import of_type
14
+
15
+ _TYPE_CHECKING: Final = "TYPE_CHECKING"
16
+
17
+
18
+ def flow_offences(found: list[Finding], policy: FixPolicy) -> list[Offence]:
19
+ """Report value-flow findings as offences, a finding's kind as its code.
20
+
21
+ Each declaration LVA008 or LVA010 would narrow gets one fix, a guess (`--unsafe-fixes`: a
22
+ declared type can be wider on purpose): the LVA008 finding's narrowed type if there's one,
23
+ since it's already within the members the values use, else the union without its unused
24
+ members, on the first LVA010 finding. Two rewrites of one annotation can't both apply. `policy`
25
+ decides whether it's offered (`narrow`), and whether it's trusted.
26
+
27
+ Returns:
28
+ The offences.
29
+
30
+ """
31
+ offences: list[Offence] = []
32
+ fixed: set[tuple[int, int]] = set()
33
+ finding: Finding
34
+ narrowed: set[tuple[int, int]] = {(f.line, f.col) for f in found if f.kind is Kind.NARROWABLE}
35
+ for finding in sorted(found, key=lambda f: f.kind is not Kind.NARROWABLE):
36
+ where: tuple[int, int] = (finding.line, finding.col)
37
+ edit: Fix | None = None
38
+ if (
39
+ finding.span
40
+ and finding.rewrite
41
+ and where not in fixed
42
+ and (finding.kind is Kind.NARROWABLE or where not in narrowed)
43
+ ):
44
+ fixed.add(where)
45
+ reason: str = (
46
+ "the values it's bound to"
47
+ if finding.kind is Kind.NARROWABLE
48
+ else "the members its values use"
49
+ )
50
+ kinds: frozenset[str] = frozenset({NARROW})
51
+ if policy.allows(kinds):
52
+ edit = Fix(
53
+ finding.rewrite,
54
+ reason,
55
+ unsafe=not policy.trusts(kinds),
56
+ edit=Edit.REPLACE,
57
+ span=finding.span,
58
+ kinds=kinds,
59
+ )
60
+ offences.append(Offence(*where, finding.name, finding.kind.value, edit, detail=finding.detail))
61
+ return offences
62
+
63
+
64
+ def module_flow(tree: ast.Module, scopes: Sequence[Scope]) -> list[Finding]:
65
+ """Compare each name's values with its declared type, in each of `scopes`.
66
+
67
+ Returns:
68
+ Every finding, in source order.
69
+
70
+ """
71
+ escaped: frozenset[str]
72
+ checking_only: frozenset[str]
73
+ escaped, checking_only = module_names(tree)
74
+ return sorted(found for scope in scopes for found in scope.value_flow(escaped, checking_only))
75
+
76
+
77
+ @lru_cache(maxsize=16) # `module_flow` runs once per round of the checker's `_returned`, on the same tree
78
+ def module_names(tree: ast.Module) -> tuple[frozenset[str], frozenset[str]]:
79
+ """Find the names value flow leaves alone in a module.
80
+
81
+ Returns:
82
+ Those a `global` or `nonlocal` writes from elsewhere, and those annotated only for type
83
+ checkers (`if TYPE_CHECKING: x: str`), whose runtime values may differ on purpose.
84
+
85
+ """
86
+ escaped: set[str] = set()
87
+ checking_only: set[str] = set()
88
+ node: ast.AST
89
+ for node in of_type(tree, ast.Global, ast.Nonlocal, ast.If):
90
+ if isinstance(node, ast.Global | ast.Nonlocal):
91
+ escaped.update(node.names)
92
+ elif isinstance(node, ast.If) and node_name(node.test) == _TYPE_CHECKING: # the only other kind
93
+ checking_only.update(
94
+ stmt.target.id
95
+ for stmt in node.body
96
+ if isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name)
97
+ )
98
+ return frozenset(escaped), frozenset(checking_only)
Binary file
@@ -0,0 +1,136 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Parse a module once: the cross-file index and the check share its tree, in the same process.
3
+
4
+ The CLI reads every file to index what it offers other files (`fix.index.project`), then checks each; both
5
+ parse it the same way (`parse`), and the index's tree is kept (`keep`) for the check to take
6
+ (`take`), which keeps it again once it's done: a file may be checked a second time. One `--fix`
7
+ changed is freed (`take`, by the text it had). What's kept is capped (`budget`): a tree takes about
8
+ 26 bytes of memory for each byte of source, so past the cap a file is parsed again, as it always
9
+ was. The cap is a quarter of the machine's memory (`sized`), and never under 1 GB of trees.
10
+ """
11
+
12
+ import ast
13
+ import io
14
+ import os
15
+ import tokenize
16
+ import warnings
17
+ from dataclasses import dataclass, field
18
+ from pathlib import Path
19
+ from typing import TYPE_CHECKING, Final, TypeAlias
20
+
21
+ if TYPE_CHECKING:
22
+ from collections.abc import Callable
23
+
24
+ from constricter.rules.tables import Tables
25
+
26
+ _FLOOR: Final = 40 << 20 # bytes of source whose trees any run may keep (about 1 GB of them)
27
+ _TREE: Final = 26 # bytes of memory a tree takes for each byte of its source
28
+ _SHARE: Final = 4 # the kept trees' part of the machine's memory: a quarter
29
+ # A control group's memory limit (a container's), where there is one: `max` for none.
30
+ _GROUP_LIMIT: Final = Path("/sys/fs/cgroup/memory.max")
31
+
32
+
33
+ def memory() -> int | None:
34
+ """Read how much memory this process has to use: the machine's, or its control group's limit if lower.
35
+
36
+ Returns:
37
+ It, in bytes; `None` where the system doesn't say (Windows has no `sysconf`).
38
+
39
+ """
40
+ # Looked up, not named: mypyc's mypy pass on Windows rejects `os.sysconf`.
41
+ sysconf: Callable[[str], int] | None
42
+ if (sysconf := getattr(os, "sysconf", None)) is None:
43
+ return None
44
+ try:
45
+ total: int = sysconf("SC_PHYS_PAGES") * sysconf("SC_PAGE_SIZE")
46
+ except (ValueError, OSError):
47
+ return None
48
+ try:
49
+ return min(total, int(_GROUP_LIMIT.read_text(encoding="ascii")))
50
+ except (OSError, ValueError): # no control group, or no limit in it
51
+ return total
52
+
53
+
54
+ def sized(available: int | None) -> int:
55
+ """Size the kept trees' budget for a machine with `available` bytes of memory (see `memory`).
56
+
57
+ Returns:
58
+ The bytes of source whose trees a run keeps: a quarter of the memory's worth, `_FLOOR` at
59
+ least, and where the memory isn't known.
60
+
61
+ """
62
+ return _FLOOR if available is None else max(_FLOOR, available // _SHARE // _TREE)
63
+
64
+
65
+ BUDGET: Final = sized(memory()) # bytes of source whose trees a run keeps, over all its processes
66
+ # A kept tree, and the module's own tables the index read from it (the check reads the same).
67
+ Kept: TypeAlias = tuple[ast.Module, "Tables"]
68
+
69
+
70
+ @dataclass
71
+ class _Kept:
72
+ """The trees kept in this process, by their source's text, and what's left of its share."""
73
+
74
+ trees: dict[str, Kept] = field(default_factory=dict)
75
+ left: int = BUDGET
76
+
77
+
78
+ _KEPT: Final = _Kept()
79
+
80
+
81
+ def budget(processes: int) -> None:
82
+ """Share the budget among `processes`: each keeps its part (a worker's initializer, or the CLI's own)."""
83
+ _KEPT.trees.clear()
84
+ _KEPT.left = BUDGET // max(processes, 1)
85
+
86
+
87
+ def parse(source: str | bytes, filename: str = "<unknown>") -> ast.Module:
88
+ """Parse `source` with its `# type:` comments; without them if one is misplaced.
89
+
90
+ Python's warnings about the source (an invalid escape sequence in a string) are left unsaid: they're
91
+ about the code checked, not findings, and it's the checked project's to see them when it runs.
92
+
93
+ Returns:
94
+ The module. Raises `SyntaxError`.
95
+
96
+ """
97
+ with warnings.catch_warnings():
98
+ warnings.simplefilter("ignore", SyntaxWarning)
99
+ warnings.simplefilter("ignore", DeprecationWarning) # what Python 3.11 warns about them with
100
+ try:
101
+ return ast.parse(source, filename, type_comments=True)
102
+ except SyntaxError: # a misplaced `# type:` comment, or a real error raised again here
103
+ return ast.parse(source, filename)
104
+
105
+
106
+ def text(data: bytes) -> str:
107
+ """Decode a module's bytes as Python finds their encoding (a PEP 263 declaration, a BOM, else UTF-8).
108
+
109
+ Returns:
110
+ Its text. Raises `SyntaxError` for an unknown or contradictory encoding, and
111
+ `UnicodeDecodeError` for bytes it doesn't hold.
112
+
113
+ """
114
+ return data.decode(tokenize.detect_encoding(io.BytesIO(data).readline)[0])
115
+
116
+
117
+ def keep(source: str, kept: Kept) -> None:
118
+ """Keep `source`'s tree and tables for the check to take, if this process's share of the budget allows."""
119
+ if len(source) <= _KEPT.left and source not in _KEPT.trees:
120
+ _KEPT.trees[source] = kept
121
+ _KEPT.left -= len(source)
122
+
123
+
124
+ def take(source: str) -> "Kept | None":
125
+ """Take the tree and tables kept for `source`, freeing them (two files alike in every byte share them).
126
+
127
+ Only for exactly the text they were read from: a file `--fix` has changed since is parsed again.
128
+
129
+ Returns:
130
+ Them, or `None` if none were kept.
131
+
132
+ """
133
+ kept: Kept | None
134
+ if (kept := _KEPT.trees.pop(source, None)) is not None:
135
+ _KEPT.left += len(source)
136
+ return kept
Binary file
@@ -0,0 +1,141 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Quoted annotations: a string annotation as the expression it quotes, and a quoted part as its text."""
3
+
4
+ import ast
5
+ from functools import lru_cache
6
+ from typing import Final
7
+
8
+ from constricter.offences import BUILTIN_GENERICS, UNION_SYNTAX
9
+
10
+ _LITERAL: Final = "Literal" # its strings are values, not quoted types
11
+ _ANNOTATED: Final = "Annotated" # its first argument alone is a type
12
+ _QUOTES: Final = frozenset("'\"")
13
+ _QUALIFIERS: Final = frozenset({"ClassVar", "Final"}) # around an attribute's type
14
+
15
+
16
+ def _head(node: ast.expr) -> str:
17
+ """Read the last name a subscript's head is written with (`Literal`, for `typing.Literal`).
18
+
19
+ Returns:
20
+ It.
21
+
22
+ """
23
+ return ast.unparse(node).rpartition(".")[2]
24
+
25
+
26
+ @lru_cache(maxsize=256)
27
+ def parsed(annotation: ast.expr) -> ast.expr:
28
+ """Unwrap a string annotation.
29
+
30
+ `is_vague` and `depth` both call this on the same annotation; cached so it's parsed once.
31
+
32
+ Returns:
33
+ Its parsed expression, or the annotation itself if it isn't a string.
34
+
35
+ """
36
+ if isinstance(annotation, ast.Constant) and isinstance(annotation.value, str):
37
+ try:
38
+ return ast.parse(annotation.value, mode="eval").body
39
+ except SyntaxError:
40
+ return annotation
41
+ return annotation
42
+
43
+
44
+ def written(annotation: ast.expr) -> str:
45
+ """Write an annotation as text `--fix` infers from: a quoted one, or a quoted part, as what it quotes.
46
+
47
+ `"list[Node]"` and `list["Node"]` are both `list[Node]`. Not a `Literal`'s strings, nor an
48
+ `Annotated`'s metadata, which are values; nor a string that isn't an expression.
49
+
50
+ Returns:
51
+ The text.
52
+
53
+ """
54
+ text: str = ast.unparse(annotation)
55
+ return ast.unparse(_unquoted(annotation)) if _QUOTES.intersection(text) else text
56
+
57
+
58
+ def unqualified(annotation: ast.expr) -> str:
59
+ """Write an attribute's annotation (see `written`), less the `ClassVar` or `Final` around its type.
60
+
61
+ Returns:
62
+ The text; nothing for a bare `Final`, which leaves the type to the value.
63
+
64
+ """
65
+ name: str
66
+ inner: ast.expr
67
+ match annotation:
68
+ case ast.Name(id=name) | ast.Attribute(attr=name) if name in _QUALIFIERS:
69
+ return ""
70
+ case ast.Subscript(value=ast.Name(id=name) | ast.Attribute(attr=name), slice=inner) if (
71
+ name in _QUALIFIERS
72
+ ):
73
+ return unqualified(inner)
74
+ case _:
75
+ return written(annotation)
76
+
77
+
78
+ def _unquoted(node: ast.expr) -> ast.expr:
79
+ """Rebuild an annotation with each quoted type in it read as an expression (see `written`).
80
+
81
+ Returns:
82
+ The annotation: a new tree wherever it differs (the module's own is shared).
83
+
84
+ """
85
+ head: ast.expr
86
+ inner: ast.expr
87
+ first: ast.expr
88
+ rest: list[ast.expr]
89
+ parts: list[ast.expr]
90
+ left: ast.expr
91
+ right: ast.expr
92
+ found: ast.expr = node
93
+ match node:
94
+ case ast.Constant(value=str()):
95
+ found = parsed(node)
96
+ found = found if found is node else _unquoted(found)
97
+ case ast.Subscript(value=head) if _head(head) == _LITERAL:
98
+ pass
99
+ case ast.Subscript(value=head, slice=ast.Tuple(elts=[first, *rest])) if _head(head) == _ANNOTATED:
100
+ found = ast.Subscript(head, ast.Tuple([_unquoted(first), *rest]))
101
+ case ast.Subscript(value=head, slice=inner):
102
+ found = ast.Subscript(head, _unquoted(inner))
103
+ case ast.Tuple(elts=parts):
104
+ found = ast.Tuple([_unquoted(part) for part in parts])
105
+ case ast.List(elts=parts):
106
+ found = ast.List([_unquoted(part) for part in parts])
107
+ case ast.BinOp(left=left, op=ast.BitOr(), right=right):
108
+ found = ast.BinOp(_unquoted(left), ast.BitOr(), _unquoted(right))
109
+ case _:
110
+ pass
111
+ return found
112
+
113
+
114
+ def quote(annotation: str) -> str:
115
+ """Quote an annotation: in double quotes, unless it has one or a backslash (a `Literal`'s string).
116
+
117
+ Returns:
118
+ It, as a string literal.
119
+
120
+ """
121
+ plain: bool = not {'"', "\\"} & set(annotation)
122
+ return f'"{annotation}"' if plain else ast.unparse(ast.Constant(annotation))
123
+
124
+
125
+ @lru_cache(maxsize=4096) # a module's fixes are a few types, each asked again and again
126
+ def newer(annotation: str, oldest: tuple[int, int] | None) -> bool:
127
+ """Check whether `annotation` is written as Pythons from `oldest` on don't all read a type.
128
+
129
+ A union by `|` before 3.10, any subscript before 3.9 (a builtin or standard-library class's).
130
+
131
+ Returns:
132
+ Whether it is; never where `oldest` isn't known, or the annotation is quoted already.
133
+
134
+ """
135
+ if oldest is None or oldest >= UNION_SYNTAX or annotation.startswith(('"', "'")):
136
+ return False
137
+ return any(
138
+ (isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr))
139
+ or (oldest < BUILTIN_GENERICS and isinstance(node, ast.Subscript))
140
+ for node in ast.walk(ast.parse(annotation, mode="eval"))
141
+ )
@@ -0,0 +1,169 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """A first binding's fix, refitted to every value the name is bound to later, as a checker holds it to.
3
+
4
+ `x = 1` then `x = None` declares `x: int | None` before the first binding (annotated there, mypy
5
+ wouldn't narrow it to the `int` it's bound to), and `total = 0` then `total += 0.5` declares a
6
+ `float`; another type that doesn't fit (`x = 1` then `x = "a"`, a class and its base) leaves it
7
+ untyped: a union is better left to the author. A `Self` takes only `Self`. A later binding whose
8
+ type isn't known may be anything, and one whose type is a guess may be wrong: either makes the fix
9
+ a guess, and leaves a type checker's hint (`--infer-with`) no fix at all.
10
+ """
11
+
12
+ from typing import Final, NamedTuple
13
+
14
+ from constricter.fix.core.known import Inference
15
+ from constricter.fix.values.hinted import KINDS as HINTED
16
+ from constricter.offences import UNIONS, Edit, Fix, Offence
17
+ from constricter.rules.flow import Binding, Hierarchy, members
18
+
19
+ _NONE: Final = "None"
20
+ _ONLY_NONE: Final = frozenset({_NONE})
21
+ _OPTIONAL: Final = "optional" # the fix kind of a `None` added to a type
22
+ REBOUND: Final = "rebound" # the fix kind (and guessing mechanism) of a later binding's type
23
+ # The types a later binding may widen a first one's to: the numeric tower's (`int`, then `float`).
24
+ _NUMBERS: Final = frozenset({"bool", "int", "float", "complex"})
25
+ _UNION: Final = 3 # the most types a widening's union names
26
+
27
+
28
+ class Refit(NamedTuple):
29
+ """A fix refitted: its inference, what it rests on if a guess, whether it is one, and its edit."""
30
+
31
+ found: Inference
32
+ origins: frozenset[str]
33
+ unsafe: bool
34
+ edit: Edit
35
+ span: tuple[int, int]
36
+
37
+
38
+ class _Later(NamedTuple):
39
+ """What a name's later bindings say: the types that don't fit its fix, their guesses, any unknown."""
40
+
41
+ misfits: frozenset[str]
42
+ origins: frozenset[str] # what the later bindings' guessed types rest on
43
+ unknown: bool
44
+
45
+
46
+ def refit(
47
+ o: Offence,
48
+ fix: Fix,
49
+ rest: list[Binding],
50
+ hierarchy: Hierarchy,
51
+ module: tuple[str | None, bool],
52
+ ) -> Refit | Fix | None:
53
+ """Refit `o`'s `fix` to the name's `rest` of bindings (see the module docstring).
54
+
55
+ `module`: how the module spells `Self`, if it imports it; and whether values of two or three
56
+ types that fit no one of them are declared their union, a widening (`fix-widen`'s `unions`).
57
+
58
+ Returns:
59
+ `fix` itself, if it takes them all as it is; else its refit, to offer; or `None`, for no fix.
60
+
61
+ """
62
+ # `--fix`'s annotations are always readable; one that weren't would be its own one member.
63
+ declared: frozenset[str] = members(fix.annotation) or frozenset({fix.annotation})
64
+ later: _Later = _later(rest, declared, hierarchy)
65
+ # A hint is one value's type alone: it says nothing of a later one's.
66
+ if (later.misfits or later.unknown) and (module[0] in declared or HINTED & fix.kinds):
67
+ return None
68
+ if not (later.misfits or later.unknown or later.origins):
69
+ return fix
70
+ rebound: frozenset[str] = frozenset({REBOUND}) if later.unknown else frozenset()
71
+ kinds: frozenset[str] = fix.kinds | later.origins | rebound
72
+ origins: frozenset[str] = later.origins | rebound | (fix.kinds if fix.unsafe else frozenset[str]())
73
+ unsafe: bool = fix.unsafe or later.unknown or bool(later.origins)
74
+ if not later.misfits:
75
+ return Refit(Inference(fix.annotation, fix.reason, kinds), origins, unsafe, fix.edit, fix.span)
76
+ atoms: frozenset[str] = declared | later.misfits
77
+ if module[0] in atoms: # a `Self` takes only `Self`, and is taken by nothing else
78
+ return None
79
+ joined: tuple[str, frozenset[str]] | None
80
+ # A union of known types: one that's a guess makes it a guess, resting on what that one does, as
81
+ # a second pass would declare it once the first had declared the guess.
82
+ if (joined := _joined(atoms - {_NONE}, hierarchy, unions=module[1] and not later.unknown)) is None:
83
+ return None
84
+ return Refit(
85
+ Inference(
86
+ joined[0] + (" | None" if _NONE in atoms else ""),
87
+ f"{fix.reason}, and every value it's bound to later",
88
+ kinds | {_OPTIONAL if later.misfits == _ONLY_NONE else REBOUND} | joined[1],
89
+ ),
90
+ origins | joined[1],
91
+ unsafe or bool(joined[1]),
92
+ Edit.DECLARE,
93
+ fix.span if fix.edit is Edit.DECLARE else (o.line, o.col),
94
+ )
95
+
96
+
97
+ def _joined(
98
+ atoms: frozenset[str],
99
+ hierarchy: Hierarchy,
100
+ *,
101
+ unions: bool,
102
+ ) -> tuple[str, frozenset[str]] | None:
103
+ """Write the one type a name bound to values of types `atoms` is declared.
104
+
105
+ The one they all fit (see `_widest`); else, with `unions`, the union of two or three of them: a
106
+ widening, which a checker may hold a later use to.
107
+
108
+ Returns:
109
+ The type, and the widening's kind if it's one; `None` where there's neither.
110
+
111
+ """
112
+ widest: str | None
113
+ if (widest := _widest(atoms, hierarchy)) is not None:
114
+ return widest, frozenset()
115
+ return (" | ".join(sorted(atoms)), frozenset({UNIONS})) if unions and len(atoms) <= _UNION else None
116
+
117
+
118
+ def _later(rest: list[Binding], declared: frozenset[str], hierarchy: Hierarchy) -> _Later:
119
+ """Read what the later bindings of a name declared as the union `declared` say.
120
+
121
+ Returns:
122
+ Their types that don't fit it, what their guesses rest on, and whether any is unknown.
123
+
124
+ """
125
+ misfits: set[str] = set()
126
+ origins: frozenset[str] = frozenset()
127
+ unknown: bool = False
128
+ binding: Binding
129
+ for binding in rest:
130
+ text: str | None = binding.value or (None if binding.guess is None else binding.guess[0])
131
+ found: frozenset[str] | None
132
+ if text is None or (found := members(text)) is None:
133
+ unknown = True
134
+ continue
135
+ if binding.value is None and binding.guess is not None:
136
+ origins |= binding.guess[1]
137
+ misfits.update(atom for atom in found if not _fits(atom, declared, hierarchy))
138
+ return _Later(frozenset(misfits), origins, unknown)
139
+
140
+
141
+ def _fits(atom: str, declared: frozenset[str], hierarchy: Hierarchy) -> bool:
142
+ """Check whether a value of type `atom` fits a name declared as the union `declared`.
143
+
144
+ As value flow's `Hierarchy.fits` has it, but a generic only fits the same text: an annotation that
145
+ says `list[int]` doesn't take a `list[str]`.
146
+
147
+ Returns:
148
+ Whether it does.
149
+
150
+ """
151
+ return any(atom == member or member in hierarchy.wider(atom) for member in declared)
152
+
153
+
154
+ def _widest(atoms: frozenset[str], hierarchy: Hierarchy) -> str | None:
155
+ """Find the one type all of `atoms` fit: itself, if it's one; else the widest, if they're numbers.
156
+
157
+ Not a class's base (`Base` for `Base` and `Sub`): a checker may see only one of them bound (the
158
+ other, say, under `if not TYPE_CHECKING:`), and narrow to it.
159
+
160
+ Returns:
161
+ It, or `None`.
162
+
163
+ """
164
+ if len(atoms) == 1:
165
+ return next(iter(atoms))
166
+ widest: list[str] = [
167
+ atom for atom in atoms if all(_fits(other, frozenset({atom}), hierarchy) for other in atoms)
168
+ ]
169
+ return widest[0] if widest and atoms <= _NUMBERS else None