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,578 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""What a member of a value whose type is known gives: an attribute, a method call, a subscript.
|
|
3
|
+
|
|
4
|
+
`inference` works out the receiver's type (a local, `self.index`, `f()`, `x[0]`, anything it can
|
|
5
|
+
type), and `member` looks the member up on it through `SOURCES`, in order: the first that knows it
|
|
6
|
+
decides. Each source is certain, whatever the call's arguments are; a receiver that's only guessed
|
|
7
|
+
makes its member a guess too, as `guesses` judges.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
from collections.abc import Callable, Sequence
|
|
12
|
+
from functools import lru_cache
|
|
13
|
+
from typing import TYPE_CHECKING, Final, TypeAlias
|
|
14
|
+
|
|
15
|
+
from constricter.fix.core.imports import added_dotted
|
|
16
|
+
from constricter.fix.core.known import ImportPlan, Inference, Known
|
|
17
|
+
from constricter.fix.core.signatures import AWAIT
|
|
18
|
+
from constricter.fix.libraries import overloads, stdlib
|
|
19
|
+
from constricter.fix.libraries.library import library_awaited
|
|
20
|
+
from constricter.fix.values.returns import METHOD_RETURNS, element_method, uniform_method
|
|
21
|
+
from constricter.fix.values.targets import sole
|
|
22
|
+
from constricter.rules.annotations import dotted, node_name, roots, vague_fits
|
|
23
|
+
from constricter.rules.keys import key, optional
|
|
24
|
+
from constricter.rules.quoted import unqualified
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from constricter.fix.core.inherited import Lineage
|
|
28
|
+
|
|
29
|
+
_ATTRIBUTE: Final = "attribute" # the fix kind of an attribute's annotation
|
|
30
|
+
_METHOD: Final = "method" # the fix kind of a method's return type
|
|
31
|
+
_AWAIT: Final = "await" # and of what awaiting a checked file's `async def` gives
|
|
32
|
+
_RETURNED: Final = "returned" # and of what `return`s alone decide
|
|
33
|
+
_GENERIC: Final = "[" # in a base's text: a generic class's arguments
|
|
34
|
+
_SLICE: Final = "slice" # an index of this type slices
|
|
35
|
+
_INT: Final = "int"
|
|
36
|
+
_STR: Final = "str"
|
|
37
|
+
_SUBSCRIPT: Final = "subscript" # the fix kind of a subscript
|
|
38
|
+
_OPTIONAL: Final = "Optional"
|
|
39
|
+
# One way to type a member: given the receiver's type as text, the member's name, and the call
|
|
40
|
+
# (`None` for an attribute), its inference, or `None` if this source doesn't know it.
|
|
41
|
+
MemberSource: TypeAlias = Callable[[str, str, ast.Call | None, Known], Inference | None]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@lru_cache(maxsize=4096)
|
|
45
|
+
def parsed(annotation: str) -> ast.expr:
|
|
46
|
+
"""Parse an annotation `--fix` wrote or read, once for all its lookups.
|
|
47
|
+
|
|
48
|
+
`annotation` is always `ast.unparse`'s own output (an annotation, or an earlier inference),
|
|
49
|
+
never user text, so it's always valid Python. The tree is shared: only read it.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Its expression.
|
|
53
|
+
|
|
54
|
+
"""
|
|
55
|
+
return ast.parse(annotation, mode="eval").body
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@lru_cache(maxsize=4096) # a module's fixes are a few types, each asked again and again
|
|
59
|
+
def fits(annotation: str, level: int) -> bool:
|
|
60
|
+
"""Check that a type, as text, is no vaguer than `level` allows (see `vague_fits`).
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Whether it is.
|
|
64
|
+
|
|
65
|
+
"""
|
|
66
|
+
return vague_fits(parsed(annotation), level)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def class_of(receiver: str) -> str | None:
|
|
70
|
+
"""Read the class a `type[C]` names (what `cls` is in a classmethod of `C`).
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
`C`, or `None` if `receiver` isn't a `type[...]` of a plain name.
|
|
74
|
+
|
|
75
|
+
"""
|
|
76
|
+
name: str
|
|
77
|
+
match parsed(receiver):
|
|
78
|
+
case ast.Subscript(value=ast.Name(id="type"), slice=ast.Name(id=name)):
|
|
79
|
+
return name
|
|
80
|
+
case _:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def present(receiver: str, attr: str) -> str:
|
|
85
|
+
"""Read the `X` of a receiver typed `X | None` (or `Optional[X]`), whose member `attr` is looked up.
|
|
86
|
+
|
|
87
|
+
Only an `X` has the member: a type checker has narrowed the receiver to it there, or reports the
|
|
88
|
+
access. Not a member `None` has too (`__class__`), nor a union of more types.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
The `X` as text; any other receiver as it is.
|
|
92
|
+
|
|
93
|
+
"""
|
|
94
|
+
kept: ast.expr
|
|
95
|
+
head: ast.expr
|
|
96
|
+
match parsed(receiver):
|
|
97
|
+
case _ if attr.startswith("__"):
|
|
98
|
+
return receiver
|
|
99
|
+
case (
|
|
100
|
+
ast.BinOp(left=kept, op=ast.BitOr(), right=ast.Constant(value=None))
|
|
101
|
+
| ast.BinOp(left=ast.Constant(value=None), op=ast.BitOr(), right=kept)
|
|
102
|
+
) if not isinstance(kept, ast.BinOp):
|
|
103
|
+
return ast.unparse(kept)
|
|
104
|
+
case ast.Subscript(value=head, slice=kept) if node_name(head) == _OPTIONAL:
|
|
105
|
+
return ast.unparse(kept)
|
|
106
|
+
case _:
|
|
107
|
+
return receiver
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _class_side(receiver: str, name: str, call: ast.Call | None, known: Known) -> Inference | None:
|
|
111
|
+
"""Type a class's own attribute or classmethod, on a `type[C]` (`cls.limit`, `cls.build()`).
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Its inference, or `None`.
|
|
115
|
+
|
|
116
|
+
"""
|
|
117
|
+
owner: str | None
|
|
118
|
+
if (owner := class_of(receiver)) is None:
|
|
119
|
+
return None
|
|
120
|
+
if call is None:
|
|
121
|
+
found: str | None = known.class_side.attributes.get(owner, {}).get(name)
|
|
122
|
+
return (
|
|
123
|
+
None
|
|
124
|
+
if found is None
|
|
125
|
+
else Inference(found, _annotation_of(receiver, name), frozenset({_ATTRIBUTE}))
|
|
126
|
+
)
|
|
127
|
+
found = known.class_side.methods.get(owner, {}).get(name)
|
|
128
|
+
return None if found is None else Inference(found, _declared_return(receiver, name), frozenset({_METHOD}))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _fixed(receiver: str, name: str, call: ast.Call | None, _known: Known) -> Inference | None:
|
|
132
|
+
"""Type a `str`, `bytes`, `int` or `float` method with a fixed return type (`METHOD_RETURNS`).
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Its inference, or `None`.
|
|
136
|
+
|
|
137
|
+
"""
|
|
138
|
+
found: str | None = None if call is None else METHOD_RETURNS.get(receiver, {}).get(name)
|
|
139
|
+
return (
|
|
140
|
+
None
|
|
141
|
+
if found is None
|
|
142
|
+
else Inference(found, f"`{receiver}.{name}`'s fixed return type", frozenset({_METHOD}))
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _declared(
|
|
147
|
+
receiver: str,
|
|
148
|
+
name: str,
|
|
149
|
+
call: ast.Call | None,
|
|
150
|
+
known: Known,
|
|
151
|
+
prefix: str = "",
|
|
152
|
+
) -> Inference | None:
|
|
153
|
+
"""Type an annotated attribute or property, or a method's declared return, of a known class.
|
|
154
|
+
|
|
155
|
+
One this module defines, or another checked file does (see `Known.classes`, `Known.methods`). A
|
|
156
|
+
member the class doesn't define is the base's that does (see `Lineage`); one declared to return
|
|
157
|
+
`Self` there gives the receiver's own class, and one of another file's class returning that
|
|
158
|
+
class, which may be its `Self`, nothing. `prefix`: before a method's name in the tables, for an
|
|
159
|
+
`async def`'s (`AWAIT`): what awaiting its call gives.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Its inference, or `None`.
|
|
163
|
+
|
|
164
|
+
"""
|
|
165
|
+
found: str | None
|
|
166
|
+
if (found := None if call is not None else known.classes.get(receiver, {}).get(name)) is not None:
|
|
167
|
+
return _attribute(found, receiver, name)
|
|
168
|
+
lineage: Lineage = known.class_side.lineage
|
|
169
|
+
owner: str | None = _declarer(receiver, name, known) if call is None else lineage.definer(receiver, name)
|
|
170
|
+
found = (known.methods if call is not None else known.classes).get(owner or "", {}).get(prefix + name)
|
|
171
|
+
if found is None or owner is None:
|
|
172
|
+
return None
|
|
173
|
+
if call is None:
|
|
174
|
+
# Another file's base's: not one typed as that class, which may be its property's `Self`.
|
|
175
|
+
return None if found == owner else _attribute(found, owner, name)
|
|
176
|
+
if owner != receiver and name in lineage.selfish.get(owner, ()):
|
|
177
|
+
found = receiver
|
|
178
|
+
elif owner != receiver and owner not in lineage.bound and found == owner:
|
|
179
|
+
return None # another file's class, or its `Self`: the receiver's own class
|
|
180
|
+
return Inference(found, _declared_return(owner, name), frozenset({_METHOD}))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _attribute(found: str, owner: str, name: str) -> Inference | None:
|
|
184
|
+
"""Offer an attribute's declared type, less the `ClassVar` or `Final` around it.
|
|
185
|
+
|
|
186
|
+
The tables keep those: a subclass's variable a base declares a `ClassVar` isn't one to annotate.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
The inference; `None` for a bare `Final`, which leaves the type to the value.
|
|
190
|
+
|
|
191
|
+
"""
|
|
192
|
+
text: str = unqualified(parsed(found))
|
|
193
|
+
return Inference(text, _annotation_of(owner, name), frozenset({_ATTRIBUTE})) if text else None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def awaited(value: ast.expr, known: Known, infer: Callable[[ast.expr], Inference | None]) -> Inference | None:
|
|
197
|
+
"""Infer `await` of a call: a checked file's `async def`'s, or a standard-library coroutine's.
|
|
198
|
+
|
|
199
|
+
A function's by its declared return (`known.indirect.awaits`), and a method's on a receiver whose
|
|
200
|
+
type `infer` knows, the class's own or the base's that defines it (see `_declared`); else as
|
|
201
|
+
`library_awaited` has it.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
The inference, or `None`.
|
|
205
|
+
|
|
206
|
+
"""
|
|
207
|
+
func: ast.expr
|
|
208
|
+
receiver: ast.expr
|
|
209
|
+
name: str
|
|
210
|
+
match value:
|
|
211
|
+
case ast.Await(value=ast.Call(func=ast.Name() | ast.Attribute() as func)) if (
|
|
212
|
+
dotted(func) in known.indirect.awaits
|
|
213
|
+
):
|
|
214
|
+
spelled: str = ast.unparse(func)
|
|
215
|
+
reason: str = f"`{spelled}`'s declared return type, awaited"
|
|
216
|
+
return Inference(known.indirect.awaits[spelled], reason, frozenset({_AWAIT}))
|
|
217
|
+
case ast.Await(value=ast.Call(func=ast.Attribute(value=receiver, attr=name) as func)):
|
|
218
|
+
owner: Inference | None = infer(receiver)
|
|
219
|
+
found: Inference | None = (
|
|
220
|
+
None
|
|
221
|
+
if owner is None
|
|
222
|
+
else _declared(present(owner.annotation, name), name, ast.Call(func, [], []), known, AWAIT)
|
|
223
|
+
)
|
|
224
|
+
if owner is not None and found is not None:
|
|
225
|
+
kinds: frozenset[str] = found.kinds | {_AWAIT} | owner.kinds - {"copy"}
|
|
226
|
+
return found._replace(reason=f"{found.reason}, awaited", kinds=kinds)
|
|
227
|
+
case _:
|
|
228
|
+
pass
|
|
229
|
+
return _awaited_returns(value, known, infer) or library_awaited(value, known, infer)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _awaited_returns(
|
|
233
|
+
value: ast.expr,
|
|
234
|
+
known: Known,
|
|
235
|
+
infer: Callable[[ast.expr], Inference | None],
|
|
236
|
+
) -> Inference | None:
|
|
237
|
+
"""Infer `await` of a call to a checked file's `async def` declaring no return, by its `return`s.
|
|
238
|
+
|
|
239
|
+
A function's, or a method's on a receiver whose type `infer` knows (see `awaited_origins`).
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
The inference, or `None`.
|
|
243
|
+
|
|
244
|
+
"""
|
|
245
|
+
call: ast.Call
|
|
246
|
+
match value:
|
|
247
|
+
case ast.Await(value=ast.Call() as call):
|
|
248
|
+
found: tuple[str, str, frozenset[str] | None] | None
|
|
249
|
+
if (found := _loose_await(call, known, infer)) is None:
|
|
250
|
+
return None
|
|
251
|
+
reason: str = f"`{found[0].replace(AWAIT, '')}`'s `return`s, awaited"
|
|
252
|
+
return Inference(found[1], reason, (found[2] or frozenset()) | {_RETURNED})
|
|
253
|
+
case _:
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def awaited_origins(
|
|
258
|
+
call: ast.Call,
|
|
259
|
+
known: Known,
|
|
260
|
+
infer: Callable[[ast.expr], Inference | None],
|
|
261
|
+
) -> frozenset[str] | None:
|
|
262
|
+
"""Name what awaiting `call` rests on as a guess, where an `async def`'s `return`s type it.
|
|
263
|
+
|
|
264
|
+
A function's `return`s' own guesses; a method's too, and `returned`: a subclass may override it.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Them (none: it's certain), or `None` for any other call.
|
|
268
|
+
|
|
269
|
+
"""
|
|
270
|
+
found: tuple[str, str, frozenset[str] | None] | None
|
|
271
|
+
if (found := _loose_await(call, known, infer)) is None:
|
|
272
|
+
return None
|
|
273
|
+
rests: frozenset[str] = known.returned.guesses.get(found[0], frozenset())
|
|
274
|
+
return rests if found[2] is None else rests | {_RETURNED}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _loose_await(
|
|
278
|
+
call: ast.Call,
|
|
279
|
+
known: Known,
|
|
280
|
+
infer: Callable[[ast.expr], Inference | None],
|
|
281
|
+
) -> tuple[str, str, frozenset[str] | None] | None:
|
|
282
|
+
"""Find the `async def` declaring no return that `call` calls, typed by its `return`s (see `returned`).
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
Its key in `Returned.guesses`, what awaiting it gives, and for a method the fix kinds its
|
|
286
|
+
receiver adds (`None`: a function); or `None`.
|
|
287
|
+
|
|
288
|
+
"""
|
|
289
|
+
receiver: ast.expr
|
|
290
|
+
name: str
|
|
291
|
+
callee: str | None = dotted(call.func)
|
|
292
|
+
if callee is not None and f"{AWAIT}{callee}" in known.returned.calls:
|
|
293
|
+
return f"{AWAIT}{callee}", known.returned.calls[f"{AWAIT}{callee}"], None
|
|
294
|
+
match call.func:
|
|
295
|
+
case ast.Attribute(value=receiver, attr=name) if any(
|
|
296
|
+
f"{AWAIT}{name}" in methods for methods in known.returned.methods.values()
|
|
297
|
+
):
|
|
298
|
+
owner: Inference | None = infer(receiver)
|
|
299
|
+
defined: tuple[str, str] | None = (
|
|
300
|
+
None
|
|
301
|
+
if owner is None
|
|
302
|
+
else returned_method(present(owner.annotation, name), f"{AWAIT}{name}", known)
|
|
303
|
+
)
|
|
304
|
+
if owner is None or defined is None:
|
|
305
|
+
return None
|
|
306
|
+
return f"{defined[0]}.{AWAIT}{name}", defined[1], owner.kinds - {"copy"}
|
|
307
|
+
case _:
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _declarer(receiver: str, name: str, known: Known) -> str | None:
|
|
312
|
+
"""Find the base whose declared attribute `name` an instance of `receiver` has.
|
|
313
|
+
|
|
314
|
+
The first of its order that declares it (in a method too, `self.x: T`, which no class body
|
|
315
|
+
binds), unless `receiver` or a class of the module's before it binds the name another way.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
It, or `None`.
|
|
319
|
+
|
|
320
|
+
"""
|
|
321
|
+
lineage: Lineage = known.class_side.lineage
|
|
322
|
+
if name in lineage.bound.get(receiver, ()):
|
|
323
|
+
return None
|
|
324
|
+
base: str
|
|
325
|
+
for base in lineage.order.get(receiver, ()):
|
|
326
|
+
if name in known.classes.get(base, {}):
|
|
327
|
+
return base
|
|
328
|
+
if name in lineage.bound.get(base, ()):
|
|
329
|
+
break
|
|
330
|
+
return None
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _elements(receiver: str, name: str, call: ast.Call | None, _known: Known) -> Inference | None:
|
|
334
|
+
"""Type a builtin container's method by the receiver's type: its element's, or one all of them give.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Its inference, or `None` (see `element_method`, `uniform_method`).
|
|
338
|
+
|
|
339
|
+
"""
|
|
340
|
+
root: ast.expr = parsed(receiver)
|
|
341
|
+
found: str | None = (
|
|
342
|
+
None
|
|
343
|
+
if call is None
|
|
344
|
+
else element_method(root, receiver, call, name) or uniform_method(root, receiver, name)
|
|
345
|
+
)
|
|
346
|
+
return (
|
|
347
|
+
None
|
|
348
|
+
if found is None
|
|
349
|
+
else Inference(found, f"`{receiver}.{name}` on its element types", frozenset({_METHOD}))
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _library_base(receiver: str, name: str, call: ast.Call | None, known: Known) -> Inference | None:
|
|
354
|
+
"""Type a member a class of the module's takes from a standard-library base (see `stdlib.bases`).
|
|
355
|
+
|
|
356
|
+
`self.id()` in a `unittest.TestCase`: the base's that the class's order ends at, where no class
|
|
357
|
+
before it binds the name (see `Lineage`); `self.data` under a `UserDict[str, bytes]`.
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
Its inference, or `None`.
|
|
361
|
+
|
|
362
|
+
"""
|
|
363
|
+
owner: str | None = known.class_side.lineage.definer(receiver, name)
|
|
364
|
+
if owner is None or owner == receiver:
|
|
365
|
+
return None
|
|
366
|
+
if _GENERIC in owner: # a generic one, given its arguments: they type its attributes
|
|
367
|
+
return None if call is not None else overloads.generic_member(owner, name, call, known)
|
|
368
|
+
return stdlib.library_member(owner, name, call, known, inherited=True)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# Where a member's type can come from, in the order they're asked: the one place to add another.
|
|
372
|
+
SOURCES: Final[tuple[MemberSource, ...]] = (
|
|
373
|
+
_class_side,
|
|
374
|
+
_fixed,
|
|
375
|
+
_declared,
|
|
376
|
+
_elements,
|
|
377
|
+
stdlib.library_member,
|
|
378
|
+
overloads.generic_member,
|
|
379
|
+
_library_base,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def member(receiver: str, name: str, call: ast.Call | None, known: Known) -> Inference | None:
|
|
384
|
+
"""Type the member `name` of a value typed `receiver`: an attribute, or (`call`) a method's call.
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
The first of `SOURCES`' inferences, or `None` if none knows it.
|
|
388
|
+
|
|
389
|
+
"""
|
|
390
|
+
source: MemberSource
|
|
391
|
+
found: Inference | None
|
|
392
|
+
for source in SOURCES:
|
|
393
|
+
if (found := source(receiver, name, call, known)) is not None:
|
|
394
|
+
return found
|
|
395
|
+
return None
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def keyed(receiver: str, index: ast.expr, known: Known) -> Inference | None:
|
|
399
|
+
"""Type `d["key"]`, `d` a value typed as a `TypedDict` class: the key's declared type.
|
|
400
|
+
|
|
401
|
+
The class's own key, or one it takes from a base (see `keys.key`, `_declared`).
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
Its inference, or `None` for any other index or receiver, or a key the class doesn't declare.
|
|
405
|
+
|
|
406
|
+
"""
|
|
407
|
+
name: str
|
|
408
|
+
match index:
|
|
409
|
+
case ast.Constant(value=str() as name):
|
|
410
|
+
found: Inference | None = _declared(receiver, key(name), None, known)
|
|
411
|
+
reason: str = f"`{receiver}`'s `{name}` key"
|
|
412
|
+
return None if found is None else Inference(found.annotation, reason, frozenset({_SUBSCRIPT}))
|
|
413
|
+
case _:
|
|
414
|
+
return None
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def may_miss(receiver: str, index: ast.expr, known: Known) -> bool:
|
|
418
|
+
"""Check whether a `TypedDict` class's literal key may be missing (see `keys.optional`).
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
Whether it may: `d.get("key")` is then `None` too.
|
|
422
|
+
|
|
423
|
+
"""
|
|
424
|
+
return isinstance(index, ast.Constant) and (
|
|
425
|
+
_declared(receiver, optional(str(index.value)), None, known) is not None
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def returned_method(receiver: str, name: str, known: Known) -> tuple[str, str] | None:
|
|
430
|
+
"""Look up a method of a value typed `receiver` typed only by its `return`s (a guess, see `Returned`).
|
|
431
|
+
|
|
432
|
+
The receiver's class's own, or the base's that defines it (see `Lineage`): but not one whose
|
|
433
|
+
type names that base, which may be the receiver's own class (`return self`). A class named by
|
|
434
|
+
an import a fix added (`Tool`, by `from tools import Tool`) is looked up as the module spells
|
|
435
|
+
it (`tools.Tool`): it's a standard-library class another checked file defines. An `async def`'s
|
|
436
|
+
is under `AWAIT` before its name: what awaiting its call gives.
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
The class that defines it and its type, or `None` if it isn't one (a certain source is asked
|
|
440
|
+
first, see `member`).
|
|
441
|
+
|
|
442
|
+
"""
|
|
443
|
+
owner: str | None = known.class_side.lineage.definer(receiver, name.removeprefix(AWAIT))
|
|
444
|
+
found: str | None = known.returned.methods.get(owner or "", {}).get(name)
|
|
445
|
+
plan: ImportPlan | None = known.names.plan
|
|
446
|
+
if found is None and owner == receiver and plan is not None and receiver in plan.added:
|
|
447
|
+
found = known.returned.methods.get(added_dotted(plan.added[receiver]), {}).get(name)
|
|
448
|
+
if found is None or owner is None or (owner != receiver and owner in roots(found)):
|
|
449
|
+
return None
|
|
450
|
+
return owner, found
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def partial_method(receiver: str, name: str, known: Known) -> str | None:
|
|
454
|
+
"""Look up a method of a value typed `receiver` declared to return a tuple with a vague part.
|
|
455
|
+
|
|
456
|
+
The receiver's class's own, or the base's that defines it (see `Lineage`, `Partial`).
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
Its return, or `None` if it isn't one.
|
|
460
|
+
|
|
461
|
+
"""
|
|
462
|
+
owner: str | None = known.class_side.lineage.definer(receiver, name)
|
|
463
|
+
return known.indirect.partial.methods.get(owner or "", {}).get(name)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def assigned_owner(receiver: str, name: str, known: Known) -> str | None:
|
|
467
|
+
"""Find the class whose assignments type the attribute `name` of a value typed `receiver`.
|
|
468
|
+
|
|
469
|
+
The receiver's own class, or the first of its order that has one, unless a class of the
|
|
470
|
+
module's before it binds the name in its body.
|
|
471
|
+
|
|
472
|
+
Returns:
|
|
473
|
+
It, or `None`.
|
|
474
|
+
|
|
475
|
+
"""
|
|
476
|
+
lineage: Lineage = known.class_side.lineage
|
|
477
|
+
owner: str
|
|
478
|
+
for owner in (receiver, *lineage.order.get(receiver, ())):
|
|
479
|
+
if name in known.returned.attributes.get(owner, {}):
|
|
480
|
+
return owner
|
|
481
|
+
if name in lineage.bound.get(owner, ()):
|
|
482
|
+
break
|
|
483
|
+
return None
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def assigned_attribute(receiver: str, name: str, known: Known) -> str | None:
|
|
487
|
+
"""Look up an attribute of a value typed `receiver` typed by its assignments alone (see `assigned_owner`).
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
Its type, or `None` if it isn't one (a certain source is asked first, see `member`).
|
|
491
|
+
|
|
492
|
+
"""
|
|
493
|
+
owner: str | None = assigned_owner(receiver, name, known)
|
|
494
|
+
return None if owner is None else known.returned.attributes[owner][name]
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def class_variable(receiver: str, name: str, known: Known) -> str | None:
|
|
498
|
+
"""Look up a plain class's variable typed by its value alone (see `constricter.fix.values.classvars`).
|
|
499
|
+
|
|
500
|
+
On an instance of the class, or the class itself (`type[C]`); one of a base of the module's that
|
|
501
|
+
binds it, too.
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
Its type, or `None` if it isn't one (a certain source is asked first, see `member`), or is
|
|
505
|
+
one of another file's that this one can't write.
|
|
506
|
+
|
|
507
|
+
"""
|
|
508
|
+
owner: str | None = known.class_side.lineage.definer(class_of(receiver) or receiver, name)
|
|
509
|
+
return known.class_side.variables.get(owner or "", {}).get(name) or None
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def subscripted(container: str, node: ast.Subscript, index: str | None) -> str | None:
|
|
513
|
+
"""Infer `container[...]`'s type, given `container`'s own type as text, and the index's (`index`).
|
|
514
|
+
|
|
515
|
+
A slice (`x[1:2]`, or an index typed `slice`) of a `list`, `str`, `bytes` or `tuple[T, ...]` is
|
|
516
|
+
the same type as `container` itself; an index typed `int` into one is its element type (a
|
|
517
|
+
`bytes`'s, an `int`), and one of another or no known type decides nothing: it may be a slice. Any
|
|
518
|
+
index into a `dict` is its value type. A fixed-length `tuple[T1, T2]`'s part is
|
|
519
|
+
the one a literal index names (`pair[0]`, `pair[-1]`).
|
|
520
|
+
|
|
521
|
+
Returns:
|
|
522
|
+
The annotation as source text, or `None` if the subscript doesn't decide one.
|
|
523
|
+
|
|
524
|
+
"""
|
|
525
|
+
sliced: bool = isinstance(node.slice, ast.Slice) or index == _SLICE
|
|
526
|
+
element: ast.expr
|
|
527
|
+
last: ast.expr
|
|
528
|
+
name: str
|
|
529
|
+
match parsed(container):
|
|
530
|
+
case ast.Name(id="str" | "bytes" as name):
|
|
531
|
+
# A `bytes`'s plain index is one byte's `int`; an index of unknown type may be a slice.
|
|
532
|
+
return container if sliced or name == _STR else _INT if index == _INT else None
|
|
533
|
+
case ast.Subscript(value=ast.Name(id="list" | "List"), slice=element):
|
|
534
|
+
return container if sliced else ast.unparse(sole(element)) if index == _INT else None
|
|
535
|
+
case ast.Subscript(
|
|
536
|
+
value=ast.Name(id="dict" | "Dict"),
|
|
537
|
+
slice=ast.Tuple(elts=[_, element]),
|
|
538
|
+
) if not sliced:
|
|
539
|
+
return ast.unparse(element)
|
|
540
|
+
case ast.Subscript(
|
|
541
|
+
value=ast.Name(id="tuple" | "Tuple"),
|
|
542
|
+
slice=ast.Tuple(elts=[element, last]),
|
|
543
|
+
) if isinstance(last, ast.Constant) and last.value is Ellipsis:
|
|
544
|
+
return container if sliced else ast.unparse(element) if index == _INT else None
|
|
545
|
+
case ast.Subscript(value=ast.Name(id="tuple" | "Tuple"), slice=element) if not sliced:
|
|
546
|
+
return _part(element.elts if isinstance(element, ast.Tuple) else [element], node.slice)
|
|
547
|
+
case _:
|
|
548
|
+
return None
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _part(parts: Sequence[ast.expr], index: ast.expr) -> str | None:
|
|
552
|
+
"""Pick the part of a fixed-length tuple's type that a literal `index` names (`0`, `-1`).
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
It, as text; `None` for any other index, one past the tuple's ends, or a tuple that unpacks
|
|
556
|
+
another (`tuple[int, *Ts]`).
|
|
557
|
+
|
|
558
|
+
"""
|
|
559
|
+
at: int
|
|
560
|
+
match index:
|
|
561
|
+
case ast.Constant(value=bool()) | ast.UnaryOp(operand=ast.Constant(value=bool())):
|
|
562
|
+
return None # an `int` to Python, and no index to a type checker
|
|
563
|
+
case ast.Constant(value=int() as at):
|
|
564
|
+
pass
|
|
565
|
+
case ast.UnaryOp(op=ast.USub(), operand=ast.Constant(value=int() as at)):
|
|
566
|
+
at = -at
|
|
567
|
+
case _:
|
|
568
|
+
return None
|
|
569
|
+
starred: bool = any(isinstance(part, ast.Starred) for part in parts)
|
|
570
|
+
return None if starred or not -len(parts) <= at < len(parts) else ast.unparse(parts[at])
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _annotation_of(receiver: str, name: str) -> str:
|
|
574
|
+
return f"the annotation of `{receiver}.{name}`"
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def _declared_return(receiver: str, name: str) -> str:
|
|
578
|
+
return f"`{receiver}.{name}`'s declared return type"
|
|
Binary file
|