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,654 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Small shapes `--fix` types from their parts' types.
3
+
4
+ A union the author would write (`a if c else None`, `a or b`), a container or an empty one (`a or []`),
5
+ `type(x)`, `d.get(key, default)`, `os.environ[key]`, a module's own `__file__` and `__name__`, and a
6
+ call an unpacking alone can use (`partly`). `infer` types a part as
7
+ `constricter.fix.values.inference` does.
8
+
9
+ A part that is a read (`x`, `self.x`, `d[k]`) is taken as its declared type, which a type checker
10
+ narrows where the function tests it: it's named in the inference's `reads` (see `doubts`), and one
11
+ whose type is a union of two types or more isn't taken at all.
12
+ """
13
+
14
+ import ast
15
+ import keyword
16
+ import re
17
+ from collections.abc import Callable, Iterator
18
+ from typing import Final, TypeAlias
19
+
20
+ from constricter.fix.core.known import ImportPlan, Inference, Known, Partial
21
+ from constricter.fix.libraries import stdlib
22
+ from constricter.fix.values import displays
23
+ from constricter.fix.values.members import keyed, may_miss, parsed, partial_method
24
+ from constricter.rules.annotations import dotted
25
+ from constricter.rules.flow import members
26
+
27
+ Infer: TypeAlias = Callable[[ast.expr], Inference | None]
28
+ Comprehension: TypeAlias = ast.ListComp | ast.SetComp | ast.DictComp | ast.GeneratorExp
29
+ _NONE: Final = "None"
30
+ # The builtin classes an `isinstance` check names that say all there is of a value so checked.
31
+ _SCALARS: Final = frozenset({"bool", "bytes", "complex", "float", "int", "str"})
32
+ _HOLDS_NONE: Final = re.compile(r"\b(?:None|Optional)\b")
33
+ _READS: Final = (ast.Name, ast.Attribute, ast.Subscript)
34
+ _CONDITIONAL: Final = "conditional" # the fix kind of `a if c else b`
35
+ _BOOLEAN: Final = "boolean" # the fix kind of `a or b`
36
+ _BUILTIN: Final = "builtin"
37
+ _CALL: Final = "call"
38
+ _METHOD: Final = "method"
39
+ _STDLIB: Final = "stdlib"
40
+ _TYPE: Final = "type"
41
+ _GET: Final = "get"
42
+ _ENVIRON: Final = "os.environ"
43
+ _QUOTES: Final = frozenset("'\"")
44
+ _GETATTR: Final = "getattr"
45
+ _TYPING_ANY: Final = "typing.Any"
46
+ _WITH_DEFAULT: Final = 2 # `getattr(obj, name)`'s arguments; a third is its default
47
+ _MODULE_TEXTS: Final = frozenset({"__file__", "__name__"}) # a module's own names that hold a `str`
48
+ _CLASS_TEXTS: Final = frozenset({"__module__", "__name__", "__qualname__"}) # what a class holds a `str` in
49
+ _CLASS: Final = "__class__"
50
+
51
+
52
+ def is_none(node: ast.expr) -> bool:
53
+ """Check whether a value is the literal `None`.
54
+
55
+ Returns:
56
+ Whether it is.
57
+
58
+ """
59
+ return isinstance(node, ast.Constant) and node.value is None
60
+
61
+
62
+ def typed(node: ast.expr, infer: Infer) -> Inference | None:
63
+ """Type one part of a shape, a read only as the module docstring has it.
64
+
65
+ Returns:
66
+ Its inference, a read named in its `reads`; or `None`.
67
+
68
+ """
69
+ found: Inference | None = infer(node)
70
+ if found is None or not isinstance(node, _READS):
71
+ return found
72
+ if len((members(found.annotation) or frozenset()) - {_NONE}) > 1:
73
+ return None
74
+ return found._replace(reads=(*found.reads, ast.unparse(node)))
75
+
76
+
77
+ def or_none(annotation: str) -> str | None:
78
+ """Write `annotation`, or `None`: as it is if it allows `None` already.
79
+
80
+ Returns:
81
+ The union, or `None` for an annotation that can't be read, or holds a string (a forward
82
+ reference can't take `| None` where it's evaluated).
83
+
84
+ """
85
+ found: frozenset[str] | None = members(annotation)
86
+ if found is None or _QUOTES.intersection(annotation):
87
+ return None
88
+ return annotation if _NONE in found else f"{annotation} | {_NONE}"
89
+
90
+
91
+ def optional(value: ast.IfExp, infer: Infer) -> Inference | None:
92
+ """Infer `a if c else None` (or `None if c else a`): `a`'s type, or `None`.
93
+
94
+ Returns:
95
+ The inference, or `None` if neither side or both are `None`, the other's type isn't known, or
96
+ `c` tests it (`x if isinstance(x, C) else None`): it's narrowed there.
97
+
98
+ """
99
+ sides: list[ast.expr] = [side for side in (value.body, value.orelse) if not is_none(side)]
100
+ tested: set[str] = {ast.unparse(node) for node in ast.walk(value.test) if isinstance(node, _READS)}
101
+ known: bool = len(sides) == 1 and ast.unparse(sides[0]) not in tested
102
+ found: Inference | None = typed(sides[0], infer) if known else None
103
+ annotation: str | None = None if found is None else or_none(found.annotation)
104
+ if found is None or annotation is None:
105
+ return None
106
+ return Inference(
107
+ annotation,
108
+ "one side of a conditional, or `None`",
109
+ found.kinds | {_CONDITIONAL},
110
+ found.reads,
111
+ )
112
+
113
+
114
+ def sifted(value: Comprehension, part: ast.expr, element: Inference) -> Inference | None:
115
+ """Type what a comprehension keeps of a name it filters, by `is not None` or `isinstance`.
116
+
117
+ `part`: its element, or a `dict`'s key or value, typed `element`. A test narrows where it must
118
+ hold for the part to be kept: a condition itself, or an operand of its `and`.
119
+
120
+ Returns:
121
+ The part's inference: less `None`, or as the one class it's checked for; `None` where that
122
+ leaves nothing, it's checked for anything but a class named there or checked any other way
123
+ (`not isinstance(x, C)`), or it's a display holding a name so narrowed (`(key, x)`).
124
+
125
+ """
126
+ tests: list[ast.expr] = [test for each in value.generators for test in each.ifs]
127
+ if not isinstance(part, ast.Name):
128
+ return None if any(_narrowed(tests, held.id) for held in displayed(part)) else element
129
+ name: str = part.id
130
+ classes: list[ast.expr] = _instance_of(tests, name)
131
+ named: list[str] = [
132
+ ast.unparse(found) for found in classes if isinstance(found, ast.Name | ast.Attribute)
133
+ ]
134
+ if classes or _instance_checked(tests, name):
135
+ kept: bool = len(classes) == len(named) == _instance_checked(tests, name) == 1 and (
136
+ named[0][:1].isupper() or named[0] in _SCALARS
137
+ )
138
+ return element._replace(annotation=named[0]) if kept else None
139
+ types: frozenset[str] = members(element.annotation) or frozenset()
140
+ if not (_NONE in types and name in _not_none(tests)):
141
+ return element
142
+ rest: list[str] = sorted(types - {_NONE})
143
+ return element._replace(annotation=" | ".join(rest)) if rest else None
144
+
145
+
146
+ def displayed(node: ast.expr | None) -> Iterator[ast.Name]:
147
+ """List the bare names a value is, or a display holds, however deep: `(key, d)`'s `key` and `d`.
148
+
149
+ Yields:
150
+ Each name.
151
+
152
+ """
153
+ parts: list[ast.expr]
154
+ keys: list[ast.expr | None]
155
+ part: ast.expr | None
156
+ inner: ast.expr
157
+ match node:
158
+ case ast.Name():
159
+ yield node
160
+ case ast.Tuple(elts=parts) | ast.List(elts=parts) | ast.Set(elts=parts):
161
+ for part in parts:
162
+ yield from displayed(part)
163
+ case ast.Dict(keys=keys, values=parts):
164
+ for part in (*keys, *parts):
165
+ yield from displayed(part)
166
+ case ast.Starred(value=inner):
167
+ yield from displayed(inner)
168
+ case _:
169
+ pass
170
+
171
+
172
+ def _narrowed(tests: list[ast.expr], name: str) -> bool: # whether the conditions narrow `name`
173
+ return bool(_instance_checked(tests, name)) or name in _not_none(tests)
174
+
175
+
176
+ def _held(tests: list[ast.expr]) -> Iterator[ast.expr]:
177
+ """List the tests that hold wherever all of `tests` do: each, or the operands of its `and`.
178
+
179
+ Yields:
180
+ Each test.
181
+
182
+ """
183
+ test: ast.expr
184
+ for test in tests:
185
+ if isinstance(test, ast.BoolOp) and isinstance(test.op, ast.And):
186
+ yield from _held(test.values)
187
+ else:
188
+ yield test
189
+
190
+
191
+ def _not_none(tests: list[ast.expr]) -> frozenset[str]: # each `x` of an `x is not None` that holds
192
+ name: str
193
+ test: ast.expr
194
+ found: set[str] = set()
195
+ for test in _held(tests):
196
+ match test:
197
+ case ast.Compare(
198
+ left=ast.Name(id=name),
199
+ ops=[ast.IsNot()],
200
+ comparators=[ast.Constant(value=None)],
201
+ ):
202
+ found.add(name)
203
+ case _:
204
+ pass
205
+ return frozenset(found)
206
+
207
+
208
+ def _instance_of(tests: list[ast.expr], name: str) -> list[ast.expr]:
209
+ """Find what an `isinstance(name, C)` that holds checks `name` for.
210
+
211
+ Returns:
212
+ Each `C`.
213
+
214
+ """
215
+ checked: str
216
+ found: ast.expr
217
+ classes: list[ast.expr] = []
218
+ test: ast.expr
219
+ for test in _held(tests):
220
+ match test:
221
+ case ast.Call(func=ast.Name(id="isinstance"), args=[ast.Name(id=checked), found]) if (
222
+ checked == name
223
+ ):
224
+ classes.append(found)
225
+ case _:
226
+ pass
227
+ return classes
228
+
229
+
230
+ def _instance_checked(tests: list[ast.expr], name: str) -> int:
231
+ """Count the `isinstance` checks of `name` anywhere in `tests`: under a `not` or an `or` too.
232
+
233
+ Returns:
234
+ How many.
235
+
236
+ """
237
+ checked: str
238
+ count: int = 0
239
+ test: ast.expr
240
+ node: ast.AST
241
+ for test in tests:
242
+ for node in ast.walk(test):
243
+ match node:
244
+ case ast.Call(func=ast.Name(id="isinstance"), args=[ast.Name(id=checked), *_]) if (
245
+ checked == name
246
+ ):
247
+ count += 1
248
+ case _:
249
+ pass
250
+ return count
251
+
252
+
253
+ def boolean(value: ast.BoolOp, infer: Infer) -> Inference | None:
254
+ """Infer `a or b` and `a and b` whose operands have one type: that type.
255
+
256
+ `or` gives an operand before its last only if it's true, which `None` never is: `a or b` with
257
+ `a: T | None` and `b: T` is a `T`, as `a or []` is with `a: list[T] | None`.
258
+
259
+ Returns:
260
+ The last operand's type, or `None` if an operand's isn't known or they differ.
261
+
262
+ """
263
+ dropped: frozenset[str] = frozenset({_NONE}) if isinstance(value.op, ast.Or) else frozenset()
264
+ built: str | None = _empty(value.values[-1]) if dropped else None
265
+ parts: list[Inference | None] = [
266
+ typed(operand, infer) for operand in value.values[: -1 if built else None]
267
+ ]
268
+ found: list[Inference] = [part for part in parts if part is not None]
269
+ if len(found) != len(parts):
270
+ return None
271
+ types: set[frozenset[str]] = {(members(part.annotation) or frozenset()) - dropped for part in found}
272
+ if len(types) != 1 or not next(iter(types)):
273
+ return None
274
+ annotation: str = found[-1].annotation
275
+ if built is not None:
276
+ # An empty display takes the type of the container before it.
277
+ annotation = min(next(iter(types)))
278
+ if len(next(iter(types))) != 1 or not annotation.startswith(f"{built}["):
279
+ return None
280
+ operator: str = "or" if dropped else "and"
281
+ return Inference(
282
+ annotation,
283
+ f"the operands of `{operator}`, of one type",
284
+ frozenset({_BOOLEAN}).union(*(part.kinds for part in found)),
285
+ tuple(read for part in found for read in part.reads),
286
+ )
287
+
288
+
289
+ def _empty(node: ast.expr) -> str | None:
290
+ """Name the builtin container an empty display builds: `[]` a `list`, `{}` a `dict`.
291
+
292
+ Returns:
293
+ Its name, or `None` for any other value.
294
+
295
+ """
296
+ match node:
297
+ case ast.List(elts=[]):
298
+ return "list"
299
+ case ast.Dict(keys=[]):
300
+ return "dict"
301
+ case _:
302
+ return None
303
+
304
+
305
+ def emptied(value: ast.IfExp, infer: Infer) -> Inference | None:
306
+ """Infer `a if c else []` (or `{} if c else a`): `a`'s type, a `list` (or a `dict`) of something.
307
+
308
+ `a if a else []` is never `None`, whatever `a` may be.
309
+
310
+ Returns:
311
+ The inference, or `None` if neither side or both are an empty display, the other's type isn't
312
+ that container's, or it may be `None` and `c` tests it another way, or holds what `c` tests
313
+ and may be `None`: it's narrowed there.
314
+
315
+ """
316
+ sides: list[ast.expr] = [side for side in (value.body, value.orelse) if _empty(side) is None]
317
+ built: str | None = _empty(value.body) or _empty(value.orelse)
318
+ found: Inference | None = typed(sides[0], infer) if len(sides) == 1 else None
319
+ types: frozenset[str] = frozenset() if found is None else members(found.annotation) or frozenset()
320
+ container: str = min(types - {_NONE}, default="")
321
+ if found is None or len(types - {_NONE}) != 1 or not container.startswith(f"{built}["):
322
+ return None
323
+ annotation: str = found.annotation
324
+ tested: set[str] = {ast.unparse(node) for node in ast.walk(value.test) if isinstance(node, _READS)}
325
+ held: bool = any(
326
+ ast.unparse(node) in tested
327
+ for node in ast.walk(sides[0])
328
+ if node is not sides[0] and isinstance(node, _READS)
329
+ )
330
+ if held and _HOLDS_NONE.search(container): # `[a] if a else []`: `a` is narrowed in the display
331
+ return None
332
+ if _NONE in types and ast.unparse(sides[0]) in tested:
333
+ if sides[0] is not value.body or ast.unparse(value.test) != ast.unparse(sides[0]):
334
+ return None
335
+ annotation = container
336
+ return Inference(
337
+ annotation,
338
+ "one side of a conditional, the other an empty display",
339
+ found.kinds | {_CONDITIONAL},
340
+ found.reads,
341
+ )
342
+
343
+
344
+ def module_text(value: ast.expr, known: Known) -> Inference | None:
345
+ """Infer a module's own `__file__` or `__name__`: a `str`.
346
+
347
+ Returns:
348
+ The inference, or `None` for any other value, or a module that binds the name itself.
349
+
350
+ """
351
+ name: str
352
+ match value:
353
+ case ast.Name(id=name) if name in _MODULE_TEXTS and (
354
+ known.names.plan is None or name not in known.names.plan.taken
355
+ ):
356
+ return Inference("str", f"the module's own `{name}`", frozenset({_BUILTIN}))
357
+ case _:
358
+ return None
359
+
360
+
361
+ def _classed(value: ast.expr, known: Known) -> ast.expr | None:
362
+ """Read the `x` of `type(x)` or `x.__class__`.
363
+
364
+ Returns:
365
+ It, or `None` for anything else, or a module that binds `type` itself.
366
+
367
+ """
368
+ arg: ast.expr
369
+ attr: str
370
+ match value:
371
+ case ast.Call(func=ast.Name(id="type"), args=[arg], keywords=[]) if known.is_builtin(
372
+ _TYPE,
373
+ ) and not isinstance(arg, ast.Starred):
374
+ return arg
375
+ case ast.Attribute(value=arg, attr=attr) if attr == _CLASS:
376
+ return arg
377
+ case _:
378
+ return None
379
+
380
+
381
+ def class_text(value: ast.expr, known: Known) -> Inference | None:
382
+ """Infer a class's `__name__`, `__qualname__` or `__module__`: a `str`.
383
+
384
+ Read of `type(x)` or `x.__class__`, whatever `x` is.
385
+
386
+ Returns:
387
+ The inference, or `None` for any other value.
388
+
389
+ """
390
+ owner: ast.expr
391
+ attr: str
392
+ match value:
393
+ case ast.Attribute(value=owner, attr=attr) if attr in _CLASS_TEXTS and _classed(owner, known):
394
+ return Inference("str", f"a class's `{attr}`", frozenset({_BUILTIN}))
395
+ case _:
396
+ return None
397
+
398
+
399
+ def class_of(value: ast.expr, known: Known, infer: Infer) -> Inference | None:
400
+ """Infer `type(x)` or `x.__class__`, with `x`'s type `C` known: `type[C]`.
401
+
402
+ Returns:
403
+ The inference, or `None` for anything else, a module that binds `type` itself, or an `x`
404
+ whose type is a union, `None` or a class itself.
405
+
406
+ """
407
+ arg: ast.expr | None
408
+ if (arg := _classed(value, known)) is None:
409
+ return None
410
+ found: Inference | None = typed(arg, infer)
411
+ types: frozenset[str] = frozenset() if found is None else members(found.annotation) or frozenset()
412
+ if found is None or len(types) != 1 or _NONE in types:
413
+ return None
414
+ if isinstance(value, ast.Attribute) and found.annotation.startswith(f"{_TYPE}["):
415
+ return None # a class's own class is its metaclass
416
+ return Inference(
417
+ f"{_TYPE}[{found.annotation}]",
418
+ f"`type` of {found.reason}",
419
+ found.kinds | {_BUILTIN},
420
+ found.reads,
421
+ )
422
+
423
+
424
+ def defaulted(receiver: str, call: ast.Call, infer: Infer, known: Known) -> Inference | None:
425
+ """Infer `d.get(key, default)` on a `dict[K, V]`, by a default of type `V`: `V` (`V | None`, by `None`).
426
+
427
+ And on a `TypedDict`'s instance, by the literal key's type `V` (see `members.keyed`), with no
428
+ default too: `V` for a required key, `V | None` for one that may be missing.
429
+
430
+ Returns:
431
+ The inference, or `None` for any other call, or a default of another type.
432
+
433
+ """
434
+ index: ast.expr
435
+ rest: list[ast.expr]
436
+ attr: str
437
+ match call:
438
+ case ast.Call(func=ast.Attribute(attr=attr), args=[index, *rest], keywords=[]) if (
439
+ attr == _GET and len(rest) <= 1
440
+ ):
441
+ pass
442
+ case _:
443
+ return None
444
+ item: ast.expr
445
+ text: str
446
+ reason: str
447
+ match parsed(receiver):
448
+ case ast.Subscript(value=ast.Name(id="dict" | "Dict"), slice=ast.Tuple(elts=[_, item])) if rest:
449
+ text, reason = ast.unparse(item), "`dict.get` with a default of its values' type"
450
+ case _:
451
+ key: Inference | None
452
+ if (key := keyed(receiver, index, known)) is None:
453
+ return None
454
+ text, reason = key.annotation, f"`get` of {key.reason}"
455
+ if not may_miss(receiver, index, known) and _NONE not in (members(text) or ()):
456
+ if not rest:
457
+ return Inference(text, reason, frozenset({_METHOD}))
458
+ if is_none(rest[0]): # `V` to pyright, `V | None` to mypy
459
+ return None
460
+ if not rest or is_none(rest[0]):
461
+ union: str | None = or_none(text)
462
+ return None if union is None else Inference(union, reason, frozenset({_METHOD}))
463
+ found: Inference | None = infer(rest[0])
464
+ same: bool = found is not None and found.annotation == text
465
+ return Inference(text, reason, found.kinds | {_METHOD}) if found and same else None
466
+
467
+
468
+ def partly(value: ast.expr, known: Known, infer: Infer) -> Inference | None:
469
+ """Infer a call whose declared return has a vague part: no fix past `vague`, but what's read of it is.
470
+
471
+ A function's, or a method's on a receiver whose type is known (see `known.Partial`): an
472
+ unpacking splits it, and a loop over it has its elements (`for name in hints()`, a `str`).
473
+
474
+ Returns:
475
+ The whole return, vague parts and all; or `None` for any other value.
476
+
477
+ """
478
+ func: ast.expr
479
+ receiver: ast.expr
480
+ name: str
481
+ partial: Partial = known.indirect.partial
482
+ match value:
483
+ case ast.Call(func=ast.Name() | ast.Attribute() as func) if (
484
+ partial.calls and ast.unparse(func) in partial.calls
485
+ ):
486
+ callee: str = ast.unparse(func)
487
+ return Inference(partial.calls[callee], f"`{callee}`'s declared return type", frozenset({_CALL}))
488
+ case ast.Call(func=ast.Attribute(value=receiver, attr=name)):
489
+ owner: Inference | None = infer(receiver)
490
+ found: str | None = None if owner is None else partial_method(owner.annotation, name, known)
491
+ if owner is None or found is None:
492
+ return None
493
+ return Inference(
494
+ found,
495
+ f"`{owner.annotation}.{name}`'s declared return type",
496
+ frozenset({_METHOD}) if isinstance(receiver, ast.Name) else owner.kinds | {_METHOD},
497
+ )
498
+ case _:
499
+ return None
500
+
501
+
502
+ def _getattr_args(value: ast.expr, known: Known) -> list[ast.expr] | None:
503
+ """Find the arguments of a call to the builtin `getattr`: an object, a name and maybe a default.
504
+
505
+ Returns:
506
+ The arguments, or `None` for any other value, or a module that binds `getattr` itself.
507
+
508
+ """
509
+ args: list[ast.expr]
510
+ match value:
511
+ case ast.Call(func=ast.Name(id="getattr"), args=args, keywords=[]) if (
512
+ _WITH_DEFAULT <= len(args) <= _WITH_DEFAULT + 1
513
+ and known.is_builtin(_GETATTR)
514
+ and not any(isinstance(arg, ast.Starred) for arg in args)
515
+ ):
516
+ return args
517
+ case _:
518
+ return None
519
+
520
+
521
+ def named_read(value: ast.expr, known: Known) -> ast.Attribute | None:
522
+ """Write `getattr(obj, "name", ...)` as the read it makes, `obj.name`.
523
+
524
+ Returns:
525
+ The read, or `None` for any other value, or a name that isn't one `obj.name` could spell (a
526
+ keyword, or `"__x"`, which a class's body would mangle).
527
+
528
+ """
529
+ args: list[ast.expr] | None = _getattr_args(value, known)
530
+ obj: ast.expr
531
+ name: str
532
+ match args:
533
+ case [obj, ast.Constant(value=str() as name), *_] if (
534
+ name.isidentifier()
535
+ and not keyword.iskeyword(name)
536
+ and not (name.startswith("__") and not name.endswith("__"))
537
+ ):
538
+ return ast.copy_location(ast.Attribute(obj, name, ast.Load()), value)
539
+ case _:
540
+ return None
541
+
542
+
543
+ def _named(value: ast.expr, args: list[ast.expr], known: Known, infer: Infer) -> Inference | None:
544
+ """Infer `getattr(obj, "name")` as `obj.name` is; with a `None` default, or `None`.
545
+
546
+ With a default of another known type, the union of both, where `displays.union` writes one.
547
+
548
+ Returns:
549
+ The inference, or `None` where the read, or a default, has no known type.
550
+
551
+ """
552
+ read: ast.Attribute | None = named_read(value, known)
553
+ found: Inference | None = None if read is None else typed(read, infer)
554
+ defaulted_none: bool = len(args) > _WITH_DEFAULT and is_none(args[_WITH_DEFAULT])
555
+ # An `X | None` read is nearly always narrowed first, as `doubts.narrowed_first` has it.
556
+ if found is None or (_NONE in (members(found.annotation) or ()) and not defaulted_none):
557
+ return None
558
+ reason: str = f"`getattr` of `{ast.unparse(read or value)}`, {found.reason}"
559
+ kinds: frozenset[str] = found.kinds | {_BUILTIN}
560
+ if len(args) == _WITH_DEFAULT:
561
+ return Inference(found.annotation, reason, kinds, found.reads)
562
+ default: ast.expr = args[_WITH_DEFAULT]
563
+ if defaulted_none:
564
+ union: str | None = or_none(found.annotation)
565
+ return None if union is None else Inference(union, f"{reason}, or `None`", kinds, found.reads)
566
+ other: Inference | None = infer(default)
567
+ joined: str | None = (
568
+ None
569
+ if other is None
570
+ else found.annotation
571
+ if other.annotation == found.annotation
572
+ else displays.union([found.annotation, other.annotation])
573
+ )
574
+ if other is None or joined is None:
575
+ return None
576
+ return Inference(joined, f"{reason}, or its default", kinds | other.kinds, found.reads)
577
+
578
+
579
+ def attribute_of(value: ast.expr, known: Known, infer: Infer) -> Inference | None:
580
+ """Infer `getattr(obj, name)`: by a literal name, as `obj.name` is (see `_named`); else an `Any`.
581
+
582
+ The `Any` with a default of a known type `T` is an `Any | T`, where `vague` lets it be written
583
+ (from 1), `Any` named as the module can.
584
+
585
+ Returns:
586
+ The inference, or `None` for anything else, a module that binds `getattr` itself, or a
587
+ default of no known type.
588
+
589
+ """
590
+ args: list[ast.expr] | None
591
+ if (args := _getattr_args(value, known)) is None:
592
+ return None
593
+ found: Inference | None = _named(value, args, known, infer)
594
+ if found is not None or known.limits.vague <= 0:
595
+ return found
596
+ plan: ImportPlan | None = known.names.plan
597
+ spelled: str | None = None if plan is None else plan.spell(_TYPING_ANY)
598
+ default: Inference | None = infer(args[2]) if len(args) > _WITH_DEFAULT else None
599
+ if spelled is None or (len(args) > _WITH_DEFAULT and default is None and not is_none(args[2])):
600
+ return None
601
+ union: str = (
602
+ spelled
603
+ if len(args) == _WITH_DEFAULT
604
+ else f"{spelled} | {_NONE if default is None else default.annotation}"
605
+ )
606
+ return Inference(
607
+ union,
608
+ "`getattr`'s return type",
609
+ frozenset({_BUILTIN}) | (default.kinds if default else frozenset()),
610
+ )
611
+
612
+
613
+ def environment(value: ast.expr, known: Known) -> Inference | None:
614
+ """Infer `os.environ[key]`, however the module names `os.environ`: a `str`.
615
+
616
+ Returns:
617
+ The inference, or `None` for anything else, a slice included.
618
+
619
+ """
620
+ mapping: ast.expr
621
+ index: ast.expr
622
+ match value:
623
+ case ast.Subscript(value=ast.Name() | ast.Attribute() as mapping, slice=index) if (
624
+ not isinstance(index, ast.Slice) and stdlib.resolved(mapping, known.names.stdlib) == _ENVIRON
625
+ ):
626
+ return Inference("str", f"`{_ENVIRON}`'s values", frozenset({_STDLIB}))
627
+ case _:
628
+ return None
629
+
630
+
631
+ def cast(value: ast.expr, spellings: frozenset[str]) -> Inference | None:
632
+ """Infer `typing.cast(T, x)`: `T` as written, or a string's contents.
633
+
634
+ Returns:
635
+ The inference, or `None` if `value` isn't such a call, or `T` isn't an expression.
636
+
637
+ """
638
+ func: ast.expr
639
+ target: ast.expr
640
+ match value:
641
+ case ast.Call(func=func, args=[target, _], keywords=[]) if dotted(func) in spellings:
642
+ pass
643
+ case _:
644
+ return None
645
+ text: str = (
646
+ target.value.strip()
647
+ if isinstance(target, ast.Constant) and isinstance(target.value, str)
648
+ else ast.unparse(target)
649
+ )
650
+ try:
651
+ written: ast.expr = ast.parse(text, mode="eval").body
652
+ except SyntaxError:
653
+ return None
654
+ return Inference(ast.unparse(written), "`cast`'s target type", frozenset({"cast"}))