python-constricter 0.4.1__cp311-cp311-win_amd64.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__.cp311-win_amd64.pyd +0 -0
- constricter/fix/__init__.py +2 -0
- constricter/fix/core/__init__.cp311-win_amd64.pyd +0 -0
- constricter/fix/core/__init__.py +2 -0
- constricter/fix/core/asked.cp311-win_amd64.pyd +0 -0
- constricter/fix/core/asked.py +56 -0
- constricter/fix/core/fixes.cp311-win_amd64.pyd +0 -0
- constricter/fix/core/fixes.py +317 -0
- constricter/fix/core/imports.cp311-win_amd64.pyd +0 -0
- constricter/fix/core/imports.py +580 -0
- constricter/fix/core/inherited.cp311-win_amd64.pyd +0 -0
- constricter/fix/core/inherited.py +311 -0
- constricter/fix/core/known.cp311-win_amd64.pyd +0 -0
- constricter/fix/core/known.py +730 -0
- constricter/fix/core/signatures.cp311-win_amd64.pyd +0 -0
- constricter/fix/core/signatures.py +132 -0
- constricter/fix/index/__init__.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/__init__.py +2 -0
- constricter/fix/index/awaits.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/awaits.py +90 -0
- constricter/fix/index/beyond.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/beyond.py +149 -0
- constricter/fix/index/callers.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/callers.py +155 -0
- constricter/fix/index/crossed.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/crossed.py +327 -0
- constricter/fix/index/decorated.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/decorated.py +140 -0
- constricter/fix/index/fixtures.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/fixtures.py +176 -0
- constricter/fix/index/linked.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/linked.py +142 -0
- constricter/fix/index/loose.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/loose.py +103 -0
- constricter/fix/index/managed.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/managed.py +63 -0
- constricter/fix/index/modules.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/modules.py +741 -0
- constricter/fix/index/offers.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/offers.py +252 -0
- constricter/fix/index/order.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/order.py +155 -0
- constricter/fix/index/origins.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/origins.py +238 -0
- constricter/fix/index/own_methods.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/own_methods.py +91 -0
- constricter/fix/index/own_overloads.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/own_overloads.py +70 -0
- constricter/fix/index/own_types.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/own_types.py +330 -0
- constricter/fix/index/params.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/params.py +81 -0
- constricter/fix/index/plain.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/plain.py +221 -0
- constricter/fix/index/project.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/project.py +797 -0
- constricter/fix/index/sides.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/sides.py +160 -0
- constricter/fix/index/strings.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/strings.py +46 -0
- constricter/fix/index/stubs/__init__.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/__init__.py +2 -0
- constricter/fix/index/stubs/atoms.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/atoms.py +425 -0
- constricter/fix/index/stubs/chained.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/chained.py +69 -0
- constricter/fix/index/stubs/classnames.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/classnames.py +124 -0
- constricter/fix/index/stubs/declared.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/declared.py +508 -0
- constricter/fix/index/stubs/installed.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/installed.py +425 -0
- constricter/fix/index/stubs/names.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/names.py +200 -0
- constricter/fix/index/stubs/read.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/read.py +70 -0
- constricter/fix/index/stubs/signatures.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/signatures.py +656 -0
- constricter/fix/index/stubs/stubbed.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/stubbed.py +208 -0
- constricter/fix/index/taken.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/taken.py +45 -0
- constricter/fix/index/tuples.cp311-win_amd64.pyd +0 -0
- constricter/fix/index/tuples.py +112 -0
- constricter/fix/libraries/__init__.cp311-win_amd64.pyd +0 -0
- constricter/fix/libraries/__init__.py +2 -0
- constricter/fix/libraries/library.cp311-win_amd64.pyd +0 -0
- constricter/fix/libraries/library.py +420 -0
- constricter/fix/libraries/opened.cp311-win_amd64.pyd +0 -0
- constricter/fix/libraries/opened.py +147 -0
- constricter/fix/libraries/overloads.cp311-win_amd64.pyd +0 -0
- constricter/fix/libraries/overloads.py +960 -0
- constricter/fix/libraries/receivers.cp311-win_amd64.pyd +0 -0
- constricter/fix/libraries/receivers.py +274 -0
- constricter/fix/libraries/stdlib.cp311-win_amd64.pyd +0 -0
- constricter/fix/libraries/stdlib.py +752 -0
- constricter/fix/libraries/walks.cp311-win_amd64.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__.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/__init__.py +2 -0
- constricter/fix/values/aliased.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/aliased.py +195 -0
- constricter/fix/values/bodies.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/bodies.py +76 -0
- constricter/fix/values/callables.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/callables.py +323 -0
- constricter/fix/values/called.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/called.py +194 -0
- constricter/fix/values/classvars.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/classvars.py +629 -0
- constricter/fix/values/decided.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/decided.py +429 -0
- constricter/fix/values/displays.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/displays.py +101 -0
- constricter/fix/values/doubts.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/doubts.py +677 -0
- constricter/fix/values/entered.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/entered.py +332 -0
- constricter/fix/values/fills.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/fills.py +446 -0
- constricter/fix/values/guesses.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/guesses.py +376 -0
- constricter/fix/values/hinted.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/hinted.py +811 -0
- constricter/fix/values/inference.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/inference.py +698 -0
- constricter/fix/values/loops.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/loops.py +335 -0
- constricter/fix/values/members.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/members.py +578 -0
- constricter/fix/values/narrowed.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/narrowed.py +220 -0
- constricter/fix/values/operated.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/operated.py +202 -0
- constricter/fix/values/ordered.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/ordered.py +74 -0
- constricter/fix/values/returned.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/returned.py +479 -0
- constricter/fix/values/returns.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/returns.py +237 -0
- constricter/fix/values/shapes.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/shapes.py +654 -0
- constricter/fix/values/stored.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/stored.py +564 -0
- constricter/fix/values/targets.cp311-win_amd64.pyd +0 -0
- constricter/fix/values/targets.py +419 -0
- constricter/fix/values/unchecked.cp311-win_amd64.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__.cp311-win_amd64.pyd +0 -0
- constricter/rules/__init__.py +2 -0
- constricter/rules/annotations.cp311-win_amd64.pyd +0 -0
- constricter/rules/annotations.py +296 -0
- constricter/rules/binding.cp311-win_amd64.pyd +0 -0
- constricter/rules/binding.py +652 -0
- constricter/rules/calls.cp311-win_amd64.pyd +0 -0
- constricter/rules/calls.py +460 -0
- constricter/rules/checker.cp311-win_amd64.pyd +0 -0
- constricter/rules/checker.py +536 -0
- constricter/rules/declarations.cp311-win_amd64.pyd +0 -0
- constricter/rules/declarations.py +730 -0
- constricter/rules/decorators.cp311-win_amd64.pyd +0 -0
- constricter/rules/decorators.py +351 -0
- constricter/rules/flow.cp311-win_amd64.pyd +0 -0
- constricter/rules/flow.py +502 -0
- constricter/rules/keys.cp311-win_amd64.pyd +0 -0
- constricter/rules/keys.py +79 -0
- constricter/rules/late.cp311-win_amd64.pyd +0 -0
- constricter/rules/late.py +969 -0
- constricter/rules/narrowing.cp311-win_amd64.pyd +0 -0
- constricter/rules/narrowing.py +98 -0
- constricter/rules/parsed.cp311-win_amd64.pyd +0 -0
- constricter/rules/parsed.py +136 -0
- constricter/rules/quoted.cp311-win_amd64.pyd +0 -0
- constricter/rules/quoted.py +141 -0
- constricter/rules/rebinding.cp311-win_amd64.pyd +0 -0
- constricter/rules/rebinding.py +169 -0
- constricter/rules/recorded.cp311-win_amd64.pyd +0 -0
- constricter/rules/recorded.py +168 -0
- constricter/rules/redundant.cp311-win_amd64.pyd +0 -0
- constricter/rules/redundant.py +160 -0
- constricter/rules/scope.cp311-win_amd64.pyd +0 -0
- constricter/rules/scope.py +802 -0
- constricter/rules/scoped.cp311-win_amd64.pyd +0 -0
- constricter/rules/scoped.py +232 -0
- constricter/rules/scoping.cp311-win_amd64.pyd +0 -0
- constricter/rules/scoping.py +346 -0
- constricter/rules/settings.cp311-win_amd64.pyd +0 -0
- constricter/rules/settings.py +189 -0
- constricter/rules/syntax.cp311-win_amd64.pyd +0 -0
- constricter/rules/syntax.py +472 -0
- constricter/rules/tables.cp311-win_amd64.pyd +0 -0
- constricter/rules/tables.py +169 -0
- constricter/rules/walked.cp311-win_amd64.pyd +0 -0
- constricter/rules/walked.py +127 -0
- constricter/rules/widened.cp311-win_amd64.pyd +0 -0
- constricter/rules/widened.py +322 -0
- constricter/trace.py +374 -0
- e45ce06b2fdd5c43f782__mypyc.cp311-win_amd64.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,960 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Standard-library calls whose arguments decide their type: the signatures a call may match.
|
|
3
|
+
|
|
4
|
+
`tables/overloads.json` holds each such function's signatures, in order, as each platform and Python
|
|
5
|
+
version reads them (see `stdlib_tables/overloads.py`): every parameter's kind, whether it has a
|
|
6
|
+
default, and which argument types (`SCALARS`) it certainly takes or refuses; and the return, as a
|
|
7
|
+
template naming classes by their dotted paths and type variables by their names.
|
|
8
|
+
|
|
9
|
+
A type checker picks the first overload whose parameters take a call's arguments. `--fix` types the
|
|
10
|
+
call only when that's certain enough: every signature that may be the one (not certainly refusing
|
|
11
|
+
them, up to the first that certainly takes them; an argument whose type isn't known, or a verdict
|
|
12
|
+
that depends on more than its type, leaves one open) gives the same type, in every variant. A call
|
|
13
|
+
none takes is left to the type checker, which rejects it anyway.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import ast
|
|
17
|
+
import builtins
|
|
18
|
+
from collections.abc import Callable, Iterator, Mapping, Sequence
|
|
19
|
+
from functools import lru_cache
|
|
20
|
+
from typing import Final, NamedTuple, TypeAlias, cast
|
|
21
|
+
|
|
22
|
+
from constricter.fix.core.known import SPELLED, Inference, Known
|
|
23
|
+
from constricter.fix.core.signatures import (
|
|
24
|
+
AWAIT,
|
|
25
|
+
CLASS_BINDS,
|
|
26
|
+
CLASS_BOUNDED,
|
|
27
|
+
CLASS_TUPLE,
|
|
28
|
+
CLASS_VERDICT,
|
|
29
|
+
CONTAINER_BINDS,
|
|
30
|
+
CONTAINER_VERDICTS,
|
|
31
|
+
ELEMENT_VERDICTS,
|
|
32
|
+
FUTURE_BINDS,
|
|
33
|
+
OWN,
|
|
34
|
+
OWN_ELEMENTS,
|
|
35
|
+
Accepts,
|
|
36
|
+
Constant,
|
|
37
|
+
Parameter,
|
|
38
|
+
ReadSignature,
|
|
39
|
+
Signature,
|
|
40
|
+
Variant,
|
|
41
|
+
)
|
|
42
|
+
from constricter.fix.libraries import stdlib
|
|
43
|
+
from constricter.fix.libraries.receivers import (
|
|
44
|
+
MAYBE,
|
|
45
|
+
NO,
|
|
46
|
+
SELF_TYPE,
|
|
47
|
+
UNFOLLOWED,
|
|
48
|
+
YES,
|
|
49
|
+
Receiver,
|
|
50
|
+
both,
|
|
51
|
+
matches,
|
|
52
|
+
parsed,
|
|
53
|
+
receiver_of,
|
|
54
|
+
)
|
|
55
|
+
from constricter.rules.walked import walk
|
|
56
|
+
|
|
57
|
+
SCALARS: Final = ("str", "LiteralString", "bytes", "bytearray", "int", "float", "complex", "bool", "None")
|
|
58
|
+
_COLUMNS: Final = {scalar: index for index, scalar in enumerate(SCALARS)}
|
|
59
|
+
_LITERAL_STRING: Final = "LiteralString" # the type of a `str` literal
|
|
60
|
+
_NONE: Final = "None"
|
|
61
|
+
_POSITIONAL: Final = frozenset({"p", "e"})
|
|
62
|
+
_KEYWORD: Final = frozenset({"e", "k"})
|
|
63
|
+
_STAR: Final = "a"
|
|
64
|
+
_STARS: Final = "w"
|
|
65
|
+
_BUILTINS: Final = frozenset({*dir(builtins), _NONE})
|
|
66
|
+
_KIND: Final = "stdlib"
|
|
67
|
+
# The builtin numbers a type checker takes where each of these is declared.
|
|
68
|
+
_PROMOTED: Final = {
|
|
69
|
+
"int": ("bool",),
|
|
70
|
+
"float": ("int", "bool"),
|
|
71
|
+
"complex": ("float", "int", "bool"),
|
|
72
|
+
}
|
|
73
|
+
_GENERATOR: Final = "collections.abc.Generator"
|
|
74
|
+
_ITERATOR: Final = "collections.abc.Iterator"
|
|
75
|
+
_TUPLE: Final = "tuple"
|
|
76
|
+
_ANYTHING: Final = "t" # `Accepts`' key for a parameter any argument binds
|
|
77
|
+
_RETURNED: Final = "r" # `Accepts`' key for a callable parameter a function's return binds
|
|
78
|
+
_Taken: TypeAlias = tuple[str, dict[str, str]] # whether an operand is taken, and what it binds
|
|
79
|
+
_NESTED: Final = "ei" # `Accepts`' key for an iterable of iterables' inner containers
|
|
80
|
+
_AWAITED: Final = "w" # and for an awaitable one, which what awaiting a coroutine's call gives binds
|
|
81
|
+
_CALL: Final = "call" # the fix kind of a declared return
|
|
82
|
+
# The builtin containers whose type arguments bind a parameter's type variable (`Iterable[_T]`'s).
|
|
83
|
+
CONTAINERS: Final = ("list", _TUPLE, "set", "frozenset", "dict") # in the `scalars` table's order
|
|
84
|
+
_CONTAINERS: Final = frozenset(CONTAINERS)
|
|
85
|
+
_SELF: Final = "self" # a signature's key for the type arguments its method's instance must have
|
|
86
|
+
# The builtin classes, each a class as an argument (`assertRaises(ValueError)`).
|
|
87
|
+
_BUILTIN_CLASSES: Final = frozenset(
|
|
88
|
+
name for name in dir(builtins) if isinstance(cast("object", getattr(builtins, name)), type)
|
|
89
|
+
)
|
|
90
|
+
_SEQUENCES: Final = frozenset({"list", "tuple", "set", "frozenset"}) # whose elements `own_e` is asked of
|
|
91
|
+
_TYPE_OF: Final = "type[" # the annotation of a class as a value
|
|
92
|
+
_Infer: TypeAlias = Callable[[ast.expr], Inference | None]
|
|
93
|
+
# A signature that may be the one: its return template and its type variables' types (`None`:
|
|
94
|
+
# a return `--fix` can't write).
|
|
95
|
+
_Picked: TypeAlias = tuple[str, dict[str, str]] | None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class Argument(NamedTuple):
|
|
99
|
+
"""One argument of a call: its type among `SCALARS` (`None` if it's none of them, or unknown).
|
|
100
|
+
|
|
101
|
+
`constant`: the literal it is, if it is one (in a 1-tuple, as its value may be `None`);
|
|
102
|
+
`found`: what `--fix` inferred it to be, whose kinds the call's type rests on too; `elements`:
|
|
103
|
+
for a builtin container (`list[str]`), its name and type arguments (`tuple[str, ...]`'s `str`),
|
|
104
|
+
and `inner`, the same of its first one (a `list[list[int]]`'s `list[int]`);
|
|
105
|
+
`reads`: the names, attributes and subscripts in it, as source text; `returns`: for a function
|
|
106
|
+
the module knows the declared return of (`helper`, `u.helper`), that return; `klass`: for a class
|
|
107
|
+
passed as it is (`np.float64`), its name as written; `awaited`: for a coroutine's call, what
|
|
108
|
+
awaiting it gives.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
type: str | None
|
|
112
|
+
constant: tuple[Constant] | None = None
|
|
113
|
+
found: Inference | None = None
|
|
114
|
+
elements: tuple[str, tuple[str, ...]] | None = None
|
|
115
|
+
inner: tuple[str, tuple[str, ...]] | None = None # its first type argument's, a container too
|
|
116
|
+
reads: tuple[str, ...] = ()
|
|
117
|
+
returns: Inference | None = None
|
|
118
|
+
klass: str | None = None
|
|
119
|
+
awaited: Inference | None = None
|
|
120
|
+
own: tuple[str, ...] = () # its class's lineage, for a class the module names (`LibraryNames.lineage`)
|
|
121
|
+
own_elements: tuple[str, ...] = () # a builtin sequence's or set's elements' class's
|
|
122
|
+
# Whether `klass` is a library's class (a standard-library one, or what a value typed `type[C]`
|
|
123
|
+
# is), which may be outside a `type[T]`'s bound as a builtin one may, and is a class only to a
|
|
124
|
+
# parameter that speaks of classes: to another, it's the value `found` says.
|
|
125
|
+
library: bool = False
|
|
126
|
+
several: bool = False # whether `klass` is the union of a tuple of classes (`(OSError, ValueError)`)
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def text(self) -> str | None:
|
|
130
|
+
"""Its type as the module writes it, or `None` if unknown (a literal's is its `type`)."""
|
|
131
|
+
return None if self.found is None else self.found.annotation
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class Arguments(NamedTuple):
|
|
135
|
+
"""A call's arguments: positional, then by keyword."""
|
|
136
|
+
|
|
137
|
+
args: list[Argument]
|
|
138
|
+
keywords: dict[str, Argument]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def chosen(
|
|
142
|
+
name: str,
|
|
143
|
+
call: ast.Call,
|
|
144
|
+
known: Known,
|
|
145
|
+
infer: _Infer,
|
|
146
|
+
method: stdlib.Method | None = None,
|
|
147
|
+
) -> Inference | None:
|
|
148
|
+
"""Type a call to standard-library function `name` by the signatures its arguments may match.
|
|
149
|
+
|
|
150
|
+
`name` is an entry of `OVERLOADS` (of `AWAITED_OVERLOADS`, after `AWAIT`: an `async def`'s call,
|
|
151
|
+
awaited), or `method_signatures` for a `method`'s call on an instance
|
|
152
|
+
(whose type binds its class's type parameters); or an installed package's function, as the
|
|
153
|
+
module calls it (`np.empty`: see `LibraryNames.installed`). `infer` types an argument.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
The inference, its classes spelled (and imported, if they must be) as the module can; or
|
|
157
|
+
`None` if they don't all agree, or its type can't be written.
|
|
158
|
+
|
|
159
|
+
"""
|
|
160
|
+
read: Arguments | None
|
|
161
|
+
if (read := _arguments(call, infer, known)) is None:
|
|
162
|
+
return None
|
|
163
|
+
installed: tuple[ReadSignature, ...] | None = known.names.installed.get(name)
|
|
164
|
+
variants: tuple[tuple[ReadSignature, ...], ...] = _signatures(name) if installed is None else (installed,)
|
|
165
|
+
instance: list[str] | None = None if method is None else method.instance
|
|
166
|
+
picks: list[_Picked] = [
|
|
167
|
+
_receiving(picked, method)
|
|
168
|
+
for variant in variants
|
|
169
|
+
for picked in _picked(
|
|
170
|
+
variant,
|
|
171
|
+
read,
|
|
172
|
+
instance,
|
|
173
|
+
None if method is None else receiver_of(method, known),
|
|
174
|
+
)
|
|
175
|
+
]
|
|
176
|
+
found: set[str | None] = {None if picked is None else write(picked, known) for picked in picks}
|
|
177
|
+
if len(found) > 1 and None not in found:
|
|
178
|
+
# Where Python versions differ only in which iterator they declare: the one all of them are.
|
|
179
|
+
found = {None if picked is None else write(_iterator(picked), known) for picked in picks}
|
|
180
|
+
annotation: str | None
|
|
181
|
+
if (annotation := found.pop() if len(found) == 1 else None) is None: # none, or several that disagree
|
|
182
|
+
return None
|
|
183
|
+
parts: list[Argument] = [*read.args, *read.keywords.values()]
|
|
184
|
+
return Inference(
|
|
185
|
+
annotation,
|
|
186
|
+
f"`{name}`'s return type, for its arguments",
|
|
187
|
+
frozenset({_KIND if installed is None else _CALL}).union(
|
|
188
|
+
*(part.found.kinds for part in parts if part.found is not None),
|
|
189
|
+
*(part.returns.kinds for part in parts if part.returns is not None),
|
|
190
|
+
),
|
|
191
|
+
# What a type variable may take as its type: an argument of any type but a scalar's.
|
|
192
|
+
tuple(text for part in parts if part.type is None for text in part.reads),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _iterator(picked: tuple[str, dict[str, str]]) -> tuple[str, dict[str, str]]:
|
|
197
|
+
"""Write a pick whose template is a `Generator[T, ...]` as the `Iterator[T]` every generator is.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
It; any other pick as it is.
|
|
201
|
+
|
|
202
|
+
"""
|
|
203
|
+
head: ast.expr
|
|
204
|
+
index: ast.expr
|
|
205
|
+
match ast.parse(picked[0].removeprefix(SPELLED), mode="eval").body:
|
|
206
|
+
case ast.Subscript(value=ast.Attribute() as head, slice=index) if ast.unparse(head) == _GENERATOR:
|
|
207
|
+
first: ast.expr = index.elts[0] if isinstance(index, ast.Tuple) else index
|
|
208
|
+
return f"{_ITERATOR}[{ast.unparse(first)}]", picked[1]
|
|
209
|
+
case _:
|
|
210
|
+
return picked
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def generic_member(receiver: str, name: str, call: ast.Call | None, known: Known) -> Inference | None:
|
|
214
|
+
"""Type a generic standard-library class's own attribute or property, bound by the receiver's type.
|
|
215
|
+
|
|
216
|
+
`m.string` on an `re.Match[str]` is a `str`; a method (`call`) is `method_signatures`' to type.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
The inference, or `None` if the tables don't have it, or a type parameter it names is unbound.
|
|
220
|
+
|
|
221
|
+
"""
|
|
222
|
+
found: tuple[str, str, dict[str, str]] | None = (
|
|
223
|
+
stdlib.generic_attribute(receiver, name, known) if call is None else None
|
|
224
|
+
)
|
|
225
|
+
if found is None:
|
|
226
|
+
return None
|
|
227
|
+
annotation: str | None = write((found[1], found[2]), known)
|
|
228
|
+
return (
|
|
229
|
+
None
|
|
230
|
+
if annotation is None
|
|
231
|
+
else Inference(annotation, f"`{found[0]}.{name}`'s annotation in typeshed", frozenset({_KIND}))
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def operated(
|
|
236
|
+
method: stdlib.Method,
|
|
237
|
+
operands: tuple[str, str],
|
|
238
|
+
known: Known,
|
|
239
|
+
arguments: tuple[str, ...] | None = None,
|
|
240
|
+
*,
|
|
241
|
+
same: bool = False,
|
|
242
|
+
) -> Inference | None:
|
|
243
|
+
"""Type a binary operator by its left operand's `method` (`__sub__`), for its right operand's type.
|
|
244
|
+
|
|
245
|
+
`operands`: both as `stdlib.operand` names them, a generic class's instance by its class. The
|
|
246
|
+
first signature whose operand takes the right one answers, as a type checker picks it: the same
|
|
247
|
+
type, a class under it, or a number promoted to it (an `int` where a `float` is taken).
|
|
248
|
+
`arguments`: the right operand's type arguments, where it's a generic class's instance, which
|
|
249
|
+
bind the operand's own type variables (`Counter.__add__` takes a `Counter[_S]`); `same`: whether
|
|
250
|
+
it's the left one's own type, as an operand of `Self` must be.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
The inference, or `None`: no signature certainly takes it, one before that may, the Python
|
|
254
|
+
versions differ, or the right operand's class is under the left's (its reflected method may
|
|
255
|
+
answer first).
|
|
256
|
+
|
|
257
|
+
"""
|
|
258
|
+
left: str
|
|
259
|
+
right: str
|
|
260
|
+
left, right = operands
|
|
261
|
+
if left != right and stdlib.inherits(right, left):
|
|
262
|
+
return None
|
|
263
|
+
found: set[str | None] = {
|
|
264
|
+
_answered(
|
|
265
|
+
variant,
|
|
266
|
+
method,
|
|
267
|
+
known,
|
|
268
|
+
lambda member: _taken(member, operands, arguments, method.types, same=same),
|
|
269
|
+
)
|
|
270
|
+
for variant in stdlib.method_signatures()[method.entry]
|
|
271
|
+
}
|
|
272
|
+
annotation: str | None
|
|
273
|
+
if (annotation := found.pop() if len(found) == 1 else None) is None:
|
|
274
|
+
return None
|
|
275
|
+
return Inference(annotation, f"`{method.entry}`'s return type, for its operand", frozenset({_KIND}))
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _answered(
|
|
279
|
+
variant: Variant,
|
|
280
|
+
method: stdlib.Method,
|
|
281
|
+
known: Known,
|
|
282
|
+
taking: Callable[[str], _Taken],
|
|
283
|
+
) -> str | None:
|
|
284
|
+
"""Type an operator by one variant of its method's signatures (see `operated`); `taking`, `_taken`'s.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
What the first signature taking the right operand returns, or `None`: a signature before it
|
|
288
|
+
can't be read, or may take it.
|
|
289
|
+
|
|
290
|
+
"""
|
|
291
|
+
signature: Signature
|
|
292
|
+
for signature in variant:
|
|
293
|
+
takes: str | None
|
|
294
|
+
if (takes := signature.get("takes")) is None:
|
|
295
|
+
return None
|
|
296
|
+
taken: list[_Taken] = [taking(member) for member in _members(parsed(takes))]
|
|
297
|
+
if any(verdict == MAYBE for verdict, _ in taken):
|
|
298
|
+
return None
|
|
299
|
+
bound: dict[str, str] | None
|
|
300
|
+
if (bound := next((binds for verdict, binds in taken if verdict == YES), None)) is not None:
|
|
301
|
+
template: str | None = signature["returns"]
|
|
302
|
+
written: str | None = (
|
|
303
|
+
None if template is None else write((template, {**method.types, **bound}), known)
|
|
304
|
+
)
|
|
305
|
+
return None if written is None else _deduplicated(written)
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _taken(
|
|
310
|
+
member: str,
|
|
311
|
+
operands: tuple[str, str],
|
|
312
|
+
arguments: tuple[str, ...] | None,
|
|
313
|
+
types: Mapping[str, str],
|
|
314
|
+
*,
|
|
315
|
+
same: bool,
|
|
316
|
+
) -> _Taken:
|
|
317
|
+
"""Decide whether an operand typed `member` takes the right operand, and what that binds.
|
|
318
|
+
|
|
319
|
+
A generic class's instance (`arguments`) is taken by `Self` where it's the left one's own type,
|
|
320
|
+
and by its own class's template (`Counter[_S]`) where each type argument the left one's binds
|
|
321
|
+
(`types`) is its own, binding the others; another class's template (a base's) may take it.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
`YES`, `NO` or `MAYBE`, and the type variables it binds.
|
|
325
|
+
|
|
326
|
+
"""
|
|
327
|
+
left: str
|
|
328
|
+
right: str
|
|
329
|
+
left, right = operands
|
|
330
|
+
if arguments is None:
|
|
331
|
+
return YES if _takes(member, left, right) else NO, {}
|
|
332
|
+
if member == SELF_TYPE:
|
|
333
|
+
return YES if same else NO, {}
|
|
334
|
+
tree: ast.expr = parsed(member)
|
|
335
|
+
# A base's (`dict[_KT, _VT]`, of an `OrderedDict`): the tables may not know it is.
|
|
336
|
+
if ast.unparse(tree.value if isinstance(tree, ast.Subscript) else tree) != right:
|
|
337
|
+
return MAYBE, {}
|
|
338
|
+
params: list[ast.expr] = (
|
|
339
|
+
[]
|
|
340
|
+
if not isinstance(tree, ast.Subscript)
|
|
341
|
+
else tree.slice.elts
|
|
342
|
+
if isinstance(tree.slice, ast.Tuple)
|
|
343
|
+
else [tree.slice]
|
|
344
|
+
)
|
|
345
|
+
if len(params) != len(arguments) or not all(isinstance(param, ast.Name) for param in params):
|
|
346
|
+
return MAYBE, {}
|
|
347
|
+
bound: dict[str, str] = {}
|
|
348
|
+
name: str
|
|
349
|
+
argument: str
|
|
350
|
+
for name, argument in zip((cast("ast.Name", param).id for param in params), arguments, strict=True):
|
|
351
|
+
wanted: str | None = types.get(name, bound.get(name))
|
|
352
|
+
if wanted is not None and wanted != argument:
|
|
353
|
+
return NO, {}
|
|
354
|
+
bound[name] = argument
|
|
355
|
+
return YES, bound
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _takes(member: str, left: str, right: str) -> bool:
|
|
359
|
+
"""Check whether an operand typed `member` (`Self`: the left operand's own class) takes `right`.
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
Whether it does.
|
|
363
|
+
|
|
364
|
+
"""
|
|
365
|
+
wanted: str = left if member == SELF_TYPE else member
|
|
366
|
+
return right == wanted or right in _PROMOTED.get(wanted, ()) or stdlib.inherits(right, wanted)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _deduplicated(annotation: str) -> str:
|
|
370
|
+
"""Write each union in an annotation with each member once (`Counter[str | str]`: `Counter[str]`).
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
The annotation.
|
|
374
|
+
|
|
375
|
+
"""
|
|
376
|
+
tree: ast.expr = ast.parse(annotation, mode="eval").body
|
|
377
|
+
node: ast.AST
|
|
378
|
+
for node in ast.walk(tree):
|
|
379
|
+
if isinstance(node, ast.Subscript):
|
|
380
|
+
slices: list[ast.expr] = node.slice.elts if isinstance(node.slice, ast.Tuple) else [node.slice]
|
|
381
|
+
joined: list[ast.expr] = [
|
|
382
|
+
ast.parse(" | ".join(dict.fromkeys(_members(part))), mode="eval").body for part in slices
|
|
383
|
+
]
|
|
384
|
+
node.slice = ast.Tuple(joined, ast.Load()) if isinstance(node.slice, ast.Tuple) else joined[0]
|
|
385
|
+
return ast.unparse(tree)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def library_element(receiver: str, known: Known) -> Inference | None:
|
|
389
|
+
"""Type what iterating a standard-library class's instance gives, bound by the receiver's type.
|
|
390
|
+
|
|
391
|
+
A `for` loop over an `io.TextIOWrapper` binds a `str`, over an `itertools.chain[int]` an `int`.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
The inference, or `None` if the tables don't have it, or a type parameter it names is unbound.
|
|
395
|
+
|
|
396
|
+
"""
|
|
397
|
+
found: tuple[str, str, dict[str, str]] | None = stdlib.element(receiver, known)
|
|
398
|
+
annotation: str | None = None if found is None else write((found[1], found[2]), known)
|
|
399
|
+
return (
|
|
400
|
+
None
|
|
401
|
+
if found is None or annotation is None
|
|
402
|
+
else Inference(annotation, f"the elements of a `{found[0]}`", frozenset({_KIND}))
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _receiving(picked: _Picked, method: stdlib.Method | None) -> _Picked:
|
|
407
|
+
"""Add what a method's receiver binds its class's type parameters to (`Method.types`) to a pick.
|
|
408
|
+
|
|
409
|
+
Through an alias, its return's class parameters are first written as the alias has them
|
|
410
|
+
(`Method.templates`).
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
It, or `None` if an argument binds one differently (the call is an error).
|
|
414
|
+
|
|
415
|
+
"""
|
|
416
|
+
types: Mapping[str, str] = {} if method is None else method.types
|
|
417
|
+
if picked is None or any(types.get(name, text) != text for name, text in picked[1].items()):
|
|
418
|
+
return None
|
|
419
|
+
return (
|
|
420
|
+
picked[0] if method is None else substituted(picked[0], method.templates),
|
|
421
|
+
{**types, **picked[1]},
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def substituted(template: str, types: Mapping[str, str]) -> str:
|
|
426
|
+
"""Write a template or pattern with each bare name in `types` replaced by its text.
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
It.
|
|
430
|
+
|
|
431
|
+
"""
|
|
432
|
+
if not types:
|
|
433
|
+
return template
|
|
434
|
+
tree: ast.expr = parsed(template)
|
|
435
|
+
if isinstance(tree, ast.Name):
|
|
436
|
+
return types.get(tree.id, template)
|
|
437
|
+
node: ast.AST
|
|
438
|
+
for node in ast.walk(tree): # a node's children are listed before it's changed: no text is replaced twice
|
|
439
|
+
field: str
|
|
440
|
+
for field in node._fields:
|
|
441
|
+
setattr(node, field, _replaced(cast("object", getattr(node, field, None)), types))
|
|
442
|
+
return ast.unparse(tree)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _replaced(value: object, types: Mapping[str, str]) -> object:
|
|
446
|
+
"""Replace a field's name, or each name in its list, by its text in `types`.
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
The field's new value.
|
|
450
|
+
|
|
451
|
+
"""
|
|
452
|
+
if isinstance(value, list):
|
|
453
|
+
return [_replaced(item, types) for item in cast("list[object]", value)]
|
|
454
|
+
return parsed(types[value.id]) if isinstance(value, ast.Name) and value.id in types else value
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _arguments(call: ast.Call, infer: _Infer, known: Known) -> Arguments | None:
|
|
458
|
+
"""Read a call's arguments.
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
Them; or `None` for a call unpacking `*args` or `**kwargs`, whose arguments can't be matched.
|
|
462
|
+
|
|
463
|
+
"""
|
|
464
|
+
if any(isinstance(arg, ast.Starred) for arg in call.args):
|
|
465
|
+
return None
|
|
466
|
+
keywords: dict[str, Argument] = {}
|
|
467
|
+
keyword: ast.keyword
|
|
468
|
+
for keyword in call.keywords:
|
|
469
|
+
if keyword.arg is None:
|
|
470
|
+
return None
|
|
471
|
+
keywords[keyword.arg] = _argument(keyword.value, infer, known)
|
|
472
|
+
return Arguments([_argument(arg, infer, known) for arg in call.args], keywords)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _argument(value: ast.expr, infer: _Infer, known: Known) -> Argument:
|
|
476
|
+
constant: Constant
|
|
477
|
+
named: tuple[str, bool] | None = _class_named(value, known)
|
|
478
|
+
elts: list[ast.expr]
|
|
479
|
+
match value:
|
|
480
|
+
case ast.Constant(value=None):
|
|
481
|
+
return Argument(_NONE, (None,))
|
|
482
|
+
case ast.Constant(value=bool() | int() | float() | complex() | str() | bytes() as constant):
|
|
483
|
+
kind: str = _LITERAL_STRING if isinstance(constant, str) else type(constant).__name__
|
|
484
|
+
return Argument(kind, (constant,))
|
|
485
|
+
case ast.Name() | ast.Attribute() if named is not None and not named[1]:
|
|
486
|
+
return Argument(None, reads=(named[0],), klass=named[0])
|
|
487
|
+
case ast.Name() | ast.Attribute() if named is not None:
|
|
488
|
+
# A standard-library class, as the module writes it (`csv.Error`, `sqlite.OperationalError`).
|
|
489
|
+
return _inferred(value, infer, known, klass=named[0])
|
|
490
|
+
case ast.Tuple(elts=elts) if elts and all(_class_named(elt, known) is not None for elt in elts):
|
|
491
|
+
# A tuple of classes (`(OSError, ValueError)`): their union, to a parameter that takes one.
|
|
492
|
+
found: list[tuple[str, bool]] = [
|
|
493
|
+
cast("tuple[str, bool]", _class_named(elt, known)) for elt in elts
|
|
494
|
+
]
|
|
495
|
+
union: str = " | ".join(dict.fromkeys(text for text, _ in found))
|
|
496
|
+
return _inferred(value, infer, known, klass=union)._replace(several=True)
|
|
497
|
+
case _:
|
|
498
|
+
return _inferred(value, infer, known)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _class_named(value: ast.expr, known: Known) -> tuple[str, bool] | None:
|
|
502
|
+
"""Read a name or dotted path as a class the module knows, as written.
|
|
503
|
+
|
|
504
|
+
Returns:
|
|
505
|
+
It, and whether it's a library's (a standard-library class, which may be outside a `type[T]`'s
|
|
506
|
+
bound), or `None` for anything else: the module's own class, an installed package's or a
|
|
507
|
+
builtin, else the standard library's.
|
|
508
|
+
|
|
509
|
+
"""
|
|
510
|
+
if not isinstance(value, ast.Name | ast.Attribute):
|
|
511
|
+
return None
|
|
512
|
+
written: str = ast.unparse(value)
|
|
513
|
+
if (
|
|
514
|
+
written in known.classes
|
|
515
|
+
or written in known.names.classes
|
|
516
|
+
or (written in _BUILTIN_CLASSES and known.is_builtin(written))
|
|
517
|
+
):
|
|
518
|
+
return written, False
|
|
519
|
+
return (written, True) if stdlib.class_argument(value, known) is not None else None
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _inferred(value: ast.expr, infer: _Infer, known: Known, klass: str | None = None) -> Argument:
|
|
523
|
+
"""Read an argument by what `--fix` infers it to be.
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
It; `klass` is the class it is where that's known already, else the class a value typed
|
|
527
|
+
`type[C]` is (`self.failureException`, a `type[BaseException]`).
|
|
528
|
+
|
|
529
|
+
"""
|
|
530
|
+
found: Inference | None = infer(value)
|
|
531
|
+
typed: str | None = None if found is None else found.annotation
|
|
532
|
+
held: tuple[str, tuple[str, ...]] | None = None if typed is None else _elements(typed, known)
|
|
533
|
+
lineage: Mapping[str, tuple[str, ...]] = known.names.lineage
|
|
534
|
+
return Argument(
|
|
535
|
+
typed if typed in _COLUMNS and typed != _LITERAL_STRING else None,
|
|
536
|
+
found=found,
|
|
537
|
+
elements=held,
|
|
538
|
+
inner=_elements(held[1][0], known) if held and held[1] else None,
|
|
539
|
+
own=lineage.get(typed or "", ()),
|
|
540
|
+
own_elements=lineage.get(held[1][0], ()) if held and held[0] in _SEQUENCES and held[1] else (),
|
|
541
|
+
reads=tuple(
|
|
542
|
+
ast.unparse(node)
|
|
543
|
+
for node in walk(value)
|
|
544
|
+
if isinstance(node, ast.Name | ast.Attribute | ast.Subscript)
|
|
545
|
+
),
|
|
546
|
+
returns=_function_return(value, known),
|
|
547
|
+
awaited=infer(ast.Await(value)) if isinstance(value, ast.Call | ast.Name) else None,
|
|
548
|
+
klass=_class_of(typed) if klass is None else klass,
|
|
549
|
+
library=True,
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _class_of(typed: str | None) -> str | None:
|
|
554
|
+
"""Read the class a value typed `type[C]` is.
|
|
555
|
+
|
|
556
|
+
Returns:
|
|
557
|
+
`C`, if `typed` is `type[C]` with `C` a name or dotted path; else `None`.
|
|
558
|
+
|
|
559
|
+
"""
|
|
560
|
+
if typed is None or not typed.startswith(_TYPE_OF) or not typed.endswith("]"):
|
|
561
|
+
return None
|
|
562
|
+
inner: str = typed[len(_TYPE_OF) : -1]
|
|
563
|
+
named: bool = bool(inner) and inner != SELF_TYPE and all(part.isidentifier() for part in inner.split("."))
|
|
564
|
+
return inner if named else None
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _function_return(value: ast.expr, known: Known) -> Inference | None:
|
|
568
|
+
"""Type what a function passed as an argument returns, by its declared return (`partial(helper, x)`).
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
The inference, or `None` for anything but a function whose return the module knows.
|
|
572
|
+
|
|
573
|
+
"""
|
|
574
|
+
name: str = ast.unparse(value)
|
|
575
|
+
if not isinstance(value, ast.Name | ast.Attribute) or name not in known.calls:
|
|
576
|
+
return None
|
|
577
|
+
return Inference(known.calls[name], f"`{name}`'s declared return type", frozenset({_CALL}))
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _elements(annotation: str, known: Known) -> tuple[str, tuple[str, ...]] | None:
|
|
581
|
+
"""Read a builtin container's type arguments (`dict[str, int]`), one for a tuple of one type.
|
|
582
|
+
|
|
583
|
+
Returns:
|
|
584
|
+
Its name and them, or `None` for anything else (a tuple of several types among them).
|
|
585
|
+
|
|
586
|
+
"""
|
|
587
|
+
tree: ast.expr = ast.parse(annotation, mode="eval").body
|
|
588
|
+
name: str
|
|
589
|
+
index: ast.expr
|
|
590
|
+
match tree:
|
|
591
|
+
case ast.Subscript(value=ast.Name(id=name), slice=index) if name in _CONTAINERS:
|
|
592
|
+
pass
|
|
593
|
+
case _:
|
|
594
|
+
return None
|
|
595
|
+
if not known.is_builtin(name):
|
|
596
|
+
return None
|
|
597
|
+
texts: list[str] = [ast.unparse(arg) for arg in (index.elts if isinstance(index, ast.Tuple) else [index])]
|
|
598
|
+
if name == _TUPLE: # `tuple[str, ...]`, or `tuple[str, str]`: `str`
|
|
599
|
+
texts = [text for text in texts if text not in {"...", "()"}]
|
|
600
|
+
return (name, (texts[0],)) if len(set(texts)) == 1 else None
|
|
601
|
+
return name, tuple(texts)
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
@lru_cache(maxsize=512)
|
|
605
|
+
def _signatures(name: str) -> tuple[tuple[ReadSignature, ...], ...]:
|
|
606
|
+
"""Read a function's (or method's) variants from the tables, once, each parameter in full.
|
|
607
|
+
|
|
608
|
+
Returns:
|
|
609
|
+
Each variant's signatures.
|
|
610
|
+
|
|
611
|
+
"""
|
|
612
|
+
variants: list[Variant] = (
|
|
613
|
+
stdlib.AWAITED_OVERLOADS[name.removeprefix(AWAIT)]
|
|
614
|
+
if name.startswith(AWAIT)
|
|
615
|
+
else stdlib.OVERLOADS.get(name) or stdlib.method_signatures()[name]
|
|
616
|
+
)
|
|
617
|
+
return tuple(
|
|
618
|
+
tuple(
|
|
619
|
+
ReadSignature(
|
|
620
|
+
# Each a list, as JSON holds it, where the tables' types say a tuple.
|
|
621
|
+
tuple(
|
|
622
|
+
_parameter(param) for param in cast("list[Sequence[object] | str]", signature["params"])
|
|
623
|
+
),
|
|
624
|
+
signature["returns"],
|
|
625
|
+
signature.get("self"),
|
|
626
|
+
)
|
|
627
|
+
for signature in variant
|
|
628
|
+
)
|
|
629
|
+
for variant in variants
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _parameter(written: Sequence[object] | str) -> Parameter:
|
|
634
|
+
"""Read a parameter as the tables write it (`"name kind="`: one every signature has alike).
|
|
635
|
+
|
|
636
|
+
Returns:
|
|
637
|
+
It in full.
|
|
638
|
+
|
|
639
|
+
"""
|
|
640
|
+
if not isinstance(written, str):
|
|
641
|
+
return cast("Parameter", tuple(written))
|
|
642
|
+
name: str
|
|
643
|
+
kind: str
|
|
644
|
+
name, _, kind = written.partition(" ")
|
|
645
|
+
return name, kind.removesuffix("="), kind.endswith("="), None
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _picked(
|
|
649
|
+
variant: Sequence[ReadSignature],
|
|
650
|
+
read: Arguments,
|
|
651
|
+
instance: Sequence[str] | None,
|
|
652
|
+
receiver: Receiver | None,
|
|
653
|
+
) -> Iterator[_Picked]:
|
|
654
|
+
"""Find the signatures a call may match: up to the first that certainly takes its arguments.
|
|
655
|
+
|
|
656
|
+
`instance`: for a method, its receiver's type arguments (`Pattern[str]`'s), if it has them; a
|
|
657
|
+
signature for another instance type (`self: Pattern[bytes]`) refuses it. An installed method's
|
|
658
|
+
signature declaring its `self` is matched against `receiver`'s type (see `Receiver`).
|
|
659
|
+
|
|
660
|
+
Yields:
|
|
661
|
+
Each one that doesn't certainly refuse them: its return template and type variables' types.
|
|
662
|
+
|
|
663
|
+
"""
|
|
664
|
+
signature: ReadSignature
|
|
665
|
+
for signature in variant:
|
|
666
|
+
verdict: str
|
|
667
|
+
bound: dict[str, str]
|
|
668
|
+
verdict, bound = _matched(signature.params, read)
|
|
669
|
+
if signature.instance is not None and signature.instance != instance:
|
|
670
|
+
verdict = NO if instance is not None else MAYBE if verdict == YES else verdict
|
|
671
|
+
if signature.receiver is not None and verdict != NO:
|
|
672
|
+
verdict = both(verdict, MAYBE if receiver is None else matches(receiver, signature, bound))
|
|
673
|
+
if verdict != NO:
|
|
674
|
+
yield None if signature.returns is None else (signature.returns, bound)
|
|
675
|
+
if verdict == YES:
|
|
676
|
+
return
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def _matched(params: Sequence[Parameter], read: Arguments) -> tuple[str, dict[str, str]]:
|
|
680
|
+
"""Match a call's arguments to one signature's parameters.
|
|
681
|
+
|
|
682
|
+
Returns:
|
|
683
|
+
Whether it takes them (`YES`, `NO`, `MAYBE`), and the type variables they bind.
|
|
684
|
+
|
|
685
|
+
"""
|
|
686
|
+
pairs: list[tuple[Parameter, Argument]] | None
|
|
687
|
+
if (pairs := _bound(params, read)) is None:
|
|
688
|
+
return NO, {}
|
|
689
|
+
verdicts: set[str] = set()
|
|
690
|
+
types: dict[str, str] = {}
|
|
691
|
+
clashing: set[str] = set() # type variables two arguments bind differently: unwritable
|
|
692
|
+
param: Parameter
|
|
693
|
+
arg: Argument
|
|
694
|
+
for param, arg in pairs:
|
|
695
|
+
verdict: str = _verdict(param[3], arg)
|
|
696
|
+
verdicts.add(verdict)
|
|
697
|
+
bound: tuple[str, str] | None
|
|
698
|
+
if (bound := None if verdict != YES else _binding(param[3], arg)) is not None:
|
|
699
|
+
variable: str
|
|
700
|
+
text: str
|
|
701
|
+
variable, text = bound
|
|
702
|
+
if types.setdefault(variable, text) != text:
|
|
703
|
+
verdicts.add(MAYBE)
|
|
704
|
+
clashing.add(variable)
|
|
705
|
+
if NO in verdicts:
|
|
706
|
+
return NO, {}
|
|
707
|
+
return (MAYBE if MAYBE in verdicts else YES), {
|
|
708
|
+
name: text for name, text in types.items() if name not in clashing
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _binding(accepts: Accepts | None, arg: Argument) -> tuple[str, str] | None:
|
|
713
|
+
"""Find the type variable an argument binds, taken by a parameter that is one (or has one).
|
|
714
|
+
|
|
715
|
+
A scalar by the parameter's own verdicts (`var`); else anything by its type, for a parameter that
|
|
716
|
+
is an unbounded type variable (`t`), and a builtin container by its type argument, for one that
|
|
717
|
+
is a generic class of one (`e`, `of`).
|
|
718
|
+
|
|
719
|
+
Returns:
|
|
720
|
+
Its name and type, or `None`.
|
|
721
|
+
|
|
722
|
+
"""
|
|
723
|
+
kind: str | None = arg.type
|
|
724
|
+
if accepts is None or (arg.klass is not None and (CLASS_BINDS in accepts or not arg.library)):
|
|
725
|
+
# A class binds `type[T]`'s `T` (`kv`), or nothing; a library's is a value to any other
|
|
726
|
+
# parameter; a tuple of classes binds it to their union (one taking no tuple refused it, see
|
|
727
|
+
# `_verdict`).
|
|
728
|
+
variable: str | None = None if accepts is None else accepts.get(CLASS_BINDS)
|
|
729
|
+
return None if variable is None or arg.klass is None else (variable, arg.klass)
|
|
730
|
+
if kind is not None:
|
|
731
|
+
binds: str | dict[str, list[str]] = accepts.get("var", {})
|
|
732
|
+
if isinstance(binds, str): # each type binds it to itself; a `str` literal to `str`
|
|
733
|
+
return binds, "str" if kind == _LITERAL_STRING else kind
|
|
734
|
+
found: list[str] | None = binds.get(kind)
|
|
735
|
+
return None if found is None else (found[0], found[1])
|
|
736
|
+
# Anything, by its type (`t`); a function, by its declared return (`r`).
|
|
737
|
+
named: str | None
|
|
738
|
+
text: str | None
|
|
739
|
+
for named, text in (
|
|
740
|
+
(accepts.get(_ANYTHING), arg.text),
|
|
741
|
+
(accepts.get(_RETURNED), None if arg.returns is None else arg.returns.annotation),
|
|
742
|
+
(accepts.get(_AWAITED), None if arg.awaited is None else arg.awaited.annotation),
|
|
743
|
+
(accepts.get(FUTURE_BINDS), None if arg.awaited is None else arg.text),
|
|
744
|
+
):
|
|
745
|
+
if named is not None and text is not None:
|
|
746
|
+
return named, text
|
|
747
|
+
of: dict[str, int] = accepts.get("of", {})
|
|
748
|
+
elements: tuple[str, tuple[str, ...]] | None = arg.elements
|
|
749
|
+
if elements is not None and elements[0] in of:
|
|
750
|
+
return _element_binding(accepts, arg, elements[1][of[elements[0]]])
|
|
751
|
+
# A bounded type variable a builtin container argument binds, its bound certainly taking it.
|
|
752
|
+
container: str | None = accepts.get(CONTAINER_BINDS)
|
|
753
|
+
return None if container is None or elements is None or arg.text is None else (container, arg.text)
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def _bound(params: Sequence[Parameter], read: Arguments) -> list[tuple[Parameter, Argument]] | None:
|
|
757
|
+
"""Bind a call's arguments to a signature's parameters, as Python does.
|
|
758
|
+
|
|
759
|
+
Returns:
|
|
760
|
+
Each parameter with its argument (a `*args` or `**kwargs` one with each it takes), or `None` if
|
|
761
|
+
they don't fit: too many, an unknown keyword, one bound twice, a required parameter missing.
|
|
762
|
+
|
|
763
|
+
"""
|
|
764
|
+
positional: list[Parameter] = [param for param in params if param[1] in _POSITIONAL]
|
|
765
|
+
star: Parameter | None = next((param for param in params if param[1] == _STAR), None)
|
|
766
|
+
stars: Parameter | None = next((param for param in params if param[1] == _STARS), None)
|
|
767
|
+
pairs: list[tuple[Parameter, Argument]] = []
|
|
768
|
+
index: int
|
|
769
|
+
arg: Argument
|
|
770
|
+
for index, arg in enumerate(read.args):
|
|
771
|
+
taking: Parameter | None
|
|
772
|
+
if (taking := positional[index] if index < len(positional) else star) is None:
|
|
773
|
+
return None
|
|
774
|
+
pairs.append((taking, arg))
|
|
775
|
+
filled: set[str] = {param[0] for param in positional[: len(read.args)]}
|
|
776
|
+
keyword: str
|
|
777
|
+
for keyword, arg in read.keywords.items():
|
|
778
|
+
named: Parameter | None = next(
|
|
779
|
+
(param for param in params if param[0] == keyword and param[1] in _KEYWORD),
|
|
780
|
+
stars,
|
|
781
|
+
)
|
|
782
|
+
if named is None or keyword in filled:
|
|
783
|
+
return None
|
|
784
|
+
filled.add(keyword)
|
|
785
|
+
pairs.append((named, arg))
|
|
786
|
+
required: list[str] = [param[0] for param in params if not param[2] and param[1] not in {_STAR, _STARS}]
|
|
787
|
+
return pairs if set(required) <= filled else None
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def _verdict(accepts: Accepts | None, arg: Argument) -> str:
|
|
791
|
+
"""Decide whether a parameter takes an argument.
|
|
792
|
+
|
|
793
|
+
Returns:
|
|
794
|
+
`YES`, `NO`, or `MAYBE` (its type unknown, or not enough to tell).
|
|
795
|
+
|
|
796
|
+
"""
|
|
797
|
+
if accepts is None:
|
|
798
|
+
return YES
|
|
799
|
+
if arg.klass is not None and (CLASS_VERDICT in accepts or CLASS_BOUNDED in accepts or not arg.library):
|
|
800
|
+
# A builtin or library class may be outside a `type[T]`'s bound (`dtype=bool`, `T` a numpy
|
|
801
|
+
# scalar): the signature that takes it then is another's.
|
|
802
|
+
outside: bool = (arg.library or arg.klass in _BUILTIN_CLASSES) and accepts.get(CLASS_BOUNDED, False)
|
|
803
|
+
untaken: bool = arg.several and not accepts.get(CLASS_TUPLE, False) # a tuple, where one class is
|
|
804
|
+
return NO if untaken else MAYBE if outside else accepts.get(CLASS_VERDICT, MAYBE)
|
|
805
|
+
if arg.constant is not None and any(_same(arg.constant[0], value) for value in accepts.get("lit", [])):
|
|
806
|
+
return YES
|
|
807
|
+
table: str = accepts.get("c", accepts["v"]) if arg.constant is not None else accepts["v"]
|
|
808
|
+
if arg.type is not None:
|
|
809
|
+
return table[_COLUMNS[arg.type]]
|
|
810
|
+
# A checked file's parameter that takes only checked files' classes, by the argument's lineage.
|
|
811
|
+
wanted: list[str] | None
|
|
812
|
+
line: tuple[str, ...]
|
|
813
|
+
for wanted, line in ((accepts.get(OWN), arg.own), (accepts.get(OWN_ELEMENTS), arg.own_elements)):
|
|
814
|
+
if wanted is not None and line:
|
|
815
|
+
return YES if set(wanted) & set(line) else MAYBE if UNFOLLOWED in line else NO
|
|
816
|
+
taken: bool = (
|
|
817
|
+
(_ANYTHING in accepts and arg.text is not None)
|
|
818
|
+
or (FUTURE_BINDS in accepts and arg.awaited is not None and arg.text is not None)
|
|
819
|
+
or (
|
|
820
|
+
arg.elements is not None
|
|
821
|
+
and arg.elements[0] in accepts.get("of", {})
|
|
822
|
+
and (_NESTED not in accepts or _nested(accepts, arg) is not None)
|
|
823
|
+
)
|
|
824
|
+
or (_RETURNED in accepts and arg.returns is not None)
|
|
825
|
+
or (_AWAITED in accepts and arg.awaited is not None)
|
|
826
|
+
)
|
|
827
|
+
return YES if taken else _container_verdict(accepts, arg)
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def _element_binding(accepts: Accepts, arg: Argument, element: str) -> tuple[str, str] | None:
|
|
831
|
+
"""Bind a parameter's element type variable (`e`) by a container argument's `element`.
|
|
832
|
+
|
|
833
|
+
Of an iterable of iterables (`Iterable[Iterable[_S]]`, `ei`): by that element's own.
|
|
834
|
+
|
|
835
|
+
Returns:
|
|
836
|
+
The variable's name and type, or `None`.
|
|
837
|
+
|
|
838
|
+
"""
|
|
839
|
+
if _NESTED not in accepts:
|
|
840
|
+
return accepts.get("e", ""), element
|
|
841
|
+
inner: tuple[str, tuple[str, ...]] | None = _nested(accepts, arg)
|
|
842
|
+
return None if inner is None else (accepts.get("e", ""), inner[1][accepts[_NESTED][inner[0]]])
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
def _nested(accepts: Accepts, arg: Argument) -> tuple[str, tuple[str, ...]] | None:
|
|
846
|
+
"""Find the container an argument's elements are, where a parameter takes an iterable of them (`ei`).
|
|
847
|
+
|
|
848
|
+
Returns:
|
|
849
|
+
Its name and type arguments, or `None` if the elements' type isn't one it takes, or the outer
|
|
850
|
+
container's type argument it reads isn't its first.
|
|
851
|
+
|
|
852
|
+
"""
|
|
853
|
+
of: dict[str, int] = accepts.get("of", {})
|
|
854
|
+
inner: tuple[str, tuple[str, ...]] | None = arg.inner
|
|
855
|
+
first: bool = arg.elements is not None and of.get(arg.elements[0]) == 0
|
|
856
|
+
return inner if first and inner is not None and inner[0] in accepts.get(_NESTED, {}) else None
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def _container_verdict(accepts: Accepts, arg: Argument) -> str:
|
|
860
|
+
"""Decide whether a parameter takes a builtin container argument, by its elements' type where it says.
|
|
861
|
+
|
|
862
|
+
Returns:
|
|
863
|
+
The verdict: `MAYBE` for any other argument, or a parameter that doesn't say.
|
|
864
|
+
|
|
865
|
+
"""
|
|
866
|
+
elements: tuple[str, tuple[str, ...]] | None
|
|
867
|
+
if (elements := arg.elements) is None:
|
|
868
|
+
# A parameter bounded by a future takes no coroutine's call: awaitable, but no future.
|
|
869
|
+
return NO if FUTURE_BINDS in accepts and arg.awaited is not None else MAYBE
|
|
870
|
+
verdict: str = accepts.get(CONTAINER_VERDICTS, {}).get(elements[0], MAYBE)
|
|
871
|
+
of: str | None = accepts.get(ELEMENT_VERDICTS, {}).get(elements[0])
|
|
872
|
+
if verdict != YES or of is None:
|
|
873
|
+
return verdict
|
|
874
|
+
element: str = elements[1][0]
|
|
875
|
+
return of[_COLUMNS[element]] if element in _COLUMNS else MAYBE
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
def _same(constant: Constant, value: Constant) -> bool:
|
|
879
|
+
"""Compare a literal argument with a `Literal[...]`'s value, types too (`True` isn't `1`).
|
|
880
|
+
|
|
881
|
+
Returns:
|
|
882
|
+
Whether they're the same.
|
|
883
|
+
|
|
884
|
+
"""
|
|
885
|
+
return type(constant) is type(value) and constant == value
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
def write(picked: tuple[str, dict[str, str]], known: Known) -> str | None:
|
|
889
|
+
"""Write a return template as the module can: its type variables bound, its classes spelled.
|
|
890
|
+
|
|
891
|
+
Returns:
|
|
892
|
+
The annotation, each union member once; or `None` if a type variable is unbound, a builtin
|
|
893
|
+
rebound, or a class can't be named. A template that's spelled already (`SPELLED`) is its own.
|
|
894
|
+
|
|
895
|
+
"""
|
|
896
|
+
template: str
|
|
897
|
+
types: dict[str, str]
|
|
898
|
+
template, types = picked
|
|
899
|
+
if template.startswith(SPELLED):
|
|
900
|
+
return template.removeprefix(SPELLED)
|
|
901
|
+
tree: ast.expr | None = _rewritten(ast.parse(template, mode="eval").body, types, known)
|
|
902
|
+
return None if tree is None else " | ".join(dict.fromkeys(_members(tree)))
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def _rewritten(node: ast.expr, types: Mapping[str, str], known: Known) -> ast.expr | None:
|
|
906
|
+
"""Rewrite a freshly parsed template in place: each name and dotted path as `_spelled` has it.
|
|
907
|
+
|
|
908
|
+
A template is names, dotted paths, subscripts, tuples of their arguments, unions and `...`.
|
|
909
|
+
|
|
910
|
+
Returns:
|
|
911
|
+
The tree, or `None` if a name can't be spelled.
|
|
912
|
+
|
|
913
|
+
"""
|
|
914
|
+
text: str | None
|
|
915
|
+
parts: list[ast.expr | None]
|
|
916
|
+
match node:
|
|
917
|
+
case ast.Name() | ast.Attribute():
|
|
918
|
+
return (
|
|
919
|
+
None if (text := _spelled(node, types, known)) is None else ast.parse(text, mode="eval").body
|
|
920
|
+
)
|
|
921
|
+
case ast.Subscript():
|
|
922
|
+
parts = [_rewritten(node.value, types, known), _rewritten(node.slice, types, known)]
|
|
923
|
+
node.value, node.slice = parts[0] or node.value, parts[1] or node.slice
|
|
924
|
+
case ast.Tuple():
|
|
925
|
+
parts = [_rewritten(element, types, known) for element in node.elts]
|
|
926
|
+
node.elts = [part for part in parts if part is not None]
|
|
927
|
+
case ast.BinOp():
|
|
928
|
+
parts = [_rewritten(node.left, types, known), _rewritten(node.right, types, known)]
|
|
929
|
+
node.left, node.right = parts[0] or node.left, parts[1] or node.right
|
|
930
|
+
case _:
|
|
931
|
+
parts = []
|
|
932
|
+
return None if None in parts else node
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
def _spelled(node: ast.Name | ast.Attribute, types: Mapping[str, str], known: Known) -> str | None:
|
|
936
|
+
"""Spell one name of a template: a class's dotted path, a bound type variable, a builtin.
|
|
937
|
+
|
|
938
|
+
Returns:
|
|
939
|
+
Its text, or `None`.
|
|
940
|
+
|
|
941
|
+
"""
|
|
942
|
+
if isinstance(node, ast.Attribute):
|
|
943
|
+
return None if known.names.plan is None else known.names.plan.spell(ast.unparse(node))
|
|
944
|
+
if node.id in types:
|
|
945
|
+
return types[node.id]
|
|
946
|
+
return node.id if node.id in _BUILTINS and (node.id == _NONE or known.is_builtin(node.id)) else None
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
def _members(tree: ast.expr) -> Iterator[str]:
|
|
950
|
+
"""Walk a union's members (`a | b | c`), not into brackets.
|
|
951
|
+
|
|
952
|
+
Yields:
|
|
953
|
+
Each one's text.
|
|
954
|
+
|
|
955
|
+
"""
|
|
956
|
+
if isinstance(tree, ast.BinOp) and isinstance(tree.op, ast.BitOr):
|
|
957
|
+
yield from _members(tree.left)
|
|
958
|
+
yield from _members(tree.right)
|
|
959
|
+
else:
|
|
960
|
+
yield ast.unparse(tree)
|