python-constricter 0.4.1__cp314-cp314-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__.cp314-win_amd64.pyd +0 -0
  29. constricter/fix/__init__.py +2 -0
  30. constricter/fix/core/__init__.cp314-win_amd64.pyd +0 -0
  31. constricter/fix/core/__init__.py +2 -0
  32. constricter/fix/core/asked.cp314-win_amd64.pyd +0 -0
  33. constricter/fix/core/asked.py +56 -0
  34. constricter/fix/core/fixes.cp314-win_amd64.pyd +0 -0
  35. constricter/fix/core/fixes.py +317 -0
  36. constricter/fix/core/imports.cp314-win_amd64.pyd +0 -0
  37. constricter/fix/core/imports.py +580 -0
  38. constricter/fix/core/inherited.cp314-win_amd64.pyd +0 -0
  39. constricter/fix/core/inherited.py +311 -0
  40. constricter/fix/core/known.cp314-win_amd64.pyd +0 -0
  41. constricter/fix/core/known.py +730 -0
  42. constricter/fix/core/signatures.cp314-win_amd64.pyd +0 -0
  43. constricter/fix/core/signatures.py +132 -0
  44. constricter/fix/index/__init__.cp314-win_amd64.pyd +0 -0
  45. constricter/fix/index/__init__.py +2 -0
  46. constricter/fix/index/awaits.cp314-win_amd64.pyd +0 -0
  47. constricter/fix/index/awaits.py +90 -0
  48. constricter/fix/index/beyond.cp314-win_amd64.pyd +0 -0
  49. constricter/fix/index/beyond.py +149 -0
  50. constricter/fix/index/callers.cp314-win_amd64.pyd +0 -0
  51. constricter/fix/index/callers.py +155 -0
  52. constricter/fix/index/crossed.cp314-win_amd64.pyd +0 -0
  53. constricter/fix/index/crossed.py +327 -0
  54. constricter/fix/index/decorated.cp314-win_amd64.pyd +0 -0
  55. constricter/fix/index/decorated.py +140 -0
  56. constricter/fix/index/fixtures.cp314-win_amd64.pyd +0 -0
  57. constricter/fix/index/fixtures.py +176 -0
  58. constricter/fix/index/linked.cp314-win_amd64.pyd +0 -0
  59. constricter/fix/index/linked.py +142 -0
  60. constricter/fix/index/loose.cp314-win_amd64.pyd +0 -0
  61. constricter/fix/index/loose.py +103 -0
  62. constricter/fix/index/managed.cp314-win_amd64.pyd +0 -0
  63. constricter/fix/index/managed.py +63 -0
  64. constricter/fix/index/modules.cp314-win_amd64.pyd +0 -0
  65. constricter/fix/index/modules.py +741 -0
  66. constricter/fix/index/offers.cp314-win_amd64.pyd +0 -0
  67. constricter/fix/index/offers.py +252 -0
  68. constricter/fix/index/order.cp314-win_amd64.pyd +0 -0
  69. constricter/fix/index/order.py +155 -0
  70. constricter/fix/index/origins.cp314-win_amd64.pyd +0 -0
  71. constricter/fix/index/origins.py +238 -0
  72. constricter/fix/index/own_methods.cp314-win_amd64.pyd +0 -0
  73. constricter/fix/index/own_methods.py +91 -0
  74. constricter/fix/index/own_overloads.cp314-win_amd64.pyd +0 -0
  75. constricter/fix/index/own_overloads.py +70 -0
  76. constricter/fix/index/own_types.cp314-win_amd64.pyd +0 -0
  77. constricter/fix/index/own_types.py +330 -0
  78. constricter/fix/index/params.cp314-win_amd64.pyd +0 -0
  79. constricter/fix/index/params.py +81 -0
  80. constricter/fix/index/plain.cp314-win_amd64.pyd +0 -0
  81. constricter/fix/index/plain.py +221 -0
  82. constricter/fix/index/project.cp314-win_amd64.pyd +0 -0
  83. constricter/fix/index/project.py +797 -0
  84. constricter/fix/index/sides.cp314-win_amd64.pyd +0 -0
  85. constricter/fix/index/sides.py +160 -0
  86. constricter/fix/index/strings.cp314-win_amd64.pyd +0 -0
  87. constricter/fix/index/strings.py +46 -0
  88. constricter/fix/index/stubs/__init__.cp314-win_amd64.pyd +0 -0
  89. constricter/fix/index/stubs/__init__.py +2 -0
  90. constricter/fix/index/stubs/atoms.cp314-win_amd64.pyd +0 -0
  91. constricter/fix/index/stubs/atoms.py +425 -0
  92. constricter/fix/index/stubs/chained.cp314-win_amd64.pyd +0 -0
  93. constricter/fix/index/stubs/chained.py +69 -0
  94. constricter/fix/index/stubs/classnames.cp314-win_amd64.pyd +0 -0
  95. constricter/fix/index/stubs/classnames.py +124 -0
  96. constricter/fix/index/stubs/declared.cp314-win_amd64.pyd +0 -0
  97. constricter/fix/index/stubs/declared.py +508 -0
  98. constricter/fix/index/stubs/installed.cp314-win_amd64.pyd +0 -0
  99. constricter/fix/index/stubs/installed.py +425 -0
  100. constricter/fix/index/stubs/names.cp314-win_amd64.pyd +0 -0
  101. constricter/fix/index/stubs/names.py +200 -0
  102. constricter/fix/index/stubs/read.cp314-win_amd64.pyd +0 -0
  103. constricter/fix/index/stubs/read.py +70 -0
  104. constricter/fix/index/stubs/signatures.cp314-win_amd64.pyd +0 -0
  105. constricter/fix/index/stubs/signatures.py +656 -0
  106. constricter/fix/index/stubs/stubbed.cp314-win_amd64.pyd +0 -0
  107. constricter/fix/index/stubs/stubbed.py +208 -0
  108. constricter/fix/index/taken.cp314-win_amd64.pyd +0 -0
  109. constricter/fix/index/taken.py +45 -0
  110. constricter/fix/index/tuples.cp314-win_amd64.pyd +0 -0
  111. constricter/fix/index/tuples.py +112 -0
  112. constricter/fix/libraries/__init__.cp314-win_amd64.pyd +0 -0
  113. constricter/fix/libraries/__init__.py +2 -0
  114. constricter/fix/libraries/library.cp314-win_amd64.pyd +0 -0
  115. constricter/fix/libraries/library.py +420 -0
  116. constricter/fix/libraries/opened.cp314-win_amd64.pyd +0 -0
  117. constricter/fix/libraries/opened.py +147 -0
  118. constricter/fix/libraries/overloads.cp314-win_amd64.pyd +0 -0
  119. constricter/fix/libraries/overloads.py +960 -0
  120. constricter/fix/libraries/receivers.cp314-win_amd64.pyd +0 -0
  121. constricter/fix/libraries/receivers.py +274 -0
  122. constricter/fix/libraries/stdlib.cp314-win_amd64.pyd +0 -0
  123. constricter/fix/libraries/stdlib.py +752 -0
  124. constricter/fix/libraries/walks.cp314-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__.cp314-win_amd64.pyd +0 -0
  148. constricter/fix/values/__init__.py +2 -0
  149. constricter/fix/values/aliased.cp314-win_amd64.pyd +0 -0
  150. constricter/fix/values/aliased.py +195 -0
  151. constricter/fix/values/bodies.cp314-win_amd64.pyd +0 -0
  152. constricter/fix/values/bodies.py +76 -0
  153. constricter/fix/values/callables.cp314-win_amd64.pyd +0 -0
  154. constricter/fix/values/callables.py +323 -0
  155. constricter/fix/values/called.cp314-win_amd64.pyd +0 -0
  156. constricter/fix/values/called.py +194 -0
  157. constricter/fix/values/classvars.cp314-win_amd64.pyd +0 -0
  158. constricter/fix/values/classvars.py +629 -0
  159. constricter/fix/values/decided.cp314-win_amd64.pyd +0 -0
  160. constricter/fix/values/decided.py +429 -0
  161. constricter/fix/values/displays.cp314-win_amd64.pyd +0 -0
  162. constricter/fix/values/displays.py +101 -0
  163. constricter/fix/values/doubts.cp314-win_amd64.pyd +0 -0
  164. constricter/fix/values/doubts.py +677 -0
  165. constricter/fix/values/entered.cp314-win_amd64.pyd +0 -0
  166. constricter/fix/values/entered.py +332 -0
  167. constricter/fix/values/fills.cp314-win_amd64.pyd +0 -0
  168. constricter/fix/values/fills.py +446 -0
  169. constricter/fix/values/guesses.cp314-win_amd64.pyd +0 -0
  170. constricter/fix/values/guesses.py +376 -0
  171. constricter/fix/values/hinted.cp314-win_amd64.pyd +0 -0
  172. constricter/fix/values/hinted.py +811 -0
  173. constricter/fix/values/inference.cp314-win_amd64.pyd +0 -0
  174. constricter/fix/values/inference.py +698 -0
  175. constricter/fix/values/loops.cp314-win_amd64.pyd +0 -0
  176. constricter/fix/values/loops.py +335 -0
  177. constricter/fix/values/members.cp314-win_amd64.pyd +0 -0
  178. constricter/fix/values/members.py +578 -0
  179. constricter/fix/values/narrowed.cp314-win_amd64.pyd +0 -0
  180. constricter/fix/values/narrowed.py +220 -0
  181. constricter/fix/values/operated.cp314-win_amd64.pyd +0 -0
  182. constricter/fix/values/operated.py +202 -0
  183. constricter/fix/values/ordered.cp314-win_amd64.pyd +0 -0
  184. constricter/fix/values/ordered.py +74 -0
  185. constricter/fix/values/returned.cp314-win_amd64.pyd +0 -0
  186. constricter/fix/values/returned.py +479 -0
  187. constricter/fix/values/returns.cp314-win_amd64.pyd +0 -0
  188. constricter/fix/values/returns.py +237 -0
  189. constricter/fix/values/shapes.cp314-win_amd64.pyd +0 -0
  190. constricter/fix/values/shapes.py +654 -0
  191. constricter/fix/values/stored.cp314-win_amd64.pyd +0 -0
  192. constricter/fix/values/stored.py +564 -0
  193. constricter/fix/values/targets.cp314-win_amd64.pyd +0 -0
  194. constricter/fix/values/targets.py +419 -0
  195. constricter/fix/values/unchecked.cp314-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__.cp314-win_amd64.pyd +0 -0
  208. constricter/rules/__init__.py +2 -0
  209. constricter/rules/annotations.cp314-win_amd64.pyd +0 -0
  210. constricter/rules/annotations.py +296 -0
  211. constricter/rules/binding.cp314-win_amd64.pyd +0 -0
  212. constricter/rules/binding.py +652 -0
  213. constricter/rules/calls.cp314-win_amd64.pyd +0 -0
  214. constricter/rules/calls.py +460 -0
  215. constricter/rules/checker.cp314-win_amd64.pyd +0 -0
  216. constricter/rules/checker.py +536 -0
  217. constricter/rules/declarations.cp314-win_amd64.pyd +0 -0
  218. constricter/rules/declarations.py +730 -0
  219. constricter/rules/decorators.cp314-win_amd64.pyd +0 -0
  220. constricter/rules/decorators.py +351 -0
  221. constricter/rules/flow.cp314-win_amd64.pyd +0 -0
  222. constricter/rules/flow.py +502 -0
  223. constricter/rules/keys.cp314-win_amd64.pyd +0 -0
  224. constricter/rules/keys.py +79 -0
  225. constricter/rules/late.cp314-win_amd64.pyd +0 -0
  226. constricter/rules/late.py +969 -0
  227. constricter/rules/narrowing.cp314-win_amd64.pyd +0 -0
  228. constricter/rules/narrowing.py +98 -0
  229. constricter/rules/parsed.cp314-win_amd64.pyd +0 -0
  230. constricter/rules/parsed.py +136 -0
  231. constricter/rules/quoted.cp314-win_amd64.pyd +0 -0
  232. constricter/rules/quoted.py +141 -0
  233. constricter/rules/rebinding.cp314-win_amd64.pyd +0 -0
  234. constricter/rules/rebinding.py +169 -0
  235. constricter/rules/recorded.cp314-win_amd64.pyd +0 -0
  236. constricter/rules/recorded.py +168 -0
  237. constricter/rules/redundant.cp314-win_amd64.pyd +0 -0
  238. constricter/rules/redundant.py +160 -0
  239. constricter/rules/scope.cp314-win_amd64.pyd +0 -0
  240. constricter/rules/scope.py +802 -0
  241. constricter/rules/scoped.cp314-win_amd64.pyd +0 -0
  242. constricter/rules/scoped.py +232 -0
  243. constricter/rules/scoping.cp314-win_amd64.pyd +0 -0
  244. constricter/rules/scoping.py +346 -0
  245. constricter/rules/settings.cp314-win_amd64.pyd +0 -0
  246. constricter/rules/settings.py +189 -0
  247. constricter/rules/syntax.cp314-win_amd64.pyd +0 -0
  248. constricter/rules/syntax.py +472 -0
  249. constricter/rules/tables.cp314-win_amd64.pyd +0 -0
  250. constricter/rules/tables.py +169 -0
  251. constricter/rules/walked.cp314-win_amd64.pyd +0 -0
  252. constricter/rules/walked.py +127 -0
  253. constricter/rules/widened.cp314-win_amd64.pyd +0 -0
  254. constricter/rules/widened.py +322 -0
  255. constricter/trace.py +374 -0
  256. e45ce06b2fdd5c43f782__mypyc.cp314-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,232 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """What a scope (`constricter.rules.scope.Scope`) is checked with, and what it holds as it's checked.
3
+
4
+ Its module's `Settings` (and the parameters' types given from outside, `Seeded`); its `Kind` of
5
+ body; its plain bindings (`Assignments`), which LVA012 and `--fix` read; and what `--fix` has typed
6
+ in it so far (`Inferred`), late fixes included (`Late`).
7
+ """
8
+
9
+ import ast
10
+ from collections.abc import Mapping, Sequence
11
+ from dataclasses import dataclass, field
12
+ from typing import NamedTuple, TypeAlias
13
+
14
+ from constricter.empty import Empty
15
+ from constricter.fix.core.known import Hints, Inference, Known, Passed, Typed
16
+ from constricter.fix.values import aliased
17
+ from constricter.fix.values.doubts import (
18
+ Facts,
19
+ )
20
+ from constricter.offences import (
21
+ Checks,
22
+ )
23
+ from constricter.rules.flow import Hierarchy, members
24
+ from constricter.rules.quoted import parsed as unquoted
25
+ from constricter.rules.quoted import unqualified
26
+ from constricter.rules.rebinding import REBOUND
27
+ from constricter.rules.syntax import FunctionDef, Start
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class Settings:
32
+ """One module's options, and its source lines (to place a `**rest` capture)."""
33
+
34
+ # The checks asked for (a module written for Python 2 counts its `# type:` comments whatever they say).
35
+ checks: Checks
36
+ lines: Sequence[str]
37
+ known: Known # what the module declares that `--fix` infers types from
38
+ hierarchy: Hierarchy # which types are narrower than which, for value flow
39
+ owners: dict[int, str] # each method's class, by `id()`, to type its `self`, for `--fix`
40
+ # Where each of the module's `:=` starts, in source order: most modules have none, and needn't be
41
+ # looked through for one; a statement's part without one needn't be either.
42
+ walruses: tuple[Start, ...]
43
+ # Type checkers' types, for what `--fix` can't type (`--infer-with`): each checker's, in order.
44
+ hints: tuple[Hints, ...] = ()
45
+ facts: Facts = field(default_factory=Facts) # what a type checker sees otherwise (see `doubts`)
46
+ parameters: "Seeded | None" = None # what its functions' parameters are given from outside them
47
+
48
+
49
+ class Seeded(NamedTuple):
50
+ """The types a module's functions' unannotated parameters are given from outside them: guesses.
51
+
52
+ `callers`: what every call passes each parameter of its top-level functions, by `id()` (see
53
+ `constricter.fix.index.callers`); `fixtures`: the pytest fixtures its tests can take, each one's
54
+ value's type (see `constricter.fix.index.fixtures`). And `module`: the names the module binds
55
+ once, at its top level, each with its type there, which every function reads it as. `marked`:
56
+ what a class's `parametrize` gives each of its methods, by `id()` (see `calls.marked`).
57
+ """
58
+
59
+ callers: Mapping[int, Mapping[str, Passed]] = Empty()
60
+ fixtures: Mapping[str, Passed] = Empty()
61
+ module: Mapping[str, Passed] = Empty()
62
+ marked: Mapping[int, Mapping[str, Passed]] = Empty()
63
+
64
+
65
+ class Kind(NamedTuple):
66
+ """What kind of body a scope is: the code its unannotated names get, and whether `--fix` fixes it."""
67
+
68
+ unannotated: str # LVA001 in a function, LVA004 in a module or class body
69
+ # A class body isn't: annotating a dataclass's variable makes it a field. Only a plain class's
70
+ # variable typed by its value is fixed there (see `constricter.fix.values.classvars`).
71
+ fixable: bool
72
+ function: FunctionDef | None = None # a function's own: its body, its returns
73
+ owner: str | None = None # a class body's: its class
74
+
75
+ def body(self) -> Sequence[ast.stmt]:
76
+ """Find the function's body.
77
+
78
+ Returns:
79
+ It, or nothing for a module or class body's scope.
80
+
81
+ """
82
+ return () if self.function is None else self.function.body
83
+
84
+
85
+ # A name's first plain assignment: its target, its value, and the value's inferred type, if any.
86
+ Plain: TypeAlias = tuple[ast.Name, ast.expr, Inference | None]
87
+ Bare: TypeAlias = tuple[ast.AnnAssign, Inference | None]
88
+ # One plain assignment: where, and whether it's in a loop's body.
89
+ Placed: TypeAlias = tuple[tuple[int, int], bool]
90
+
91
+
92
+ @dataclass
93
+ class Assignments:
94
+ """A scope's plain `name = value` (or `name: T = value`) bindings, for LVA012 and `--fix`.
95
+
96
+ `chained`: where the statement starts that first binds a name it can't annotate (`a = b = 0`,
97
+ a loop's, an unpacking's, a `with`'s): a fix made late (`optional`, `filled`) declares it there.
98
+ """
99
+
100
+ found: dict[str, list[Placed]] = field(default_factory=dict[str, list[Placed]]) # each name's
101
+ looping: int = 0 # how many loops deep the statement being visited is
102
+ empty: dict[str, str] = field(default_factory=dict[str, str]) # names first bound empty: their kind
103
+ chained: dict[str, tuple[int, int]] = field(default_factory=dict[str, tuple[int, int]])
104
+ widened: set[str] = field(default_factory=set[str]) # the names a marked line annotates (see `widened`)
105
+ # Each name `late.widens` may offer a wider type: its target, its value (what a loop's, an
106
+ # unpacking's or a `with`'s is taken from: then in `chained` too), and the type `--fix` infers.
107
+ plain: dict[str, "Plain"] = field(default_factory=dict[str, "Plain"])
108
+ # Each marked declaration with no value (`x: Any`, before a loop), and what its name is bound to.
109
+ bare: dict[str, "Bare"] = field(default_factory=dict[str, "Bare"])
110
+
111
+
112
+ # A name typed late: its type, and what that rests on if it's a guess (`FIX_KINDS`).
113
+ Late: TypeAlias = tuple[str, frozenset[str]]
114
+
115
+
116
+ @dataclass
117
+ class Inferred:
118
+ """What `--fix` knows of a scope's names so far."""
119
+
120
+ types: Typed = field(default_factory=Typed) # each known type, for `x = y`'s
121
+ guesses: set[str] = field(default_factory=set[str]) # `types` from an unsafe fix: copies are too
122
+ # Each guess's guessing mechanisms (`FIX_KINDS`), for `unsafe-fix-select` to trust or not.
123
+ origins: dict[str, frozenset[str]] = field(default_factory=dict[str, frozenset[str]])
124
+ returns: list[ast.expr | None] = field(default_factory=list[ast.expr | None]) # its `return`s' values
125
+ # Its `self.x = value` assignments: each attribute, and its value.
126
+ assigned: list[tuple[str, ast.expr]] = field(default_factory=list[tuple[str, ast.expr]])
127
+ # Names typed only once the whole scope was seen (a container filled later, `None` rebound): the
128
+ # type, and what it rests on if a guess; and those it was checked again knowing.
129
+ late: dict[str, Late] = field(default_factory=dict[str, "Late"])
130
+ seeded: dict[str, Late] = field(default_factory=dict[str, "Late"])
131
+ # The names declared type aliases (`X: TypeAlias = ...`): `TypeAlias` isn't their values' type.
132
+ aliases: set[str] = field(default_factory=set[str])
133
+ unions: Mapping[str, str] = field(default_factory=dict[str, str]) # see `Indirect.unions`
134
+
135
+ def members(self, annotation: str) -> frozenset[str]:
136
+ """Split a type into its union's members, an alias of a union read as the union it names.
137
+
138
+ Returns:
139
+ Them; none for a type that can't be read.
140
+
141
+ """
142
+ return members(self.unions.get(annotation, annotation)) or frozenset()
143
+
144
+ def declare(self, name: str, annotation: ast.expr, held: str | None = None) -> None:
145
+ """Record the type `name` is annotated with: its own from here on, or an alias's declaration.
146
+
147
+ A `Final[T]`'s or a `ClassVar[T]`'s is `T`, and a bare one's its value's (`held`, if known).
148
+ """
149
+ text: str | None = unqualified(unquoted(annotation)) or held
150
+ if aliased.declares(annotation):
151
+ self.aliases.add(name)
152
+ elif text:
153
+ _ = self.types.setdefault(name, text)
154
+
155
+ def learn(
156
+ self,
157
+ name: str,
158
+ annotation: str,
159
+ origins: frozenset[str] | None,
160
+ *,
161
+ again: bool = False,
162
+ ) -> None:
163
+ """Record `name`'s type, the first time it's typed; `origins`: what it rests on, if it's a guess.
164
+
165
+ `again`: whether it was bound before, to a value of no known type, which it may still hold
166
+ (another branch's): its type is then a guess, resting on `rebound`.
167
+ """
168
+ if name in self.types:
169
+ return
170
+ self.types[name] = annotation
171
+ if origins is None and again:
172
+ origins = frozenset({REBOUND})
173
+ if origins is not None:
174
+ self.guess(name, origins)
175
+
176
+ def guess(self, name: str, origins: frozenset[str]) -> None:
177
+ """Take what's inferred from `name` from here on for a guess, resting on `origins`."""
178
+ self.guesses.add(name)
179
+ self.origins[name] = origins
180
+
181
+ def rebound(self, name: str, typed: str | None, guess: Late | None = None) -> None:
182
+ """Record `name` bound again, to a value of type `typed` (`None`: unknown).
183
+
184
+ A type checker narrows a name to what it's assigned: from here on it's `typed`, if known. That's
185
+ certain for a member of a declared union (`int | None`, then `1`), which every checker narrows;
186
+ otherwise (mypy narrows nothing else, and an unknown value may be anything) what's inferred
187
+ from it is a guess, resting on `rebound`. `guess`: the value's type where it's only a guess
188
+ (`config = config or Config()`), and what that rests on: the name's from here on, as one. A
189
+ union bound to a value of no known type is narrowed to that: the name has no type until its
190
+ branch ends.
191
+ """
192
+ current: str | None = self.types.get(name)
193
+ self.types.hold(name, None)
194
+ if current is None or typed == current:
195
+ return
196
+ if typed is None and guess is not None:
197
+ self.types[name] = guess[0]
198
+ self.guess(name, self.origins.get(name, frozenset()) | guess[1])
199
+ return
200
+ union: frozenset[str] = self.members(current)
201
+ if typed is None and len(union) > 1:
202
+ self.types.forget(name) # its type again past a branch (see `rejoined`), as a guess
203
+ if typed is not None:
204
+ self.types[name] = typed
205
+ if typed in union and len(union) > 1:
206
+ return
207
+ if name not in self.guesses:
208
+ self.guess(name, frozenset({REBOUND}))
209
+
210
+ def rejoined(self, before: Mapping[str, str], untyped: frozenset[str]) -> None:
211
+ """Take each name a branch (`if`, a loop, `try`, `match`) retyped back to its type `before` it.
212
+
213
+ The branch may not have run: past it, a name is what it was, or what the branch made it. That's
214
+ its type before, certainly, if that's a union the branch's type is a member of (`int | None`,
215
+ narrowed to `int` inside `if`); otherwise `rebound` already made it a guess, as it is where
216
+ the branch left it no type. One of `untyped`, bound before the branch or in an arm of it to a
217
+ value of no known type, may still hold it: it has none past it.
218
+ """
219
+ name: str
220
+ annotation: str
221
+ for name, annotation in before.items():
222
+ if name not in self.types and name not in self.guesses:
223
+ self.guess(name, frozenset({REBOUND}))
224
+ self.types[name] = annotation
225
+ for name in self.types.keys() & untyped:
226
+ self.forget(name)
227
+
228
+ def forget(self, name: str) -> None:
229
+ """Take `name` for a value of no known type from here on."""
230
+ self.types.forget(name)
231
+ self.guesses.discard(name)
232
+ _ = self.origins.pop(name, None)
@@ -0,0 +1,346 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """A module's scopes, built for a check: each function's and class body's, their statements visited.
3
+
4
+ What each binds and declares, in order, and which of them a check already finished.
5
+ """
6
+
7
+ import ast
8
+ from collections.abc import Mapping, Sequence
9
+ from typing import Final, cast
10
+
11
+ from constricter.fix.core import imports
12
+ from constricter.fix.core.known import (
13
+ Passed,
14
+ )
15
+ from constricter.fix.values import ordered, returned
16
+ from constricter.offences import (
17
+ UNANNOTATED,
18
+ UNANNOTATED_MEMBER,
19
+ )
20
+ from constricter.rules import binding, late, recorded
21
+ from constricter.rules.annotations import node_name
22
+ from constricter.rules.calls import seed_parameters, unshadowed
23
+ from constricter.rules.declarations import (
24
+ imported_from,
25
+ )
26
+ from constricter.rules.narrowing import module_names
27
+ from constricter.rules.quoted import written
28
+ from constricter.rules.scope import Scope
29
+ from constricter.rules.scoped import Kind, Late, Seeded, Settings
30
+ from constricter.rules.syntax import (
31
+ BRANCHING,
32
+ FunctionDef,
33
+ Start,
34
+ arm_ends,
35
+ child_statements,
36
+ collect_functions,
37
+ expressions,
38
+ has_within,
39
+ target_names,
40
+ )
41
+ from constricter.rules.walked import classes
42
+
43
+ # The node class of `type X = ...` statements, by name: Python 3.11's `ast` has no `TypeAlias`.
44
+ _TYPE_ALIAS: Final = "TypeAlias"
45
+ _CLASSMETHOD: Final = "classmethod"
46
+
47
+
48
+ # Enum members mustn't be annotated: a base imported from here is one, however it's aliased.
49
+ _ENUM_MODULES: Final = frozenset({"enum"})
50
+ # The conventional name of an instance method's first parameter: typed as its class, for `--fix`.
51
+ _SELF: Final = "self"
52
+
53
+
54
+ def bound_once(tree: ast.Module, settings: Settings) -> dict[str, Passed]:
55
+ """Type the names the module binds once, anywhere in it, at its top level: what its functions read.
56
+
57
+ `LIMIT = 10` is an `int` in every function, and `NAMES: Final = ["a"]` a `list[str]`: a name bound
58
+ once means the same thing throughout (see `imports.rebound_names`), and a type checker takes its
59
+ one value's type for its own. As the module's body types it, before its functions are checked.
60
+
61
+ Returns:
62
+ Each such name's type, and what it rests on if it's a guess.
63
+
64
+ """
65
+ scope: Scope = _body_scope(tree.body, settings, Kind(UNANNOTATED_MEMBER, fixable=True))
66
+ rebound: Mapping[str, list[Start]] = imports.rebound_names(tree)
67
+ return {
68
+ name: (held, scope.inferred.origins.get(name, frozenset()))
69
+ for name, held in scope.inferred.types.items()
70
+ if name not in rebound
71
+ }
72
+
73
+
74
+ def module_scopes(tree: ast.Module, settings: Settings, table: returned.Table | None = None) -> list["Scope"]:
75
+ """Collect the scopes to check.
76
+
77
+ With a `table` (see `returned.Table`), the functions are checked callees first, each one's
78
+ return type recorded as soon as it's checked: a caller checked later knows it the first time.
79
+
80
+ Returns:
81
+ Every function's scope, and with `all_scopes` every module and class body's.
82
+
83
+ """
84
+ functions: list[FunctionDef] = []
85
+ collect_functions(tree.body, functions)
86
+ if table is not None:
87
+ functions = ordered.in_call_order(tree, functions)
88
+ scopes: list[Scope] = _function_scopes(functions, settings, table)
89
+ if settings.checks.all_scopes:
90
+ scopes += body_scopes(tree, settings)
91
+ return scopes
92
+
93
+
94
+ def body_scopes(tree: ast.Module, settings: Settings) -> list["Scope"]:
95
+ """Collect the module and class bodies (LVA004).
96
+
97
+ Returns:
98
+ Their scopes, but for an enum's.
99
+
100
+ """
101
+ imported: frozenset[str] = imported_from(tree, _ENUM_MODULES)
102
+ # A class body isn't fixed: annotating a dataclass's variable makes it a field. A plain class's
103
+ # variable typed by its value is (see `Scope.assign`).
104
+ return [
105
+ _body_scope(tree.body, settings, Kind(UNANNOTATED_MEMBER, fixable=True)),
106
+ *(
107
+ _body_scope(node.body, settings, Kind(UNANNOTATED_MEMBER, fixable=False, owner=node.name))
108
+ for node in classes(tree)
109
+ if not _is_enum(node, imported)
110
+ ),
111
+ ]
112
+
113
+
114
+ def _body_scope(body: list[ast.stmt], settings: Settings, kind: Kind) -> "Scope":
115
+ """Check one module's or class's body.
116
+
117
+ Returns:
118
+ Its scope.
119
+
120
+ """
121
+ scope: Scope = Scope({"_"}, [], settings, kind)
122
+ stmt: ast.stmt
123
+ for stmt in body:
124
+ _visit(scope, stmt)
125
+ return scope
126
+
127
+
128
+ def _is_enum(node: ast.ClassDef, imported: frozenset[str]) -> bool:
129
+ """Check whether a base is an enum: enum members mustn't be annotated.
130
+
131
+ Returns:
132
+ Whether a base is imported from `enum` (`imported`, however it's aliased), or else its name
133
+ ends in `Enum` or `Flag` (for one imported some other way).
134
+
135
+ """
136
+ return any(
137
+ node_name(base) in imported or node_name(base).endswith(("Enum", "Flag")) for base in node.bases
138
+ )
139
+
140
+
141
+ def _function_scopes(
142
+ functions: list[FunctionDef],
143
+ settings: Settings,
144
+ table: returned.Table | None = None,
145
+ ) -> list["Scope"]:
146
+ """Check `functions` and every function defined inside them (recording each in `table`).
147
+
148
+ Returns:
149
+ Their scopes.
150
+
151
+ """
152
+ scopes: list[Scope] = []
153
+ func: FunctionDef
154
+ for func in functions:
155
+ nested: list[FunctionDef] = []
156
+ scope: Scope = function_scope(func, nested, settings)
157
+ scopes.append(scope)
158
+ if table is not None:
159
+ finish(table.module, [scope])
160
+ # Its returns are typed as it's checked; but one with late types (`None` rebound, ...) is
161
+ # checked again knowing them (`_returned`), and until then nothing's recorded: a caller
162
+ # would take its type too soon (`int`, for `None` rebound to `int`: `int | None`).
163
+ settled: bool = not scope.inferred.late.keys() - scope.inferred.seeded.keys()
164
+ table.checked(
165
+ func,
166
+ recorded.returns(scope, func, table.module) if settled else [],
167
+ (recorded.assigned(scope), recorded.used(scope, func, table.module)) if settled else ([], []),
168
+ )
169
+ scopes += _function_scopes(nested, scope.settings, table)
170
+ return scopes
171
+
172
+
173
+ def function_scope(
174
+ func: FunctionDef,
175
+ functions: list[FunctionDef],
176
+ settings: Settings,
177
+ seed: Mapping[str, Late] | None = None,
178
+ ) -> "Scope":
179
+ """Check one function; functions defined in it are collected into `functions`.
180
+
181
+ `seed`: names already known to be typed late (see `Inferred.late`), known from the start.
182
+
183
+ Returns:
184
+ Its scope.
185
+
186
+ """
187
+ settings = unshadowed(settings, func)
188
+ args: ast.arguments = func.args
189
+ named: tuple[ast.arg, ...] = (*args.posonlyargs, *args.args, *args.kwonlyargs)
190
+ params: set[str] = {a.arg for a in named}
191
+ params.update(extra.arg for extra in (args.vararg, args.kwarg) if extra is not None)
192
+ # `_` is a discard.
193
+ scope: Scope = Scope(
194
+ params | {"_"},
195
+ functions,
196
+ settings,
197
+ Kind(UNANNOTATED, fixable=True, function=func),
198
+ )
199
+ # A copy of a plain, annotated parameter (`*args`/`**kwargs` aren't the type they're annotated
200
+ # with) can be typed the same way, the moment it's assigned.
201
+ scope.inferred.types.taking(
202
+ {arg.arg: written(arg.annotation) for arg in named if arg.annotation is not None},
203
+ )
204
+ # A method's `self` is its class's instance; so is the `self` a function defined in it reads.
205
+ owner: str | None = settings.owners.get(id(func))
206
+ if owner is not None and (_SELF not in params or [arg.arg for arg in named[:1]] == [_SELF]):
207
+ _ = scope.inferred.types.setdefault(_SELF, owner)
208
+ # A classmethod's first parameter is its class (`type[C]`), whatever it's called.
209
+ if owner is not None and named and [node_name(d) for d in func.decorator_list] == [_CLASSMETHOD]:
210
+ _ = scope.inferred.types.setdefault(named[0].arg, f"type[{owner}]")
211
+ seed_parameters(scope, func, named)
212
+ # What the module's top level binds once, the function reads as it's typed there.
213
+ name: str
214
+ passed: Passed
215
+ for name, passed in (settings.parameters or Seeded()).module.items():
216
+ scope.inferred.learn(name, passed[0], passed[1] or None)
217
+ arg: ast.arg
218
+ for arg in named:
219
+ # A parameter holds whatever its callers pass: its declared type, as far as value flow knows.
220
+ if arg.annotation is not None:
221
+ scope.lifetime(arg.arg).declare(ast.unparse(arg.annotation), (arg.lineno, arg.col_offset))
222
+ scope.lifetime(arg.arg).bind(
223
+ (arg.lineno, arg.col_offset),
224
+ None if arg.annotation is None else ast.unparse(arg.annotation),
225
+ )
226
+ scope.opaque(extra.arg for extra in (args.vararg, args.kwarg) if extra is not None)
227
+ typed: Late
228
+ for name, typed in (seed or {}).items():
229
+ scope.inferred.seeded[name] = typed
230
+ scope.inferred.learn(name, typed[0], typed[1] or None)
231
+ stmt: ast.stmt
232
+ for stmt in func.body:
233
+ _visit(scope, stmt)
234
+ return scope
235
+
236
+
237
+ def _visit(scope: Scope, stmt: ast.stmt, head: ast.stmt | None = None) -> None:
238
+ """Bind the names `stmt` binds, as Python would, then visit its nested statements.
239
+
240
+ `head`: for an `elif`, the `if` it belongs to, which a declaration of its `:=`'s goes before.
241
+ """
242
+ part: ast.AST
243
+ walruses: tuple[Start, ...]
244
+ if walruses := scope.settings.walruses: # most modules have no `:=`: none of their parts need a look
245
+ for part in expressions(stmt):
246
+ if has_within(walruses, part): # a `:=` in it
247
+ binding.walruses(scope, part, head or stmt)
248
+ _declare(scope, stmt)
249
+ binding.bind(scope, stmt)
250
+ if isinstance(stmt, ast.Return):
251
+ scope.inferred.returns.append(stmt.value)
252
+ elif isinstance(stmt, ast.Assign):
253
+ scope.inferred.assigned.extend(
254
+ (target.attr, stmt.value)
255
+ for target in stmt.targets
256
+ if isinstance(target, ast.Attribute)
257
+ and isinstance(target.value, ast.Name)
258
+ and target.value.id == _SELF
259
+ )
260
+ # A loop's body runs again and again (not its `else`), for LVA012.
261
+ body: set[int] = (
262
+ {id(s) for s in stmt.body} if isinstance(stmt, ast.For | ast.AsyncFor | ast.While) else set()
263
+ )
264
+ before: dict[str, str] | None = dict(scope.inferred.types) if isinstance(stmt, BRANCHING) else None
265
+ # What has no known type before it, or where one of its arms ends: it may still, past it.
266
+ untyped: set[str] = set() if before is None else scope.declared - scope.inferred.types.keys()
267
+ ends: frozenset[int] = frozenset() if before is None else arm_ends(stmt)
268
+ child: ast.stmt
269
+ for child in child_statements(stmt):
270
+ scope.assignments.looping += id(child) in body
271
+ _visit(scope, child, (head or stmt) if _is_elif(stmt, child) else None)
272
+ scope.assignments.looping -= id(child) in body
273
+ if before is not None and id(child) in ends:
274
+ untyped |= scope.declared - scope.inferred.types.keys() - before.keys()
275
+ if before is not None:
276
+ scope.inferred.rejoined(before, frozenset(untyped))
277
+
278
+
279
+ def _is_elif(stmt: ast.stmt, child: ast.stmt) -> bool:
280
+ """Check whether `child` is `stmt`'s `elif`: an `if`'s only `else` statement, an `if` at its own column.
281
+
282
+ Returns:
283
+ Whether it is.
284
+
285
+ """
286
+ return (
287
+ isinstance(stmt, ast.If)
288
+ and isinstance(child, ast.If)
289
+ and stmt.orelse == [child]
290
+ and child.col_offset == stmt.col_offset
291
+ )
292
+
293
+
294
+ def _declare(scope: Scope, stmt: ast.stmt) -> None:
295
+ """Bind the names `stmt` binds that need no annotation, or carry their own."""
296
+ aliases: list[ast.alias]
297
+ names: list[str]
298
+ handlers: list[ast.ExceptHandler]
299
+ target: ast.Name
300
+ match stmt:
301
+ case ast.FunctionDef() | ast.AsyncFunctionDef():
302
+ scope.declared.add(stmt.name)
303
+ scope.opaque([stmt.name])
304
+ scope.nested.append(stmt) # its body is its own scope
305
+ case ast.ClassDef():
306
+ scope.declared.add(stmt.name)
307
+ scope.opaque([stmt.name])
308
+ collect_functions(stmt.body, scope.nested) # methods of a class defined in a function
309
+ case ast.Import(names=aliases) | ast.ImportFrom(names=aliases):
310
+ names = [(alias.asname or alias.name).split(".")[0] for alias in aliases]
311
+ scope.declared.update(names)
312
+ scope.opaque(names)
313
+ case ast.Global(names=names) | ast.Nonlocal(names=names):
314
+ scope.declared.update(names)
315
+ case ast.AnnAssign(target=ast.Name() as target):
316
+ binding.annotated(scope, stmt, target)
317
+ case _ if type(stmt).__name__ == _TYPE_ALIAS:
318
+ alias: ast.expr = cast("ast.expr", next(ast.iter_child_nodes(stmt))) # its first field, the name
319
+ scope.declared.update(name.id for name in target_names(alias))
320
+ case ast.Try(handlers=handlers) | ast.TryStar(handlers=handlers):
321
+ names = [handler.name for handler in handlers if handler.name]
322
+ scope.declared.update(names)
323
+ scope.opaque(names)
324
+ case _:
325
+ pass
326
+
327
+
328
+ def finish(tree: ast.Module, scopes: Sequence[Scope]) -> None:
329
+ """Finish `scopes` for their late fixes (`None` rebound, filled containers, shadowed types).
330
+
331
+ Those need the names written out of sight marked first. Safe to run again: marking is
332
+ idempotent, and no fix redoes one it made.
333
+ """
334
+ scope: Scope
335
+ for scope in scopes:
336
+ scope.mark_escaped(module_names(tree)[0])
337
+ late.optionals(scope)
338
+ late.rebinds(scope)
339
+ late.extended(scope)
340
+ late.fills(scope)
341
+ late.lambdas(scope)
342
+ late.widens(scope, tree)
343
+ binding.redeclared(scope)
344
+ late.shadowed(scope)
345
+ late.excused(scope)
346
+ late.unchecked(scope, tree)