python-constricter 0.4.1__cp312-cp312-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__.cp312-win32.pyd +0 -0
  29. constricter/fix/__init__.py +2 -0
  30. constricter/fix/core/__init__.cp312-win32.pyd +0 -0
  31. constricter/fix/core/__init__.py +2 -0
  32. constricter/fix/core/asked.cp312-win32.pyd +0 -0
  33. constricter/fix/core/asked.py +56 -0
  34. constricter/fix/core/fixes.cp312-win32.pyd +0 -0
  35. constricter/fix/core/fixes.py +317 -0
  36. constricter/fix/core/imports.cp312-win32.pyd +0 -0
  37. constricter/fix/core/imports.py +580 -0
  38. constricter/fix/core/inherited.cp312-win32.pyd +0 -0
  39. constricter/fix/core/inherited.py +311 -0
  40. constricter/fix/core/known.cp312-win32.pyd +0 -0
  41. constricter/fix/core/known.py +730 -0
  42. constricter/fix/core/signatures.cp312-win32.pyd +0 -0
  43. constricter/fix/core/signatures.py +132 -0
  44. constricter/fix/index/__init__.cp312-win32.pyd +0 -0
  45. constricter/fix/index/__init__.py +2 -0
  46. constricter/fix/index/awaits.cp312-win32.pyd +0 -0
  47. constricter/fix/index/awaits.py +90 -0
  48. constricter/fix/index/beyond.cp312-win32.pyd +0 -0
  49. constricter/fix/index/beyond.py +149 -0
  50. constricter/fix/index/callers.cp312-win32.pyd +0 -0
  51. constricter/fix/index/callers.py +155 -0
  52. constricter/fix/index/crossed.cp312-win32.pyd +0 -0
  53. constricter/fix/index/crossed.py +327 -0
  54. constricter/fix/index/decorated.cp312-win32.pyd +0 -0
  55. constricter/fix/index/decorated.py +140 -0
  56. constricter/fix/index/fixtures.cp312-win32.pyd +0 -0
  57. constricter/fix/index/fixtures.py +176 -0
  58. constricter/fix/index/linked.cp312-win32.pyd +0 -0
  59. constricter/fix/index/linked.py +142 -0
  60. constricter/fix/index/loose.cp312-win32.pyd +0 -0
  61. constricter/fix/index/loose.py +103 -0
  62. constricter/fix/index/managed.cp312-win32.pyd +0 -0
  63. constricter/fix/index/managed.py +63 -0
  64. constricter/fix/index/modules.cp312-win32.pyd +0 -0
  65. constricter/fix/index/modules.py +741 -0
  66. constricter/fix/index/offers.cp312-win32.pyd +0 -0
  67. constricter/fix/index/offers.py +252 -0
  68. constricter/fix/index/order.cp312-win32.pyd +0 -0
  69. constricter/fix/index/order.py +155 -0
  70. constricter/fix/index/origins.cp312-win32.pyd +0 -0
  71. constricter/fix/index/origins.py +238 -0
  72. constricter/fix/index/own_methods.cp312-win32.pyd +0 -0
  73. constricter/fix/index/own_methods.py +91 -0
  74. constricter/fix/index/own_overloads.cp312-win32.pyd +0 -0
  75. constricter/fix/index/own_overloads.py +70 -0
  76. constricter/fix/index/own_types.cp312-win32.pyd +0 -0
  77. constricter/fix/index/own_types.py +330 -0
  78. constricter/fix/index/params.cp312-win32.pyd +0 -0
  79. constricter/fix/index/params.py +81 -0
  80. constricter/fix/index/plain.cp312-win32.pyd +0 -0
  81. constricter/fix/index/plain.py +221 -0
  82. constricter/fix/index/project.cp312-win32.pyd +0 -0
  83. constricter/fix/index/project.py +797 -0
  84. constricter/fix/index/sides.cp312-win32.pyd +0 -0
  85. constricter/fix/index/sides.py +160 -0
  86. constricter/fix/index/strings.cp312-win32.pyd +0 -0
  87. constricter/fix/index/strings.py +46 -0
  88. constricter/fix/index/stubs/__init__.cp312-win32.pyd +0 -0
  89. constricter/fix/index/stubs/__init__.py +2 -0
  90. constricter/fix/index/stubs/atoms.cp312-win32.pyd +0 -0
  91. constricter/fix/index/stubs/atoms.py +425 -0
  92. constricter/fix/index/stubs/chained.cp312-win32.pyd +0 -0
  93. constricter/fix/index/stubs/chained.py +69 -0
  94. constricter/fix/index/stubs/classnames.cp312-win32.pyd +0 -0
  95. constricter/fix/index/stubs/classnames.py +124 -0
  96. constricter/fix/index/stubs/declared.cp312-win32.pyd +0 -0
  97. constricter/fix/index/stubs/declared.py +508 -0
  98. constricter/fix/index/stubs/installed.cp312-win32.pyd +0 -0
  99. constricter/fix/index/stubs/installed.py +425 -0
  100. constricter/fix/index/stubs/names.cp312-win32.pyd +0 -0
  101. constricter/fix/index/stubs/names.py +200 -0
  102. constricter/fix/index/stubs/read.cp312-win32.pyd +0 -0
  103. constricter/fix/index/stubs/read.py +70 -0
  104. constricter/fix/index/stubs/signatures.cp312-win32.pyd +0 -0
  105. constricter/fix/index/stubs/signatures.py +656 -0
  106. constricter/fix/index/stubs/stubbed.cp312-win32.pyd +0 -0
  107. constricter/fix/index/stubs/stubbed.py +208 -0
  108. constricter/fix/index/taken.cp312-win32.pyd +0 -0
  109. constricter/fix/index/taken.py +45 -0
  110. constricter/fix/index/tuples.cp312-win32.pyd +0 -0
  111. constricter/fix/index/tuples.py +112 -0
  112. constricter/fix/libraries/__init__.cp312-win32.pyd +0 -0
  113. constricter/fix/libraries/__init__.py +2 -0
  114. constricter/fix/libraries/library.cp312-win32.pyd +0 -0
  115. constricter/fix/libraries/library.py +420 -0
  116. constricter/fix/libraries/opened.cp312-win32.pyd +0 -0
  117. constricter/fix/libraries/opened.py +147 -0
  118. constricter/fix/libraries/overloads.cp312-win32.pyd +0 -0
  119. constricter/fix/libraries/overloads.py +960 -0
  120. constricter/fix/libraries/receivers.cp312-win32.pyd +0 -0
  121. constricter/fix/libraries/receivers.py +274 -0
  122. constricter/fix/libraries/stdlib.cp312-win32.pyd +0 -0
  123. constricter/fix/libraries/stdlib.py +752 -0
  124. constricter/fix/libraries/walks.cp312-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__.cp312-win32.pyd +0 -0
  148. constricter/fix/values/__init__.py +2 -0
  149. constricter/fix/values/aliased.cp312-win32.pyd +0 -0
  150. constricter/fix/values/aliased.py +195 -0
  151. constricter/fix/values/bodies.cp312-win32.pyd +0 -0
  152. constricter/fix/values/bodies.py +76 -0
  153. constricter/fix/values/callables.cp312-win32.pyd +0 -0
  154. constricter/fix/values/callables.py +323 -0
  155. constricter/fix/values/called.cp312-win32.pyd +0 -0
  156. constricter/fix/values/called.py +194 -0
  157. constricter/fix/values/classvars.cp312-win32.pyd +0 -0
  158. constricter/fix/values/classvars.py +629 -0
  159. constricter/fix/values/decided.cp312-win32.pyd +0 -0
  160. constricter/fix/values/decided.py +429 -0
  161. constricter/fix/values/displays.cp312-win32.pyd +0 -0
  162. constricter/fix/values/displays.py +101 -0
  163. constricter/fix/values/doubts.cp312-win32.pyd +0 -0
  164. constricter/fix/values/doubts.py +677 -0
  165. constricter/fix/values/entered.cp312-win32.pyd +0 -0
  166. constricter/fix/values/entered.py +332 -0
  167. constricter/fix/values/fills.cp312-win32.pyd +0 -0
  168. constricter/fix/values/fills.py +446 -0
  169. constricter/fix/values/guesses.cp312-win32.pyd +0 -0
  170. constricter/fix/values/guesses.py +376 -0
  171. constricter/fix/values/hinted.cp312-win32.pyd +0 -0
  172. constricter/fix/values/hinted.py +811 -0
  173. constricter/fix/values/inference.cp312-win32.pyd +0 -0
  174. constricter/fix/values/inference.py +698 -0
  175. constricter/fix/values/loops.cp312-win32.pyd +0 -0
  176. constricter/fix/values/loops.py +335 -0
  177. constricter/fix/values/members.cp312-win32.pyd +0 -0
  178. constricter/fix/values/members.py +578 -0
  179. constricter/fix/values/narrowed.cp312-win32.pyd +0 -0
  180. constricter/fix/values/narrowed.py +220 -0
  181. constricter/fix/values/operated.cp312-win32.pyd +0 -0
  182. constricter/fix/values/operated.py +202 -0
  183. constricter/fix/values/ordered.cp312-win32.pyd +0 -0
  184. constricter/fix/values/ordered.py +74 -0
  185. constricter/fix/values/returned.cp312-win32.pyd +0 -0
  186. constricter/fix/values/returned.py +479 -0
  187. constricter/fix/values/returns.cp312-win32.pyd +0 -0
  188. constricter/fix/values/returns.py +237 -0
  189. constricter/fix/values/shapes.cp312-win32.pyd +0 -0
  190. constricter/fix/values/shapes.py +654 -0
  191. constricter/fix/values/stored.cp312-win32.pyd +0 -0
  192. constricter/fix/values/stored.py +564 -0
  193. constricter/fix/values/targets.cp312-win32.pyd +0 -0
  194. constricter/fix/values/targets.py +419 -0
  195. constricter/fix/values/unchecked.cp312-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__.cp312-win32.pyd +0 -0
  208. constricter/rules/__init__.py +2 -0
  209. constricter/rules/annotations.cp312-win32.pyd +0 -0
  210. constricter/rules/annotations.py +296 -0
  211. constricter/rules/binding.cp312-win32.pyd +0 -0
  212. constricter/rules/binding.py +652 -0
  213. constricter/rules/calls.cp312-win32.pyd +0 -0
  214. constricter/rules/calls.py +460 -0
  215. constricter/rules/checker.cp312-win32.pyd +0 -0
  216. constricter/rules/checker.py +536 -0
  217. constricter/rules/declarations.cp312-win32.pyd +0 -0
  218. constricter/rules/declarations.py +730 -0
  219. constricter/rules/decorators.cp312-win32.pyd +0 -0
  220. constricter/rules/decorators.py +351 -0
  221. constricter/rules/flow.cp312-win32.pyd +0 -0
  222. constricter/rules/flow.py +502 -0
  223. constricter/rules/keys.cp312-win32.pyd +0 -0
  224. constricter/rules/keys.py +79 -0
  225. constricter/rules/late.cp312-win32.pyd +0 -0
  226. constricter/rules/late.py +969 -0
  227. constricter/rules/narrowing.cp312-win32.pyd +0 -0
  228. constricter/rules/narrowing.py +98 -0
  229. constricter/rules/parsed.cp312-win32.pyd +0 -0
  230. constricter/rules/parsed.py +136 -0
  231. constricter/rules/quoted.cp312-win32.pyd +0 -0
  232. constricter/rules/quoted.py +141 -0
  233. constricter/rules/rebinding.cp312-win32.pyd +0 -0
  234. constricter/rules/rebinding.py +169 -0
  235. constricter/rules/recorded.cp312-win32.pyd +0 -0
  236. constricter/rules/recorded.py +168 -0
  237. constricter/rules/redundant.cp312-win32.pyd +0 -0
  238. constricter/rules/redundant.py +160 -0
  239. constricter/rules/scope.cp312-win32.pyd +0 -0
  240. constricter/rules/scope.py +802 -0
  241. constricter/rules/scoped.cp312-win32.pyd +0 -0
  242. constricter/rules/scoped.py +232 -0
  243. constricter/rules/scoping.cp312-win32.pyd +0 -0
  244. constricter/rules/scoping.py +346 -0
  245. constricter/rules/settings.cp312-win32.pyd +0 -0
  246. constricter/rules/settings.py +189 -0
  247. constricter/rules/syntax.cp312-win32.pyd +0 -0
  248. constricter/rules/syntax.py +472 -0
  249. constricter/rules/tables.cp312-win32.pyd +0 -0
  250. constricter/rules/tables.py +169 -0
  251. constricter/rules/walked.cp312-win32.pyd +0 -0
  252. constricter/rules/walked.py +127 -0
  253. constricter/rules/widened.cp312-win32.pyd +0 -0
  254. constricter/rules/widened.py +322 -0
  255. constricter/trace.py +374 -0
  256. e45ce06b2fdd5c43f782__mypyc.cp312-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,2 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """What a value, and the module it's in, make its type; and where a type checker sees it otherwise."""
