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.
- constricter/__init__.py +55 -0
- constricter/__main__.py +26 -0
- constricter/_version.py +4 -0
- constricter/cli/__init__.py +2 -0
- constricter/cli/baseline.py +107 -0
- constricter/cli/collecting.py +54 -0
- constricter/cli/command.py +547 -0
- constricter/cli/config.py +322 -0
- constricter/cli/explain.py +104 -0
- constricter/cli/lsp/__init__.py +2 -0
- constricter/cli/lsp/connection.py +475 -0
- constricter/cli/lsp/edits.py +188 -0
- constricter/cli/lsp/guard.py +133 -0
- constricter/cli/lsp/hints.py +391 -0
- constricter/cli/lsp/protocol.py +192 -0
- constricter/cli/lsp/remembered.py +81 -0
- constricter/cli/lsp/stored.py +200 -0
- constricter/cli/options.py +799 -0
- constricter/cli/paths.py +151 -0
- constricter/cli/reading.py +36 -0
- constricter/cli/report.py +397 -0
- constricter/cli/rounds.py +328 -0
- constricter/cli/runs.py +90 -0
- constricter/cli/schedule.py +463 -0
- constricter/cli/traced.py +401 -0
- constricter/cli/workers.py +286 -0
- constricter/empty.py +66 -0
- constricter/fix/__init__.cp313-win32.pyd +0 -0
- constricter/fix/__init__.py +2 -0
- constricter/fix/core/__init__.cp313-win32.pyd +0 -0
- constricter/fix/core/__init__.py +2 -0
- constricter/fix/core/asked.cp313-win32.pyd +0 -0
- constricter/fix/core/asked.py +56 -0
- constricter/fix/core/fixes.cp313-win32.pyd +0 -0
- constricter/fix/core/fixes.py +317 -0
- constricter/fix/core/imports.cp313-win32.pyd +0 -0
- constricter/fix/core/imports.py +580 -0
- constricter/fix/core/inherited.cp313-win32.pyd +0 -0
- constricter/fix/core/inherited.py +311 -0
- constricter/fix/core/known.cp313-win32.pyd +0 -0
- constricter/fix/core/known.py +730 -0
- constricter/fix/core/signatures.cp313-win32.pyd +0 -0
- constricter/fix/core/signatures.py +132 -0
- constricter/fix/index/__init__.cp313-win32.pyd +0 -0
- constricter/fix/index/__init__.py +2 -0
- constricter/fix/index/awaits.cp313-win32.pyd +0 -0
- constricter/fix/index/awaits.py +90 -0
- constricter/fix/index/beyond.cp313-win32.pyd +0 -0
- constricter/fix/index/beyond.py +149 -0
- constricter/fix/index/callers.cp313-win32.pyd +0 -0
- constricter/fix/index/callers.py +155 -0
- constricter/fix/index/crossed.cp313-win32.pyd +0 -0
- constricter/fix/index/crossed.py +327 -0
- constricter/fix/index/decorated.cp313-win32.pyd +0 -0
- constricter/fix/index/decorated.py +140 -0
- constricter/fix/index/fixtures.cp313-win32.pyd +0 -0
- constricter/fix/index/fixtures.py +176 -0
- constricter/fix/index/linked.cp313-win32.pyd +0 -0
- constricter/fix/index/linked.py +142 -0
- constricter/fix/index/loose.cp313-win32.pyd +0 -0
- constricter/fix/index/loose.py +103 -0
- constricter/fix/index/managed.cp313-win32.pyd +0 -0
- constricter/fix/index/managed.py +63 -0
- constricter/fix/index/modules.cp313-win32.pyd +0 -0
- constricter/fix/index/modules.py +741 -0
- constricter/fix/index/offers.cp313-win32.pyd +0 -0
- constricter/fix/index/offers.py +252 -0
- constricter/fix/index/order.cp313-win32.pyd +0 -0
- constricter/fix/index/order.py +155 -0
- constricter/fix/index/origins.cp313-win32.pyd +0 -0
- constricter/fix/index/origins.py +238 -0
- constricter/fix/index/own_methods.cp313-win32.pyd +0 -0
- constricter/fix/index/own_methods.py +91 -0
- constricter/fix/index/own_overloads.cp313-win32.pyd +0 -0
- constricter/fix/index/own_overloads.py +70 -0
- constricter/fix/index/own_types.cp313-win32.pyd +0 -0
- constricter/fix/index/own_types.py +330 -0
- constricter/fix/index/params.cp313-win32.pyd +0 -0
- constricter/fix/index/params.py +81 -0
- constricter/fix/index/plain.cp313-win32.pyd +0 -0
- constricter/fix/index/plain.py +221 -0
- constricter/fix/index/project.cp313-win32.pyd +0 -0
- constricter/fix/index/project.py +797 -0
- constricter/fix/index/sides.cp313-win32.pyd +0 -0
- constricter/fix/index/sides.py +160 -0
- constricter/fix/index/strings.cp313-win32.pyd +0 -0
- constricter/fix/index/strings.py +46 -0
- constricter/fix/index/stubs/__init__.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/__init__.py +2 -0
- constricter/fix/index/stubs/atoms.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/atoms.py +425 -0
- constricter/fix/index/stubs/chained.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/chained.py +69 -0
- constricter/fix/index/stubs/classnames.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/classnames.py +124 -0
- constricter/fix/index/stubs/declared.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/declared.py +508 -0
- constricter/fix/index/stubs/installed.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/installed.py +425 -0
- constricter/fix/index/stubs/names.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/names.py +200 -0
- constricter/fix/index/stubs/read.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/read.py +70 -0
- constricter/fix/index/stubs/signatures.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/signatures.py +656 -0
- constricter/fix/index/stubs/stubbed.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/stubbed.py +208 -0
- constricter/fix/index/taken.cp313-win32.pyd +0 -0
- constricter/fix/index/taken.py +45 -0
- constricter/fix/index/tuples.cp313-win32.pyd +0 -0
- constricter/fix/index/tuples.py +112 -0
- constricter/fix/libraries/__init__.cp313-win32.pyd +0 -0
- constricter/fix/libraries/__init__.py +2 -0
- constricter/fix/libraries/library.cp313-win32.pyd +0 -0
- constricter/fix/libraries/library.py +420 -0
- constricter/fix/libraries/opened.cp313-win32.pyd +0 -0
- constricter/fix/libraries/opened.py +147 -0
- constricter/fix/libraries/overloads.cp313-win32.pyd +0 -0
- constricter/fix/libraries/overloads.py +960 -0
- constricter/fix/libraries/receivers.cp313-win32.pyd +0 -0
- constricter/fix/libraries/receivers.py +274 -0
- constricter/fix/libraries/stdlib.cp313-win32.pyd +0 -0
- constricter/fix/libraries/stdlib.py +752 -0
- constricter/fix/libraries/walks.cp313-win32.pyd +0 -0
- constricter/fix/libraries/walks.py +49 -0
- constricter/fix/tables/aliases.json +147 -0
- constricter/fix/tables/attributes.json +700 -0
- constricter/fix/tables/awaited.json +69 -0
- constricter/fix/tables/awaited_overloads.json +14 -0
- constricter/fix/tables/bases.json +888 -0
- constricter/fix/tables/classes.json +1941 -0
- constricter/fix/tables/elements.json +156 -0
- constricter/fix/tables/functions.json +58 -0
- constricter/fix/tables/generic_attributes.json +254 -0
- constricter/fix/tables/method_overloads.json +461 -0
- constricter/fix/tables/method_signatures.json +954 -0
- constricter/fix/tables/methods.json +954 -0
- constricter/fix/tables/overloads.json +540 -0
- constricter/fix/tables/platform.json +37 -0
- constricter/fix/tables/returns.json +1362 -0
- constricter/fix/tables/scalar_members.json +16 -0
- constricter/fix/tables/scalars.json +2707 -0
- constricter/fix/tables/source.json +28 -0
- constricter/fix/tables/subscriptable.json +77 -0
- constricter/fix/tables/type_parameters.json +178 -0
- constricter/fix/tables/variables.json +2962 -0
- constricter/fix/values/__init__.cp313-win32.pyd +0 -0
- constricter/fix/values/__init__.py +2 -0
- constricter/fix/values/aliased.cp313-win32.pyd +0 -0
- constricter/fix/values/aliased.py +195 -0
- constricter/fix/values/bodies.cp313-win32.pyd +0 -0
- constricter/fix/values/bodies.py +76 -0
- constricter/fix/values/callables.cp313-win32.pyd +0 -0
- constricter/fix/values/callables.py +323 -0
- constricter/fix/values/called.cp313-win32.pyd +0 -0
- constricter/fix/values/called.py +194 -0
- constricter/fix/values/classvars.cp313-win32.pyd +0 -0
- constricter/fix/values/classvars.py +629 -0
- constricter/fix/values/decided.cp313-win32.pyd +0 -0
- constricter/fix/values/decided.py +429 -0
- constricter/fix/values/displays.cp313-win32.pyd +0 -0
- constricter/fix/values/displays.py +101 -0
- constricter/fix/values/doubts.cp313-win32.pyd +0 -0
- constricter/fix/values/doubts.py +677 -0
- constricter/fix/values/entered.cp313-win32.pyd +0 -0
- constricter/fix/values/entered.py +332 -0
- constricter/fix/values/fills.cp313-win32.pyd +0 -0
- constricter/fix/values/fills.py +446 -0
- constricter/fix/values/guesses.cp313-win32.pyd +0 -0
- constricter/fix/values/guesses.py +376 -0
- constricter/fix/values/hinted.cp313-win32.pyd +0 -0
- constricter/fix/values/hinted.py +811 -0
- constricter/fix/values/inference.cp313-win32.pyd +0 -0
- constricter/fix/values/inference.py +698 -0
- constricter/fix/values/loops.cp313-win32.pyd +0 -0
- constricter/fix/values/loops.py +335 -0
- constricter/fix/values/members.cp313-win32.pyd +0 -0
- constricter/fix/values/members.py +578 -0
- constricter/fix/values/narrowed.cp313-win32.pyd +0 -0
- constricter/fix/values/narrowed.py +220 -0
- constricter/fix/values/operated.cp313-win32.pyd +0 -0
- constricter/fix/values/operated.py +202 -0
- constricter/fix/values/ordered.cp313-win32.pyd +0 -0
- constricter/fix/values/ordered.py +74 -0
- constricter/fix/values/returned.cp313-win32.pyd +0 -0
- constricter/fix/values/returned.py +479 -0
- constricter/fix/values/returns.cp313-win32.pyd +0 -0
- constricter/fix/values/returns.py +237 -0
- constricter/fix/values/shapes.cp313-win32.pyd +0 -0
- constricter/fix/values/shapes.py +654 -0
- constricter/fix/values/stored.cp313-win32.pyd +0 -0
- constricter/fix/values/stored.py +564 -0
- constricter/fix/values/targets.cp313-win32.pyd +0 -0
- constricter/fix/values/targets.py +419 -0
- constricter/fix/values/unchecked.cp313-win32.pyd +0 -0
- constricter/fix/values/unchecked.py +278 -0
- constricter/jsonc.py +50 -0
- constricter/noqa.py +47 -0
- constricter/notebook.py +115 -0
- constricter/offences.py +436 -0
- constricter/overrides.py +24 -0
- constricter/plugins/__init__.py +2 -0
- constricter/plugins/flake8.py +133 -0
- constricter/plugins/pylint.py +186 -0
- constricter/py.typed +0 -0
- constricter/recording.py +660 -0
- constricter/rules/__init__.cp313-win32.pyd +0 -0
- constricter/rules/__init__.py +2 -0
- constricter/rules/annotations.cp313-win32.pyd +0 -0
- constricter/rules/annotations.py +296 -0
- constricter/rules/binding.cp313-win32.pyd +0 -0
- constricter/rules/binding.py +652 -0
- constricter/rules/calls.cp313-win32.pyd +0 -0
- constricter/rules/calls.py +460 -0
- constricter/rules/checker.cp313-win32.pyd +0 -0
- constricter/rules/checker.py +536 -0
- constricter/rules/declarations.cp313-win32.pyd +0 -0
- constricter/rules/declarations.py +730 -0
- constricter/rules/decorators.cp313-win32.pyd +0 -0
- constricter/rules/decorators.py +351 -0
- constricter/rules/flow.cp313-win32.pyd +0 -0
- constricter/rules/flow.py +502 -0
- constricter/rules/keys.cp313-win32.pyd +0 -0
- constricter/rules/keys.py +79 -0
- constricter/rules/late.cp313-win32.pyd +0 -0
- constricter/rules/late.py +969 -0
- constricter/rules/narrowing.cp313-win32.pyd +0 -0
- constricter/rules/narrowing.py +98 -0
- constricter/rules/parsed.cp313-win32.pyd +0 -0
- constricter/rules/parsed.py +136 -0
- constricter/rules/quoted.cp313-win32.pyd +0 -0
- constricter/rules/quoted.py +141 -0
- constricter/rules/rebinding.cp313-win32.pyd +0 -0
- constricter/rules/rebinding.py +169 -0
- constricter/rules/recorded.cp313-win32.pyd +0 -0
- constricter/rules/recorded.py +168 -0
- constricter/rules/redundant.cp313-win32.pyd +0 -0
- constricter/rules/redundant.py +160 -0
- constricter/rules/scope.cp313-win32.pyd +0 -0
- constricter/rules/scope.py +802 -0
- constricter/rules/scoped.cp313-win32.pyd +0 -0
- constricter/rules/scoped.py +232 -0
- constricter/rules/scoping.cp313-win32.pyd +0 -0
- constricter/rules/scoping.py +346 -0
- constricter/rules/settings.cp313-win32.pyd +0 -0
- constricter/rules/settings.py +189 -0
- constricter/rules/syntax.cp313-win32.pyd +0 -0
- constricter/rules/syntax.py +472 -0
- constricter/rules/tables.cp313-win32.pyd +0 -0
- constricter/rules/tables.py +169 -0
- constricter/rules/walked.cp313-win32.pyd +0 -0
- constricter/rules/walked.py +127 -0
- constricter/rules/widened.cp313-win32.pyd +0 -0
- constricter/rules/widened.py +322 -0
- constricter/trace.py +374 -0
- e45ce06b2fdd5c43f782__mypyc.cp313-win32.pyd +0 -0
- python_constricter-0.4.1.dist-info/METADATA +400 -0
- python_constricter-0.4.1.dist-info/RECORD +261 -0
- python_constricter-0.4.1.dist-info/WHEEL +4 -0
- python_constricter-0.4.1.dist-info/entry_points.txt +5 -0
- 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"}))
|
|
Binary file
|