python-constricter 0.4.1__cp313-cp313-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__.cp313-win32.pyd +0 -0
  29. constricter/fix/__init__.py +2 -0
  30. constricter/fix/core/__init__.cp313-win32.pyd +0 -0
  31. constricter/fix/core/__init__.py +2 -0
  32. constricter/fix/core/asked.cp313-win32.pyd +0 -0
  33. constricter/fix/core/asked.py +56 -0
  34. constricter/fix/core/fixes.cp313-win32.pyd +0 -0
  35. constricter/fix/core/fixes.py +317 -0
  36. constricter/fix/core/imports.cp313-win32.pyd +0 -0
  37. constricter/fix/core/imports.py +580 -0
  38. constricter/fix/core/inherited.cp313-win32.pyd +0 -0
  39. constricter/fix/core/inherited.py +311 -0
  40. constricter/fix/core/known.cp313-win32.pyd +0 -0
  41. constricter/fix/core/known.py +730 -0
  42. constricter/fix/core/signatures.cp313-win32.pyd +0 -0
  43. constricter/fix/core/signatures.py +132 -0
  44. constricter/fix/index/__init__.cp313-win32.pyd +0 -0
  45. constricter/fix/index/__init__.py +2 -0
  46. constricter/fix/index/awaits.cp313-win32.pyd +0 -0
  47. constricter/fix/index/awaits.py +90 -0
  48. constricter/fix/index/beyond.cp313-win32.pyd +0 -0
  49. constricter/fix/index/beyond.py +149 -0
  50. constricter/fix/index/callers.cp313-win32.pyd +0 -0
  51. constricter/fix/index/callers.py +155 -0
  52. constricter/fix/index/crossed.cp313-win32.pyd +0 -0
  53. constricter/fix/index/crossed.py +327 -0
  54. constricter/fix/index/decorated.cp313-win32.pyd +0 -0
  55. constricter/fix/index/decorated.py +140 -0
  56. constricter/fix/index/fixtures.cp313-win32.pyd +0 -0
  57. constricter/fix/index/fixtures.py +176 -0
  58. constricter/fix/index/linked.cp313-win32.pyd +0 -0
  59. constricter/fix/index/linked.py +142 -0
  60. constricter/fix/index/loose.cp313-win32.pyd +0 -0
  61. constricter/fix/index/loose.py +103 -0
  62. constricter/fix/index/managed.cp313-win32.pyd +0 -0
  63. constricter/fix/index/managed.py +63 -0
  64. constricter/fix/index/modules.cp313-win32.pyd +0 -0
  65. constricter/fix/index/modules.py +741 -0
  66. constricter/fix/index/offers.cp313-win32.pyd +0 -0
  67. constricter/fix/index/offers.py +252 -0
  68. constricter/fix/index/order.cp313-win32.pyd +0 -0
  69. constricter/fix/index/order.py +155 -0
  70. constricter/fix/index/origins.cp313-win32.pyd +0 -0
  71. constricter/fix/index/origins.py +238 -0
  72. constricter/fix/index/own_methods.cp313-win32.pyd +0 -0
  73. constricter/fix/index/own_methods.py +91 -0
  74. constricter/fix/index/own_overloads.cp313-win32.pyd +0 -0
  75. constricter/fix/index/own_overloads.py +70 -0
  76. constricter/fix/index/own_types.cp313-win32.pyd +0 -0
  77. constricter/fix/index/own_types.py +330 -0
  78. constricter/fix/index/params.cp313-win32.pyd +0 -0
  79. constricter/fix/index/params.py +81 -0
  80. constricter/fix/index/plain.cp313-win32.pyd +0 -0
  81. constricter/fix/index/plain.py +221 -0
  82. constricter/fix/index/project.cp313-win32.pyd +0 -0
  83. constricter/fix/index/project.py +797 -0
  84. constricter/fix/index/sides.cp313-win32.pyd +0 -0
  85. constricter/fix/index/sides.py +160 -0
  86. constricter/fix/index/strings.cp313-win32.pyd +0 -0
  87. constricter/fix/index/strings.py +46 -0
  88. constricter/fix/index/stubs/__init__.cp313-win32.pyd +0 -0
  89. constricter/fix/index/stubs/__init__.py +2 -0
  90. constricter/fix/index/stubs/atoms.cp313-win32.pyd +0 -0
  91. constricter/fix/index/stubs/atoms.py +425 -0
  92. constricter/fix/index/stubs/chained.cp313-win32.pyd +0 -0
  93. constricter/fix/index/stubs/chained.py +69 -0
  94. constricter/fix/index/stubs/classnames.cp313-win32.pyd +0 -0
  95. constricter/fix/index/stubs/classnames.py +124 -0
  96. constricter/fix/index/stubs/declared.cp313-win32.pyd +0 -0
  97. constricter/fix/index/stubs/declared.py +508 -0
  98. constricter/fix/index/stubs/installed.cp313-win32.pyd +0 -0
  99. constricter/fix/index/stubs/installed.py +425 -0
  100. constricter/fix/index/stubs/names.cp313-win32.pyd +0 -0
  101. constricter/fix/index/stubs/names.py +200 -0
  102. constricter/fix/index/stubs/read.cp313-win32.pyd +0 -0
  103. constricter/fix/index/stubs/read.py +70 -0
  104. constricter/fix/index/stubs/signatures.cp313-win32.pyd +0 -0
  105. constricter/fix/index/stubs/signatures.py +656 -0
  106. constricter/fix/index/stubs/stubbed.cp313-win32.pyd +0 -0
  107. constricter/fix/index/stubs/stubbed.py +208 -0
  108. constricter/fix/index/taken.cp313-win32.pyd +0 -0
  109. constricter/fix/index/taken.py +45 -0
  110. constricter/fix/index/tuples.cp313-win32.pyd +0 -0
  111. constricter/fix/index/tuples.py +112 -0
  112. constricter/fix/libraries/__init__.cp313-win32.pyd +0 -0
  113. constricter/fix/libraries/__init__.py +2 -0
  114. constricter/fix/libraries/library.cp313-win32.pyd +0 -0
  115. constricter/fix/libraries/library.py +420 -0
  116. constricter/fix/libraries/opened.cp313-win32.pyd +0 -0
  117. constricter/fix/libraries/opened.py +147 -0
  118. constricter/fix/libraries/overloads.cp313-win32.pyd +0 -0
  119. constricter/fix/libraries/overloads.py +960 -0
  120. constricter/fix/libraries/receivers.cp313-win32.pyd +0 -0
  121. constricter/fix/libraries/receivers.py +274 -0
  122. constricter/fix/libraries/stdlib.cp313-win32.pyd +0 -0
  123. constricter/fix/libraries/stdlib.py +752 -0
  124. constricter/fix/libraries/walks.cp313-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__.cp313-win32.pyd +0 -0
  148. constricter/fix/values/__init__.py +2 -0
  149. constricter/fix/values/aliased.cp313-win32.pyd +0 -0
  150. constricter/fix/values/aliased.py +195 -0
  151. constricter/fix/values/bodies.cp313-win32.pyd +0 -0
  152. constricter/fix/values/bodies.py +76 -0
  153. constricter/fix/values/callables.cp313-win32.pyd +0 -0
  154. constricter/fix/values/callables.py +323 -0
  155. constricter/fix/values/called.cp313-win32.pyd +0 -0
  156. constricter/fix/values/called.py +194 -0
  157. constricter/fix/values/classvars.cp313-win32.pyd +0 -0
  158. constricter/fix/values/classvars.py +629 -0
  159. constricter/fix/values/decided.cp313-win32.pyd +0 -0
  160. constricter/fix/values/decided.py +429 -0
  161. constricter/fix/values/displays.cp313-win32.pyd +0 -0
  162. constricter/fix/values/displays.py +101 -0
  163. constricter/fix/values/doubts.cp313-win32.pyd +0 -0
  164. constricter/fix/values/doubts.py +677 -0
  165. constricter/fix/values/entered.cp313-win32.pyd +0 -0
  166. constricter/fix/values/entered.py +332 -0
  167. constricter/fix/values/fills.cp313-win32.pyd +0 -0
  168. constricter/fix/values/fills.py +446 -0
  169. constricter/fix/values/guesses.cp313-win32.pyd +0 -0
  170. constricter/fix/values/guesses.py +376 -0
  171. constricter/fix/values/hinted.cp313-win32.pyd +0 -0
  172. constricter/fix/values/hinted.py +811 -0
  173. constricter/fix/values/inference.cp313-win32.pyd +0 -0
  174. constricter/fix/values/inference.py +698 -0
  175. constricter/fix/values/loops.cp313-win32.pyd +0 -0
  176. constricter/fix/values/loops.py +335 -0
  177. constricter/fix/values/members.cp313-win32.pyd +0 -0
  178. constricter/fix/values/members.py +578 -0
  179. constricter/fix/values/narrowed.cp313-win32.pyd +0 -0
  180. constricter/fix/values/narrowed.py +220 -0
  181. constricter/fix/values/operated.cp313-win32.pyd +0 -0
  182. constricter/fix/values/operated.py +202 -0
  183. constricter/fix/values/ordered.cp313-win32.pyd +0 -0
  184. constricter/fix/values/ordered.py +74 -0
  185. constricter/fix/values/returned.cp313-win32.pyd +0 -0
  186. constricter/fix/values/returned.py +479 -0
  187. constricter/fix/values/returns.cp313-win32.pyd +0 -0
  188. constricter/fix/values/returns.py +237 -0
  189. constricter/fix/values/shapes.cp313-win32.pyd +0 -0
  190. constricter/fix/values/shapes.py +654 -0
  191. constricter/fix/values/stored.cp313-win32.pyd +0 -0
  192. constricter/fix/values/stored.py +564 -0
  193. constricter/fix/values/targets.cp313-win32.pyd +0 -0
  194. constricter/fix/values/targets.py +419 -0
  195. constricter/fix/values/unchecked.cp313-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__.cp313-win32.pyd +0 -0
  208. constricter/rules/__init__.py +2 -0
  209. constricter/rules/annotations.cp313-win32.pyd +0 -0
  210. constricter/rules/annotations.py +296 -0
  211. constricter/rules/binding.cp313-win32.pyd +0 -0
  212. constricter/rules/binding.py +652 -0
  213. constricter/rules/calls.cp313-win32.pyd +0 -0
  214. constricter/rules/calls.py +460 -0
  215. constricter/rules/checker.cp313-win32.pyd +0 -0
  216. constricter/rules/checker.py +536 -0
  217. constricter/rules/declarations.cp313-win32.pyd +0 -0
  218. constricter/rules/declarations.py +730 -0
  219. constricter/rules/decorators.cp313-win32.pyd +0 -0
  220. constricter/rules/decorators.py +351 -0
  221. constricter/rules/flow.cp313-win32.pyd +0 -0
  222. constricter/rules/flow.py +502 -0
  223. constricter/rules/keys.cp313-win32.pyd +0 -0
  224. constricter/rules/keys.py +79 -0
  225. constricter/rules/late.cp313-win32.pyd +0 -0
  226. constricter/rules/late.py +969 -0
  227. constricter/rules/narrowing.cp313-win32.pyd +0 -0
  228. constricter/rules/narrowing.py +98 -0
  229. constricter/rules/parsed.cp313-win32.pyd +0 -0
  230. constricter/rules/parsed.py +136 -0
  231. constricter/rules/quoted.cp313-win32.pyd +0 -0
  232. constricter/rules/quoted.py +141 -0
  233. constricter/rules/rebinding.cp313-win32.pyd +0 -0
  234. constricter/rules/rebinding.py +169 -0
  235. constricter/rules/recorded.cp313-win32.pyd +0 -0
  236. constricter/rules/recorded.py +168 -0
  237. constricter/rules/redundant.cp313-win32.pyd +0 -0
  238. constricter/rules/redundant.py +160 -0
  239. constricter/rules/scope.cp313-win32.pyd +0 -0
  240. constricter/rules/scope.py +802 -0
  241. constricter/rules/scoped.cp313-win32.pyd +0 -0
  242. constricter/rules/scoped.py +232 -0
  243. constricter/rules/scoping.cp313-win32.pyd +0 -0
  244. constricter/rules/scoping.py +346 -0
  245. constricter/rules/settings.cp313-win32.pyd +0 -0
  246. constricter/rules/settings.py +189 -0
  247. constricter/rules/syntax.cp313-win32.pyd +0 -0
  248. constricter/rules/syntax.py +472 -0
  249. constricter/rules/tables.cp313-win32.pyd +0 -0
  250. constricter/rules/tables.py +169 -0
  251. constricter/rules/walked.cp313-win32.pyd +0 -0
  252. constricter/rules/walked.py +127 -0
  253. constricter/rules/widened.cp313-win32.pyd +0 -0
  254. constricter/rules/widened.py +322 -0
  255. constricter/trace.py +374 -0
  256. e45ce06b2fdd5c43f782__mypyc.cp313-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,220 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Where a type checker narrows a value: the lines a test governs, and what it tests there.