@@ -0,0 +1,195 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """`--fix` for a module's type alias: `Json = dict[str, "Json"]`, declared `Json: TypeAlias`.
3
+
4
+ Only a value that can be nothing but a type made of others: a subscript of what `typing` or
5
+ `collections.abc` define (`Union[A, B]`, `Callable[..., R]`), of a builtin generic
6
+ (`dict[str, int]`) or of a generic class the module names (its own, another checked file's, the
7
+ standard library's); or a union of those, of builtin classes, of classes the module knows and of
8
+ `None`; or a copy of a name the module declares an alias (`Rows = Table`). Not a bare class's alias
9
+ (`Alias = Class`), which declared one loses the class's type parameters; nor a name the module
10
+ binds again, which is a variable to a type checker.
11
+
12
+ `TypeAlias` is named as the module's imports can, else imported from `typing`, which has it from
13
+ Python 3.10: certain where every Python the module runs on has it there (`min-python`), or the
14
+ module imports the name already; a guess where `min-python` isn't known, and no fix where it says
15
+ some Python lacks it.
16
+ """
17
+
18
+ import ast
19
+ import builtins
20
+ from typing import Final, TypeAlias, cast
21
+
22
+ from constricter.fix.core.known import ImportPlan, Inference, Known
23
+ from constricter.fix.libraries import stdlib
24
+ from constricter.fix.values import hinted
25
+ from constricter.fix.values.doubts import Facts
26
+ from constricter.rules.annotations import dotted, node_name
27
+
28
+ KIND: Final = "alias" # the fix kind
29
+ _SINCE: Final = (3, 10) # the first Python whose `typing` has `TypeAlias`
30
+ # Where every name that takes a subscript makes a type of it.
31
+ _FORMS: Final = frozenset({"typing", "typing_extensions", "collections.abc"})
32
+ _GENERICS: Final = frozenset({"dict", "frozenset", "list", "set", "tuple", "type"})
33
+ _CLASSES: Final = frozenset(
34
+ name for name in dir(builtins) if isinstance(cast("object", getattr(builtins, name)), type)
35
+ )
36
+ # Imports that bind `TypeAlias`, or a module that has it, on every Python.
37
+ _ALWAYS: Final = frozenset({"typing.TypeAlias", "typing_extensions.TypeAlias", "typing_extensions"})
38
+ _REASON: Final = "a type alias, written as a type made of others"
39
+ # What makes a type of a call's result, by where it's from.
40
+ _FACTORIES: Final = frozenset(
41
+ {
42
+ *(
43
+ f"{module}.{name}"
44
+ for module in ("typing", "typing_extensions")
45
+ for name in (
46
+ "NamedTuple",
47
+ "NewType",
48
+ "ParamSpec",
49
+ "TypeAliasType",
50
+ "TypeVar",
51
+ "TypeVarTuple",
52
+ "TypedDict",
53
+ )
54
+ ),
55
+ *(f"enum.{name}" for name in ("Enum", "Flag", "IntEnum", "IntFlag", "StrEnum")),
56
+ "collections.namedtuple",
57
+ },
58
+ )
59
+ # An inference, whether it's a guess, and what the guess rests on: what `Scope.valued` gives.
60
+ _Valued: TypeAlias = tuple[Inference, bool, frozenset[str]]
61
+
62
+
63
+ def factory(value: ast.expr, known: Known) -> bool:
64
+ """Check whether `value` makes a type, not a value: a `TypeVar`, a `NewType`, a functional `NamedTuple`.
65
+
66
+ What it's bound to can't be annotated: a type checker would take the name for a variable. However
67
+ the factory is imported (`TypeVar`, `typing.TypeVar`, `t.TypeVar`), as long as it's `typing`'s,
68
+ `typing_extensions`', `enum`'s or `collections.namedtuple`.
69
+
70
+ Returns:
71
+ Whether it does.
72
+
73
+ """
74
+ plan: ImportPlan | None = known.names.plan
75
+ return (
76
+ isinstance(value, ast.Call)
77
+ and plan is not None
78
+ and stdlib.resolved(value.func, plan.bound) in _FACTORIES
79
+ )
80
+
81
+
82
+ def declares(annotation: ast.expr) -> bool:
83
+ """Check whether an annotation declares its name a type alias: `TypeAlias`, however it's spelled.
84
+
85
+ Returns:
86
+ Whether it does.
87
+
88
+ """
89
+ return node_name(annotation) == hinted.ALIAS
90
+
91
+
92
+ def declared(
93
+ target: ast.Name,
94
+ value: ast.expr,
95
+ known: Known,
96
+ facts: Facts,
97
+ scope: tuple[frozenset[str], tuple[int, int] | None],
98
+ ) -> _Valued | None:
99
+ """Declare the type alias a module body's `target = value` binds (see the module docstring).
100
+
101
+ `scope`: the names the module body has declared aliases so far, and the oldest Python the
102
+ module runs on (`min-python`).
103
+
104
+ Returns:
105
+ `TypeAlias` as the module names it, whether that's a guess, and what the guess rests on; or
106
+ `None` for anything else, or a module that can't name it there.
107
+
108
+ """
109
+ plan: ImportPlan | None = known.names.plan
110
+ if plan is None or facts.rebound is None or target.id in facts.rebound:
111
+ return None
112
+ aliases: frozenset[str]
113
+ min_python: tuple[int, int] | None
114
+ aliases, min_python = scope
115
+ older: bool = min_python is not None and min_python < _SINCE
116
+ if older and not _ALWAYS.intersection(plan.bound.values()):
117
+ return None
118
+ copied: bool = isinstance(value, ast.Name) and value.id in aliases
119
+ named: str | None = (
120
+ hinted.type_alias(known, target.lineno)
121
+ if copied or composite(value, known, plan, facts.generics)
122
+ else None
123
+ )
124
+ if named is None:
125
+ return None
126
+ always: bool = plan.bound.get(named.partition(".")[0]) in _ALWAYS
127
+ if older and not always:
128
+ return None
129
+ certain: bool = min_python is not None or always
130
+ return (
131
+ Inference(named, _REASON, frozenset({KIND})),
132
+ not certain,
133
+ frozenset() if certain else frozenset({KIND}),
134
+ )
135
+
136
+
137
+ def composite(value: ast.expr, known: Known, plan: ImportPlan, generics: frozenset[str]) -> bool:
138
+ """Check whether `value` can only be a type made of others: a subscript of a generic, or a union.
139
+
140
+ `generics`: the generic classes the module names (see `Facts.generics`).
141
+
142
+ Returns:
143
+ Whether it is.
144
+
145
+ """
146
+ head: ast.expr
147
+ left: ast.expr
148
+ right: ast.expr
149
+ match value:
150
+ case ast.Subscript(value=ast.Name() | ast.Attribute() as head):
151
+ return _generic(head, known, plan, generics)
152
+ case ast.BinOp(op=ast.BitOr(), left=left, right=right):
153
+ return all(_member(side, known, plan, generics) for side in (left, right))
154
+ case _:
155
+ return False
156
+
157
+
158
+ def _generic(
159
+ head: ast.Name | ast.Attribute,
160
+ known: Known,
161
+ plan: ImportPlan,
162
+ generics: frozenset[str],
163
+ ) -> bool:
164
+ """Check whether a subscript's `head` is what only a type's is: a special form, or a generic class.
165
+
166
+ Returns:
167
+ Whether it is, by what the module's imports say it is; never a name it binds as a value.
168
+
169
+ """
170
+ spelled: str | None = dotted(head)
171
+ if spelled is None or spelled.partition(".")[0] in plan.values:
172
+ return False
173
+ if spelled in _GENERICS:
174
+ return known.is_builtin(spelled)
175
+ origin: str = stdlib.resolved(head, plan.bound) or ""
176
+ return spelled in generics or origin.rpartition(".")[0] in _FORMS
177
+
178
+
179
+ def _member(node: ast.expr, known: Known, plan: ImportPlan, generics: frozenset[str]) -> bool:
180
+ """Check whether a union's member is a type: `None`, a class, or a type made of others.
181
+
182
+ Returns:
183
+ Whether it is: a builtin class, or one the module defines or imports from a checked file.
184
+
185
+ """
186
+ name: str
187
+ match node:
188
+ case ast.Constant(value=None):
189
+ return True
190
+ case ast.Name(id=name):
191
+ return (name in _CLASSES and known.is_builtin(name)) or (
192
+ name in known.classes and name not in plan.values
193
+ )
194
+ case _:
195
+ return composite(node, known, plan, generics)
@@ -0,0 +1,76 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """What a function's body says of its calls: whether it's a generator, and whether it can fall off its end."""
3
+
4
+ import ast
5
+ from collections.abc import Sequence
6
+ from functools import lru_cache
7
+ from typing import cast
8
+
9
+ from constricter.rules.syntax import FunctionDef, Start, has_within, own_nodes
10
+ from constricter.rules.walked import of_type
11
+
12
+
13
+ def is_generator(module: ast.Module, func: FunctionDef) -> bool:
14
+ """Check whether `func`, in `module`, is a generator: a `yield` in its own body (not a nested function's).
15
+
16
+ Most functions have no `yield` anywhere in them (see `_yields`): only one with some is walked.
17
+
18
+ Returns:
19
+ Whether it is.
20
+
21
+ """
22
+ return has_within(_yields(module), func) and yields_itself(func)
23
+
24
+
25
+ # By the function alone, not its module too: a cache holding modules keeps checked trees alive, and
26
+ # the garbage collector then looks through them again and again (the standard library's check took
27
+ # 10% longer).
28
+ @lru_cache(maxsize=4096) # asked of the same functions once per round
29
+ def yields_itself(func: FunctionDef) -> bool:
30
+ """Check whether a `yield` is in `func`'s own body, not a nested function's.
31
+
32
+ Returns:
33
+ Whether one is.
34
+
35
+ """
36
+ return any(isinstance(node, ast.Yield | ast.YieldFrom) for node in own_nodes(func.body))
37
+
38
+
39
+ @lru_cache(maxsize=4) # asked of the same module's functions, each round
40
+ def _yields(module: ast.Module) -> list[Start]:
41
+ """Find where each of the module's `yield`s starts.
42
+
43
+ Returns:
44
+ Them, in source order.
45
+
46
+ """
47
+ return sorted(
48
+ (node.lineno, node.col_offset)
49
+ for node in cast("list[ast.expr]", of_type(module, ast.Yield, ast.YieldFrom))
50
+ )
51
+
52
+
53
+ def terminates(body: Sequence[ast.stmt]) -> bool:
54
+ """Check that running `body` can't reach its end: it always returns or raises first.
55
+
56
+ Its last statement returns or raises, or is an `if` or `try` whose every way through does (a
57
+ `try`'s body with its `else`, and each handler), or a `with` whose body does.
58
+
59
+ Returns:
60
+ Whether it can't.
61
+
62
+ """
63
+ if not body:
64
+ return False
65
+ last: ast.stmt = body[-1]
66
+ match last:
67
+ case ast.Return() | ast.Raise():
68
+ return True
69
+ case ast.If():
70
+ return terminates(last.body) and terminates(last.orelse)
71
+ case ast.With() | ast.AsyncWith():
72
+ return terminates(last.body)
73
+ case ast.Try() | ast.TryStar():
74
+ return terminates(last.orelse or last.body) and all(terminates(h.body) for h in last.handlers)
75
+ case _:
76
+ return False
@@ -0,0 +1,323 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """`--fix` for a function, a bound method or a lambda bound to a name: `dump = json.dumps`, `grow = it.grow`.
3
+
4
+ A `Callable[..., R]`, `R` what a call of it gives whatever it's passed: a function declaring its
5
+ return (the module's, another checked file's, a builtin's or the standard library's with one), or
6
+ a method one does on a receiver whose type is known. Its parameters are left open (`...`): a
7
+ `Callable[[A], R]` would refuse the keywords and defaults a call through the name may use; they're
8
+ listed where the function has neither, and no call through the name passes one (`_taken`). Not a
9
+ class (a `type[C]`), a callee typed only by its `return`s, nor a name the function reads an
10
+ attribute of (`run.cache_clear()`), which a `Callable` doesn't have. A lambda is one of what its
11
+ body gives, where that rests on none of its parameters or its function's names: `Callable[[], R]`
12
+ if it takes none; one resting on its parameters, by what every call of it in its function passes
13
+ them (`called`). A module's or a class body's name is one too: a plain class's, as a guess.
14
+ """
15
+
16
+ import ast
17
+ import builtins
18
+ from collections import Counter
19
+ from collections.abc import Callable, Mapping, Sequence
20
+ from itertools import islice
21
+ from typing import Final, NamedTuple, TypeAlias
22
+
23
+ from constricter.empty import Empty
24
+ from constricter.fix.core.known import ImportPlan, Inference, Known
25
+ from constricter.fix.libraries import stdlib
26
+ from constricter.fix.values.fills import Uses
27
+ from constricter.fix.values.guesses import fixed_by_callee
28
+ from constricter.fix.values.inference import inference
29
+ from constricter.rules.annotations import dotted, roots
30
+ from constricter.rules.declarations import defined_type_vars
31
+ from constricter.rules.syntax import FunctionDef
32
+ from constricter.rules.walked import classes, walk
33
+
34
+ KIND: Final = "callable" # the fix kind of a function or method bound to a name
35
+ _CALLERS: Final = "callers" # and of what every call passes a parameter (see `constricter.fix.index.callers`)
36
+ _CALLABLE: Final = "collections.abc.Callable"
37
+ _OPEN: Final = "..." # a `Callable`'s parameters, left open
38
+ _SELF: Final = "Self"
39
+ _Infer: TypeAlias = Callable[[ast.expr], Inference | None]
40
+
41
+
42
+ class Site(NamedTuple):
43
+ """Where a name is bound to a callable.
44
+
45
+ `function`: the function it's bound in (`None`: a module's or a class's body); `owner`: a class
46
+ body's class; `attributed`: the names the module reads an attribute of, anywhere; `positional`:
47
+ its functions' parameters' types, where a `Callable` can list them (see `signatures`).
48
+ """
49
+
50
+ function: FunctionDef | None
51
+ owner: str | None = None
52
+ attributed: frozenset[str] = frozenset()
53
+ positional: Mapping[str, tuple[str, ...]] = Empty()
54
+
55
+
56
+ def signatures(tree: ast.Module) -> dict[str, tuple[str, ...]]:
57
+ """Read what each of the module's functions takes, where a `Callable` can say it: `[int, str]`.
58
+
59
+ A plain `def` at its top level, or (as `Class.method`, less its first parameter) directly in a
60
+ class, defined once and undecorated, every parameter positional, annotated and without a
61
+ default, none naming a type variable or `Self`.
62
+
63
+ Returns:
64
+ Each one's parameters' types, in order.
65
+
66
+ """
67
+ unusable: frozenset[str] = defined_type_vars(tree) | {_SELF}
68
+ found: dict[str, tuple[str, ...]] = _signatures(tree.body, "", unusable)
69
+ owners: Counter[str] = Counter(node.name for node in classes(tree))
70
+ node: ast.ClassDef
71
+ for node in classes(tree):
72
+ if owners[node.name] == 1:
73
+ found.update(_signatures(node.body, f"{node.name}.", unusable))
74
+ return found
75
+
76
+
77
+ def _signatures(
78
+ body: Sequence[ast.stmt],
79
+ prefix: str,
80
+ unusable: frozenset[str],
81
+ ) -> dict[str, tuple[str, ...]]:
82
+ """Read what the functions directly in `body` take (see `signatures`); `prefix`: a class's, its methods'.
83
+
84
+ Returns:
85
+ Each one's parameters' types, by its name after `prefix`.
86
+
87
+ """
88
+ counts: Counter[str] = Counter(
89
+ stmt.name for stmt in body if isinstance(stmt, ast.FunctionDef | ast.AsyncFunctionDef)
90
+ )
91
+ found: dict[str, tuple[str, ...]] = {}
92
+ stmt: ast.stmt
93
+ for stmt in body:
94
+ if not isinstance(stmt, ast.FunctionDef) or counts[stmt.name] != 1 or stmt.decorator_list:
95
+ continue
96
+ args: ast.arguments = stmt.args
97
+ skipped: int = 1 if prefix else 0 # a method's `self`
98
+ named: list[ast.arg] = [*args.posonlyargs, *args.args][skipped:]
99
+ types: list[str] = [ast.unparse(arg.annotation) for arg in named if arg.annotation is not None]
100
+ plain: bool = not (args.defaults or args.kwonlyargs or args.vararg or args.kwarg)
101
+ if plain and len(types) == len(named) and not unusable & roots(f"[{', '.join(types)}]"):
102
+ found[f"{prefix}{stmt.name}"] = tuple(types)
103
+ return found
104
+
105
+
106
+ def aliased(
107
+ target: ast.Name,
108
+ value: ast.expr,
109
+ site: Site,
110
+ known: Known,
111
+ typed: tuple[Mapping[str, str], _Infer],
112
+ ) -> tuple[Inference, ast.expr] | None:
113
+ """Infer a name bound to a function, a bound method or a lambda, at `site`: a `Callable[..., R]`.
114
+
115
+ `typed`: the scope's typed names, and what infers a value with them. Its parameters are listed
116
+ (`Callable[[A], R]`) where the function declares them all positional, and every call through
117
+ the name in its function passes them so.
118
+
119
+ Returns:
120
+ The inference, and the call that gives `R` (whose guesses are its own); `None` for any other
121
+ value, one whose call's type its arguments decide or nothing declares, a class, a module that
122
+ can't name `Callable`, or a name an attribute is read of.
123
+
124
+ """
125
+ if isinstance(value, ast.Lambda):
126
+ return _lambda(target, value, site, known)
127
+ if not isinstance(value, ast.Name | ast.Attribute) or _is_class(value, known):
128
+ return None
129
+ call: ast.Call = ast.copy_location(ast.Call(value, [], []), value)
130
+ fixed: bool = fixed_by_callee(call, known, typed[0]) or (
131
+ stdlib.resolved(value, known.names.stdlib) in stdlib.RETURNS
132
+ )
133
+ found: Inference | None = typed[1](call) if fixed else _method(value, site, known)
134
+ plan: ImportPlan | None = known.names.plan
135
+ if found is None or plan is None or _read(target.id, site):
136
+ return None
137
+ spelled: str | None
138
+ if (spelled := plan.spell(_CALLABLE)) is None:
139
+ return None
140
+ taken: str = _taken(target.id, value, site, known, typed[1])
141
+ reason: str = f"`{ast.unparse(value)}`, whose call gives {found.reason}"
142
+ return Inference(f"{spelled}[{taken}, {found.annotation}]", reason, found.kinds | {KIND}), call
143
+
144
+
145
+ def _method(value: ast.expr, site: Site, known: Known) -> Inference | None:
146
+ """Type a call of one of a class's own methods, named in its body (`items = _items`).
147
+
148
+ Returns:
149
+ Its declared return, or `None` for anything else, anywhere else.
150
+
151
+ """
152
+ declared: str | None = (
153
+ known.methods.get(site.owner or "", {}).get(value.id) if isinstance(value, ast.Name) else None
154
+ )
155
+ if declared is None or not isinstance(value, ast.Name):
156
+ return None
157
+ return Inference(declared, f"`{site.owner}.{value.id}`'s declared return type", frozenset({"method"}))
158
+
159
+
160
+ def _taken(name: str, value: ast.Name | ast.Attribute, site: Site, known: Known, infer: _Infer) -> str:
161
+ """Write what a callable bound to `name` takes: its parameters' types, or `...`.
162
+
163
+ Listed for one of the module's functions, or a method of its classes on a receiver whose type
164
+ is known, that `signatures` reads, bound in a function whose every call through the name passes
165
+ as many arguments, by position.
166
+
167
+ Returns:
168
+ A `Callable`'s first argument.
169
+
170
+ """
171
+ key: str | None = None
172
+ owner: Inference | None = None if isinstance(value, ast.Name) else infer(value.value)
173
+ if isinstance(value, ast.Name):
174
+ key = value.id
175
+ elif owner is not None:
176
+ key = f"{known.class_side.lineage.definer(owner.annotation, value.attr)}.{value.attr}"
177
+ taken: tuple[str, ...] | None = site.positional.get(key or "")
178
+ if taken is None or site.function is None:
179
+ return _OPEN
180
+ calls: list[ast.Call] = [
181
+ node
182
+ for node in walk(site.function)
183
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == name
184
+ ]
185
+ positional: bool = all(
186
+ not call.keywords
187
+ and len(call.args) == len(taken)
188
+ and not any(isinstance(arg, ast.Starred) for arg in call.args)
189
+ for call in calls
190
+ )
191
+ return f"[{', '.join(taken)}]" if positional else _OPEN
192
+
193
+
194
+ def _lambda(
195
+ target: ast.Name,
196
+ value: ast.Lambda,
197
+ site: Site,
198
+ known: Known,
199
+ ) -> tuple[Inference, ast.expr] | None:
200
+ """Infer a name bound to a lambda: a `Callable` of what its body gives, whatever its names hold.
201
+
202
+ `Callable[[], R]` for one that takes nothing, `Callable[..., R]` otherwise: nothing types its
203
+ parameters, and its function's names are read only when it's called, so its body's type rests
204
+ on neither.
205
+
206
+ Returns:
207
+ The inference, and the body (whose guesses are its own); or `None` (see `aliased`).
208
+
209
+ """
210
+ args: ast.arguments = value.args
211
+ params: bool = any((args.posonlyargs, args.args, args.kwonlyargs, args.vararg, args.kwarg))
212
+ # With none of the function's names: it reads them when it's called, whatever they hold then.
213
+ found: Inference | None = inference(value.body, known, {})
214
+ plan: ImportPlan | None = known.names.plan
215
+ spelled: str | None = None if plan is None or found is None else plan.spell(_CALLABLE)
216
+ if found is None or spelled is None or _read(target.id, site):
217
+ return None
218
+ taken: str = _OPEN if params else "[]"
219
+ reason: str = f"a lambda, whose body is {found.reason}"
220
+ return Inference(f"{spelled}[{taken}, {found.annotation}]", reason, found.kinds | {KIND}), value.body
221
+
222
+
223
+ def called(
224
+ value: ast.Lambda,
225
+ name: str,
226
+ uses: Uses,
227
+ known: Known,
228
+ declared: Mapping[str, str],
229
+ ) -> tuple[Inference, list[ast.expr]] | None:
230
+ """Infer a lambda bound to `name` whose body rests on its parameters, by what its calls pass them.
231
+
232
+ Every use of the name in its function (`uses`) a call passing each parameter by position, each
233
+ one's arguments of one type: its body is typed with those, and none of the function's names. A
234
+ lambda of plain positional parameters, without defaults. A guess (`callers`): a call added
235
+ later may pass another type.
236
+
237
+ Returns:
238
+ The inference (a `Callable[..., R]`), and the arguments it rests on; or `None`.
239
+
240
+ """
241
+ args: ast.arguments = value.args
242
+ params: list[ast.arg] = [*args.posonlyargs, *args.args]
243
+ passed: list[ast.expr] | None = _passed(name, uses, len(params))
244
+ if args.defaults or args.kwonlyargs or args.vararg or args.kwarg or name in uses.nested:
245
+ return None
246
+ if not (params and passed):
247
+ return None
248
+ given: list[Inference | None] = [inference(arg, known, declared) for arg in passed]
249
+ typed: dict[str, str | None] = {
250
+ param.arg: _one(
251
+ {None if each is None else each.annotation for each in islice(given, at, None, len(params))},
252
+ )
253
+ for at, param in enumerate(params)
254
+ }
255
+ found: Inference | None = (
256
+ None
257
+ if None in typed.values()
258
+ else inference(value.body, known, {param: text for param, text in typed.items() if text})
259
+ )
260
+ plan: ImportPlan | None = known.names.plan
261
+ spelled: str | None = None if plan is None or found is None else plan.spell(_CALLABLE)
262
+ if found is None or spelled is None:
263
+ return None
264
+ reason: str = f"a lambda, whose body is {found.reason}, given what its calls pass it"
265
+ kinds: frozenset[str] = found.kinds.union({KIND, _CALLERS}, *(each.kinds for each in given if each))
266
+ return Inference(f"{spelled}[{_OPEN}, {found.annotation}]", reason, kinds), passed
267
+
268
+
269
+ def _one(types: set[str | None]) -> str | None: # the one type every call passes, if they agree
270
+ return next(iter(types)) if len(types) == 1 else None
271
+
272
+
273
+ def _passed(name: str, uses: Uses, count: int) -> list[ast.expr] | None:
274
+ """List what every call of `name` in its function passes it: `count` arguments each, by position.
275
+
276
+ Returns:
277
+ The arguments, call after call; `None` where a use isn't such a call.
278
+
279
+ """
280
+ found: list[ast.expr] = []
281
+ node: ast.Name
282
+ for node in uses.names.get(name, []):
283
+ parent: ast.AST | None = uses.parents.get(id(node))
284
+ if (
285
+ not isinstance(parent, ast.Call)
286
+ or parent.func is not node
287
+ or parent.keywords
288
+ or len(parent.args) != count
289
+ or any(isinstance(arg, ast.Starred) for arg in parent.args)
290
+ ):
291
+ return None
292
+ found.extend(parent.args)
293
+ return found
294
+
295
+
296
+ def _is_class(value: ast.expr, known: Known) -> bool:
297
+ """Check whether a name is a class the module knows, or a builtin one it doesn't rebind.
298
+
299
+ Returns:
300
+ Whether it is: calling it constructs one, and the name is a `type[C]`.
301
+
302
+ """
303
+ name: str | None
304
+ if (name := dotted(value)) is None:
305
+ return False
306
+ return name in known.classes or (known.is_builtin(name) and isinstance(vars(builtins).get(name), type))
307
+
308
+
309
+ def _read(name: str, site: Site) -> bool:
310
+ """Check whether an attribute of `name` is read where it's bound (`name.cache_clear()`, `name.__name__`).
311
+
312
+ In its function; anywhere in the module, for a module's or a class body's name.
313
+
314
+ Returns:
315
+ Whether one is.
316
+
317
+ """
318
+ if site.function is None:
319
+ return name in site.attributed
320
+ return any(
321
+ isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name) and node.value.id == name
322
+ for node in walk(site.function)
323
+ )