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,536 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """The rules: every local variable is typed where it's first bound (see README)."""
3
+
4
+ import ast
5
+ from collections.abc import Iterator, Mapping, Sequence
6
+ from dataclasses import replace
7
+ from typing import Final, NamedTuple, cast
8
+
9
+ from constricter.fix.core import imports
10
+ from constricter.fix.core.known import (
11
+ Guarded,
12
+ ImportPlan,
13
+ Observed,
14
+ Outside,
15
+ Passed,
16
+ Returned,
17
+ Returns,
18
+ )
19
+ from constricter.fix.values import returned, stored
20
+ from constricter.jsonc import as_text
21
+ from constricter.offences import (
22
+ DEFAULT_CHECKS,
23
+ UNANNOTATED,
24
+ UNANNOTATED_MEMBER,
25
+ UNTYPED_TARGET,
26
+ Checks,
27
+ Offence,
28
+ )
29
+ from constricter.rules import late, parsed, recorded
30
+ from constricter.rules.calls import keyed, observed
31
+ from constricter.rules.flow import Finding
32
+ from constricter.rules.narrowing import flow_offences, module_flow
33
+ from constricter.rules.redundant import redundant
34
+ from constricter.rules.scope import Scope
35
+ from constricter.rules.scoped import Seeded, Settings
36
+ from constricter.rules.scoping import body_scopes, bound_once, finish, function_scope, module_scopes
37
+ from constricter.rules.settings import module_settings
38
+ from constricter.rules.syntax import (
39
+ FUNCTION_DEFS,
40
+ FunctionDef,
41
+ child_statements,
42
+ )
43
+ from constricter.rules.tables import Tables, module_tables
44
+
45
+ _KEPT: Final = "_constricter_kept" # the attribute a tree holds its last check in
46
+ _ROUNDS: Final = 5 # how many times to re-check what calls an unannotated function, at most
47
+ _TYPE_CHECKING: Final = "TYPE_CHECKING"
48
+
49
+
50
+ def check_source(
51
+ source: str | bytes,
52
+ filename: str = "<unknown>",
53
+ checks: Checks = DEFAULT_CHECKS,
54
+ *,
55
+ outside: Outside | None = None,
56
+ ) -> list[Offence]:
57
+ """Return the offences in `source`, sorted. Raises `SyntaxError`.
58
+
59
+ `outside` adds what's known of it from other files and a type checker, for `--fix` (see `Outside`).
60
+
61
+ Returns:
62
+ Every offence; `# noqa` comments are the caller's to apply.
63
+
64
+ """
65
+ return checked_source(source, filename, checks, outside=outside).offences
66
+
67
+
68
+ def checked_source(
69
+ source: str | bytes,
70
+ filename: str = "<unknown>",
71
+ checks: Checks = DEFAULT_CHECKS,
72
+ *,
73
+ outside: Outside | None = None,
74
+ ) -> "Checked":
75
+ """Check `source`, as `check_source` does.
76
+
77
+ Returns:
78
+ Its offences, and what its functions return (see `Checked`).
79
+
80
+ """
81
+ tree: ast.Module
82
+ own: Tables | None
83
+ tree, own = _parse(source, filename)
84
+ found: Checked = checked_tree(tree, checks, lines=as_text(source).splitlines(), outside=outside, own=own)
85
+ if own is not None and isinstance(source, str):
86
+ # Kept again: a file is checked again once its callers type its parameters, or its cycle's
87
+ # files change what it imports. One `--fix` changed since is no longer this text.
88
+ parsed.keep(source, (tree, own))
89
+ return found
90
+
91
+
92
+ def _parse(source: str | bytes, filename: str) -> tuple[ast.Module, Tables | None]:
93
+ """Parse `source` (see `parsed.parse`), or take the tree the index kept for it (`parsed.take`).
94
+
95
+ Returns:
96
+ The module, and the tables the index read from it if it was kept. Raises `SyntaxError`.
97
+
98
+ """
99
+ kept: parsed.Kept | None = parsed.take(source) if isinstance(source, str) else None
100
+ return kept or (parsed.parse(source, filename), None)
101
+
102
+
103
+ def check_tree(
104
+ tree: ast.Module,
105
+ checks: Checks = DEFAULT_CHECKS,
106
+ *,
107
+ lines: Sequence[str] = (),
108
+ outside: Outside | None = None,
109
+ own: Tables | None = None,
110
+ ) -> list[Offence]:
111
+ """Return the offences in a parsed module, sorted.
112
+
113
+ `# type:` comments are seen only if it was parsed with `type_comments=True`; they count for `=`
114
+ and `with` too in a module written to run on Python 2. With its source `lines`, a `**rest`
115
+ capture is reported at its name rather than at its pattern's start. `outside` adds what's known
116
+ of it from other files and a type checker, for `--fix` (see `Outside`). `own`: the module's own
117
+ tables, if already read from this tree (see `parsed.keep`).
118
+
119
+ Returns:
120
+ Every offence, in source order.
121
+
122
+ """
123
+ return checked_tree(tree, checks, lines=lines, outside=outside, own=own).offences
124
+
125
+
126
+ class Checked(NamedTuple):
127
+ """A module's offences, what its unannotated functions return, and what it passes others' functions.
128
+
129
+ What they return is for the files importing them; what it passes, for `fix.index.callers`.
130
+ """
131
+
132
+ offences: list[Offence]
133
+ returned: Returns
134
+ calls: Observed = Observed()
135
+
136
+
137
+ def checked_tree(
138
+ tree: ast.Module,
139
+ checks: Checks = DEFAULT_CHECKS,
140
+ *,
141
+ lines: Sequence[str] = (),
142
+ outside: Outside | None = None,
143
+ own: Tables | None = None,
144
+ ) -> Checked:
145
+ """Check a parsed module, as `check_tree` does.
146
+
147
+ Returns:
148
+ Its offences, in source order, and what its functions return (see `returned.exported`).
149
+
150
+ """
151
+ own = own or module_tables(tree)
152
+ read: ImportPlan = imports.plan(tree)
153
+ outside = None if outside is None else outside.usable(read.taken, imports.present(read, outside.guarded))
154
+ # A second check's scopes it didn't check again are its first's, finished then.
155
+ finished: frozenset[int] = _kept_scopes(tree)
156
+ done: _Round = _resumed(tree, checks, outside) or _started(tree, checks, lines, own, outside)
157
+ # Kept with the tree, for as long as it is: a second check may only know its parameters anew.
158
+ vars(tree)[_KEPT] = _Kept(outside, checks, done)
159
+ settings: Settings = done.settings
160
+ scopes: list[Scope] = done.scopes()
161
+ # A finding's kind is the code that reports it (LVA008, LVA009, LVA010).
162
+ finish(tree, [scope for scope in scopes if id(scope) not in finished])
163
+ finals: list[Offence] = [o for scope in scopes for o in late.finals(scope)] if checks.final else []
164
+ reported: list[Offence] = [o for scope in scopes for o in scope.reported()]
165
+ exported: Returns = returned.exported(done.found, settings.known.class_side.lineage)
166
+ plan: ImportPlan = settings.known.names.plan or imports.plan(tree)
167
+ # With the imports its fixes add under `if TYPE_CHECKING:`, which `outside.guarded` doesn't hold.
168
+ guarded: dict[str, Guarded] = {
169
+ **{name: each for name, each in plan.guarded.items() if each.statement is not None},
170
+ **({} if outside is None else outside.guarded),
171
+ }
172
+ return Checked(
173
+ sorted(
174
+ [
175
+ *reported,
176
+ *redundant(tree, settings.checks.fixes),
177
+ *flow_offences(module_flow(tree, scopes), settings.checks.fixes),
178
+ *finals,
179
+ ],
180
+ ),
181
+ exported._replace(names=imports.exported_names(exported, guarded, plan.added)),
182
+ observed(tree, scopes, settings.known, {} if outside is None else outside.callees),
183
+ )
184
+
185
+
186
+ def _kept_scopes(tree: ast.Module) -> frozenset[int]:
187
+ """Identify the scopes of a module's last check, kept on its tree (see `_Kept`).
188
+
189
+ Returns:
190
+ Their `id`s; none before its first.
191
+
192
+ """
193
+ kept: _Kept | None = cast("_Kept | None", vars(tree).get(_KEPT))
194
+ return frozenset() if kept is None else frozenset(id(scope) for scope in kept.done.scopes())
195
+
196
+
197
+ def _started(
198
+ tree: ast.Module,
199
+ checks: Checks,
200
+ lines: Sequence[str],
201
+ own: Tables,
202
+ outside: Outside | None,
203
+ ) -> "_Round":
204
+ """Check a module from the start (see `_settled`).
205
+
206
+ Returns:
207
+ The check.
208
+
209
+ """
210
+ first: Settings = module_settings(tree, checks, lines, own, outside)
211
+ imported: Returns = Returns() if outside is None else outside.returned
212
+ return _settled(tree, first, imported, bound_once(tree, first))
213
+
214
+
215
+ class _Round(NamedTuple):
216
+ """A module's check as its rounds leave it: what checking it again starts from (see `_resumed`).
217
+
218
+ The settings with what its functions return, each function with its latest scope, the module's
219
+ and classes' bodies' scopes, the table its functions' `return`s are recorded in, and what they
220
+ return.
221
+ """
222
+
223
+ settings: Settings
224
+ functions: list[tuple[Scope, FunctionDef]]
225
+ bodies: list[Scope]
226
+ table: returned.Table
227
+ found: Returned
228
+
229
+ def scopes(self) -> list[Scope]:
230
+ """List every scope: the functions', then the bodies'.
231
+
232
+ Returns:
233
+ Them.
234
+
235
+ """
236
+ return [scope for scope, _ in self.functions] + self.bodies
237
+
238
+
239
+ class _Kept(NamedTuple):
240
+ """A module's last check, kept on its tree: what it knew from outside, its checks, and how it ended."""
241
+
242
+ outside: Outside | None
243
+ checks: Checks
244
+ done: _Round
245
+
246
+ def __deepcopy__(self, _memo: dict[int, object]) -> None:
247
+ """Keep nothing in a copied tree: its nodes are new, so its check starts over.
248
+
249
+ Any node's deep copy can reach the tree: `ast.Load()` is one object every tree shares, and
250
+ pyflakes gives it a parent.
251
+ """
252
+
253
+
254
+ def _resumed(tree: ast.Module, checks: Checks, outside: Outside | None) -> _Round | None:
255
+ """Check a module again from where its last check ended, where only its parameters are typed anew.
256
+
257
+ A file whose functions' parameters every call types (`Outside.parameters`) is checked a second
258
+ time knowing them: with the same tree, checks and everything else from outside, only those
259
+ functions are checked again, and what their new types reach, as any round's are (`_rounds`).
260
+
261
+ Returns:
262
+ The check; or `None` where it must start over: no check kept, anything else changed, LVA012
263
+ asked for (its fix rewrites a scope's own), or a name of the module's top level typed anew.
264
+
265
+ """
266
+ kept: _Kept | None = cast("_Kept | None", vars(tree).get(_KEPT))
267
+ if kept is None or outside is None or kept.outside is None or checks.final or kept.checks != checks:
268
+ return None
269
+ if replace(outside, parameters={}) != replace(kept.outside, parameters={}):
270
+ return None
271
+ seeded: Seeded = kept.done.settings.parameters or Seeded()
272
+ passed: Mapping[int, Mapping[str, Passed]] = keyed(tree, outside.parameters)
273
+ again: set[int] = {
274
+ id(scope)
275
+ for scope, func in kept.done.functions
276
+ if seeded.callers.get(id(func)) != passed.get(id(func))
277
+ }
278
+ settings: Settings = replace(kept.done.settings, parameters=seeded._replace(callers=passed))
279
+ done: _Round = _rounds(tree, kept.done._replace(settings=settings), outside.returned, again)
280
+ return done if bound_once(tree, done.settings) == seeded.module else None
281
+
282
+
283
+ def _settled(
284
+ tree: ast.Module,
285
+ first: Settings,
286
+ imported: Returns,
287
+ names: dict[str, Passed],
288
+ rounds: int = _ROUNDS,
289
+ ) -> _Round:
290
+ """Check the module's scopes, its functions reading its top level's `names` as typed (`bound_once`).
291
+
292
+ A name the module binds to what an unannotated function returns is typed only once that
293
+ function is checked: the scopes are then checked once more, knowing it, until no name's type
294
+ is new (`rounds` times at most).
295
+
296
+ Returns:
297
+ The settings with what its functions return, the scopes checked with them, and what its own
298
+ return.
299
+
300
+ """
301
+ table: returned.Table = returned.Table(tree, imported, first.known.vouched)
302
+ seeded: Seeded = first.parameters or Seeded()
303
+ # The table, filled in as the functions are checked in call order, is what they all read.
304
+ settings: Settings = replace(
305
+ first,
306
+ known=replace(first.known, returned=table.returned),
307
+ parameters=seeded._replace(module=names),
308
+ )
309
+ checked: _Round = _returned(tree, settings, module_scopes(tree, settings, table), table, imported)
310
+ latest: dict[str, Passed] = bound_once(tree, checked.settings)
311
+ return checked if latest == names or rounds <= 1 else _settled(tree, first, imported, latest, rounds - 1)
312
+
313
+
314
+ class Coverage(NamedTuple):
315
+ """How many of a module's typeable first bindings are typed, of how many.
316
+
317
+ `widened`: those a widening annotated (see `widened`), counted apart: not among the typed.
318
+ """
319
+
320
+ typed: int
321
+ total: int
322
+ widened: int = 0
323
+
324
+ @property
325
+ def percent(self) -> float:
326
+ """The typed share, as a percentage (100 when there's nothing to type)."""
327
+ return 100 * self.typed / self.total if self.total else 100.0
328
+
329
+
330
+ # The codes that mean a binding has no type at all (LVA003's type comment is a type).
331
+ _UNTYPED: Final = frozenset({UNANNOTATED, UNTYPED_TARGET, UNANNOTATED_MEMBER})
332
+
333
+
334
+ def annotation_coverage(source: str | bytes, checks: Checks = DEFAULT_CHECKS) -> Coverage:
335
+ """Count the first bindings in `source` the rules cover, and how many are typed.
336
+
337
+ Returns:
338
+ The counts; `# noqa` comments don't make a binding typed. Raises `SyntaxError`.
339
+
340
+ """
341
+ tree: ast.Module = _parse(source, "<unknown>")[0]
342
+ settings: Settings = module_settings(tree, checks, as_text(source).splitlines(), module_tables(tree))
343
+ scopes: list[Scope] = module_scopes(tree, settings)
344
+ total: int = sum(len(scope.bound()) for scope in scopes)
345
+ untyped: int = sum(o.code in _UNTYPED for scope in scopes for o in scope.reported())
346
+ wider: int = sum(len(scope.assignments.widened.intersection(scope.bound())) for scope in scopes)
347
+ return Coverage(total - untyped - wider, total, wider)
348
+
349
+
350
+ def _returned(
351
+ tree: ast.Module,
352
+ settings: Settings,
353
+ scopes: list[Scope],
354
+ table: returned.Table,
355
+ imported: Returns,
356
+ ) -> _Round:
357
+ """Check again what the first pass, in call order, couldn't type the first time.
358
+
359
+ That pass typed each function knowing its callees' returns (see `module_scopes`); what's left is a
360
+ function checked before a callee of its was typed (a cycle, `table.stale`), or with a late-typed
361
+ name it didn't know from the start: `_rounds` checks those again.
362
+
363
+ Returns:
364
+ The check (see `_Round`), its settings with what the functions it imports return too.
365
+
366
+ """
367
+ found: Returned = returned.returned(
368
+ tree,
369
+ table.recorded,
370
+ table.assigned,
371
+ table.used,
372
+ settings.known.vouched,
373
+ )
374
+ settings = replace(settings, known=replace(settings.known, returned=returned.joined(imported, found)))
375
+ functions: list[tuple[Scope, FunctionDef]] = [
376
+ (scope, scope.kind.function) for scope in scopes if scope.kind.function is not None
377
+ ]
378
+ # The first pass knew no attribute's type: they're read once every method is checked.
379
+ again: set[int] = {
380
+ id(scope)
381
+ for scope, func in functions
382
+ if table.stale(func)
383
+ or scope.inferred.late.keys() - scope.inferred.seeded.keys()
384
+ or stored.reads_own(tree, func, settings.owners, found)
385
+ }
386
+ bodies: list[Scope] = [scope for scope in scopes if scope.kind.function is None]
387
+ return _rounds(tree, _Round(settings, functions, bodies, table, found), imported, again)
388
+
389
+
390
+ def _rounds(tree: ast.Module, start: _Round, imported: Returns, again: set[int]) -> _Round:
391
+ """Check the functions whose scopes are in `again` once more, and then what their new types reach.
392
+
393
+ Each round checks them again, reads what the unannotated functions return, and checks again any
394
+ function calling one whose type is new; repeated until nothing changes, so `--fix` finds in one
395
+ run what it would over several. The bodies are checked again if the rounds typed what they use.
396
+
397
+ Returns:
398
+ The check as the rounds leave it.
399
+
400
+ """
401
+ settings: Settings = start.settings
402
+ functions: list[tuple[Scope, FunctionDef]] = start.functions
403
+ table: returned.Table = start.table
404
+ found: Returned = start.found
405
+ changed: bool = False
406
+ retyped: set[str] = set() # the attributes the rounds typed anew
407
+ _round: int
408
+ for _round in range(_ROUNDS):
409
+ if not again:
410
+ break
411
+ functions = _checked_again(tree, functions, again, settings, table)
412
+ latest: Returned = returned.returned(
413
+ tree,
414
+ table.recorded,
415
+ table.assigned,
416
+ table.used,
417
+ settings.known.vouched,
418
+ )
419
+ typed: bool = latest != found and returned.called(tree, tree, latest) # even if only a body calls one
420
+ # Attributes typed anew: what reads one, of any value, may be typed now.
421
+ newly: set[str] = returned.retyped(found, latest)
422
+ # Kept even when nothing here calls it: other files import what the module's functions return.
423
+ if latest != found:
424
+ found = latest
425
+ settings = replace(
426
+ settings,
427
+ known=replace(settings.known, returned=returned.joined(imported, found)),
428
+ )
429
+ changed = changed or typed
430
+ retyped |= newly
431
+ again = {
432
+ id(scope)
433
+ for scope, func in functions
434
+ if (typed and returned.called(tree, func, found))
435
+ or stored.reads(tree, func, newly, anywhere=True)
436
+ or scope.inferred.late.keys() - scope.inferred.seeded.keys()
437
+ }
438
+ bodies: list[Scope] = _bodies(
439
+ tree,
440
+ settings,
441
+ start.bodies,
442
+ (retyped if changed or retyped else None, found),
443
+ )
444
+ return _Round(settings, functions, bodies, table, found)
445
+
446
+
447
+ def _checked_again(
448
+ tree: ast.Module,
449
+ functions: list[tuple[Scope, FunctionDef]],
450
+ again: set[int],
451
+ settings: Settings,
452
+ table: returned.Table,
453
+ ) -> list[tuple[Scope, FunctionDef]]:
454
+ """Check the functions whose scopes are in `again` once more, recording their `return`s and assignments.
455
+
456
+ Returns:
457
+ Every function, with its latest scope.
458
+
459
+ """
460
+ fresh: list[tuple[Scope, FunctionDef]] = [
461
+ (function_scope(func, [], settings, scope.inferred.late), func)
462
+ for scope, func in functions
463
+ if id(scope) in again
464
+ ]
465
+ renewed: dict[int, Scope] = {id(func): scope for scope, func in fresh}
466
+ finish(tree, [scope for scope, _ in fresh])
467
+ table.recorded.update((id(func), recorded.returns(scope, func, tree)) for scope, func in fresh)
468
+ table.assigned.update((id(func), recorded.assigned(scope)) for scope, func in fresh)
469
+ table.used.update((id(func), recorded.used(scope, func, tree)) for scope, func in fresh)
470
+ return [(renewed.get(id(func), scope), func) for scope, func in functions]
471
+
472
+
473
+ def _bodies(
474
+ tree: ast.Module,
475
+ settings: Settings,
476
+ bodies: list[Scope],
477
+ news: tuple[set[str] | None, Returned],
478
+ ) -> list[Scope]:
479
+ """Check the module and class bodies again, if the rounds typed anything they use.
480
+
481
+ They were checked after every function, knowing the first pass's types: only what the rounds
482
+ typed since is news to them (`news`: the attributes they typed, `None` if nothing at all; and
483
+ what the module's own functions return).
484
+
485
+ Returns:
486
+ Their scopes, checked again or as they were.
487
+
488
+ """
489
+ retyped: set[str] | None
490
+ own: Returned
491
+ retyped, own = news
492
+ if (
493
+ retyped is not None
494
+ and bodies
495
+ and any(
496
+ returned.called(tree, stmt, own) or stored.reads(tree, stmt, retyped, anywhere=True)
497
+ for stmt in _body_statements(tree.body)
498
+ )
499
+ ):
500
+ return body_scopes(tree, settings)
501
+ return bodies
502
+
503
+
504
+ def _body_statements(body: Sequence[ast.stmt]) -> Iterator[ast.stmt]:
505
+ """Walk a module's and its classes' own statements, not their functions'.
506
+
507
+ Yields:
508
+ Each statement (a function's definition too, but not what's inside it).
509
+
510
+ """
511
+ stmt: ast.stmt
512
+ for stmt in body:
513
+ yield stmt
514
+ if isinstance(stmt, ast.ClassDef):
515
+ yield from _body_statements(stmt.body)
516
+ elif not isinstance(stmt, FUNCTION_DEFS):
517
+ yield from _body_statements(child_statements(stmt))
518
+
519
+
520
+ def value_flow(
521
+ source: str | bytes,
522
+ filename: str = "<unknown>",
523
+ checks: Checks = DEFAULT_CHECKS,
524
+ ) -> list[Finding]:
525
+ """Return the value-flow findings in `source`, sorted (see `constricter.flow`). Raises `SyntaxError`.
526
+
527
+ `check_tree` reports each as the code its kind names (LVA008, LVA009, LVA010).
528
+
529
+ Returns:
530
+ Every finding, in source order.
531
+
532
+ """
533
+ tree: ast.Module = _parse(source, filename)[0]
534
+ # Its lines place a `**rest` capture at its name, as `check_source` does.
535
+ settings: Settings = module_settings(tree, checks, as_text(source).splitlines(), module_tables(tree))
536
+ return module_flow(tree, module_scopes(tree, settings))