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
|
Binary file
|
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""What `--fix` infers a value's type from: literals, calls, and the locals a scope already typed."""
|
|
3
|
+
|
|
4
|
+
import ast
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import TYPE_CHECKING, Final, TypeAlias
|
|
7
|
+
|
|
8
|
+
from constricter.fix.core import asked
|
|
9
|
+
from constricter.fix.core.known import PARAM, Inference, Known, kinds_of, stood
|
|
10
|
+
from constricter.fix.libraries import overloads, stdlib
|
|
11
|
+
from constricter.fix.libraries.library import (
|
|
12
|
+
installed_call,
|
|
13
|
+
installed_chain,
|
|
14
|
+
installed_method,
|
|
15
|
+
library_call,
|
|
16
|
+
library_class,
|
|
17
|
+
library_operator,
|
|
18
|
+
library_variable,
|
|
19
|
+
)
|
|
20
|
+
from constricter.fix.libraries.opened import opened, opened_path
|
|
21
|
+
from constricter.fix.values import called, decided, displays, operated, shapes
|
|
22
|
+
from constricter.fix.values.loops import comprehended, dict_view, looped, rebased
|
|
23
|
+
from constricter.fix.values.members import (
|
|
24
|
+
assigned_attribute,
|
|
25
|
+
awaited,
|
|
26
|
+
class_variable,
|
|
27
|
+
keyed,
|
|
28
|
+
member,
|
|
29
|
+
present,
|
|
30
|
+
returned_method,
|
|
31
|
+
subscripted,
|
|
32
|
+
)
|
|
33
|
+
from constricter.fix.values.targets import (
|
|
34
|
+
SAME_ELEMENTS,
|
|
35
|
+
dict_parts,
|
|
36
|
+
iterator_call,
|
|
37
|
+
)
|
|
38
|
+
from constricter.offences import MEMBER
|
|
39
|
+
from constricter.rules.annotations import dotted
|
|
40
|
+
from constricter.rules.flow import members
|
|
41
|
+
|
|
42
|
+
if TYPE_CHECKING:
|
|
43
|
+
from types import EllipsisType
|
|
44
|
+
|
|
45
|
+
# Comparisons whose result is always a real `bool`, whatever the operands: `in` converts `__contains__`'s
|
|
46
|
+
# result, and identity can't be overridden (`==` and `<` can return anything, as numpy's do).
|
|
47
|
+
_BOOLEAN_TESTS: Final = (ast.In, ast.NotIn, ast.Is, ast.IsNot)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
_NUMBERS: Final = (int, float, complex)
|
|
51
|
+
# The builtin classes whose comparisons (`==`, `<`, ...) give a real `bool` (`None`'s, by identity).
|
|
52
|
+
_COMPARABLE: Final = frozenset(
|
|
53
|
+
{
|
|
54
|
+
"bool",
|
|
55
|
+
"int",
|
|
56
|
+
"float",
|
|
57
|
+
"complex",
|
|
58
|
+
"str",
|
|
59
|
+
"bytes",
|
|
60
|
+
"bytearray",
|
|
61
|
+
"list",
|
|
62
|
+
"tuple",
|
|
63
|
+
"dict",
|
|
64
|
+
"set",
|
|
65
|
+
"frozenset",
|
|
66
|
+
"range",
|
|
67
|
+
"None",
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
RETURNED: Final = "returned" # the fix kind of an unannotated function's `return`s
|
|
71
|
+
ASSIGNED: Final = "assigned" # the fix kind of an instance attribute typed by its assignments
|
|
72
|
+
_SUBSCRIPT: Final = "subscript" # the fix kind of a subscript
|
|
73
|
+
_GET_ITEM: Final = "__getitem__" # what types one of a class's instance
|
|
74
|
+
_Pair: TypeAlias = tuple[Inference | None, Inference | None] # a `dict` display's entry: key, value
|
|
75
|
+
# Builtins that build a container of their argument's elements, and the type they build.
|
|
76
|
+
CONTAINER_BUILDERS: Final = {
|
|
77
|
+
"sorted": "list[{}]",
|
|
78
|
+
"list": "list[{}]",
|
|
79
|
+
"set": "set[{}]",
|
|
80
|
+
"frozenset": "frozenset[{}]",
|
|
81
|
+
"tuple": "tuple[{}, ...]",
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def inferred(value: ast.expr, known: Known, declared: Mapping[str, str]) -> str | None:
|
|
86
|
+
"""Return the annotation `value` makes unambiguous, given what the module declares (`known`).
|
|
87
|
+
|
|
88
|
+
`inference`'s annotation, without its reason.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
The annotation as source text, or `None` if the value doesn't decide one.
|
|
92
|
+
|
|
93
|
+
"""
|
|
94
|
+
found: Inference | None = inference(value, known, declared)
|
|
95
|
+
return None if found is None else found.annotation
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def inference(value: ast.expr, known: Known, declared: Mapping[str, str]) -> Inference | None:
|
|
99
|
+
"""Infer the annotation `value` makes unambiguous, and say how.
|
|
100
|
+
|
|
101
|
+
A literal's type (containers too, when their elements agree), a call to a module function that
|
|
102
|
+
declares its return type, a class it constructs, or (`declared`) another local this scope
|
|
103
|
+
already gave a type: a plain copy, a subscript of a known container, an attribute of a class
|
|
104
|
+
defined in this module, or a method call on it (a `str`/`bytes` or `list`/`set`/`dict` method,
|
|
105
|
+
or a method of a class defined in this module; see `method_return`).
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
The annotation as source text and its reason, or `None` if the value doesn't decide one.
|
|
109
|
+
|
|
110
|
+
"""
|
|
111
|
+
held: tuple[Inference | None] | None
|
|
112
|
+
if (held := asked.asked(value, known, declared)) is not None:
|
|
113
|
+
return held[0]
|
|
114
|
+
found: Inference | None = _from_local(value, known, declared) or _from_value(value, known, declared)
|
|
115
|
+
asked.keep(value, known, declared, found)
|
|
116
|
+
return found
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _from_local(value: ast.expr, known: Known, declared: Mapping[str, str]) -> Inference | None:
|
|
120
|
+
"""Infer from what this scope already typed: a copy of a local, or a member of any typed value.
|
|
121
|
+
|
|
122
|
+
A member (an attribute, a method call, a subscript) has its receiver typed as any value is (a
|
|
123
|
+
local, `self.index`, `f()`, `x[0]`), and is looked up on it (see `members.member`); a method
|
|
124
|
+
typed only by its `return`s is a guess. What typed a receiver that isn't a plain local decided
|
|
125
|
+
the member too (its fix kinds).
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
The inference, or `None`.
|
|
129
|
+
|
|
130
|
+
"""
|
|
131
|
+
name: str
|
|
132
|
+
receiver: ast.expr
|
|
133
|
+
attr: str
|
|
134
|
+
match value:
|
|
135
|
+
case ast.Name(id=name) if name in declared:
|
|
136
|
+
return Inference(declared[name], f"a copy of `{name}`", frozenset({"copy"}))
|
|
137
|
+
case ast.Attribute(value=ast.Name(id=name), attr="param") if f"{name}{PARAM}" in declared:
|
|
138
|
+
return Inference(declared[f"{name}{PARAM}"], "its fixture's `params`", frozenset({"copy"}))
|
|
139
|
+
case (
|
|
140
|
+
ast.Attribute(value=receiver, attr=attr) | ast.Call(func=ast.Attribute(value=receiver, attr=attr))
|
|
141
|
+
):
|
|
142
|
+
pass
|
|
143
|
+
case ast.Subscript(value=receiver):
|
|
144
|
+
attr = ""
|
|
145
|
+
case _:
|
|
146
|
+
return None
|
|
147
|
+
typed: Inference | None
|
|
148
|
+
if (typed := inference(receiver, known, declared)) is None:
|
|
149
|
+
return installed_chain(stood(value, declared), known, lambda arg: inference(arg, known, declared))
|
|
150
|
+
held: str = present(typed.annotation, attr)
|
|
151
|
+
found: Inference | None = _member_of(
|
|
152
|
+
value,
|
|
153
|
+
held if attr else rebased(held, _GET_ITEM, known),
|
|
154
|
+
attr,
|
|
155
|
+
known,
|
|
156
|
+
declared,
|
|
157
|
+
)
|
|
158
|
+
if found is None:
|
|
159
|
+
return None
|
|
160
|
+
return found if isinstance(receiver, ast.Name) else found._replace(kinds=found.kinds | typed.kinds)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _member_of(
|
|
164
|
+
value: ast.expr,
|
|
165
|
+
receiver: str,
|
|
166
|
+
attr: str,
|
|
167
|
+
known: Known,
|
|
168
|
+
declared: Mapping[str, str],
|
|
169
|
+
) -> Inference | None:
|
|
170
|
+
"""Type `value`, a subscript, a method call or an attribute (`attr`) of a value typed `receiver`.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
The inference, or `None`.
|
|
174
|
+
|
|
175
|
+
"""
|
|
176
|
+
text: str | None
|
|
177
|
+
match value:
|
|
178
|
+
case ast.Subscript():
|
|
179
|
+
text = subscripted(receiver, value, inferred(value.slice, known, declared))
|
|
180
|
+
return (
|
|
181
|
+
keyed(receiver, value.slice, known) or _item(value, receiver, known, declared)
|
|
182
|
+
if text is None
|
|
183
|
+
else Inference(
|
|
184
|
+
text,
|
|
185
|
+
f"a subscript of `{ast.unparse(value.value)}`, a `{receiver}`",
|
|
186
|
+
frozenset({_SUBSCRIPT}),
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
case ast.Call():
|
|
190
|
+
found: Inference | None = member(receiver, attr, value, known)
|
|
191
|
+
method: stdlib.Method | None
|
|
192
|
+
# A class of the module's takes a library base's method, matched as the base's own is.
|
|
193
|
+
base: str = known.class_side.lineage.definer(receiver, attr) or receiver
|
|
194
|
+
if found is None and (
|
|
195
|
+
(method := stdlib.overloaded_method(receiver, attr, known)) is not None
|
|
196
|
+
or (method := stdlib.overloaded_method(base, attr, known)) is not None
|
|
197
|
+
or (method := installed_method(receiver, attr, known)) is not None
|
|
198
|
+
or (method := installed_method(base, attr, known)) is not None
|
|
199
|
+
):
|
|
200
|
+
found = overloads.chosen(
|
|
201
|
+
method.entry,
|
|
202
|
+
value,
|
|
203
|
+
known,
|
|
204
|
+
lambda arg: inference(arg, known, declared),
|
|
205
|
+
stdlib.for_receiver(method, receiver),
|
|
206
|
+
)
|
|
207
|
+
# The base itself is its `Self`: the receiver's own class, which the base isn't.
|
|
208
|
+
found = None if found is not None and receiver != base == found.annotation else found
|
|
209
|
+
if found is None:
|
|
210
|
+
found = shapes.defaulted(
|
|
211
|
+
receiver,
|
|
212
|
+
value,
|
|
213
|
+
lambda arg: inference(arg, known, declared),
|
|
214
|
+
known,
|
|
215
|
+
) or opened_path(receiver, value, known)
|
|
216
|
+
defined: tuple[str, str] | None = (
|
|
217
|
+
returned_method(receiver, attr, known) if found is None else None
|
|
218
|
+
)
|
|
219
|
+
return (
|
|
220
|
+
found
|
|
221
|
+
if defined is None
|
|
222
|
+
else Inference(defined[1], f"`{defined[0]}.{attr}`'s `return`s", frozenset({RETURNED}))
|
|
223
|
+
)
|
|
224
|
+
case _:
|
|
225
|
+
return member(receiver, attr, None, known) or _unannotated(receiver, attr, known)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _item(
|
|
229
|
+
value: ast.Subscript,
|
|
230
|
+
receiver: str,
|
|
231
|
+
known: Known,
|
|
232
|
+
declared: Mapping[str, str],
|
|
233
|
+
) -> Inference | None:
|
|
234
|
+
"""Type a subscript of a standard-library class's instance: what its `__getitem__` gives the index.
|
|
235
|
+
|
|
236
|
+
As a call passing it is typed: `proxy["k"]` on a `MappingProxyType[str, int]` is an `int`.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
The inference, or `None`.
|
|
240
|
+
|
|
241
|
+
"""
|
|
242
|
+
call: ast.Call = ast.copy_location(
|
|
243
|
+
ast.Call(ast.Attribute(value.value, _GET_ITEM, ast.Load()), [value.slice], []),
|
|
244
|
+
value,
|
|
245
|
+
)
|
|
246
|
+
found: Inference | None = stdlib.library_member(receiver, _GET_ITEM, call, known)
|
|
247
|
+
method: stdlib.Method | None
|
|
248
|
+
if found is None and (method := stdlib.overloaded_method(receiver, _GET_ITEM, known)) is not None:
|
|
249
|
+
found = overloads.chosen(
|
|
250
|
+
method.entry,
|
|
251
|
+
call,
|
|
252
|
+
known,
|
|
253
|
+
lambda arg: inference(arg, known, declared),
|
|
254
|
+
method,
|
|
255
|
+
)
|
|
256
|
+
return None if found is None else found._replace(kinds=found.kinds | {_SUBSCRIPT})
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _unannotated(receiver: str, attr: str, known: Known) -> Inference | None:
|
|
260
|
+
"""Type an attribute nothing declares: by its class's assignments, or its value in the class's body.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
The inference (a guess), or `None`.
|
|
264
|
+
|
|
265
|
+
"""
|
|
266
|
+
text: str | None
|
|
267
|
+
if (text := assigned_attribute(receiver, attr, known)) is not None:
|
|
268
|
+
return Inference(text, f"`{receiver}.{attr}`'s assignments", frozenset({ASSIGNED}))
|
|
269
|
+
if (text := class_variable(receiver, attr, known)) is not None:
|
|
270
|
+
return Inference(text, f"`{attr}`'s value in its class's body", frozenset({MEMBER}))
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _scalar_reason(value: ast.expr) -> str:
|
|
275
|
+
"""Say how `_scalar` typed `value`.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
The reason.
|
|
279
|
+
|
|
280
|
+
"""
|
|
281
|
+
match value:
|
|
282
|
+
case ast.JoinedStr():
|
|
283
|
+
return "an f-string"
|
|
284
|
+
case ast.UnaryOp(op=ast.Not()):
|
|
285
|
+
return "`not`, always a `bool`"
|
|
286
|
+
case ast.Compare():
|
|
287
|
+
return "`in` or `is`, always a `bool`"
|
|
288
|
+
case _:
|
|
289
|
+
return "a literal"
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _from_value(value: ast.expr, known: Known, declared: Mapping[str, str]) -> Inference | None:
|
|
293
|
+
"""Infer from the value itself: a literal, a container of them, or a call.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
The inference, or `None`.
|
|
297
|
+
|
|
298
|
+
"""
|
|
299
|
+
found: str | None
|
|
300
|
+
if found := scalar(value):
|
|
301
|
+
return Inference(found, _scalar_reason(value), frozenset({"literal"}))
|
|
302
|
+
# Each kind of value is asked only of what types one: most values are names, and calls.
|
|
303
|
+
match value:
|
|
304
|
+
case ast.Call():
|
|
305
|
+
return _computed(value, known, declared) or _from_call(value, known, declared)
|
|
306
|
+
case ast.Name() | ast.Attribute():
|
|
307
|
+
return (
|
|
308
|
+
shapes.module_text(value, known)
|
|
309
|
+
or shapes.class_text(value, known)
|
|
310
|
+
or shapes.class_of(value, known, lambda arg: inference(arg, known, declared))
|
|
311
|
+
or library_variable(value, known)
|
|
312
|
+
)
|
|
313
|
+
case ast.Subscript():
|
|
314
|
+
return shapes.environment(value, known)
|
|
315
|
+
case ast.List() | ast.Set() | ast.Tuple() | ast.Dict():
|
|
316
|
+
return _container(value, known, declared)
|
|
317
|
+
case _:
|
|
318
|
+
return _computed(value, known, declared)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _from_call(value: ast.Call, known: Known, declared: Mapping[str, str]) -> Inference | None:
|
|
322
|
+
"""Infer a call by what it calls: a builtin, a library's function or class, or the project's own.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
The inference, or `None`.
|
|
326
|
+
|
|
327
|
+
"""
|
|
328
|
+
return (
|
|
329
|
+
decided.builtin(
|
|
330
|
+
value,
|
|
331
|
+
known,
|
|
332
|
+
lambda arg: inference(arg, known, declared),
|
|
333
|
+
lambda arg: looped(arg, known, declared, inference),
|
|
334
|
+
)
|
|
335
|
+
or shapes.cast(value, known.names.casts)
|
|
336
|
+
or opened(value, known)
|
|
337
|
+
or library_class(value, known)
|
|
338
|
+
or library_call(value, known, lambda arg: inference(arg, known, declared))
|
|
339
|
+
or installed_call(value, known, lambda arg: inference(arg, known, declared))
|
|
340
|
+
or _returns(value, known)
|
|
341
|
+
or shapes.class_of(value, known, lambda arg: inference(arg, known, declared))
|
|
342
|
+
or called.typed_call(value, known)
|
|
343
|
+
or called.result(value, known, declared, lambda arg: inference(arg, known, declared))
|
|
344
|
+
or shapes.partly(value, known, lambda arg: inference(arg, known, declared))
|
|
345
|
+
or shapes.attribute_of(value, known, lambda arg: inference(arg, known, declared))
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _returns(value: ast.expr, known: Known) -> Inference | None:
|
|
350
|
+
"""Infer a call to an unannotated function (the module's, or imported) whose `return`s decide its type.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
The inference, or `None`.
|
|
354
|
+
|
|
355
|
+
"""
|
|
356
|
+
func: ast.expr
|
|
357
|
+
callee: str | None
|
|
358
|
+
match value:
|
|
359
|
+
case ast.Call(func=ast.Name() | ast.Attribute() as func) if (
|
|
360
|
+
callee := dotted(func)
|
|
361
|
+
) in known.returned.calls and callee not in known.calls:
|
|
362
|
+
return Inference(
|
|
363
|
+
known.returned.calls[callee or ""],
|
|
364
|
+
f"`{callee}`'s `return`s",
|
|
365
|
+
frozenset({RETURNED}),
|
|
366
|
+
)
|
|
367
|
+
case _:
|
|
368
|
+
return None
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _computed(value: ast.expr, known: Known, declared: Mapping[str, str]) -> Inference | None:
|
|
372
|
+
"""Infer a value computed from others whose types are known.
|
|
373
|
+
|
|
374
|
+
`a if c else b` when both agree, or one is `None`; `a or b` and `a and b` of one type (see
|
|
375
|
+
`constricter.fix.values.shapes`); arithmetic on builtin scalars, and their comparisons (`_operated`);
|
|
376
|
+
a list, set or dict comprehension whose elements' type is known, its targets typed as a loop's;
|
|
377
|
+
`sorted`, `list`, `set`, `frozenset` or `tuple` of something whose elements are known; and
|
|
378
|
+
`await` of a call to one of the module's `async def`s.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
The inference, or `None`.
|
|
382
|
+
|
|
383
|
+
"""
|
|
384
|
+
name: str
|
|
385
|
+
first: ast.expr
|
|
386
|
+
keywords: list[ast.keyword]
|
|
387
|
+
match value:
|
|
388
|
+
case ast.IfExp() | ast.BoolOp():
|
|
389
|
+
return _joined(value, known, declared)
|
|
390
|
+
case ast.BinOp() | ast.Compare() | ast.UnaryOp(op=ast.USub() | ast.UAdd() | ast.Invert()):
|
|
391
|
+
return _operated(value, known, declared)
|
|
392
|
+
case ast.ListComp() | ast.SetComp() | ast.DictComp():
|
|
393
|
+
return _comprehension(value, known, declared)
|
|
394
|
+
case ast.Call(func=ast.Name(id=name), args=[first], keywords=keywords) if (
|
|
395
|
+
name in CONTAINER_BUILDERS
|
|
396
|
+
and known.is_builtin(name)
|
|
397
|
+
# `sorted`'s keywords only order what it gives.
|
|
398
|
+
and (not keywords or (name in SAME_ELEMENTS and iterator_call(value) is not None))
|
|
399
|
+
):
|
|
400
|
+
found: Inference | None = looped(first, known, declared, inference)
|
|
401
|
+
built: str = CONTAINER_BUILDERS[name]
|
|
402
|
+
return (
|
|
403
|
+
None
|
|
404
|
+
if found is None
|
|
405
|
+
else Inference(
|
|
406
|
+
built.format(found.annotation),
|
|
407
|
+
f"`{name}` of {found.reason}",
|
|
408
|
+
kinds_of(found, kind="builder"),
|
|
409
|
+
)
|
|
410
|
+
)
|
|
411
|
+
case _:
|
|
412
|
+
# What awaiting a checked file's or a standard-library coroutine's call gives, or nothing.
|
|
413
|
+
return awaited(value, known, lambda arg: inference(arg, known, declared))
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _operated(
|
|
417
|
+
value: ast.BinOp | ast.Compare | ast.UnaryOp,
|
|
418
|
+
known: Known,
|
|
419
|
+
declared: Mapping[str, str],
|
|
420
|
+
) -> Inference | None:
|
|
421
|
+
"""Infer an operator's result on builtin values: arithmetic, a comparison, or a number's sign.
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
The inference, or `None`.
|
|
425
|
+
|
|
426
|
+
"""
|
|
427
|
+
if isinstance(value, ast.BinOp):
|
|
428
|
+
return _arithmetic(value, known, declared)
|
|
429
|
+
if isinstance(value, ast.Compare):
|
|
430
|
+
return _compared(value, known, declared)
|
|
431
|
+
return decided.unary(value, lambda operand: inference(operand, known, declared))
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _joined(value: ast.IfExp | ast.BoolOp, known: Known, declared: Mapping[str, str]) -> Inference | None:
|
|
435
|
+
"""Infer `a if c else b`, and `a or b`: the one type their parts have (see `shapes`).
|
|
436
|
+
|
|
437
|
+
A conditional's both sides', or one side's and `None`.
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
The inference, or `None` if the parts' types differ, or one isn't known.
|
|
441
|
+
|
|
442
|
+
"""
|
|
443
|
+
if isinstance(value, ast.BoolOp):
|
|
444
|
+
return shapes.boolean(value, lambda part: inference(part, known, declared))
|
|
445
|
+
sides: tuple[Inference | None, Inference | None] = (
|
|
446
|
+
inference(value.body, known, declared),
|
|
447
|
+
inference(value.orelse, known, declared),
|
|
448
|
+
)
|
|
449
|
+
if sides[0] and sides[1] and sides[0].annotation == sides[1].annotation:
|
|
450
|
+
return Inference(
|
|
451
|
+
sides[0].annotation,
|
|
452
|
+
"both sides of a conditional",
|
|
453
|
+
kinds_of(*sides, kind="conditional"),
|
|
454
|
+
)
|
|
455
|
+
return shapes.optional(value, lambda part: inference(part, known, declared)) or shapes.emptied(
|
|
456
|
+
value,
|
|
457
|
+
lambda part: inference(part, known, declared),
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _arithmetic(value: ast.BinOp, known: Known, declared: Mapping[str, str]) -> Inference | None:
|
|
462
|
+
"""Infer arithmetic on builtin values, whose operators nothing can overload (see `operated`).
|
|
463
|
+
|
|
464
|
+
And a `pathlib` path's `/` with a `str` or another (`stdlib.joins_path`).
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
The inference, or `None` for any other operator or operand.
|
|
468
|
+
|
|
469
|
+
"""
|
|
470
|
+
sides: tuple[Inference | None, Inference | None] = (
|
|
471
|
+
inference(value.left, known, declared) or _keys(value.left, known, declared),
|
|
472
|
+
inference(value.right, known, declared) or _keys(value.right, known, declared),
|
|
473
|
+
)
|
|
474
|
+
left: str | None = None if sides[0] is None else sides[0].annotation
|
|
475
|
+
right: str | None = None if sides[1] is None else sides[1].annotation
|
|
476
|
+
kinds: frozenset[str] = kinds_of(*sides, kind="arithmetic")
|
|
477
|
+
if isinstance(value.op, ast.Div) and left is not None and stdlib.joins_path(left, right, known):
|
|
478
|
+
return Inference(left, "a path joined by `/`", kinds)
|
|
479
|
+
text: str | None = operated.operated(value, left, right, known.limits.max_length)
|
|
480
|
+
builtin: str = "arithmetic on builtin types"
|
|
481
|
+
return library_operator(value.op, sides, known) if text is None else Inference(text, builtin, kinds)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _keys(value: ast.expr, known: Known, declared: Mapping[str, str]) -> Inference | None:
|
|
485
|
+
"""Type a `dict`'s `.keys()` as an operand: its operators give a `set` of its keys, as a `set`'s do.
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
The `set[K]`, or `None` if `value` isn't such a call.
|
|
489
|
+
|
|
490
|
+
"""
|
|
491
|
+
receiver: ast.expr
|
|
492
|
+
match value:
|
|
493
|
+
case ast.Call(func=ast.Attribute(value=receiver, attr="keys"), args=[], keywords=[]):
|
|
494
|
+
found: Inference | None = dict_view(receiver, "keys", known, declared, inference)
|
|
495
|
+
return None if found is None else found._replace(annotation=f"set[{found.annotation}]")
|
|
496
|
+
case _:
|
|
497
|
+
return None
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _compared(value: ast.Compare, known: Known, declared: Mapping[str, str]) -> Inference | None:
|
|
501
|
+
"""Infer a comparison of builtin values: `n < 3`, `len(xs) == 0`, `name != "x"`.
|
|
502
|
+
|
|
503
|
+
Each builtin's own comparison gives a real `bool`, or declines for the other side's to (and one
|
|
504
|
+
both decline is `is`/`is not`, a `bool` too); a class's may give anything (numpy's arrays).
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
`bool`, if every operand's type (each member of a union's) is a builtin class; else `None`.
|
|
508
|
+
|
|
509
|
+
"""
|
|
510
|
+
parts: list[Inference | None] = [
|
|
511
|
+
inference(part, known, declared) for part in (value.left, *value.comparators)
|
|
512
|
+
]
|
|
513
|
+
builtin: bool = all(
|
|
514
|
+
part is not None
|
|
515
|
+
and all(
|
|
516
|
+
_root(member) in _COMPARABLE and known.is_builtin(_root(member))
|
|
517
|
+
for member in members(part.annotation) or ()
|
|
518
|
+
)
|
|
519
|
+
for part in parts
|
|
520
|
+
)
|
|
521
|
+
return (
|
|
522
|
+
Inference("bool", "a comparison of builtin values", kinds_of(*parts, kind="compare"))
|
|
523
|
+
if builtin
|
|
524
|
+
else None
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _root(annotation: str) -> str:
|
|
529
|
+
"""Name an annotation's outer type (`list` for `list[int]`).
|
|
530
|
+
|
|
531
|
+
Returns:
|
|
532
|
+
Its text before any `[`.
|
|
533
|
+
|
|
534
|
+
"""
|
|
535
|
+
return annotation.partition("[")[0]
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _comprehension(
|
|
539
|
+
value: ast.ListComp | ast.SetComp | ast.DictComp,
|
|
540
|
+
known: Known,
|
|
541
|
+
declared: Mapping[str, str],
|
|
542
|
+
) -> Inference | None:
|
|
543
|
+
"""Infer a comprehension's type from its elements', its targets typed as a loop's are.
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
The inference, or `None` if an element's type isn't known.
|
|
547
|
+
|
|
548
|
+
"""
|
|
549
|
+
inside: Mapping[str, str] = comprehended(value, known, declared, inference)
|
|
550
|
+
reason: str = "a comprehension's elements"
|
|
551
|
+
# What the targets iterate over decided them too.
|
|
552
|
+
loops: list[Inference | None] = [looped(g.iter, known, inside, inference) for g in value.generators]
|
|
553
|
+
match value:
|
|
554
|
+
case ast.DictComp():
|
|
555
|
+
key: Inference | None = inference(value.key, known, inside)
|
|
556
|
+
key = key and shapes.sifted(value, value.key, key)
|
|
557
|
+
item: Inference | None = inference(value.value, known, inside)
|
|
558
|
+
item = item and shapes.sifted(value, value.value, item)
|
|
559
|
+
return (
|
|
560
|
+
Inference(
|
|
561
|
+
f"dict[{key.annotation}, {item.annotation}]",
|
|
562
|
+
reason,
|
|
563
|
+
kinds_of(key, item, *loops, kind="comprehension"),
|
|
564
|
+
)
|
|
565
|
+
if key and item
|
|
566
|
+
else None
|
|
567
|
+
)
|
|
568
|
+
case _:
|
|
569
|
+
element: Inference | None = inference(value.elt, known, inside)
|
|
570
|
+
element = element and shapes.sifted(value, value.elt, element)
|
|
571
|
+
kind: str = "list" if isinstance(value, ast.ListComp) else "set"
|
|
572
|
+
return (
|
|
573
|
+
Inference(
|
|
574
|
+
f"{kind}[{element.annotation}]",
|
|
575
|
+
reason,
|
|
576
|
+
kinds_of(element, *loops, kind="comprehension"),
|
|
577
|
+
)
|
|
578
|
+
if element
|
|
579
|
+
else None
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def scalar(value: ast.expr) -> str | None:
|
|
584
|
+
"""Type a value that is what it is whatever its parts are: a literal, an f-string, `not x`, `x is y`.
|
|
585
|
+
|
|
586
|
+
Returns:
|
|
587
|
+
Its type, or `None` for any other value.
|
|
588
|
+
|
|
589
|
+
"""
|
|
590
|
+
constant: str | bytes | bool | int | float | complex | EllipsisType | None
|
|
591
|
+
ops: list[ast.cmpop]
|
|
592
|
+
match value:
|
|
593
|
+
case ast.Constant(value=bool() | int() | float() | complex() | str() | bytes() as constant):
|
|
594
|
+
return type(constant).__name__
|
|
595
|
+
case ast.UnaryOp(op=ast.USub() | ast.UAdd(), operand=ast.Constant(value=constant)) if isinstance(
|
|
596
|
+
constant,
|
|
597
|
+
_NUMBERS,
|
|
598
|
+
) and not isinstance(constant, bool):
|
|
599
|
+
return type(constant).__name__
|
|
600
|
+
case ast.UnaryOp(op=ast.Not()): # `not x` always yields a real `bool`, unlike a comparison
|
|
601
|
+
return "bool"
|
|
602
|
+
case ast.Compare(ops=ops) if all(isinstance(op, _BOOLEAN_TESTS) for op in ops):
|
|
603
|
+
return "bool"
|
|
604
|
+
case ast.JoinedStr():
|
|
605
|
+
return "str"
|
|
606
|
+
case _:
|
|
607
|
+
return None
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def _container(value: ast.expr, known: Known, declared: Mapping[str, str]) -> Inference | None:
|
|
611
|
+
"""Infer a list, set, tuple or dict display whose elements' types agree.
|
|
612
|
+
|
|
613
|
+
A starred element (`[*names, s]`) gives each of what it unpacks, and `**d` a `dict`'s keys and
|
|
614
|
+
values; a tuple that unpacks one, whose length then isn't known, is a `tuple[T, ...]`.
|
|
615
|
+
|
|
616
|
+
Returns:
|
|
617
|
+
The inference, or `None`.
|
|
618
|
+
|
|
619
|
+
"""
|
|
620
|
+
elements: list[ast.expr]
|
|
621
|
+
keys: list[ast.expr | None]
|
|
622
|
+
values: list[ast.expr]
|
|
623
|
+
found: str | None = None
|
|
624
|
+
parts: list[Inference | None] = []
|
|
625
|
+
mixed: bool = False # whether the elements' types differ, and are joined
|
|
626
|
+
match value:
|
|
627
|
+
case ast.List(elts=elements) | ast.Set(elts=elements) if elements:
|
|
628
|
+
parts = [_element(element, known, declared) for element in elements]
|
|
629
|
+
element: str | None
|
|
630
|
+
element, mixed = displays.element_type(elements, parts)
|
|
631
|
+
found = f"{'list' if isinstance(value, ast.List) else 'set'}[{element}]" if element else None
|
|
632
|
+
case ast.Tuple(elts=elements) if elements:
|
|
633
|
+
parts = [_element(element, known, declared) for element in elements]
|
|
634
|
+
starred: bool = any(isinstance(element, ast.Starred) for element in elements)
|
|
635
|
+
found = displays.tuple_type(parts, -1 if starred else known.limits.max_length)
|
|
636
|
+
case ast.Dict(keys=keys, values=values) if keys:
|
|
637
|
+
pairs: list[_Pair] = [
|
|
638
|
+
_pair(key, item, known, declared) for key, item in zip(keys, values, strict=True)
|
|
639
|
+
]
|
|
640
|
+
parts = [part for pair in pairs for part in pair]
|
|
641
|
+
sides: list[tuple[str | None, bool]] = [
|
|
642
|
+
displays.element_type(keys, [pair[0] for pair in pairs]),
|
|
643
|
+
displays.element_type(values, [pair[1] for pair in pairs]),
|
|
644
|
+
]
|
|
645
|
+
found = f"dict[{sides[0][0]}, {sides[1][0]}]" if sides[0][0] and sides[1][0] else None
|
|
646
|
+
mixed = sides[0][1] or sides[1][1]
|
|
647
|
+
case _:
|
|
648
|
+
pass
|
|
649
|
+
if found is None:
|
|
650
|
+
return None
|
|
651
|
+
kinds: frozenset[str] = kinds_of(*parts, kind="container")
|
|
652
|
+
built: str = found.partition("[")[0]
|
|
653
|
+
if mixed:
|
|
654
|
+
return Inference(found, f"a {built} of its elements' types, joined", kinds | {displays.JOINED})
|
|
655
|
+
return Inference(found, f"a {built} whose elements' types agree", kinds)
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def _element(element: ast.expr, known: Known, declared: Mapping[str, str]) -> Inference | None:
|
|
659
|
+
"""Type one element of a display: itself, or each of those a starred one unpacks.
|
|
660
|
+
|
|
661
|
+
Returns:
|
|
662
|
+
The inference, or `None`.
|
|
663
|
+
|
|
664
|
+
"""
|
|
665
|
+
if isinstance(element, ast.Starred):
|
|
666
|
+
return looped(element.value, known, declared, inference)
|
|
667
|
+
return inference(element, known, declared)
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def _pair(key: ast.expr | None, item: ast.expr, known: Known, declared: Mapping[str, str]) -> _Pair:
|
|
671
|
+
"""Type one entry of a `dict` display: its key and value, or (`**item`) those of a `dict` it unpacks.
|
|
672
|
+
|
|
673
|
+
Returns:
|
|
674
|
+
The key's inference and the value's, each `None` if it isn't known.
|
|
675
|
+
|
|
676
|
+
"""
|
|
677
|
+
if key is not None:
|
|
678
|
+
return inference(key, known, declared), inference(item, known, declared)
|
|
679
|
+
found: Inference | None = inference(item, known, declared)
|
|
680
|
+
types: tuple[str, str] | None = None if found is None else dict_parts(found.annotation)
|
|
681
|
+
if found is None or types is None:
|
|
682
|
+
return None, None
|
|
683
|
+
return found._replace(annotation=types[0]), found._replace(annotation=types[1])
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def joined(value: ast.AST, known: Known, declared: Mapping[str, str]) -> bool:
|
|
687
|
+
"""Check whether `value` is a display typed by its elements' joined types: a guess.
|
|
688
|
+
|
|
689
|
+
A checker joins them too, but not always to the same union (mypy to their common base).
|
|
690
|
+
|
|
691
|
+
Returns:
|
|
692
|
+
Whether it is.
|
|
693
|
+
|
|
694
|
+
"""
|
|
695
|
+
if not isinstance(value, ast.List | ast.Set | ast.Dict):
|
|
696
|
+
return False
|
|
697
|
+
found: Inference | None = _container(value, known, declared)
|
|
698
|
+
return found is not None and displays.JOINED in found.kinds
|
|
Binary file
|