python-constricter 0.4.1__cp313-cp313-win32.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- constricter/__init__.py +55 -0
- constricter/__main__.py +26 -0
- constricter/_version.py +4 -0
- constricter/cli/__init__.py +2 -0
- constricter/cli/baseline.py +107 -0
- constricter/cli/collecting.py +54 -0
- constricter/cli/command.py +547 -0
- constricter/cli/config.py +322 -0
- constricter/cli/explain.py +104 -0
- constricter/cli/lsp/__init__.py +2 -0
- constricter/cli/lsp/connection.py +475 -0
- constricter/cli/lsp/edits.py +188 -0
- constricter/cli/lsp/guard.py +133 -0
- constricter/cli/lsp/hints.py +391 -0
- constricter/cli/lsp/protocol.py +192 -0
- constricter/cli/lsp/remembered.py +81 -0
- constricter/cli/lsp/stored.py +200 -0
- constricter/cli/options.py +799 -0
- constricter/cli/paths.py +151 -0
- constricter/cli/reading.py +36 -0
- constricter/cli/report.py +397 -0
- constricter/cli/rounds.py +328 -0
- constricter/cli/runs.py +90 -0
- constricter/cli/schedule.py +463 -0
- constricter/cli/traced.py +401 -0
- constricter/cli/workers.py +286 -0
- constricter/empty.py +66 -0
- constricter/fix/__init__.cp313-win32.pyd +0 -0
- constricter/fix/__init__.py +2 -0
- constricter/fix/core/__init__.cp313-win32.pyd +0 -0
- constricter/fix/core/__init__.py +2 -0
- constricter/fix/core/asked.cp313-win32.pyd +0 -0
- constricter/fix/core/asked.py +56 -0
- constricter/fix/core/fixes.cp313-win32.pyd +0 -0
- constricter/fix/core/fixes.py +317 -0
- constricter/fix/core/imports.cp313-win32.pyd +0 -0
- constricter/fix/core/imports.py +580 -0
- constricter/fix/core/inherited.cp313-win32.pyd +0 -0
- constricter/fix/core/inherited.py +311 -0
- constricter/fix/core/known.cp313-win32.pyd +0 -0
- constricter/fix/core/known.py +730 -0
- constricter/fix/core/signatures.cp313-win32.pyd +0 -0
- constricter/fix/core/signatures.py +132 -0
- constricter/fix/index/__init__.cp313-win32.pyd +0 -0
- constricter/fix/index/__init__.py +2 -0
- constricter/fix/index/awaits.cp313-win32.pyd +0 -0
- constricter/fix/index/awaits.py +90 -0
- constricter/fix/index/beyond.cp313-win32.pyd +0 -0
- constricter/fix/index/beyond.py +149 -0
- constricter/fix/index/callers.cp313-win32.pyd +0 -0
- constricter/fix/index/callers.py +155 -0
- constricter/fix/index/crossed.cp313-win32.pyd +0 -0
- constricter/fix/index/crossed.py +327 -0
- constricter/fix/index/decorated.cp313-win32.pyd +0 -0
- constricter/fix/index/decorated.py +140 -0
- constricter/fix/index/fixtures.cp313-win32.pyd +0 -0
- constricter/fix/index/fixtures.py +176 -0
- constricter/fix/index/linked.cp313-win32.pyd +0 -0
- constricter/fix/index/linked.py +142 -0
- constricter/fix/index/loose.cp313-win32.pyd +0 -0
- constricter/fix/index/loose.py +103 -0
- constricter/fix/index/managed.cp313-win32.pyd +0 -0
- constricter/fix/index/managed.py +63 -0
- constricter/fix/index/modules.cp313-win32.pyd +0 -0
- constricter/fix/index/modules.py +741 -0
- constricter/fix/index/offers.cp313-win32.pyd +0 -0
- constricter/fix/index/offers.py +252 -0
- constricter/fix/index/order.cp313-win32.pyd +0 -0
- constricter/fix/index/order.py +155 -0
- constricter/fix/index/origins.cp313-win32.pyd +0 -0
- constricter/fix/index/origins.py +238 -0
- constricter/fix/index/own_methods.cp313-win32.pyd +0 -0
- constricter/fix/index/own_methods.py +91 -0
- constricter/fix/index/own_overloads.cp313-win32.pyd +0 -0
- constricter/fix/index/own_overloads.py +70 -0
- constricter/fix/index/own_types.cp313-win32.pyd +0 -0
- constricter/fix/index/own_types.py +330 -0
- constricter/fix/index/params.cp313-win32.pyd +0 -0
- constricter/fix/index/params.py +81 -0
- constricter/fix/index/plain.cp313-win32.pyd +0 -0
- constricter/fix/index/plain.py +221 -0
- constricter/fix/index/project.cp313-win32.pyd +0 -0
- constricter/fix/index/project.py +797 -0
- constricter/fix/index/sides.cp313-win32.pyd +0 -0
- constricter/fix/index/sides.py +160 -0
- constricter/fix/index/strings.cp313-win32.pyd +0 -0
- constricter/fix/index/strings.py +46 -0
- constricter/fix/index/stubs/__init__.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/__init__.py +2 -0
- constricter/fix/index/stubs/atoms.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/atoms.py +425 -0
- constricter/fix/index/stubs/chained.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/chained.py +69 -0
- constricter/fix/index/stubs/classnames.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/classnames.py +124 -0
- constricter/fix/index/stubs/declared.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/declared.py +508 -0
- constricter/fix/index/stubs/installed.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/installed.py +425 -0
- constricter/fix/index/stubs/names.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/names.py +200 -0
- constricter/fix/index/stubs/read.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/read.py +70 -0
- constricter/fix/index/stubs/signatures.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/signatures.py +656 -0
- constricter/fix/index/stubs/stubbed.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/stubbed.py +208 -0
- constricter/fix/index/taken.cp313-win32.pyd +0 -0
- constricter/fix/index/taken.py +45 -0
- constricter/fix/index/tuples.cp313-win32.pyd +0 -0
- constricter/fix/index/tuples.py +112 -0
- constricter/fix/libraries/__init__.cp313-win32.pyd +0 -0
- constricter/fix/libraries/__init__.py +2 -0
- constricter/fix/libraries/library.cp313-win32.pyd +0 -0
- constricter/fix/libraries/library.py +420 -0
- constricter/fix/libraries/opened.cp313-win32.pyd +0 -0
- constricter/fix/libraries/opened.py +147 -0
- constricter/fix/libraries/overloads.cp313-win32.pyd +0 -0
- constricter/fix/libraries/overloads.py +960 -0
- constricter/fix/libraries/receivers.cp313-win32.pyd +0 -0
- constricter/fix/libraries/receivers.py +274 -0
- constricter/fix/libraries/stdlib.cp313-win32.pyd +0 -0
- constricter/fix/libraries/stdlib.py +752 -0
- constricter/fix/libraries/walks.cp313-win32.pyd +0 -0
- constricter/fix/libraries/walks.py +49 -0
- constricter/fix/tables/aliases.json +147 -0
- constricter/fix/tables/attributes.json +700 -0
- constricter/fix/tables/awaited.json +69 -0
- constricter/fix/tables/awaited_overloads.json +14 -0
- constricter/fix/tables/bases.json +888 -0
- constricter/fix/tables/classes.json +1941 -0
- constricter/fix/tables/elements.json +156 -0
- constricter/fix/tables/functions.json +58 -0
- constricter/fix/tables/generic_attributes.json +254 -0
- constricter/fix/tables/method_overloads.json +461 -0
- constricter/fix/tables/method_signatures.json +954 -0
- constricter/fix/tables/methods.json +954 -0
- constricter/fix/tables/overloads.json +540 -0
- constricter/fix/tables/platform.json +37 -0
- constricter/fix/tables/returns.json +1362 -0
- constricter/fix/tables/scalar_members.json +16 -0
- constricter/fix/tables/scalars.json +2707 -0
- constricter/fix/tables/source.json +28 -0
- constricter/fix/tables/subscriptable.json +77 -0
- constricter/fix/tables/type_parameters.json +178 -0
- constricter/fix/tables/variables.json +2962 -0
- constricter/fix/values/__init__.cp313-win32.pyd +0 -0
- constricter/fix/values/__init__.py +2 -0
- constricter/fix/values/aliased.cp313-win32.pyd +0 -0
- constricter/fix/values/aliased.py +195 -0
- constricter/fix/values/bodies.cp313-win32.pyd +0 -0
- constricter/fix/values/bodies.py +76 -0
- constricter/fix/values/callables.cp313-win32.pyd +0 -0
- constricter/fix/values/callables.py +323 -0
- constricter/fix/values/called.cp313-win32.pyd +0 -0
- constricter/fix/values/called.py +194 -0
- constricter/fix/values/classvars.cp313-win32.pyd +0 -0
- constricter/fix/values/classvars.py +629 -0
- constricter/fix/values/decided.cp313-win32.pyd +0 -0
- constricter/fix/values/decided.py +429 -0
- constricter/fix/values/displays.cp313-win32.pyd +0 -0
- constricter/fix/values/displays.py +101 -0
- constricter/fix/values/doubts.cp313-win32.pyd +0 -0
- constricter/fix/values/doubts.py +677 -0
- constricter/fix/values/entered.cp313-win32.pyd +0 -0
- constricter/fix/values/entered.py +332 -0
- constricter/fix/values/fills.cp313-win32.pyd +0 -0
- constricter/fix/values/fills.py +446 -0
- constricter/fix/values/guesses.cp313-win32.pyd +0 -0
- constricter/fix/values/guesses.py +376 -0
- constricter/fix/values/hinted.cp313-win32.pyd +0 -0
- constricter/fix/values/hinted.py +811 -0
- constricter/fix/values/inference.cp313-win32.pyd +0 -0
- constricter/fix/values/inference.py +698 -0
- constricter/fix/values/loops.cp313-win32.pyd +0 -0
- constricter/fix/values/loops.py +335 -0
- constricter/fix/values/members.cp313-win32.pyd +0 -0
- constricter/fix/values/members.py +578 -0
- constricter/fix/values/narrowed.cp313-win32.pyd +0 -0
- constricter/fix/values/narrowed.py +220 -0
- constricter/fix/values/operated.cp313-win32.pyd +0 -0
- constricter/fix/values/operated.py +202 -0
- constricter/fix/values/ordered.cp313-win32.pyd +0 -0
- constricter/fix/values/ordered.py +74 -0
- constricter/fix/values/returned.cp313-win32.pyd +0 -0
- constricter/fix/values/returned.py +479 -0
- constricter/fix/values/returns.cp313-win32.pyd +0 -0
- constricter/fix/values/returns.py +237 -0
- constricter/fix/values/shapes.cp313-win32.pyd +0 -0
- constricter/fix/values/shapes.py +654 -0
- constricter/fix/values/stored.cp313-win32.pyd +0 -0
- constricter/fix/values/stored.py +564 -0
- constricter/fix/values/targets.cp313-win32.pyd +0 -0
- constricter/fix/values/targets.py +419 -0
- constricter/fix/values/unchecked.cp313-win32.pyd +0 -0
- constricter/fix/values/unchecked.py +278 -0
- constricter/jsonc.py +50 -0
- constricter/noqa.py +47 -0
- constricter/notebook.py +115 -0
- constricter/offences.py +436 -0
- constricter/overrides.py +24 -0
- constricter/plugins/__init__.py +2 -0
- constricter/plugins/flake8.py +133 -0
- constricter/plugins/pylint.py +186 -0
- constricter/py.typed +0 -0
- constricter/recording.py +660 -0
- constricter/rules/__init__.cp313-win32.pyd +0 -0
- constricter/rules/__init__.py +2 -0
- constricter/rules/annotations.cp313-win32.pyd +0 -0
- constricter/rules/annotations.py +296 -0
- constricter/rules/binding.cp313-win32.pyd +0 -0
- constricter/rules/binding.py +652 -0
- constricter/rules/calls.cp313-win32.pyd +0 -0
- constricter/rules/calls.py +460 -0
- constricter/rules/checker.cp313-win32.pyd +0 -0
- constricter/rules/checker.py +536 -0
- constricter/rules/declarations.cp313-win32.pyd +0 -0
- constricter/rules/declarations.py +730 -0
- constricter/rules/decorators.cp313-win32.pyd +0 -0
- constricter/rules/decorators.py +351 -0
- constricter/rules/flow.cp313-win32.pyd +0 -0
- constricter/rules/flow.py +502 -0
- constricter/rules/keys.cp313-win32.pyd +0 -0
- constricter/rules/keys.py +79 -0
- constricter/rules/late.cp313-win32.pyd +0 -0
- constricter/rules/late.py +969 -0
- constricter/rules/narrowing.cp313-win32.pyd +0 -0
- constricter/rules/narrowing.py +98 -0
- constricter/rules/parsed.cp313-win32.pyd +0 -0
- constricter/rules/parsed.py +136 -0
- constricter/rules/quoted.cp313-win32.pyd +0 -0
- constricter/rules/quoted.py +141 -0
- constricter/rules/rebinding.cp313-win32.pyd +0 -0
- constricter/rules/rebinding.py +169 -0
- constricter/rules/recorded.cp313-win32.pyd +0 -0
- constricter/rules/recorded.py +168 -0
- constricter/rules/redundant.cp313-win32.pyd +0 -0
- constricter/rules/redundant.py +160 -0
- constricter/rules/scope.cp313-win32.pyd +0 -0
- constricter/rules/scope.py +802 -0
- constricter/rules/scoped.cp313-win32.pyd +0 -0
- constricter/rules/scoped.py +232 -0
- constricter/rules/scoping.cp313-win32.pyd +0 -0
- constricter/rules/scoping.py +346 -0
- constricter/rules/settings.cp313-win32.pyd +0 -0
- constricter/rules/settings.py +189 -0
- constricter/rules/syntax.cp313-win32.pyd +0 -0
- constricter/rules/syntax.py +472 -0
- constricter/rules/tables.cp313-win32.pyd +0 -0
- constricter/rules/tables.py +169 -0
- constricter/rules/walked.cp313-win32.pyd +0 -0
- constricter/rules/walked.py +127 -0
- constricter/rules/widened.cp313-win32.pyd +0 -0
- constricter/rules/widened.py +322 -0
- constricter/trace.py +374 -0
- e45ce06b2fdd5c43f782__mypyc.cp313-win32.pyd +0 -0
- python_constricter-0.4.1.dist-info/METADATA +400 -0
- python_constricter-0.4.1.dist-info/RECORD +261 -0
- python_constricter-0.4.1.dist-info/WHEEL +4 -0
- python_constricter-0.4.1.dist-info/entry_points.txt +5 -0
- python_constricter-0.4.1.dist-info/licenses/LICENSE.md +21 -0
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""An installed function's signatures, read from its stub as the standard library's tables hold theirs.
|
|
3
|
+
|
|
4
|
+
A stub's overloads (see `constricter.fix.index.stubs.declared`) are resolved across the installed modules the
|
|
5
|
+
index holds, into the signatures of `constricter.fix.libraries.overloads`: which argument types each
|
|
6
|
+
parameter certainly takes or refuses, the type variables they bind, and the return as a template
|
|
7
|
+
naming classes by their public dotted paths. What they take is worked out from the parameter's
|
|
8
|
+
annotation, through aliases (a generic one's parameters bound by its arguments), type variables'
|
|
9
|
+
bounds, unions and `Literal`s: a builtin scalar by the `scalars` table for a standard-library
|
|
10
|
+
class (`SupportsIndex`), by its members for an installed protocol, and never by any other
|
|
11
|
+
installed class; a class passed as an argument (`dtype=np.float64`) by `type[...]`, which binds
|
|
12
|
+
its type variable to it.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import ast
|
|
16
|
+
import builtins
|
|
17
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
18
|
+
from typing import Final, cast
|
|
19
|
+
|
|
20
|
+
from constricter.fix.core.known import Origin
|
|
21
|
+
from constricter.fix.core.signatures import (
|
|
22
|
+
CLASS_BINDS,
|
|
23
|
+
CLASS_BOUNDED,
|
|
24
|
+
CLASS_VERDICT,
|
|
25
|
+
CONTAINER_BINDS,
|
|
26
|
+
CONTAINER_VERDICTS,
|
|
27
|
+
ELEMENT_VERDICTS,
|
|
28
|
+
Accepts,
|
|
29
|
+
Constant,
|
|
30
|
+
Expansion,
|
|
31
|
+
Parameter,
|
|
32
|
+
ReadSignature,
|
|
33
|
+
)
|
|
34
|
+
from constricter.fix.index import own_types, project
|
|
35
|
+
from constricter.fix.index.modules import Module
|
|
36
|
+
from constricter.fix.index.stubs import classnames
|
|
37
|
+
from constricter.fix.index.stubs.atoms import (
|
|
38
|
+
ANYTHING,
|
|
39
|
+
BUILTINS_MODULE,
|
|
40
|
+
CLASS,
|
|
41
|
+
MAYBE,
|
|
42
|
+
NO,
|
|
43
|
+
NONE,
|
|
44
|
+
PARAM_SPEC,
|
|
45
|
+
TYPE,
|
|
46
|
+
TYPES,
|
|
47
|
+
TYPING,
|
|
48
|
+
UNKNOWN,
|
|
49
|
+
VAR,
|
|
50
|
+
YES,
|
|
51
|
+
Atom,
|
|
52
|
+
Scope,
|
|
53
|
+
Taken,
|
|
54
|
+
argument_of,
|
|
55
|
+
atom_path,
|
|
56
|
+
bound_alias,
|
|
57
|
+
class_binds,
|
|
58
|
+
class_bounded,
|
|
59
|
+
class_verdict,
|
|
60
|
+
combined,
|
|
61
|
+
element_of,
|
|
62
|
+
free_variables,
|
|
63
|
+
joined,
|
|
64
|
+
literal_values,
|
|
65
|
+
owned,
|
|
66
|
+
parse_text,
|
|
67
|
+
private_name,
|
|
68
|
+
scalar_binds,
|
|
69
|
+
scalar_verdict,
|
|
70
|
+
verdict_of,
|
|
71
|
+
)
|
|
72
|
+
from constricter.fix.index.stubs.declared import Alias, Class, Declarations, Signature, Variable
|
|
73
|
+
from constricter.fix.index.stubs.names import ALIAS_DEPTH, ARG, Resolved, Resolver
|
|
74
|
+
from constricter.fix.index.stubs.read import MEMO, Asked, Expanded
|
|
75
|
+
from constricter.fix.libraries.overloads import CONTAINERS, SCALARS, substituted
|
|
76
|
+
|
|
77
|
+
# What an atom is (see `Atom`).
|
|
78
|
+
_NONE_ATOM: Final = "none"
|
|
79
|
+
_LITERAL_ATOM: Final = "literal"
|
|
80
|
+
_OPTIONAL: Final = "Optional"
|
|
81
|
+
_LITERAL: Final = "Literal"
|
|
82
|
+
_ANNOTATED: Final = "Annotated"
|
|
83
|
+
_UNIONS: Final = frozenset({"Union", "Optional"}) # `_OPTIONAL` too
|
|
84
|
+
_TYPE_VARIABLE: Final = "t" # `Accepts`' key for an unbounded type variable
|
|
85
|
+
_SELF: Final = "Self"
|
|
86
|
+
_ANY: Final = "typing.Any" # a pattern's anything
|
|
87
|
+
_RENAMED: Final = "_alias" # an alias's type parameter, renamed apart (see `Expansion`)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def read_all(
|
|
91
|
+
reader: "Reader",
|
|
92
|
+
module: Module,
|
|
93
|
+
written: tuple[Signature, ...],
|
|
94
|
+
variables: frozenset[str],
|
|
95
|
+
params: Mapping[str, str | None],
|
|
96
|
+
) -> tuple[ReadSignature, ...]:
|
|
97
|
+
"""Read a function's (or method's) signatures, `params` its class's type parameters.
|
|
98
|
+
|
|
99
|
+
A parameter every signature declares the same way takes whatever a call passes it, unless it
|
|
100
|
+
names a type variable (the module's, the class's in `variables`, or the signature's own).
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Them.
|
|
104
|
+
|
|
105
|
+
"""
|
|
106
|
+
declared: Declarations | None = module.declared # none: a checked file's (see `read_signatures`)
|
|
107
|
+
shared: set[tuple[str, str, bool, str | None]] = set(written[0].params)
|
|
108
|
+
signature: Signature
|
|
109
|
+
for signature in written[1:]:
|
|
110
|
+
shared &= set(signature.params)
|
|
111
|
+
named: frozenset[str] = frozenset(
|
|
112
|
+
{
|
|
113
|
+
*(() if declared is None else declared.variables),
|
|
114
|
+
*module.type_vars,
|
|
115
|
+
*variables,
|
|
116
|
+
*(param for each in written for param, _ in each.type_params),
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
shared = {param for param in shared if not _names_any(param[3], named)}
|
|
120
|
+
return tuple(reader.signature(module, signature, shared, params) for signature in written)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _names(text: str) -> frozenset[str]:
|
|
124
|
+
"""Name the bare names a template or pattern uses (its type variables').
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Them.
|
|
128
|
+
|
|
129
|
+
"""
|
|
130
|
+
return frozenset(node.id for node in ast.walk(parse_text(text)) if isinstance(node, ast.Name))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _names_any(annotation: str | None, names: frozenset[str]) -> bool:
|
|
134
|
+
"""Check whether an annotation names any of `names`.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Whether it does.
|
|
138
|
+
|
|
139
|
+
"""
|
|
140
|
+
if annotation is None:
|
|
141
|
+
return False
|
|
142
|
+
node: ast.AST
|
|
143
|
+
for node in ast.walk(parse_text(annotation)):
|
|
144
|
+
if isinstance(node, ast.Name) and node.id in names:
|
|
145
|
+
return True
|
|
146
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str) and _names_any(node.value, names):
|
|
147
|
+
return True # a forward reference
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class Reader(Resolver):
|
|
152
|
+
"""Reads one function's signatures (see the module docstring)."""
|
|
153
|
+
|
|
154
|
+
def __init__(self, modules: Mapping[str, Module]) -> None:
|
|
155
|
+
"""Read in `modules`."""
|
|
156
|
+
super().__init__(modules)
|
|
157
|
+
self.matching: bool = False # writing a pattern to match a receiver against (see `pattern`)
|
|
158
|
+
|
|
159
|
+
def signature(
|
|
160
|
+
self,
|
|
161
|
+
module: Module,
|
|
162
|
+
signature: Signature,
|
|
163
|
+
shared: set[tuple[str, str, bool, str | None]],
|
|
164
|
+
types: Mapping[str, str | None],
|
|
165
|
+
) -> ReadSignature:
|
|
166
|
+
"""Read one signature: what each parameter takes, and its return as a template.
|
|
167
|
+
|
|
168
|
+
A parameter every signature declares the same way, naming no type variable, takes whatever a
|
|
169
|
+
call passes it (as the tables' do): only those that tell the signatures apart, or bind a
|
|
170
|
+
type variable, are read.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
It.
|
|
174
|
+
|
|
175
|
+
"""
|
|
176
|
+
scope: Scope = Scope(module, {**types, **dict(signature.type_params)}, module.name)
|
|
177
|
+
params: tuple[Parameter, ...] = tuple(
|
|
178
|
+
(
|
|
179
|
+
param[0],
|
|
180
|
+
param[1],
|
|
181
|
+
param[2],
|
|
182
|
+
None if param in shared or param[3] is None else self._accepted(param[3], scope),
|
|
183
|
+
)
|
|
184
|
+
for param in signature.params
|
|
185
|
+
)
|
|
186
|
+
returns: str | None = (
|
|
187
|
+
None
|
|
188
|
+
if signature.returns is None
|
|
189
|
+
else self._template(ast.parse(signature.returns, mode="eval").body, scope, 0)
|
|
190
|
+
)
|
|
191
|
+
if signature.self_type is None:
|
|
192
|
+
return ReadSignature(params, returns, None)
|
|
193
|
+
# A method declaring its `self` (`self: NDArray[ScalarT]`) is for such instances alone.
|
|
194
|
+
receiver: str | None = self.pattern(parse_text(signature.self_type), scope)
|
|
195
|
+
# Each type variable `self`'s pattern names, bounded (its constraints, for a constrained one).
|
|
196
|
+
variables: Mapping[str, Variable] = cast("Declarations", module.declared).variables
|
|
197
|
+
bounds: list[tuple[str, str]] = []
|
|
198
|
+
name: str
|
|
199
|
+
for name in sorted(_names(receiver or "")):
|
|
200
|
+
bound: str | tuple[ast.expr, Scope] | None = (
|
|
201
|
+
scope.params[name]
|
|
202
|
+
if name in scope.params
|
|
203
|
+
else variables[name].bound
|
|
204
|
+
if name in variables
|
|
205
|
+
else None
|
|
206
|
+
)
|
|
207
|
+
pattern: str | None = (
|
|
208
|
+
self.pattern(parse_text(bound), scope)
|
|
209
|
+
if isinstance(bound, str) and bound != PARAM_SPEC
|
|
210
|
+
else None
|
|
211
|
+
)
|
|
212
|
+
if pattern is not None:
|
|
213
|
+
bounds.append((name, pattern))
|
|
214
|
+
return ReadSignature(
|
|
215
|
+
params,
|
|
216
|
+
returns,
|
|
217
|
+
None if receiver is not None else [MAYBE],
|
|
218
|
+
receiver,
|
|
219
|
+
tuple(bounds),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def pattern(self, expr: ast.expr, scope: Scope) -> str | None:
|
|
223
|
+
"""Write an annotation as a pattern for a receiver's type to match (see `ReadSignature.receiver`).
|
|
224
|
+
|
|
225
|
+
As a template, but every class by where it's defined (`builtins.tuple`), aliases written out,
|
|
226
|
+
and anything (`Any`, `object`, a type variable from elsewhere) as `typing.Any`.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
It, or `None` if it names something that can't be matched (a `Literal`).
|
|
230
|
+
|
|
231
|
+
"""
|
|
232
|
+
self.matching = True
|
|
233
|
+
try:
|
|
234
|
+
return self._template(expr, scope, 0)
|
|
235
|
+
finally:
|
|
236
|
+
self.matching = False
|
|
237
|
+
|
|
238
|
+
def _accepted(self, text: str, scope: Scope) -> Accepts:
|
|
239
|
+
"""Work out what a parameter annotated `text` takes, once for each module and scope it's read in.
|
|
240
|
+
|
|
241
|
+
A package's signatures write the same few hundred annotations over and over (`Axis`,
|
|
242
|
+
`bool`, `str | None`).
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
It (see `_accepts`), shared by every parameter written alike: to read, never to change.
|
|
246
|
+
|
|
247
|
+
"""
|
|
248
|
+
# A signature's scope holds bounds' texts alone (see `signature`): no alias's argument.
|
|
249
|
+
bounds: Mapping[str, str | None] = cast("Mapping[str, str | None]", scope.params)
|
|
250
|
+
if not scope.module.installed: # a checked file's: by the index's classes, which the memo outlives
|
|
251
|
+
return self._accepts(parse_text(text), scope)
|
|
252
|
+
memo: dict[Asked, Accepts] = MEMO.of(self.modules).accepts
|
|
253
|
+
key: Asked
|
|
254
|
+
if (key := (scope.module.name, scope.owner, text, tuple(sorted(bounds.items())))) not in memo:
|
|
255
|
+
memo[key] = self._accepts(parse_text(text), scope)
|
|
256
|
+
return memo[key]
|
|
257
|
+
|
|
258
|
+
def _accepts(self, annotation: ast.expr, scope: Scope) -> Accepts:
|
|
259
|
+
"""Work out what a parameter takes (see `stdlib.Accepts`).
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
It.
|
|
263
|
+
|
|
264
|
+
"""
|
|
265
|
+
atoms: list[Atom] = list(self._atoms(annotation, scope, 0))
|
|
266
|
+
literals: list[Constant] = [value for atom in atoms for value in atom.values]
|
|
267
|
+
values: str = "".join(verdict_of(atoms, scalar, constant=False) for scalar in SCALARS)
|
|
268
|
+
found: Accepts = {"v": values, CLASS_VERDICT: combined({class_verdict(atom) for atom in atoms})}
|
|
269
|
+
if literals:
|
|
270
|
+
found["lit"] = literals
|
|
271
|
+
constants: str = "".join(verdict_of(atoms, scalar, constant=True) for scalar in SCALARS)
|
|
272
|
+
if constants != values:
|
|
273
|
+
found["c"] = constants
|
|
274
|
+
binds: dict[str, list[str]] = {}
|
|
275
|
+
scalar: str
|
|
276
|
+
for scalar in SCALARS:
|
|
277
|
+
bound: tuple[str, str] | None
|
|
278
|
+
if (bound := scalar_binds(atoms, scalar, scope.owner)) is not None:
|
|
279
|
+
binds[scalar] = [bound[0], bound[1]]
|
|
280
|
+
if binds:
|
|
281
|
+
found["var"] = binds
|
|
282
|
+
if (
|
|
283
|
+
len(atoms) == 1
|
|
284
|
+
and atoms[0].kind == VAR
|
|
285
|
+
and atoms[0].bound is None
|
|
286
|
+
and owned(atoms[0], scope.owner)
|
|
287
|
+
):
|
|
288
|
+
found[_TYPE_VARIABLE] = atoms[0].name
|
|
289
|
+
variable: str | None
|
|
290
|
+
if (variable := class_binds(atoms, scope.owner)) is not None:
|
|
291
|
+
found[CLASS_BINDS] = variable
|
|
292
|
+
if class_bounded(atoms):
|
|
293
|
+
found[CLASS_BOUNDED] = True
|
|
294
|
+
self._containers(atoms, scope, found)
|
|
295
|
+
if not scope.module.installed: # a checked file's: by the checked files' classes it names
|
|
296
|
+
own_types.accept(self.modules, atoms, lambda arg, where: self._atoms(arg, where, 1), found)
|
|
297
|
+
return found
|
|
298
|
+
|
|
299
|
+
def _containers(self, atoms: Sequence[Atom], scope: Scope, found: Accepts) -> None:
|
|
300
|
+
"""Add what a parameter takes of each builtin container argument to `found` (see `Accepts`).
|
|
301
|
+
|
|
302
|
+
A container every member refuses, or one member takes (its elements' types too, where that
|
|
303
|
+
member says: `tuple[int, ...]`); and the bounded type variable it binds, where the parameter
|
|
304
|
+
is only that (`ShapeT`, bound to `tuple[int, ...]`).
|
|
305
|
+
"""
|
|
306
|
+
verdicts: dict[str, str] = {}
|
|
307
|
+
elements: dict[str, str] = {}
|
|
308
|
+
container: str
|
|
309
|
+
for container in CONTAINERS:
|
|
310
|
+
each: list[Taken] = [self._container(atom, container, 0) for atom in atoms]
|
|
311
|
+
verdict: str
|
|
312
|
+
if (verdict := combined({taken for taken, _ in each})) != MAYBE:
|
|
313
|
+
verdicts[container] = verdict
|
|
314
|
+
taking: list[tuple[Atom, ...] | None] = [inner for taken, inner in each if taken != NO]
|
|
315
|
+
if verdict == YES and len(taking) == 1 and taking[0] is not None:
|
|
316
|
+
elements[container] = "".join(
|
|
317
|
+
verdict_of(taking[0], scalar, constant=False) for scalar in SCALARS
|
|
318
|
+
)
|
|
319
|
+
if verdicts:
|
|
320
|
+
found[CONTAINER_VERDICTS] = verdicts
|
|
321
|
+
if elements:
|
|
322
|
+
found[ELEMENT_VERDICTS] = elements
|
|
323
|
+
only: Atom | None = atoms[0] if len(atoms) == 1 and atoms[0].kind == VAR else None
|
|
324
|
+
if (
|
|
325
|
+
only is not None
|
|
326
|
+
and only.bound not in {None, PARAM_SPEC}
|
|
327
|
+
and not only.constrained
|
|
328
|
+
and owned(only, scope.owner)
|
|
329
|
+
):
|
|
330
|
+
found[CONTAINER_BINDS] = only.name
|
|
331
|
+
|
|
332
|
+
def _container(self, atom: Atom, container: str, hops: int) -> Taken:
|
|
333
|
+
"""Decide whether one atom takes a builtin `container` argument, and what its elements must be.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
The verdict, and the atoms its elements' type must fit where the atom says (`list[int]`,
|
|
337
|
+
`tuple[int, ...]`); `None` where it doesn't.
|
|
338
|
+
|
|
339
|
+
"""
|
|
340
|
+
verdict: str = scalar_verdict(atom, container, constant=False)
|
|
341
|
+
elements: tuple[Atom, ...] | None = None
|
|
342
|
+
if atom.kind == VAR and atom.bound not in {None, PARAM_SPEC} and hops < ALIAS_DEPTH:
|
|
343
|
+
each: list[Taken] = [self._container(inner, container, hops + 1) for inner in atom.inner]
|
|
344
|
+
taking: list[tuple[Atom, ...] | None] = [inner for taken, inner in each if taken != NO]
|
|
345
|
+
verdict = combined({taken for taken, _ in each})
|
|
346
|
+
elements = taking[0] if len(taking) == 1 else None
|
|
347
|
+
elif atom.kind == CLASS and atom.module is None and atom_path(atom) == f"builtins.{container}":
|
|
348
|
+
verdict = YES
|
|
349
|
+
of: ast.expr | None = element_of(container, atom.args)
|
|
350
|
+
if atom.args and (of is None or atom.scope is None):
|
|
351
|
+
verdict = MAYBE # `tuple[int, str]`, `dict[str, int]`: their elements aren't read
|
|
352
|
+
elif of is not None and atom.scope is not None:
|
|
353
|
+
elements = tuple(self._atoms(of, atom.scope, hops + 1))
|
|
354
|
+
return verdict, elements
|
|
355
|
+
|
|
356
|
+
def _atoms(self, expr: ast.expr, scope: Scope, hops: int) -> Iterator[Atom]:
|
|
357
|
+
"""Split an annotation into its union's members, through aliases, `Optional`, `Union`, strings.
|
|
358
|
+
|
|
359
|
+
Yields:
|
|
360
|
+
Each member.
|
|
361
|
+
|
|
362
|
+
"""
|
|
363
|
+
text: str
|
|
364
|
+
left: ast.expr
|
|
365
|
+
right: ast.expr
|
|
366
|
+
if hops > ALIAS_DEPTH:
|
|
367
|
+
yield UNKNOWN
|
|
368
|
+
return
|
|
369
|
+
match expr:
|
|
370
|
+
case ast.Constant(value=None):
|
|
371
|
+
yield Atom(_NONE_ATOM)
|
|
372
|
+
case ast.Constant(value=str() as text):
|
|
373
|
+
yield from self._atoms(parse_text(text), scope, hops + 1)
|
|
374
|
+
case ast.BinOp(left=left, op=ast.BitOr(), right=right):
|
|
375
|
+
yield from self._atoms(left, scope, hops + 1)
|
|
376
|
+
yield from self._atoms(right, scope, hops + 1)
|
|
377
|
+
case ast.Subscript():
|
|
378
|
+
yield from self._subscript_atoms(expr, scope, hops)
|
|
379
|
+
case ast.Name() | ast.Attribute():
|
|
380
|
+
yield from self._named_atoms(expr, scope, hops, ())
|
|
381
|
+
case _:
|
|
382
|
+
yield UNKNOWN
|
|
383
|
+
|
|
384
|
+
def _named_atoms(
|
|
385
|
+
self,
|
|
386
|
+
expr: ast.Name | ast.Attribute,
|
|
387
|
+
scope: Scope,
|
|
388
|
+
hops: int,
|
|
389
|
+
args: Sequence[ast.expr],
|
|
390
|
+
) -> Iterator[Atom]:
|
|
391
|
+
"""Read a name an annotation uses, subscripted with `args` (none if it isn't).
|
|
392
|
+
|
|
393
|
+
Yields:
|
|
394
|
+
Its atoms: an alias's, bound by `args`; else the one it is.
|
|
395
|
+
|
|
396
|
+
"""
|
|
397
|
+
resolved: Resolved | None
|
|
398
|
+
if (resolved := self.resolve(expr, scope)) is None:
|
|
399
|
+
yield UNKNOWN
|
|
400
|
+
elif not isinstance(resolved, Atom):
|
|
401
|
+
alias: Alias
|
|
402
|
+
where: Scope
|
|
403
|
+
alias, where, _ = resolved
|
|
404
|
+
yield from self._atoms(parse_text(alias.value), bound_alias(alias, where, args, scope), hops + 1)
|
|
405
|
+
elif resolved.kind == ARG:
|
|
406
|
+
yield from self._atoms(*argument_of(resolved), hops + 1)
|
|
407
|
+
elif resolved.kind == VAR and resolved.scope is not None and resolved.bound not in {None, PARAM_SPEC}:
|
|
408
|
+
bound: ast.expr = parse_text(resolved.bound or "")
|
|
409
|
+
yield resolved._replace(inner=tuple(self._atoms(bound, resolved.scope, hops + 1)))
|
|
410
|
+
else:
|
|
411
|
+
yield resolved._replace(subscripted=bool(args), args=tuple(args), scope=scope)
|
|
412
|
+
|
|
413
|
+
def _subscript_atoms(self, expr: ast.Subscript, scope: Scope, hops: int) -> Iterator[Atom]:
|
|
414
|
+
args: list[ast.expr] = list(expr.slice.elts) if isinstance(expr.slice, ast.Tuple) else [expr.slice]
|
|
415
|
+
base: ast.expr = expr.value
|
|
416
|
+
if not isinstance(base, ast.Name | ast.Attribute):
|
|
417
|
+
yield UNKNOWN
|
|
418
|
+
return
|
|
419
|
+
path: str | None = self._external(base, scope)
|
|
420
|
+
if path is not None and path.rpartition(".")[0] in TYPING:
|
|
421
|
+
name: str = path.rpartition(".")[2]
|
|
422
|
+
if name in _UNIONS:
|
|
423
|
+
members: list[ast.expr] = [*args, *([ast.Constant(None)] if name == _OPTIONAL else [])]
|
|
424
|
+
yield from (atom for member in members for atom in self._atoms(member, scope, hops + 1))
|
|
425
|
+
return
|
|
426
|
+
if name == _LITERAL:
|
|
427
|
+
yield Atom(_LITERAL_ATOM, tuple(literal_values(args)))
|
|
428
|
+
return
|
|
429
|
+
if name == _ANNOTATED:
|
|
430
|
+
yield from self._atoms(args[0], scope, hops + 1)
|
|
431
|
+
return
|
|
432
|
+
if path in TYPES:
|
|
433
|
+
yield Atom(TYPE, inner=tuple(self._atoms(args[0], scope, hops + 1)))
|
|
434
|
+
return
|
|
435
|
+
yield from self._named_atoms(base, scope, hops, args)
|
|
436
|
+
|
|
437
|
+
def _external(self, expr: ast.Name | ast.Attribute, scope: Scope) -> str | None:
|
|
438
|
+
"""Name what an annotation's name refers to outside the installed modules (`typing.Union`).
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
Its dotted path, or `None` if it's an installed module's, or unknown.
|
|
442
|
+
|
|
443
|
+
"""
|
|
444
|
+
resolved: Resolved | None = self.resolve(expr, scope)
|
|
445
|
+
if not isinstance(resolved, Atom) or resolved.kind != CLASS or resolved.module is not None:
|
|
446
|
+
return None
|
|
447
|
+
return atom_path(resolved)
|
|
448
|
+
|
|
449
|
+
def method(self, module: Module, klass: Class, name: str, hops: int) -> tuple[ReadSignature, ...] | None:
|
|
450
|
+
"""Read a class's method (its own, else its nearest base's that has it) as a function's are.
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
Its signatures, or `None` if neither it nor a base it can follow declares it.
|
|
454
|
+
|
|
455
|
+
"""
|
|
456
|
+
written: tuple[Signature, ...] | None
|
|
457
|
+
if (written := klass.methods.get(name)) is not None:
|
|
458
|
+
return read_all(
|
|
459
|
+
self,
|
|
460
|
+
module,
|
|
461
|
+
written,
|
|
462
|
+
frozenset(name for name, _ in klass.params),
|
|
463
|
+
dict(klass.params),
|
|
464
|
+
)
|
|
465
|
+
base: str
|
|
466
|
+
for base in klass.bases if hops < ALIAS_DEPTH else ():
|
|
467
|
+
resolved: Resolved | None = self.resolved_base(base, module)
|
|
468
|
+
if (
|
|
469
|
+
isinstance(resolved, Atom)
|
|
470
|
+
and resolved.module is not None
|
|
471
|
+
and resolved.module.declared is not None
|
|
472
|
+
):
|
|
473
|
+
inherited: Class | None = resolved.module.declared.classes.get(resolved.origin[1] or "")
|
|
474
|
+
read: tuple[ReadSignature, ...] | None = (
|
|
475
|
+
None if inherited is None else self.method(resolved.module, inherited, name, hops + 1)
|
|
476
|
+
)
|
|
477
|
+
if read is not None:
|
|
478
|
+
return read
|
|
479
|
+
return None
|
|
480
|
+
|
|
481
|
+
def expansion(self, origin: Origin) -> Expanded | None:
|
|
482
|
+
"""Expand an installed alias of a generic class (`NDArray`), as a receiver's type (see `Expansion`).
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
The class, the expansion, and the installed classes its pattern names; or `None` for an
|
|
486
|
+
alias of anything else (a union, a protocol, another alias).
|
|
487
|
+
|
|
488
|
+
"""
|
|
489
|
+
module: Module = self.modules[origin[0]]
|
|
490
|
+
alias: Alias = cast("Declarations", module.declared).aliases[origin[1] or ""]
|
|
491
|
+
value: ast.expr = parse_text(alias.value)
|
|
492
|
+
own: Scope = Scope(module, {}, module.name)
|
|
493
|
+
base: ast.Name | ast.Attribute
|
|
494
|
+
index: ast.expr
|
|
495
|
+
match value:
|
|
496
|
+
case ast.Subscript(value=ast.Name() | ast.Attribute() as base, slice=index):
|
|
497
|
+
pass
|
|
498
|
+
case _:
|
|
499
|
+
return None
|
|
500
|
+
head: Resolved | None = self.resolve(base, own)
|
|
501
|
+
if not isinstance(head, Atom) or head.kind != CLASS or head.module is None:
|
|
502
|
+
return None
|
|
503
|
+
klass: Class | None = cast("Declarations", head.module.declared).classes.get(head.origin[1] or "")
|
|
504
|
+
params: tuple[str, ...] = alias.params or free_variables(alias.value, own)
|
|
505
|
+
scope: Scope = Scope(module, dict.fromkeys(params), module.name)
|
|
506
|
+
receiver: str | None = self.pattern(value, scope)
|
|
507
|
+
if klass is None or receiver is None:
|
|
508
|
+
return None
|
|
509
|
+
renamed: dict[str, str] = {param: f"{_RENAMED}{place}" for place, param in enumerate(params)}
|
|
510
|
+
args: list[ast.expr] = list(index.elts) if isinstance(index, ast.Tuple) else [index]
|
|
511
|
+
return (
|
|
512
|
+
head.origin,
|
|
513
|
+
Expansion(
|
|
514
|
+
tuple(renamed.values()),
|
|
515
|
+
substituted(receiver, renamed),
|
|
516
|
+
self._templates(klass, args, scope, renamed),
|
|
517
|
+
),
|
|
518
|
+
self._named(receiver),
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
def _templates(
|
|
522
|
+
self,
|
|
523
|
+
klass: Class,
|
|
524
|
+
args: Sequence[ast.expr],
|
|
525
|
+
scope: Scope,
|
|
526
|
+
renamed: Mapping[str, str],
|
|
527
|
+
) -> tuple[tuple[str, str], ...]:
|
|
528
|
+
"""Write what an alias passes each of its class's type parameters, where it can be written.
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
Each parameter with its template, the alias's own parameters `renamed`.
|
|
532
|
+
|
|
533
|
+
"""
|
|
534
|
+
found: list[tuple[str, str]] = []
|
|
535
|
+
param: str
|
|
536
|
+
arg: ast.expr
|
|
537
|
+
for (param, _), arg in zip(klass.params, args, strict=False): # a class's own subscript fits
|
|
538
|
+
template: str | None
|
|
539
|
+
if (template := self._template(arg, scope, 0)) is not None:
|
|
540
|
+
found.append((param, substituted(template, renamed)))
|
|
541
|
+
return tuple(found)
|
|
542
|
+
|
|
543
|
+
def _named(self, pattern: str) -> tuple[tuple[str, Origin], ...]:
|
|
544
|
+
"""Find the installed classes a pattern names.
|
|
545
|
+
|
|
546
|
+
Returns:
|
|
547
|
+
Each by its path there, and where it's defined.
|
|
548
|
+
|
|
549
|
+
"""
|
|
550
|
+
found: dict[str, Origin] = {}
|
|
551
|
+
node: ast.AST
|
|
552
|
+
for node in ast.walk(parse_text(pattern)):
|
|
553
|
+
path: str = ast.unparse(node) if isinstance(node, ast.Attribute) else ""
|
|
554
|
+
where: Origin = (path.rpartition(".")[0], path.rpartition(".")[2])
|
|
555
|
+
if path and classnames.declares(self.modules, where):
|
|
556
|
+
found[path] = where
|
|
557
|
+
return tuple(found.items())
|
|
558
|
+
|
|
559
|
+
def _template(self, expr: ast.expr, scope: Scope, hops: int) -> str | None:
|
|
560
|
+
"""Write an annotation as a template: classes by their public dotted paths, type variables bare.
|
|
561
|
+
|
|
562
|
+
A private alias is written as what it stands for, its parameters bound by its arguments.
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
The template, or `None` if it names something `--fix` can't write (`Any`, a `Literal`, a
|
|
566
|
+
class with no public path, a type variable from elsewhere).
|
|
567
|
+
|
|
568
|
+
"""
|
|
569
|
+
text: str
|
|
570
|
+
left: ast.expr
|
|
571
|
+
right: ast.expr
|
|
572
|
+
base: ast.Name | ast.Attribute
|
|
573
|
+
found: str | None = None
|
|
574
|
+
match expr:
|
|
575
|
+
case _ if hops > ALIAS_DEPTH:
|
|
576
|
+
pass
|
|
577
|
+
case ast.Constant(value=None):
|
|
578
|
+
found = NONE
|
|
579
|
+
case ast.Constant(value=builtins.Ellipsis):
|
|
580
|
+
found = "..."
|
|
581
|
+
case ast.Constant(value=str() as text):
|
|
582
|
+
found = self._template(parse_text(text), scope, hops + 1)
|
|
583
|
+
case ast.BinOp(left=left, op=ast.BitOr(), right=right):
|
|
584
|
+
found = joined(" | ", [self._template(side, scope, hops + 1) for side in (left, right)])
|
|
585
|
+
case ast.Subscript(value=ast.Name() | ast.Attribute() as base):
|
|
586
|
+
args: list[ast.expr] = (
|
|
587
|
+
list(expr.slice.elts) if isinstance(expr.slice, ast.Tuple) else [expr.slice]
|
|
588
|
+
)
|
|
589
|
+
found = self._named_template(base, scope, hops, args)
|
|
590
|
+
case ast.Name() | ast.Attribute():
|
|
591
|
+
found = self._named_template(expr, scope, hops, [])
|
|
592
|
+
case _:
|
|
593
|
+
pass
|
|
594
|
+
return found
|
|
595
|
+
|
|
596
|
+
def _named_template(
|
|
597
|
+
self,
|
|
598
|
+
expr: ast.Name | ast.Attribute,
|
|
599
|
+
scope: Scope,
|
|
600
|
+
hops: int,
|
|
601
|
+
args: Sequence[ast.expr],
|
|
602
|
+
) -> str | None:
|
|
603
|
+
"""Write a name, subscripted with `args` (none if it isn't), as a template.
|
|
604
|
+
|
|
605
|
+
Returns:
|
|
606
|
+
It, or `None`.
|
|
607
|
+
|
|
608
|
+
"""
|
|
609
|
+
resolved: Resolved | None = self.resolve(expr, scope)
|
|
610
|
+
written: str | None
|
|
611
|
+
if resolved is None:
|
|
612
|
+
return None
|
|
613
|
+
if not isinstance(resolved, Atom):
|
|
614
|
+
alias: Alias
|
|
615
|
+
where: Scope
|
|
616
|
+
origin: Origin
|
|
617
|
+
alias, where, origin = resolved
|
|
618
|
+
public: Origin = project.public_origin(self.modules, origin)
|
|
619
|
+
if not self.matching and not private_name(public[0]) and not private_name(public[1]):
|
|
620
|
+
written = f"{public[0]}.{public[1]}"
|
|
621
|
+
elif alias.params or free_variables(alias.value, where) or not args:
|
|
622
|
+
return self._template(
|
|
623
|
+
parse_text(alias.value),
|
|
624
|
+
bound_alias(alias, where, args, scope),
|
|
625
|
+
hops + 1,
|
|
626
|
+
)
|
|
627
|
+
else: # another name for a generic class (`_dtype = dtype`): its arguments are the class's
|
|
628
|
+
written = self._template(parse_text(alias.value), where, hops + 1)
|
|
629
|
+
elif resolved.kind == ARG:
|
|
630
|
+
return self._template(*argument_of(resolved), hops + 1)
|
|
631
|
+
elif resolved.kind == VAR:
|
|
632
|
+
written = resolved.name if owned(resolved, scope.owner) else _ANY if self.matching else None
|
|
633
|
+
else:
|
|
634
|
+
written = self._class_path(resolved)
|
|
635
|
+
if written is None or not args:
|
|
636
|
+
return written
|
|
637
|
+
texts: list[str | None] = [self._template(arg, scope, hops + 1) for arg in args]
|
|
638
|
+
return None if None in texts else f"{written}[{', '.join(t for t in texts if t is not None)}]"
|
|
639
|
+
|
|
640
|
+
def _class_path(self, atom: Atom) -> str | None:
|
|
641
|
+
"""Write a class as a template names it: a builtin bare, any other by its public dotted path.
|
|
642
|
+
|
|
643
|
+
Returns:
|
|
644
|
+
It, or `None` for `typing`'s forms, `object`, `type`, or a class with no public path.
|
|
645
|
+
|
|
646
|
+
"""
|
|
647
|
+
origin: Origin = atom.origin
|
|
648
|
+
if self.matching:
|
|
649
|
+
return _ANY if atom_path(atom) in ANYTHING else None if origin[0] in TYPING else atom_path(atom)
|
|
650
|
+
if atom.module is not None:
|
|
651
|
+
origin = project.public_origin(self.modules, origin)
|
|
652
|
+
elif origin[0] == BUILTINS_MODULE:
|
|
653
|
+
return origin[1] if origin[1] not in {"object", "type"} else None
|
|
654
|
+
elif origin[0] in TYPING:
|
|
655
|
+
return _SELF if origin[1] == _SELF else None # a method's `Self`: the receiver's type
|
|
656
|
+
return None if private_name(origin[0]) or private_name(origin[1]) else f"{origin[0]}.{origin[1]}"
|
|
Binary file
|