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,730 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """What a module declares, for `--fix`.
3
+
4
+ Its functions' and methods' return types, its classes' attributes, its type variables, and the
5
+ factories and casts it imports (see `constricter.rules.annotations` for what an annotation says).
6
+ """
7
+
8
+ import ast
9
+ from collections.abc import Iterator, Sequence
10
+ from functools import lru_cache
11
+ from typing import Final, TypeAlias, cast
12
+ from weakref import WeakKeyDictionary
13
+
14
+ from constricter.rules.annotations import is_vague, node_name, words
15
+ from constricter.rules.decorators import Held, passing, spelled
16
+ from constricter.rules.keys import key, keys
17
+ from constricter.rules.quoted import written
18
+ from constricter.rules.syntax import child_statements, declared_return, top_level
19
+
20
+ _TYPE_VARS: Final = frozenset({"TypeVar", "ParamSpec", "TypeVarTuple"})
21
+ # Each module's classes and type variables, for as long as its tree lives: every class table
22
+ # reads them, for the index and then for the check.
23
+ _Classes: TypeAlias = tuple[ast.ClassDef, ...]
24
+ _Names: TypeAlias = frozenset[str]
25
+ _CLASS_NODES: Final[WeakKeyDictionary[ast.Module, _Classes]] = WeakKeyDictionary()
26
+ _TYPE_VARS_DEFINED: Final[WeakKeyDictionary[ast.Module, _Names]] = WeakKeyDictionary()
27
+ _TYPING_MODULES: Final = frozenset({"typing", "typing_extensions"})
28
+ _TYPING_VARS: Final = frozenset({"AnyStr"}) # the type variables `typing` itself defines
29
+ # Modules `_FACTORIES`' names are imported from (so an aliased or re-exported import is still found).
30
+ _FACTORY_MODULES: Final = frozenset({"enum", "typing", "typing_extensions"})
31
+ # A method returning `Self` returns its receiver's own class.
32
+ _SELF: Final = "Self"
33
+ # Decorators that make a method an attribute of its instances, or callable on its class.
34
+ _PROPERTIES: Final = frozenset({"property", "cached_property"})
35
+ _CLASS_SIDE: Final = frozenset({"classmethod", "staticmethod"})
36
+ _ACCESSORS: Final = frozenset({"setter", "deleter"}) # `@name.setter`: the same property, not a redefinition
37
+ _CLASS_VAR: Final = "ClassVar"
38
+ _TYPED_DICT: Final = "TypedDict"
39
+ _CAST: Final = "cast"
40
+ _UNDECORATED: Final[frozenset[str]] = frozenset() # no decorators: a plain method
41
+
42
+
43
+ def imported_from(tree: ast.Module, modules: frozenset[str]) -> frozenset[str]:
44
+ """Find the top-level names this module imports (`from module import name`) from one of `modules`.
45
+
46
+ Only absolute imports are resolved; a relative one (`from . import x`) names no module here.
47
+
48
+ Returns:
49
+ Those names, as they're bound here (their alias, if importing gave them one).
50
+
51
+ """
52
+ names: set[str] = set()
53
+ stmt: ast.stmt
54
+ module: str
55
+ for stmt in tree.body:
56
+ match stmt:
57
+ case ast.ImportFrom(module=str() as module, level=0) if module in modules:
58
+ names.update(alias.asname or alias.name for alias in stmt.names)
59
+ case _:
60
+ pass
61
+ return frozenset(names)
62
+
63
+
64
+ def factories(tree: ast.Module) -> frozenset[str]:
65
+ """Find names this module imports that are known to build a class or special form.
66
+
67
+ Not an instance of what they're named (`Enum`, `NamedTuple`, `TypeVar`, ... from `enum`,
68
+ `typing` or `typing_extensions`), however they're aliased.
69
+
70
+ Returns:
71
+ Those names, as they're bound here.
72
+
73
+ """
74
+ return imported_from(tree, _FACTORY_MODULES)
75
+
76
+
77
+ def casts(tree: ast.Module) -> frozenset[str]:
78
+ """Find how this module can name `typing.cast`: `cast` (or its alias), `typing.cast`, `t.cast`.
79
+
80
+ Returns:
81
+ Each spelling of a call to it, as `ast.unparse` writes the callee.
82
+
83
+ """
84
+ names: set[str] = set()
85
+ stmt: ast.stmt
86
+ module: str
87
+ for stmt in tree.body:
88
+ match stmt:
89
+ case ast.ImportFrom(module=str() as module, level=0) if module in _FACTORY_MODULES - {"enum"}:
90
+ names.update(alias.asname or alias.name for alias in stmt.names if alias.name == _CAST)
91
+ case ast.Import():
92
+ names.update(
93
+ f"{alias.asname or alias.name}.{_CAST}"
94
+ for alias in stmt.names
95
+ if alias.name in _FACTORY_MODULES - {"enum"}
96
+ )
97
+ case _:
98
+ pass
99
+ return frozenset(names)
100
+
101
+
102
+ def _class_nodes(tree: ast.Module) -> tuple[ast.ClassDef, ...]:
103
+ """Find every class the module defines, however deep.
104
+
105
+ Returns:
106
+ Them, in source order.
107
+
108
+ """
109
+ found: tuple[ast.ClassDef, ...] | None
110
+ if (found := _CLASS_NODES.get(tree)) is None:
111
+ found = _CLASS_NODES[tree] = _read_class_nodes(tree)
112
+ return found
113
+
114
+
115
+ def _read_class_nodes(tree: ast.Module) -> tuple[ast.ClassDef, ...]:
116
+ return tuple(node for node in _statements(tree.body) if isinstance(node, ast.ClassDef))
117
+
118
+
119
+ def _statements(body: Sequence[ast.stmt]) -> Iterator[ast.stmt]:
120
+ """Walk statements only, into every nested block (a function's and a class's too), not expressions.
121
+
122
+ Much less than `ast.walk` visits, for what only a statement can be: a class, an annotated
123
+ assignment.
124
+
125
+ Yields:
126
+ Each statement, before those inside it (depth first, in source order).
127
+
128
+ """
129
+ # A stack, not a recursion: a nested generator passes each statement up through every level.
130
+ # `None` at its bottom ends it: popped, the walk's done.
131
+ waiting: list[ast.stmt | None] = [None, *reversed(body)]
132
+ stmt: ast.stmt
133
+ for stmt in iter(waiting.pop, None):
134
+ yield stmt
135
+ waiting.extend(reversed(_blocks(stmt)))
136
+
137
+
138
+ def _blocks(stmt: ast.stmt) -> list[ast.stmt]:
139
+ """Collect the statements directly inside `stmt`, a function's or class's body included.
140
+
141
+ Returns:
142
+ Them, in source order.
143
+
144
+ """
145
+ return (
146
+ stmt.body
147
+ if isinstance(stmt, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef)
148
+ else child_statements(stmt)
149
+ )
150
+
151
+
152
+ def classes(tree: ast.Module) -> dict[str, dict[str, str]]:
153
+ """Map each class defined in the module to its instances' annotated attributes.
154
+
155
+ A class-body annotation (`class C: x: int`), a `self.x: int = ...` annotated assignment anywhere
156
+ in one of its methods, and a `@property`'s declared return (as `method_returns` reads a method's,
157
+ and a vague one too, as an annotation's is kept) all count; a name that names more than one class
158
+ in the module (however unlikely) gets the last one's attributes. A `TypedDict` class (under
159
+ `TypedDict`, or one of the module's defined before it; not a generic one) has its keys instead,
160
+ each spelled as `keys.key` does.
161
+
162
+ Returns:
163
+ Each class's name, mapped to its attributes' names and annotation text.
164
+
165
+ """
166
+ properties: dict[str, dict[str, str]] = _class_returns(tree, _PROPERTIES)
167
+ vague: dict[str, dict[str, str]] = _class_returns(tree, _PROPERTIES, partly=True)
168
+ found: dict[str, dict[str, str]] = {}
169
+ keyed: set[str] = set()
170
+ node: ast.ClassDef
171
+ called: dict[str, str] | None
172
+ for node in _class_nodes(tree):
173
+ if (called := _called_keys(node)) is not None:
174
+ keyed.add(node.name)
175
+ found[node.name] = called
176
+ elif not _generic(node) and any(
177
+ node_name(base) == _TYPED_DICT or (isinstance(base, ast.Name) and base.id in keyed)
178
+ for base in node.bases
179
+ ):
180
+ keyed.add(node.name)
181
+ found[node.name] = keys(node)
182
+ else:
183
+ found[node.name] = {
184
+ **properties.get(node.name, {}),
185
+ **vague.get(node.name, {}),
186
+ **_attributes(node),
187
+ }
188
+ return found
189
+
190
+
191
+ def _called_keys(node: ast.ClassDef) -> dict[str, str] | None:
192
+ """Read the keys of a class whose one base is a `TypedDict("D", {"a": int, ...})` call.
193
+
194
+ Returns:
195
+ Each key's type as written; `None` for a class under anything else.
196
+
197
+ """
198
+ func: ast.expr
199
+ names: list[ast.expr | None]
200
+ values: list[ast.expr]
201
+ match node.bases:
202
+ case [
203
+ ast.Call(
204
+ func=func,
205
+ args=[ast.Constant(value=str()), ast.Dict(keys=names, values=values)],
206
+ ),
207
+ ] if node_name(func) == _TYPED_DICT and all(
208
+ isinstance(name, ast.Constant) and isinstance(name.value, str) for name in names
209
+ ):
210
+ return {
211
+ key(str(cast("ast.Constant", name).value)): written(value)
212
+ for name, value in zip(names, values, strict=True)
213
+ }
214
+ case _:
215
+ return None
216
+
217
+
218
+ def class_attributes(tree: ast.Module) -> dict[str, dict[str, str]]:
219
+ """Map each non-generic class defined in the module to the attributes its class itself has.
220
+
221
+ A class-body annotation with a value (`limit: int = 3`), or a `ClassVar[T]` (as `T`): what `cls.x`
222
+ reads in a classmethod. A bare annotation (`x: int`) only declares an instance attribute (a
223
+ dataclass field), so it doesn't count.
224
+
225
+ Returns:
226
+ Each class's name, mapped to its class attributes' names and annotation text.
227
+
228
+ """
229
+ type_vars: frozenset[str] = defined_type_vars(tree)
230
+ found: dict[str, dict[str, str]] = {}
231
+ node: ast.AST
232
+ stmt: ast.stmt
233
+ name: str
234
+ annotation: ast.expr
235
+ value: ast.expr | None
236
+ for node in _class_nodes(tree):
237
+ if not _generic(node):
238
+ attrs: dict[str, str] = {}
239
+ for stmt in node.body:
240
+ match stmt:
241
+ case ast.AnnAssign(target=ast.Name(id=name), annotation=annotation, value=value):
242
+ text: str | None = _class_var(annotation)
243
+ if text is None and value is not None:
244
+ text = written(annotation)
245
+ if text and not type_vars & set(words(text)):
246
+ attrs[name] = text
247
+ case _:
248
+ pass
249
+ found[node.name] = attrs
250
+ return found
251
+
252
+
253
+ def _class_var(annotation: ast.expr) -> str | None:
254
+ """Unwrap `ClassVar[T]`.
255
+
256
+ Returns:
257
+ `T` as text, or `None` if `annotation` isn't a parameterised `ClassVar`.
258
+
259
+ """
260
+ outer: ast.expr
261
+ inner: ast.expr
262
+ match annotation:
263
+ case ast.Subscript(value=outer, slice=inner) if node_name(outer) == _CLASS_VAR:
264
+ return written(inner)
265
+ case _:
266
+ return None
267
+
268
+
269
+ def class_methods(tree: ast.Module) -> dict[str, dict[str, str]]:
270
+ """Map each non-generic class defined in the module to its classmethods' and staticmethods' returns.
271
+
272
+ What `cls.method()` gives in a classmethod, under `method_returns`' rules.
273
+
274
+ Returns:
275
+ Each class's name, mapped to those methods' names and return annotation text.
276
+
277
+ """
278
+ return _class_returns(tree, _CLASS_SIDE)
279
+
280
+
281
+ def held_class_methods(tree: ast.Module) -> dict[str, dict[str, Held]]:
282
+ """Find the classmethods and staticmethods declaring a return under decorators the module can't vouch for.
283
+
284
+ As `class_methods` reads them, but for those decorators: another module's may give the method
285
+ back (see `held`).
286
+
287
+ Returns:
288
+ Each class's name, mapped to those methods' names, return annotations and decorators.
289
+
290
+ """
291
+ known: frozenset[str] = _passing(tree)
292
+ declared: dict[str, dict[str, str]] = _class_returns(tree, _CLASS_SIDE, anything=True)
293
+ own: dict[str, dict[str, str]] = class_methods(tree)
294
+ found: dict[str, dict[str, Held]] = {}
295
+ node: ast.ClassDef
296
+ for node in _class_nodes(tree):
297
+ methods: dict[str, Held] = {
298
+ stmt.name: Held(
299
+ declared[node.name][stmt.name],
300
+ tuple(
301
+ sorted(
302
+ {spelled(d) or "" for d in stmt.decorator_list if node_name(d) not in _CLASS_SIDE}
303
+ - known,
304
+ ),
305
+ ),
306
+ )
307
+ for stmt in node.body
308
+ if isinstance(stmt, ast.FunctionDef)
309
+ and stmt.name in declared.get(node.name, {})
310
+ and stmt.name not in own[node.name]
311
+ }
312
+ if methods:
313
+ found[node.name] = methods
314
+ return found
315
+
316
+
317
+ def _attributes(node: ast.ClassDef) -> dict[str, str]:
318
+ attrs: dict[str, str] = {}
319
+ stmt: ast.stmt
320
+ name: str
321
+ annotation: ast.expr
322
+ for stmt in node.body:
323
+ match stmt:
324
+ case ast.AnnAssign(target=ast.Name(id=name), annotation=annotation):
325
+ attrs[name] = written(annotation)
326
+ case ast.FunctionDef() | ast.AsyncFunctionDef():
327
+ attrs.update(_self_attributes(stmt))
328
+ case _:
329
+ pass
330
+ return attrs
331
+
332
+
333
+ def _self_attributes(func: ast.FunctionDef | ast.AsyncFunctionDef) -> Iterator[tuple[str, str]]:
334
+ """Find `self.attr: T = ...` annotated assignments anywhere in a method's body.
335
+
336
+ Yields:
337
+ Each attribute's name and annotation text.
338
+
339
+ """
340
+ node: ast.stmt
341
+ name: str
342
+ annotation: ast.expr
343
+ for node in _statements(func.body): # an annotated assignment is always a statement
344
+ match node:
345
+ case ast.AnnAssign(
346
+ target=ast.Attribute(value=ast.Name(id="self"), attr=name),
347
+ annotation=annotation,
348
+ ):
349
+ yield name, written(annotation)
350
+ case _:
351
+ pass
352
+
353
+
354
+ def returns(tree: ast.Module) -> dict[str, str]:
355
+ """Return the declared return type of each plain top-level function whose calls `--fix` can annotate.
356
+
357
+ Skipped: generic, async and redefined functions, one decorated by anything but a decorator that
358
+ gives it back (see `decorators.passing`), and returns that are `None`, vague, or mention a
359
+ module-level `TypeVar` (a call's type then depends on its arguments).
360
+
361
+ Returns:
362
+ Each such function's name, and its return annotation as source text.
363
+
364
+ """
365
+ return _declared_returns(tree.body, defined_type_vars(tree), vouched=_passing(tree))
366
+
367
+
368
+ @lru_cache(maxsize=4) # asked for a module's functions, then its classes' methods
369
+ def _passing(tree: ast.Module) -> frozenset[str]:
370
+ """Spell the decorators the module alone vouches for (see `decorators.passing`).
371
+
372
+ Returns:
373
+ Them.
374
+
375
+ """
376
+ return passing(tree, defined_type_vars(tree))
377
+
378
+
379
+ def held(tree: ast.Module) -> dict[str, Held]:
380
+ """Find the plain top-level functions declaring a return under decorators the module can't vouch for.
381
+
382
+ As `returns` reads them, but for those decorators: another module's may give the function back.
383
+
384
+ Returns:
385
+ Each one's name, its return annotation and those decorators (see `Held`).
386
+
387
+ """
388
+ known: frozenset[str] = _passing(tree)
389
+ declared: dict[str, str] = _declared_returns(tree.body, defined_type_vars(tree), vouched=None)
390
+ return {
391
+ stmt.name: Held(
392
+ declared[stmt.name],
393
+ tuple(sorted({spelled(d) or "" for d in stmt.decorator_list} - known)),
394
+ )
395
+ for stmt in tree.body
396
+ if isinstance(stmt, ast.FunctionDef) and stmt.name in declared and stmt.name not in returns(tree)
397
+ }
398
+
399
+
400
+ def method_returns(tree: ast.Module, *, awaited: bool = False) -> dict[str, dict[str, str]]:
401
+ """Map each non-generic class defined in the module to its methods' (`awaited`: `async` ones') returns.
402
+
403
+ For `--fix` to type `obj.method()` on a local already typed as that class. A method counts under
404
+ the same rules as `returns`' functions: a plain `def` directly in the class body, not decorated
405
+ (so no `staticmethod`, `classmethod` or `property`) or redefined, whose return isn't `None`,
406
+ vague, or a `TypeVar`. A generic class (`class C[T]`, or any subscripted base like `Generic[T]`)
407
+ is skipped whole: its methods' returns depend on how it's parameterised. A bare `Self` return is
408
+ the class itself; one that only mentions `Self` (`list[Self]`) is skipped.
409
+
410
+ Returns:
411
+ Each class's name, mapped to its methods' names and return annotation text.
412
+
413
+ """
414
+ return _class_returns(tree, _UNDECORATED, awaited=awaited)
415
+
416
+
417
+ def partial_returns(tree: ast.Module) -> dict[str, str]:
418
+ """Return the declared return of each function `returns` leaves out for a vague part.
419
+
420
+ `tuple[Row, dict[str, Any]]` types a call only as far as `vague` allows (see `vague_fits`), and
421
+ an unpacking's names part by part.
422
+
423
+ Returns:
424
+ Each such function's name, and its return annotation as source text.
425
+
426
+ """
427
+ return _partial_returns(tree.body, defined_type_vars(tree), vouched=_passing(tree))
428
+
429
+
430
+ def partial_method_returns(tree: ast.Module) -> dict[str, dict[str, str]]:
431
+ """Map each non-generic class to its methods' declared returns that have a vague part.
432
+
433
+ As `partial_returns` reads functions': its plain methods', and its classmethods' and staticmethods'.
434
+
435
+ Returns:
436
+ Each class's name, mapped to those methods' names and return annotation text.
437
+
438
+ """
439
+ sides: dict[str, dict[str, str]] = _class_returns(tree, _CLASS_SIDE, partly=True)
440
+ return {
441
+ owner: {**sides[owner], **methods}
442
+ for owner, methods in _class_returns(tree, _UNDECORATED, partly=True).items()
443
+ }
444
+
445
+
446
+ def _class_returns(
447
+ tree: ast.Module,
448
+ decorators: frozenset[str],
449
+ *,
450
+ anything: bool = False,
451
+ partly: bool = False,
452
+ awaited: bool = False,
453
+ ) -> dict[str, dict[str, str]]:
454
+ """Map each non-generic class to the declared returns of its methods decorated by one of `decorators`.
455
+
456
+ None of them (an empty set) means plain methods; see `method_returns` for the rules. Any other
457
+ decorator must be one the module vouches for (see `decorators.passing`), or (`anything`) one
458
+ `decorators.spelled` reads. `partly`: those left out for a vague part, instead.
459
+
460
+ Returns:
461
+ Each class's name, mapped to those methods' names and return annotation text.
462
+
463
+ """
464
+ type_vars: frozenset[str] = defined_type_vars(tree)
465
+ found: dict[str, dict[str, str]] = {}
466
+ node: ast.ClassDef
467
+ for node in _class_nodes(tree):
468
+ if not _generic(node):
469
+ found[node.name] = {
470
+ name: node.name if _is_self(annotation) else annotation
471
+ for name, annotation in (_partial_returns if partly else _declared_returns)(
472
+ node.body,
473
+ type_vars,
474
+ awaited=awaited,
475
+ decorators=decorators,
476
+ vouched=None if anything else _passing(tree),
477
+ ).items()
478
+ if _is_self(annotation) or _SELF not in words(annotation)
479
+ }
480
+ return found
481
+
482
+
483
+ def self_returns(tree: ast.Module) -> dict[str, frozenset[str]]:
484
+ """Map each class to its methods (classmethods and staticmethods too) declared to return a bare `Self`.
485
+
486
+ `method_returns` types such a call as the class; but called on `self` or `cls` it's `Self`, which
487
+ a subclass's is too, so `--fix` writes that instead (see `constricter.fix.values.doubts`).
488
+
489
+ Returns:
490
+ Each class's name, and those methods' names.
491
+
492
+ """
493
+ return {
494
+ node.name: frozenset(
495
+ stmt.name
496
+ for stmt in node.body
497
+ if isinstance(stmt, ast.FunctionDef | ast.AsyncFunctionDef)
498
+ and stmt.returns is not None
499
+ and _is_self(written(stmt.returns))
500
+ )
501
+ for node in _class_nodes(tree)
502
+ }
503
+
504
+
505
+ def generic_classes(tree: ast.Module) -> frozenset[str]:
506
+ """Find the generic classes the module defines (see `_generic`).
507
+
508
+ Returns:
509
+ Their names: written bare, as `--fix` would write `self`'s type, each is missing its arguments.
510
+
511
+ """
512
+ return frozenset(node.name for node in _class_nodes(tree) if _generic(node))
513
+
514
+
515
+ def _generic(node: ast.ClassDef) -> bool:
516
+ """Check whether a class is generic: `class C[T]`, or any subscripted base like `Generic[T]`.
517
+
518
+ Returns:
519
+ Whether it is: its members' types then depend on how it's parameterised.
520
+
521
+ """
522
+ return bool(cast("object", getattr(node, "type_params", ()))) or any( # Python 3.12+'s `class C[T]`
523
+ isinstance(base, ast.Subscript) for base in node.bases
524
+ )
525
+
526
+
527
+ def _is_self(annotation: str) -> bool:
528
+ """Check whether an annotation is exactly `Self` (bare, or `typing.Self` and the like).
529
+
530
+ Returns:
531
+ Whether it is.
532
+
533
+ """
534
+ # `annotation` is always `ast.unparse`'s own output, so it's always valid Python to parse back.
535
+ return node_name(ast.parse(annotation, mode="eval").body) == _SELF
536
+
537
+
538
+ def defined_type_vars(tree: ast.Module) -> frozenset[str]:
539
+ """Find the module-level names bound to a `TypeVar`, `ParamSpec` or `TypeVarTuple`, or `typing.AnyStr`.
540
+
541
+ Returns:
542
+ Those names.
543
+
544
+ """
545
+ found: frozenset[str] | None
546
+ if (found := _TYPE_VARS_DEFINED.get(tree)) is None:
547
+ found = _TYPE_VARS_DEFINED[tree] = _read_defined_type_vars(tree)
548
+ return found
549
+
550
+
551
+ def _read_defined_type_vars(tree: ast.Module) -> frozenset[str]:
552
+ names: set[str] = set()
553
+ stmt: ast.stmt
554
+ name: str
555
+ func: ast.expr
556
+ module: str
557
+ for stmt in top_level(tree.body): # under an `if TYPE_CHECKING:` too
558
+ match stmt:
559
+ case ast.Assign(targets=[ast.Name(id=name)], value=ast.Call(func=func)) if (
560
+ node_name(func) in _TYPE_VARS
561
+ ):
562
+ names.add(name)
563
+ case ast.ImportFrom(module=str() as module, level=0) if module in _TYPING_MODULES:
564
+ names.update(alias.asname or alias.name for alias in stmt.names if alias.name in _TYPING_VARS)
565
+ case ast.Import(names=[*_]) if any(alias.name in _TYPING_MODULES for alias in stmt.names):
566
+ names.update(_TYPING_VARS) # `typing.AnyStr`: the word `AnyStr`
567
+ case _:
568
+ pass
569
+ return frozenset(names)
570
+
571
+
572
+ def awaited_returns(tree: ast.Module) -> dict[str, str]:
573
+ """Return the declared return type of each plain top-level `async def`: what awaiting a call gives.
574
+
575
+ The same rules as `returns`' functions, for `async def` instead of `def`.
576
+
577
+ Returns:
578
+ Each such function's name, and its return annotation as source text.
579
+
580
+ """
581
+ return _declared_returns(tree.body, defined_type_vars(tree), awaited=True)
582
+
583
+
584
+ def _declared_returns(
585
+ body: Sequence[ast.stmt],
586
+ type_vars: frozenset[str],
587
+ *,
588
+ awaited: bool = False,
589
+ decorators: frozenset[str] = _UNDECORATED,
590
+ vouched: frozenset[str] | None = _UNDECORATED,
591
+ ) -> dict[str, str]:
592
+ """Find the plain functions (`async` ones if `awaited`) directly in `body` `--fix` can annotate.
593
+
594
+ Plain means decorated only as `vouched` spells (`None`: by anything `decorators.spelled`
595
+ reads), and with `decorators`, by exactly one of them too. A property's `@name.setter` or
596
+ `@name.deleter` is the same property, not a redefinition. Not one whose return has a vague
597
+ part (see `_partial_returns`).
598
+
599
+ Returns:
600
+ Each such function's name, and its return annotation as source text.
601
+
602
+ """
603
+ found: dict[str, tuple[str, bool]] = _every_return(
604
+ body,
605
+ type_vars,
606
+ awaited=awaited,
607
+ decorators=decorators,
608
+ vouched=vouched,
609
+ )
610
+ return {name: annotation for name, (annotation, partial) in found.items() if not partial}
611
+
612
+
613
+ def _partial_returns(
614
+ body: Sequence[ast.stmt],
615
+ type_vars: frozenset[str],
616
+ *,
617
+ awaited: bool = False,
618
+ decorators: frozenset[str] = _UNDECORATED,
619
+ vouched: frozenset[str] | None = _UNDECORATED,
620
+ ) -> dict[str, str]:
621
+ """Find the plain functions directly in `body` whose declared return has a vague part.
622
+
623
+ As `_declared_returns` finds the others.
624
+
625
+ Returns:
626
+ Each such function's name, and its return annotation as source text.
627
+
628
+ """
629
+ found: dict[str, tuple[str, bool]] = _every_return(
630
+ body,
631
+ type_vars,
632
+ awaited=awaited,
633
+ decorators=decorators,
634
+ vouched=vouched,
635
+ )
636
+ return {name: annotation for name, (annotation, partial) in found.items() if partial}
637
+
638
+
639
+ def _every_return(
640
+ body: Sequence[ast.stmt],
641
+ type_vars: frozenset[str],
642
+ *,
643
+ awaited: bool,
644
+ decorators: frozenset[str],
645
+ vouched: frozenset[str] | None,
646
+ ) -> dict[str, tuple[str, bool]]:
647
+ """Read the declared return of each plain function directly in `body` (see `_declared_returns`).
648
+
649
+ Returns:
650
+ Each one's name, its return annotation as source text, and whether that has a vague
651
+ part.
652
+
653
+ """
654
+ counts: dict[str, int] = {}
655
+ found: dict[str, tuple[str, bool]] = {}
656
+ stmt: ast.stmt
657
+ name: str
658
+ for stmt in body:
659
+ match stmt:
660
+ case ast.FunctionDef(name=name) | ast.AsyncFunctionDef(name=name) if not _accessor(stmt):
661
+ counts[name] = counts.get(name, 0) + 1
662
+ declared: tuple[str, bool] | None = _usable_return(stmt)
663
+ if (
664
+ declared is not None
665
+ and isinstance(stmt, ast.AsyncFunctionDef) == awaited
666
+ and _plain(stmt, decorators, vouched)
667
+ ):
668
+ found[name] = declared
669
+ case _:
670
+ pass
671
+ return {
672
+ name: read
673
+ for name, read in found.items()
674
+ if counts[name] == 1 and not type_vars & set(words(read[0]))
675
+ }
676
+
677
+
678
+ def _accessor(func: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
679
+ """Check whether `func` is a property's setter or deleter (`@name.setter`).
680
+
681
+ Returns:
682
+ Whether it is.
683
+
684
+ """
685
+ return any(
686
+ isinstance(decorator, ast.Attribute) and decorator.attr in _ACCESSORS
687
+ for decorator in func.decorator_list
688
+ )
689
+
690
+
691
+ def _plain(
692
+ func: ast.FunctionDef | ast.AsyncFunctionDef,
693
+ decorators: frozenset[str],
694
+ vouched: frozenset[str] | None,
695
+ ) -> bool:
696
+ """Check that `func` is decorated as asked, for its declared return to type its calls.
697
+
698
+ Returns:
699
+ Whether it is: decorated by exactly one of `decorators` (if any), and otherwise only as
700
+ `vouched` spells (`None`: by anything spelled).
701
+
702
+ """
703
+ others: list[ast.expr] = [d for d in func.decorator_list if node_name(d) not in decorators]
704
+ spellings: list[str | None] = [spelled(decorator) for decorator in others]
705
+ return (
706
+ len(func.decorator_list) - len(others) == bool(decorators)
707
+ and None not in spellings
708
+ and (vouched is None or vouched.issuperset(spellings))
709
+ )
710
+
711
+
712
+ # By the function alone: each class's body is read for its methods, its properties, its
713
+ # classmethods and those held back, and a function's declared return is the same to them all.
714
+ @lru_cache(maxsize=4096)
715
+ def _usable_return(func: ast.FunctionDef | ast.AsyncFunctionDef) -> tuple[str, bool] | None:
716
+ """Read the return type `func` declares, if its calls always have it.
717
+
718
+ Returns:
719
+ Its annotation as source text, and whether it has a vague part (`tuple[Row, dict[str, Any]]`,
720
+ `Any`); `None` for a generic function, no return, or `None`.
721
+
722
+ """
723
+ declared: ast.expr | None = declared_return(func)
724
+ if (
725
+ cast("object", getattr(func, "type_params", ())) # Python 3.12+'s `def f[T]()`
726
+ or declared is None
727
+ or (isinstance(declared, ast.Constant) and declared.value is None)
728
+ ):
729
+ return None
730
+ return written(declared), is_vague(declared)