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,479 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """`--fix` for calls to unannotated functions: their own `return` statements decide their type.
3
+
4
+ A function (or method) counts when it's plain (a `def` directly in the module or a class body, not
5
+ decorated or redefined), declares no return type, every `return` it has gives a value whose type
6
+ `--fix` is sure of, all the same, and it can't fall off its end (which returns `None`). A generator
7
+ counts by its `yield`s instead, all of one type `T`: a `Generator[T, None, None]` (see
8
+ `constricter.rules.recorded`). An `async def`'s `return`s type what awaiting its call gives, under
9
+ `AWAIT` before its name; a function `@contextmanager` makes a manager, what `with` gives of it,
10
+ under `entered.ENTERED`. A module function's type is certain; a method's is a guess, since a
11
+ subclass may override it. Each value is typed as the checker sees it there, with the function's own
12
+ locals.
13
+ """
14
+
15
+ import ast
16
+ import operator
17
+ from collections.abc import Iterator, Mapping, Sequence
18
+ from functools import lru_cache
19
+ from typing import Final, NamedTuple, TypeAlias
20
+
21
+ from constricter.fix.core.imports import bound_within
22
+ from constricter.fix.core.inherited import Lineage
23
+ from constricter.fix.core.known import Returned, Returns
24
+ from constricter.fix.core.signatures import AWAIT
25
+ from constricter.fix.values import entered
26
+ from constricter.fix.values.bodies import is_generator, terminates
27
+ from constricter.fix.values.inference import RETURNED
28
+ from constricter.fix.values.stored import (
29
+ FUNCTIONS,
30
+ Assigned,
31
+ Recorded,
32
+ Used,
33
+ attribute_types,
34
+ class_names,
35
+ owners,
36
+ self_methods,
37
+ subclasses,
38
+ superclasses,
39
+ )
40
+ from constricter.rules.annotations import roots
41
+ from constricter.rules.decorators import FIXTURES, spelled
42
+ from constricter.rules.flow import members
43
+ from constricter.rules.syntax import FunctionDef, Start, within
44
+ from constricter.rules.walked import classes, of_type
45
+
46
+ _NONE: Final = "None"
47
+ _UNVOUCHED: Final[frozenset[str]] = frozenset() # no decorator vouched for: a module read alone
48
+ # What a call calls: a name (`f()`), or an attribute (`x.m()`), the other `None`.
49
+ _Callee: TypeAlias = tuple[str | None, str | None]
50
+ _Call: TypeAlias = tuple[tuple[int, int], _Callee] # where a call starts (line, column), and what it calls
51
+
52
+
53
+ def returned(
54
+ tree: ast.Module,
55
+ recorded: Mapping[int, Sequence[Recorded]],
56
+ assigned: Mapping[int, Sequence[Assigned]],
57
+ used: Mapping[int, Sequence[Used]],
58
+ vouched: frozenset[str] = _UNVOUCHED,
59
+ ) -> Returned:
60
+ """Read what the module's unannotated functions and methods return, and what its classes assign.
61
+
62
+ `recorded` holds each checked function's `return` statements, `assigned` its `self.x = value`
63
+ assignments and `used` its reads of `self`'s empty containers, by the function's `id()`;
64
+ `vouched` spells the decorators that give a function back (see `constricter.rules.decorators`).
65
+
66
+ Returns:
67
+ Their return types: the module's functions', and each class's methods'; and each class's
68
+ unannotated instance attributes' types (see `attribute_types`).
69
+
70
+ """
71
+ calls: dict[str, str] = {}
72
+ methods: dict[str, dict[str, str]] = {}
73
+ guesses: dict[str, frozenset[str]] = {}
74
+ name: str
75
+ annotation: str
76
+ origins: frozenset[str]
77
+ for name, annotation, origins in _typed(tree, tree.body, recorded, vouched):
78
+ calls[name] = annotation
79
+ if origins:
80
+ guesses[name] = origins
81
+ attributes: dict[str, dict[str, str]] = {}
82
+ node: ast.ClassDef
83
+ for node in classes(tree):
84
+ for name, annotation, origins in _typed(tree, node.body, recorded, vouched):
85
+ methods.setdefault(node.name, {})[name] = annotation
86
+ if origins:
87
+ guesses[f"{node.name}.{name}"] = origins
88
+ if class_names(tree)[node.name] == 1: # its attributes are looked up by its name
89
+ for name, annotation, origins in attribute_types(tree, node, (assigned, used)):
90
+ attributes.setdefault(node.name, {})[name] = annotation
91
+ guesses[f"{node.name}.{name}"] = origins
92
+ return Returned(calls, methods, guesses, attributes)
93
+
94
+
95
+ def joined(imported: Returns, own: Returned) -> Returned:
96
+ """Add what the functions a module imports return to what its own return (its own first).
97
+
98
+ Returns:
99
+ Both.
100
+
101
+ """
102
+ if not imported.calls and not imported.methods:
103
+ return own
104
+ return own._replace(
105
+ calls={**imported.calls, **own.calls},
106
+ methods={**imported.methods, **own.methods},
107
+ guesses={**imported.guesses, **own.guesses},
108
+ )
109
+
110
+
111
+ def exported(own: Returned, lineage: Lineage) -> Returns:
112
+ """Pick out what the module's own functions and its classes' methods return, for the files importing them.
113
+
114
+ A class has the methods it takes from the module's other classes too (see `Lineage`), but one
115
+ whose type names that base, which may be the class itself (`return self`).
116
+
117
+ Returns:
118
+ Their types, and their guesses' origins (a method's as `C.m`).
119
+
120
+ """
121
+ methods: dict[str, dict[str, str]] = {owner: dict(typed) for owner, typed in own.methods.items() if typed}
122
+ guesses: dict[str, frozenset[str]] = {
123
+ name: own.guesses[name]
124
+ for name in (*own.calls, *(f"{owner}.{name}" for owner, typed in methods.items() for name in typed))
125
+ if name in own.guesses
126
+ }
127
+ owner: str
128
+ bases: tuple[str, ...]
129
+ for owner, bases in lineage.order.items():
130
+ base: str
131
+ name: str
132
+ annotation: str
133
+ for base in bases:
134
+ for name, annotation in own.methods.get(base, {}).items():
135
+ if lineage.definer(owner, name) == base and base not in roots(annotation):
136
+ methods.setdefault(owner, {})[name] = annotation
137
+ if f"{base}.{name}" in own.guesses:
138
+ guesses[f"{owner}.{name}"] = own.guesses[f"{base}.{name}"]
139
+ return Returns(dict(own.calls), guesses, methods=methods)
140
+
141
+
142
+ def unannotated(body: Sequence[ast.stmt], vouched: frozenset[str] = _UNVOUCHED) -> frozenset[str]:
143
+ """Name the functions in `body` whose `return`s could type their calls (see `_return_type`).
144
+
145
+ Returns:
146
+ Each plain function's name that has no declared return, nor a decorator but those `vouched`
147
+ spells (see `constricter.rules.decorators`).
148
+
149
+ """
150
+ return frozenset(
151
+ func.name
152
+ for func in _plain(body)
153
+ if isinstance(func, ast.FunctionDef) and not _decorated(func, vouched) and func.returns is None
154
+ )
155
+
156
+
157
+ def held_back(body: Sequence[ast.stmt]) -> dict[str, tuple[str, ...]]:
158
+ """Name the functions in `body` whose `return`s could type their calls, but for their decorators.
159
+
160
+ Returns:
161
+ Each plain function's name that has no declared return, with its decorators as spelled: another
162
+ module may vouch for them (see `constricter.fix.index.decorated`). Not one with a decorator
163
+ that can't be spelled.
164
+
165
+ """
166
+ found: dict[str, tuple[str, ...]] = {}
167
+ func: FunctionDef
168
+ for func in _plain(body):
169
+ if isinstance(func, ast.FunctionDef) and func.returns is None and _decorated(func, _UNVOUCHED):
170
+ spellings: list[str | None] = [spelled(each) for each in func.decorator_list]
171
+ if None not in spellings:
172
+ found[func.name] = tuple(text for text in spellings if text is not None)
173
+ return found
174
+
175
+
176
+ def called(module: ast.Module, node: ast.AST, found: Returned) -> bool:
177
+ """Check whether `node` (the module, a function, or a statement in it) calls any function `found` types.
178
+
179
+ By name, or as a method. A function's decorators aren't its own calls: they're before it.
180
+
181
+ Returns:
182
+ Whether it does: otherwise checking it again would change nothing.
183
+
184
+ """
185
+ methods: set[str] = {_called(name) for methods in found.methods.values() for name in methods}
186
+ calls: set[str] = {_called(name) for name in found.calls}
187
+ return any(name in calls or attr in methods for name, attr in callees_in(module, node))
188
+
189
+
190
+ def _called(key: str) -> str: # a table's entry's function's name (see `_key`)
191
+ return key.removeprefix(entered.ENTERED).removeprefix(AWAIT)
192
+
193
+
194
+ def retyped(before: Returned, after: Returned) -> set[str]:
195
+ """Name the attributes, of any class, that `after` types and `before` didn't, or typed otherwise.
196
+
197
+ Returns:
198
+ Their names.
199
+
200
+ """
201
+ return {
202
+ attr
203
+ for owner, attributes in after.attributes.items()
204
+ for attr, annotation in attributes.items()
205
+ if before.attributes.get(owner, {}).get(attr) != annotation
206
+ }
207
+
208
+
209
+ def callees_in(module: ast.Module, node: ast.AST) -> Sequence[_Callee]:
210
+ """Find what `node` (the module, a function, or a statement in it) calls, by its span of the source.
211
+
212
+ Returns:
213
+ Each call's callee: a name (`f()`) or an attribute (`x.m()`).
214
+
215
+ """
216
+ starts: list[Start]
217
+ callees: list[_Callee]
218
+ starts, callees = _calls(module)
219
+ return callees[within(starts, node)]
220
+
221
+
222
+ class Slot(NamedTuple):
223
+ """Where a function's return type goes in the table: its class (`None`: the module's), and its name."""
224
+
225
+ owner: str | None
226
+ name: str
227
+
228
+
229
+ @lru_cache(maxsize=4)
230
+ def slots(module: ast.Module) -> dict[int, Slot]:
231
+ """Find the functions whose `return`s can type their calls: plain, directly in the module or a class.
232
+
233
+ As `_typed` finds them (see `_plain`).
234
+
235
+ Returns:
236
+ Each one's slot, by the function's `id()`.
237
+
238
+ """
239
+ return {
240
+ id(func): Slot(owner, _key(func))
241
+ for owner, body in ((None, module.body), *((node.name, node.body) for node in classes(module)))
242
+ for func in _plain(body)
243
+ }
244
+
245
+
246
+ def _key(func: FunctionDef) -> str:
247
+ """Name a function's entry in the tables: its name, after what says how its call is read.
248
+
249
+ Returns:
250
+ It: after `AWAIT` for an `async def`, `entered.ENTERED` for a manager (see the module docstring).
251
+
252
+ """
253
+ if isinstance(func, ast.AsyncFunctionDef):
254
+ return f"{AWAIT}{func.name}"
255
+ return f"{entered.ENTERED}{func.name}" if entered.is_manager(func) else func.name
256
+
257
+
258
+ def _decorated(func: FunctionDef, vouched: frozenset[str]) -> bool:
259
+ """Check whether a function has a decorator that may change what calling it gives.
260
+
261
+ Not a pytest fixture's: the fixture's value is what the function returns (or yields). Nor one
262
+ `vouched` spells, which gives the function back.
263
+
264
+ Returns:
265
+ Whether it has.
266
+
267
+ """
268
+ return any(spelled(each) not in FIXTURES and spelled(each) not in vouched for each in func.decorator_list)
269
+
270
+
271
+ def _plain(body: Sequence[ast.stmt]) -> list[FunctionDef]:
272
+ """Find the plain functions directly in `body`: a `def` or `async def` no other there shares a name with.
273
+
274
+ Returns:
275
+ Them, in source order.
276
+
277
+ """
278
+ counts: dict[str, int] = {}
279
+ stmt: ast.stmt
280
+ for stmt in body:
281
+ if isinstance(stmt, FUNCTIONS):
282
+ counts[stmt.name] = counts.get(stmt.name, 0) + 1
283
+ return [stmt for stmt in body if isinstance(stmt, FUNCTIONS) and counts[stmt.name] == 1]
284
+
285
+
286
+ class _Tables(NamedTuple):
287
+ """The live tables a `Table` fills in, as `Returned` holds them."""
288
+
289
+ calls: dict[str, str]
290
+ methods: dict[str, dict[str, str]]
291
+ guesses: dict[str, frozenset[str]]
292
+ attributes: dict[str, dict[str, str]]
293
+
294
+
295
+ class Table:
296
+ """What the module's unannotated functions return, filled in as each is checked, in call order.
297
+
298
+ `returned` is the live table, for the check to read as it goes. Each function's stamp is how
299
+ many entries the table had when it was checked: one checked before a callee of its was typed
300
+ (in a cycle) is `stale`, and checked again. A class's attributes are typed once all its methods
301
+ are checked: what reads them later knows them the first time.
302
+ """
303
+
304
+ def __init__(
305
+ self,
306
+ module: ast.Module,
307
+ imported: Returns | None = None,
308
+ vouched: frozenset[str] = _UNVOUCHED,
309
+ ) -> None:
310
+ """Start a table for `module`, with what the functions it imports from other files return.
311
+
312
+ `vouched` spells the decorators that give a function back (see `constricter.rules.decorators`).
313
+ """
314
+ imported = imported or Returns()
315
+ self.module: ast.Module = module
316
+ self.vouched: frozenset[str] = vouched
317
+ self.recorded: dict[int, list[Recorded]] = {}
318
+ self.assigned: dict[int, list[Assigned]] = {} # each checked function's `self.x = value`s
319
+ self.used: dict[int, list[Used]] = {} # each checked function's reads of `self`'s empty containers
320
+ methods: dict[str, dict[str, str]] = {owner: dict(typed) for owner, typed in imported.methods.items()}
321
+ self.tables: _Tables = _Tables(dict(imported.calls), methods, dict(imported.guesses), {})
322
+ self.returned: Returned = Returned(*self.tables)
323
+ self.entries: list[tuple[Slot, str]] = []
324
+ self.stamps: dict[int, int] = {}
325
+
326
+ def checked(
327
+ self,
328
+ func: FunctionDef,
329
+ returns: list[Recorded],
330
+ stored: tuple[list[Assigned], list[Used]],
331
+ ) -> None:
332
+ """Record a checked function's `return`s, and what it stores in `self`'s attributes.
333
+
334
+ `stored`: its `self.x = value`s, and its reads of `self`'s empty containers. Its return type
335
+ is recorded too, if its `return`s decide it.
336
+ """
337
+ self.stamps[id(func)] = len(self.entries)
338
+ self.recorded[id(func)] = returns
339
+ self.assigned[id(func)], self.used[id(func)] = stored
340
+ self._completed(func)
341
+ slot: Slot | None = slots(self.module).get(id(func))
342
+ found: tuple[str, frozenset[str]] | None
343
+ if slot is None or (found := _return_type(self.module, func, returns, self.vouched)) is None:
344
+ return
345
+ annotation: str
346
+ origins: frozenset[str]
347
+ annotation, origins = found
348
+ if slot.owner is None:
349
+ self.tables.calls[slot.name] = annotation
350
+ else:
351
+ self.tables.methods.setdefault(slot.owner, {})[slot.name] = annotation
352
+ if origins:
353
+ self.tables.guesses[slot.name if slot.owner is None else f"{slot.owner}.{slot.name}"] = origins
354
+ self.entries.append((slot, annotation))
355
+
356
+ def _completed(self, func: FunctionDef) -> None:
357
+ """Type the attributes of `func`'s class (and those above it) whose methods are all checked now."""
358
+ own: ast.ClassDef | None = owners(self.module).get(id(func))
359
+ above: list[ast.ClassDef] = [] if own is None else superclasses(self.module, own)
360
+ node: ast.ClassDef
361
+ for node in above if own is None else (own, *above):
362
+ # With the classes under it: their methods fill its empty containers too.
363
+ if class_names(self.module)[node.name] != 1 or any(
364
+ id(method) not in self.assigned
365
+ for each in (node, *subclasses(self.module, node))
366
+ for method in self_methods(each)
367
+ ):
368
+ continue
369
+ name: str
370
+ annotation: str
371
+ origins: frozenset[str]
372
+ for name, annotation, origins in attribute_types(self.module, node, (self.assigned, self.used)):
373
+ self.tables.attributes.setdefault(node.name, {})[name] = annotation
374
+ self.tables.guesses[f"{node.name}.{name}"] = origins
375
+
376
+ def stale(self, func: FunctionDef) -> bool:
377
+ """Check whether `func` calls a function whose type the table gained after `func` was checked.
378
+
379
+ Returns:
380
+ Whether it does: checking it again, knowing that, could type more.
381
+
382
+ """
383
+ since: slice = slice(self.stamps.get(id(func), 0), None)
384
+ newer: list[tuple[Slot, str]] = self.entries[since]
385
+ calls: dict[str, str] = {}
386
+ methods: dict[str, dict[str, str]] = {}
387
+ slot: Slot
388
+ annotation: str
389
+ for slot, annotation in newer:
390
+ if slot.owner is None:
391
+ calls[slot.name] = annotation
392
+ else:
393
+ methods.setdefault(slot.owner, {})[slot.name] = annotation
394
+ return bool(newer) and called(self.module, func, Returned(calls, methods))
395
+
396
+
397
+ @lru_cache(maxsize=4) # asked of the same module's functions, each round
398
+ def _calls(module: ast.Module) -> tuple[list[tuple[int, int]], list[_Callee]]:
399
+ """Find every call in the module, in source order, from its one shared walk (`nodes`).
400
+
401
+ A function's (or statement's) calls are then those within its span of the source: no walk of
402
+ its own, which the rounds of `checker._returned` asked of every function again and again. A
403
+ `with` statement that binds a target calls its manager's `__enter__`.
404
+
405
+ Returns:
406
+ Where each call starts (its line and column), and what it calls: a name (`f()`) or an
407
+ attribute (`x.m()`), each `None` if not.
408
+
409
+ """
410
+ found: list[_Call] = [(start, (None, entered.ENTER)) for start in entered.targets(module)]
411
+ node: ast.AST
412
+ name: str
413
+ attr: str
414
+ for node in of_type(module, ast.Call):
415
+ match node:
416
+ case ast.Call(func=ast.Name(id=name)):
417
+ found.append(((node.lineno, node.col_offset), (name, None)))
418
+ case ast.Call(func=ast.Attribute(attr=attr)):
419
+ found.append(((node.lineno, node.col_offset), (None, attr)))
420
+ case _:
421
+ pass
422
+ found.sort(key=operator.itemgetter(0))
423
+ return [start for start, _ in found], [callee for _, callee in found]
424
+
425
+
426
+ def _typed(
427
+ module: ast.Module,
428
+ body: Sequence[ast.stmt],
429
+ recorded: Mapping[int, Sequence[Recorded]],
430
+ vouched: frozenset[str],
431
+ ) -> Iterator[tuple[str, str, frozenset[str]]]:
432
+ """Find the plain unannotated functions directly in `body` whose `return`s decide their type.
433
+
434
+ Yields:
435
+ Each one's name, return type, and what that rests on if it's a guess.
436
+
437
+ """
438
+ func: FunctionDef
439
+ for func in _plain(body):
440
+ found: tuple[str, frozenset[str]] | None
441
+ if (found := _return_type(module, func, recorded.get(id(func), ()), vouched)) is not None:
442
+ yield _key(func), *found
443
+
444
+
445
+ def _return_type(
446
+ module: ast.Module,
447
+ func: FunctionDef,
448
+ returns: Sequence[Recorded],
449
+ vouched: frozenset[str],
450
+ ) -> tuple[str, frozenset[str]] | None:
451
+ """Type a plain function from its recorded `return`s.
452
+
453
+ Returns:
454
+ The one type they all give, and what it rests on if any is a guess; or `None` if it's
455
+ decorated or annotated (by a `# type:` comment too), can fall off its end (but for a
456
+ generator, recorded as its type; not an `async` one), or has a `return` without a value or
457
+ of an unknown or different type.
458
+
459
+ """
460
+ manager: bool = entered.is_manager(func)
461
+ declared: bool = func.returns is not None or bool(func.type_comment)
462
+ # A generator's type is its `yield`s' (not an `async` one's); any other must end by a `return`.
463
+ decided: bool = (
464
+ isinstance(func, ast.FunctionDef)
465
+ if is_generator(module, func)
466
+ else terminates(func.body) and not manager
467
+ )
468
+ if (_decorated(func, vouched) and not manager) or declared or not returns or not decided:
469
+ return None
470
+ types: set[str | None] = {None if found is None else found.annotation for found, _ in returns}
471
+ found: str | None
472
+ if (found := next(iter(types)) if len(types) == 1 else None) is None:
473
+ return None
474
+ if roots(found) & bound_within(func): # a class it imports or defines itself: no caller can name it
475
+ return None
476
+ # An `X | None` is a guess: a type checker takes the unannotated function's call for anything,
477
+ # and what its callers do with it unchecked for `None` is an error only once it's declared.
478
+ doubted: frozenset[str] = frozenset({RETURNED}) if _NONE in (members(found) or ()) else frozenset()
479
+ return found, doubted.union(*(origins for _, origins in returns))