3
+
4
+ An `if` or `while` narrows what its condition tests in its body (and, negated, in its `else`); a
5
+ `match` its subject in each case; an `assert`, and an `if` whose body always leaves (`return`,
6
+ `raise`, `continue`, `break`), what they test for the rest of their block. A copy of a narrowed
7
+ value there has the narrowed type, not its declared one: `--fix` offers it none (see `scope`). A
8
+ check (`isinstance`, a `TypeGuard`, a `match`) narrows any type; a truth test or a comparison only
9
+ a union (`int | None`).
10
+ """
11
+
12
+ import ast
13
+ from collections.abc import Iterator, Mapping, Sequence
14
+ from functools import lru_cache
15
+ from typing import Final, TypeAlias, cast
16
+
17
+ from constricter.rules.walked import walk
18
+
19
+ # The lines a test governs (first and last, from 1), and whether it's a check, which narrows any
20
+ # type (not a truth test or comparison, which narrows only a union); by each value it narrows.
21
+ Region: TypeAlias = tuple[int, int, bool]
22
+ Regions: TypeAlias = Mapping[str, tuple[Region, ...]]
23
+ _LEAVING: Final = (ast.Return, ast.Raise, ast.Continue, ast.Break)
24
+ _GUARDS: Final = frozenset({"isinstance", "issubclass", "callable", "hasattr"})
25
+ _READS: Final = (ast.Name, ast.Attribute, ast.Subscript)
26
+
27
+
28
+ def regions(tree: ast.Module) -> Regions:
29
+ """Find every region a test narrows a value in, in a module.
30
+
31
+ Returns:
32
+ Each value's regions.
33
+
34
+ """
35
+ found: dict[str, list[Region]] = {}
36
+ text: str
37
+ region: Region
38
+ for text, region in _block(tree.body):
39
+ found.setdefault(text, []).append(region)
40
+ return {text: tuple(spans) for text, spans in found.items()}
41
+
42
+
43
+ def narrowed_at(found: Regions, value: ast.expr, line: int, *, union: bool) -> bool:
44
+ """Check whether a read (`x`, `self.x`, `d[k]`) is narrowed at `line` by a test around it.
45
+
46
+ `union`: whether its type is one, which a truth test or a comparison narrows too.
47
+
48
+ Returns:
49
+ Whether it is.
50
+
51
+ """
52
+ return isinstance(value, _READS) and read_narrowed(found, ast.unparse(value), line, union=union)
53
+
54
+
55
+ def read_narrowed(found: Regions, read: str, line: int, *, union: bool) -> bool:
56
+ """Check whether a read, as text, is narrowed at `line` (see `narrowed_at`).
57
+
58
+ Returns:
59
+ Whether it is.
60
+
61
+ """
62
+ return any(start <= line <= end and (check or union) for start, end, check in found.get(read, ()))
63
+
64
+
65
+ @lru_cache(maxsize=64) # asked of each of a function's guessed unions
66
+ def tested_lines(function: ast.AST) -> Mapping[str, frozenset[int]]:
67
+ """Find the lines on which a test of a function's own narrows a value it goes on to read.
68
+
69
+ A condition's own lines (`if opt and opt.cb:`), a conditional expression's
70
+ (`opt.cb if opt else None`), a filtered comprehension's, and those of an `and` or `or` anywhere
71
+ (`return opt and opt.cb`).
72
+
73
+ Returns:
74
+ Each value such a test narrows, as source text, and those lines.
75
+
76
+ """
77
+ found: dict[str, set[int]] = {}
78
+ node: ast.AST
79
+ for node in walk(function):
80
+ test: ast.expr
81
+ whole: ast.expr
82
+ for test, whole in _spanned(node):
83
+ lines: range = range(min(test.lineno, whole.lineno), (whole.end_lineno or whole.lineno) + 1)
84
+ text: str
85
+ for text, _ in _narrows(test):
86
+ found.setdefault(text, set()).update(lines)
87
+ return {text: frozenset(lines) for text, lines in found.items()}
88
+
89
+
90
+ def _spanned(node: ast.AST) -> Iterator[tuple[ast.expr, ast.expr]]:
91
+ """Find the tests a node holds, each with the expression whose lines it narrows on.
92
+
93
+ Yields:
94
+ Each test, and that expression: the test itself, or what it's part of.
95
+
96
+ """
97
+ test: ast.expr
98
+ match node:
99
+ case ast.If(test=test) | ast.While(test=test) | ast.Assert(test=test):
100
+ yield test, test
101
+ case ast.IfExp(test=test):
102
+ yield test, node
103
+ case ast.BoolOp():
104
+ yield node, node
105
+ case ast.ListComp() | ast.SetComp() | ast.DictComp() | ast.GeneratorExp():
106
+ yield from ((test, node) for each in node.generators for test in each.ifs)
107
+ case _:
108
+ pass
109
+
110
+
111
+ def _block(body: Sequence[ast.stmt]) -> Iterator[tuple[str, Region]]:
112
+ """Walk a block's statements for the regions their tests narrow, inner blocks included.
113
+
114
+ Yields:
115
+ Each narrowed value, and a region.
116
+
117
+ """
118
+ end: int = (body[-1].end_lineno or body[-1].lineno) if body else 0
119
+ stmt: ast.stmt
120
+ for stmt in body:
121
+ rest: tuple[int, int] = ((stmt.end_lineno or stmt.lineno) + 1, end)
122
+ text: str
123
+ check: bool
124
+ for text, check in _tested(stmt):
125
+ yield from ((text, (*span, check)) for span in _governed(stmt, rest))
126
+ inner: list[ast.stmt]
127
+ for inner in _bodies(stmt):
128
+ yield from _block(inner)
129
+
130
+
131
+ def _tested(stmt: ast.stmt) -> frozenset[tuple[str, bool]]:
132
+ """Name what a statement's test narrows.
133
+
134
+ Returns:
135
+ Each value, as source text, and whether it's checked (see `Region`).
136
+
137
+ """
138
+ test: ast.expr
139
+ subject: ast.expr
140
+ match stmt:
141
+ case ast.If(test=test) | ast.While(test=test) | ast.Assert(test=test):
142
+ return frozenset(_narrows(test))
143
+ case ast.Match(subject=subject) if isinstance(subject, _READS):
144
+ return frozenset({(ast.unparse(subject), True)})
145
+ case _:
146
+ return frozenset()
147
+
148
+
149
+ def _narrows(test: ast.expr) -> Iterator[tuple[str, bool]]:
150
+ """Walk a condition for the values it narrows: a value tested for truth, compared, or checked.
151
+
152
+ Yields:
153
+ Each, as source text (`x`, `self.x`), not what it's compared with or checked against; and
154
+ whether it's checked (a call's argument: `isinstance`, a `TypeGuard` function).
155
+
156
+ """
157
+ operand: ast.expr
158
+ values: list[ast.expr]
159
+ left: ast.expr
160
+ args: list[ast.expr]
161
+ name: str
162
+ match test:
163
+ case ast.Name() | ast.Attribute() | ast.Subscript():
164
+ yield ast.unparse(test), False
165
+ case ast.UnaryOp(operand=operand):
166
+ yield from _narrows(operand)
167
+ case ast.BoolOp(values=values):
168
+ yield from (found for value in values for found in _narrows(value))
169
+ case ast.Compare(left=left) if isinstance(left, _READS):
170
+ yield ast.unparse(left), False
171
+ case ast.Call(func=ast.Name(id=name), args=args) if name in _GUARDS:
172
+ yield from ((ast.unparse(arg), True) for arg in args[:1] if isinstance(arg, _READS))
173
+ case ast.Call(args=args): # a `TypeGuard` or `TypeIs` function may narrow any it's passed
174
+ yield from ((ast.unparse(arg), True) for arg in args if isinstance(arg, _READS))
175
+ case _:
176
+ pass
177
+
178
+
179
+ def _governed(stmt: ast.stmt, rest: tuple[int, int]) -> Iterator[tuple[int, int]]:
180
+ """Find the lines a statement's test governs.
181
+
182
+ Yields:
183
+ Each region: its branches, and the rest of its block (`rest`) after an `assert`, or after an
184
+ `if` whose body always leaves.
185
+
186
+ """
187
+ match stmt:
188
+ case ast.Assert():
189
+ yield rest
190
+ case ast.If() | ast.While():
191
+ yield from (_span(branch) for branch in (stmt.body, stmt.orelse) if branch)
192
+ if isinstance(stmt, ast.If) and isinstance(stmt.body[-1], _LEAVING):
193
+ yield rest
194
+ case _: # a `match`, the one other statement `_tested` finds a test in
195
+ yield from (_span(case.body) for case in cast("ast.Match", stmt).cases)
196
+
197
+
198
+ def _span(body: Sequence[ast.stmt]) -> tuple[int, int]:
199
+ return body[0].lineno, body[-1].end_lineno or body[-1].lineno
200
+
201
+
202
+ def _bodies(stmt: ast.stmt) -> Iterator[list[ast.stmt]]:
203
+ """Find the blocks inside a statement: a function's or class's body, a branch, a handler, a case.
204
+
205
+ Yields:
206
+ Each.
207
+
208
+ """
209
+ match stmt:
210
+ case ast.FunctionDef() | ast.AsyncFunctionDef() | ast.ClassDef() | ast.With() | ast.AsyncWith():
211
+ yield stmt.body
212
+ case ast.If() | ast.For() | ast.AsyncFor() | ast.While():
213
+ yield from (block for block in (stmt.body, stmt.orelse) if block)
214
+ case ast.Try() | ast.TryStar():
215
+ yield from (block for block in (stmt.body, stmt.orelse, stmt.finalbody) if block)
216
+ yield from (handler.body for handler in stmt.handlers)
217
+ case ast.Match():
218
+ yield from (case.body for case in stmt.cases)
219
+ case _:
220
+ pass
@@ -0,0 +1,202 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Binary operators `--fix` types by their operands' builtin types, whose operators nothing can overload.
3
+
4
+ Numbers: `/` gives a `float`; `+`, `-`, `*`, `//` and `%` a `float` if either side is one, else an
5
+ `int`; `<<` and `>>` of integers an `int`, as their `&`, `|` and `^` are (of two `bool`s, a `bool`);
6
+ `**` an `int` by a literal exponent that isn't negative, and by any other integer a `float`, which
7
+ takes the `int` it may be as well as the `float` a negative one gives; a `float`'s by an integer a
8
+ `float`. `str` and `bytes`: `+` of two, `*` by an integer on either side, and `%`
9
+ formatting give the same type back; so do a `list[T]`'s `+` of another and `*` by an integer.
10
+ Tuples: `+` gives both sides' parts, and `*` by an integer a `tuple[T, ...]` of the one type they
11
+ have. A `set[T]`'s or a `frozenset[T]`'s `|`, `&`, `-` and `^` with another, and a `dict[K, V]`'s
12
+ `|`, give that type.
13
+ """
14
+
15
+ import ast
16
+ from typing import Final, cast
17
+
18
+ _INT: Final = "int"
19
+ _FLOAT: Final = "float"
20
+ _BOOL: Final = "bool"
21
+ _INTEGERS: Final = frozenset({_BOOL, _INT})
22
+ _NUMBERS: Final = _INTEGERS | {_FLOAT}
23
+ _TEXTS: Final = frozenset({"str", "bytes"})
24
+ _LIST: Final = "list"
25
+ _TUPLE: Final = "tuple"
26
+ _OPEN: Final = "[" # where a subscripted annotation's arguments start
27
+ _SETS: Final = frozenset({"set", "frozenset"})
28
+ _DICT: Final = "dict"
29
+ _SAME_TYPE: Final = (ast.Add, ast.Sub, ast.Mult, ast.FloorDiv, ast.Mod) # an `int`'s, or a `float`'s
30
+ _SHIFTS: Final = (ast.LShift, ast.RShift)
31
+ _BITWISE: Final = (ast.BitAnd, ast.BitOr, ast.BitXor)
32
+ _SET_OPERATORS: Final = (ast.BitOr, ast.BitAnd, ast.Sub, ast.BitXor)
33
+ _ANY_LENGTH: Final = 2 # `tuple[T, ...]`'s two parts: the element type and the ellipsis
34
+ _ELLIPSIS: Final = "..."
35
+
36
+
37
+ def operated(value: ast.BinOp, left: str | None, right: str | None, max_length: int) -> str | None:
38
+ """Type `value`, a binary operator between values typed `left` and `right` (see the module docstring).
39
+
40
+ `max_length`: the longest tuple written part by part.
41
+
42
+ Returns:
43
+ The annotation, or `None` for any other operator or operand.
44
+
45
+ """
46
+ if left is None:
47
+ return None
48
+ if left in _NUMBERS and right in _NUMBERS:
49
+ return _number(value, left, right or "")
50
+ if left in _INTEGERS and isinstance(value.op, ast.Mult):
51
+ return _repeated(right or "", left) # `3 * "ab"`: the sequence's own `*`, reflected
52
+ if _root(left) == _TUPLE:
53
+ return _tuple(value.op, left, right or "", max_length)
54
+ if _root(left) in _SETS | {_DICT}:
55
+ return _same(value.op, left, right)
56
+ text: str | None = left if left in _TEXTS or _root(left) == _LIST else None
57
+ return text if text is not None and _keeps_text(value.op, text, right) else None
58
+
59
+
60
+ def _root(annotation: str) -> str:
61
+ """Name a subscripted annotation's outer type (`list` for `list[int]`).
62
+
63
+ Returns:
64
+ Its text before the `[`, or nothing for one that isn't subscripted: a union of them too.
65
+
66
+ """
67
+ if _OPEN not in annotation: # an unknown operand's has no text at all
68
+ return ""
69
+ # `annotation` is always `ast.unparse`'s own output, so it's always valid Python to parse back.
70
+ whole: ast.expr = ast.parse(annotation, mode="eval").body
71
+ return ast.unparse(whole.value) if isinstance(whole, ast.Subscript) else ""
72
+
73
+
74
+ def _number(value: ast.BinOp, left: str, right: str) -> str | None:
75
+ """Type an operator between two builtin numbers.
76
+
77
+ Returns:
78
+ The annotation, or `None` for an operator whose result their types don't decide.
79
+
80
+ """
81
+ op: ast.operator = value.op
82
+ floating: bool = _FLOAT in {left, right}
83
+ if isinstance(op, ast.Div):
84
+ return _FLOAT
85
+ if isinstance(op, _SAME_TYPE):
86
+ return _FLOAT if floating else _INT
87
+ if isinstance(op, ast.Pow):
88
+ return _power(value.right, left, right)
89
+ if floating:
90
+ return None
91
+ if isinstance(op, _SHIFTS):
92
+ return _INT
93
+ both: bool = left == right == _BOOL
94
+ return (_BOOL if both else _INT) if isinstance(op, _BITWISE) else None
95
+
96
+
97
+ def _power(exponent: ast.expr, left: str, right: str) -> str | None:
98
+ """Type `left ** right`: an integer's by a literal that isn't negative, or by another integer.
99
+
100
+ Returns:
101
+ The annotation: `int` by such a literal; by another integer, which may be negative, a `float`,
102
+ which takes an `int` too; `None` where the exponent is a `float`: a negative number's gives a
103
+ `complex`.
104
+
105
+ """
106
+ if right not in _INTEGERS:
107
+ return None
108
+ natural: bool = isinstance(exponent, ast.Constant) and isinstance(exponent.value, int)
109
+ return _INT if left in _INTEGERS and natural else _FLOAT
110
+
111
+
112
+ def _repeated(sequence: str, count: str) -> str | None:
113
+ """Type a `str`, `bytes`, `list` or tuple repeated by a value typed `count`.
114
+
115
+ Returns:
116
+ The sequence's type (a tuple's as `tuple[T, ...]`), or `None` if it isn't one, or `count`
117
+ isn't an integer.
118
+
119
+ """
120
+ if count not in _INTEGERS:
121
+ return None
122
+ if sequence in _TEXTS or _root(sequence) == _LIST:
123
+ return sequence
124
+ parts: list[str] | None = _parts(sequence)
125
+ return None if parts is None else _any_length(parts)
126
+
127
+
128
+ def _keeps_text(op: ast.operator, text: str, right: str | None) -> bool:
129
+ """Check whether `text op right` (`text` a `str`, a `bytes` or a `list[T]`) gives `text` back.
130
+
131
+ Returns:
132
+ Whether it does: `+` of two, `*` by an integer, or a `str`'s or `bytes`'s `%` formatting.
133
+
134
+ """
135
+ if isinstance(op, ast.Add):
136
+ return right == text
137
+ if isinstance(op, ast.Mult):
138
+ return right in _INTEGERS
139
+ return isinstance(op, ast.Mod) and text in _TEXTS
140
+
141
+
142
+ def _tuple(op: ast.operator, left: str, right: str, max_length: int) -> str | None:
143
+ """Type a tuple's `+` of another, or its `*` by an integer.
144
+
145
+ Returns:
146
+ Both tuples' parts, where each has a fixed length and they're no more than `max_length`; a
147
+ `tuple[T, ...]` where every part of both is a `T`; else `None`.
148
+
149
+ """
150
+ if isinstance(op, ast.Mult):
151
+ return _repeated(left, right)
152
+ first: list[str] | None = _parts(left)
153
+ second: list[str] | None = _parts(right) if isinstance(op, ast.Add) else None
154
+ if first is None or second is None:
155
+ return None
156
+ fixed: bool = not (_is_any_length(first) or _is_any_length(second))
157
+ if fixed and len(first) + len(second) <= max_length:
158
+ return f"{_TUPLE}[{', '.join(first + second)}]"
159
+ return _any_length(first + second)
160
+
161
+
162
+ def _parts(annotation: str) -> list[str] | None:
163
+ """Read a tuple type's parts (`tuple[int, str]`, `tuple[int, ...]`).
164
+
165
+ Returns:
166
+ Them, as text, a `...` included; `None` for any other type, or a tuple that unpacks one.
167
+
168
+ """
169
+ if _root(annotation) != _TUPLE:
170
+ return None
171
+ whole: ast.expr = cast("ast.Subscript", ast.parse(annotation, mode="eval").body).slice
172
+ elements: list[ast.expr] = whole.elts if isinstance(whole, ast.Tuple) else [whole]
173
+ starred: bool = any(isinstance(element, ast.Starred) for element in elements)
174
+ return None if starred or not elements else [ast.unparse(element) for element in elements]
175
+
176
+
177
+ def _is_any_length(parts: list[str]) -> bool:
178
+ return len(parts) == _ANY_LENGTH and parts[1] == _ELLIPSIS
179
+
180
+
181
+ def _any_length(parts: list[str]) -> str | None:
182
+ """Write the `tuple[T, ...]` of tuples' `parts`.
183
+
184
+ Returns:
185
+ It, or `None` where they aren't all one type.
186
+
187
+ """
188
+ types: set[str] = set(parts) - {_ELLIPSIS}
189
+ return f"{_TUPLE}[{types.pop()}, ...]" if len(types) == 1 else None
190
+
191
+
192
+ def _same(op: ast.operator, left: str, right: str | None) -> str | None:
193
+ """Type a set's operator with another of its type, or a `dict`'s `|` with another of its.
194
+
195
+ Returns:
196
+ That type, or `None` for any other operator, or a right side of another type.
197
+
198
+ """
199
+ if left != right:
200
+ return None
201
+ merged: bool = isinstance(op, ast.BitOr) if _root(left) == _DICT else isinstance(op, _SET_OPERATORS)
202
+ return left if merged else None
@@ -0,0 +1,74 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """The order a module's functions are checked in: each after those it calls, whose `return`s then type it."""
3
+
4
+ import ast
5
+ from collections.abc import Iterator, Sequence
6
+ from typing import TypeAlias
7
+
8
+ from constricter.fix.values import returned
9
+ from constricter.rules.syntax import FunctionDef
10
+
11
+ # The functions by name: the module's (`[False]`), then the classes' methods (`[True]`).
12
+ _Named: TypeAlias = tuple[dict[str, list[FunctionDef]], dict[str, list[FunctionDef]]]
13
+ # One function to put in call order, and its callees still to put in before it.
14
+ _Visit: TypeAlias = tuple[FunctionDef, Iterator[FunctionDef]]
15
+
16
+
17
+ def in_call_order(module: ast.Module, functions: Sequence[FunctionDef]) -> list[FunctionDef]:
18
+ """Order `functions` so that each comes after the functions it calls (a cycle's, in any order).
19
+
20
+ Its callees: the module's functions it calls by name, and every method named as it calls one
21
+ (`x.m()`: any class's `m`), as `called` counts them.
22
+
23
+ Returns:
24
+ Them, callees first; otherwise in their own order.
25
+
26
+ """
27
+ named: _Named = ({}, {})
28
+ func: FunctionDef
29
+ for func in functions:
30
+ slot: returned.Slot | None
31
+ if (slot := returned.slots(module).get(id(func))) is not None:
32
+ named[slot.owner is not None].setdefault(slot.name, []).append(func)
33
+ ordered: list[FunctionDef] = []
34
+ seen: set[int] = set()
35
+ for func in functions:
36
+ if id(func) not in seen:
37
+ seen.add(id(func))
38
+ _after_callees(module, func, named, (ordered, seen))
39
+ return ordered
40
+
41
+
42
+ def _after_callees(
43
+ module: ast.Module,
44
+ func: FunctionDef,
45
+ named: _Named,
46
+ into: tuple[list[FunctionDef], set[int]],
47
+ ) -> None:
48
+ """Put `func` in the order after its callees, depth first, with a stack: a call chain can be long."""
49
+ ordered: list[FunctionDef]
50
+ seen: set[int]
51
+ ordered, seen = into
52
+ # Each entry: a function, and its callees still to put in before it; `None` at the bottom ends it.
53
+ waiting: list[_Visit | None] = [None, (func, _callees(module, func, named))]
54
+ entry: _Visit
55
+ for entry in iter(waiting.pop, None):
56
+ callee: FunctionDef | None
57
+ if (callee := next((one for one in entry[1] if id(one) not in seen), None)) is None:
58
+ ordered.append(entry[0])
59
+ else:
60
+ seen.add(id(callee))
61
+ waiting.extend([entry, (callee, _callees(module, callee, named))])
62
+
63
+
64
+ def _callees(module: ast.Module, func: FunctionDef, named: _Named) -> Iterator[FunctionDef]:
65
+ """Find the functions of `named` that `func` calls: the module's by name, methods by attribute.
66
+
67
+ Yields:
68
+ Each, in the order of its calls.
69
+
70
+ """
71
+ name: str | None
72
+ attr: str | None
73
+ for name, attr in returned.callees_in(module, func):
74
+ yield from named[False].get(name, []) if name is not None else named[True].get(attr or "", [])