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,446 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""`--fix` for an empty container filled later: `x = []`, then only `x.append(v)`, is `list[T]`.
|
|
3
|
+
|
|
4
|
+
A guess (`--unsafe-fixes`): only this function's own uses are seen, and something else could still
|
|
5
|
+
add to it. So every use of the name must be one of a few that can't: a fill whose value's type is
|
|
6
|
+
known (`append`, `insert`, `add`, `setdefault`, `x[k] = v`; `extend` and `update`, by their one
|
|
7
|
+
argument's elements), a pass to a checked file's function whose parameter declares the container
|
|
8
|
+
it takes (`add(names, "a")`, `collect(into=names)`: that type), a read (`x[k]`, `x.get(k)`,
|
|
9
|
+
iterating it, `len(x)`, `", ".join(x)`, `x + more`, `[*x]`, returning it, alone or in a tuple),
|
|
10
|
+
one that only shrinks or reorders it
|
|
11
|
+
(`pop`, `sort`, `clear`), or a local bound to it (`alias = x`) whose own uses all only read it.
|
|
12
|
+
Anything else (passing it to another function, a nested function that sees it) leaves it alone.
|
|
13
|
+
|
|
14
|
+
An instance attribute bound to one (`self.items = []`) is judged the same way, each read of it in its
|
|
15
|
+
class's methods a use (`stored`): `constricter.fix.values.returned` types it from them all.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import ast
|
|
19
|
+
from collections import Counter
|
|
20
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
21
|
+
from typing import Final, NamedTuple
|
|
22
|
+
|
|
23
|
+
from constricter.fix.core.known import Inference, Known, Takers
|
|
24
|
+
from constricter.fix.values.inference import inference
|
|
25
|
+
from constricter.fix.values.loops import looped
|
|
26
|
+
from constricter.fix.values.shapes import displayed
|
|
27
|
+
from constricter.fix.values.targets import dict_parts
|
|
28
|
+
from constricter.rules.annotations import dotted, is_vague
|
|
29
|
+
from constricter.rules.syntax import NESTED_SCOPES, own_nodes
|
|
30
|
+
from constricter.rules.walked import walk
|
|
31
|
+
|
|
32
|
+
_LIST: Final = "list"
|
|
33
|
+
_DICT: Final = "dict"
|
|
34
|
+
_SET: Final = "set"
|
|
35
|
+
# What adds an element, by container: the method, and which argument is the element (the key, for a
|
|
36
|
+
# `dict`, is argument 0).
|
|
37
|
+
_ADDERS: Final = {
|
|
38
|
+
_LIST: {"append": 0, "insert": 1},
|
|
39
|
+
_SET: {"add": 0},
|
|
40
|
+
_DICT: {"setdefault": 1},
|
|
41
|
+
}
|
|
42
|
+
# What adds every element of its one argument, by container (a `dict`'s: another's entries).
|
|
43
|
+
_SPREADERS: Final = {_LIST: "extend", _SET: "update", _DICT: "update"}
|
|
44
|
+
# Methods that only read, shrink or reorder.
|
|
45
|
+
_READERS: Final = frozenset(
|
|
46
|
+
{"pop", "get", "items", "keys", "values", "copy", "sort", "reverse", "clear", "count", "index"}
|
|
47
|
+
| {"remove", "discard", "popitem"},
|
|
48
|
+
)
|
|
49
|
+
# Builtins a container can be passed to without being changed.
|
|
50
|
+
_PURE: Final = frozenset(
|
|
51
|
+
{"len", "sorted", "list", "tuple", "set", "frozenset", "enumerate", "reversed", "any", "all"}
|
|
52
|
+
| {"sum", "min", "max", "zip", "iter", "bool", "str", "repr", "dict"},
|
|
53
|
+
)
|
|
54
|
+
_JOIN: Final = "join"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Fill(NamedTuple):
|
|
58
|
+
"""One addition: its key (a `dict`'s), and its value.
|
|
59
|
+
|
|
60
|
+
`spread`: the value isn't what's added, but holds it: its elements (`extend`), or a `dict`'s
|
|
61
|
+
entries (`update`).
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
key: ast.expr | None
|
|
65
|
+
value: ast.expr
|
|
66
|
+
spread: bool = False
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Given(NamedTuple):
|
|
70
|
+
"""One pass of the container to a call, as an argument of its own: at a position, or by a keyword."""
|
|
71
|
+
|
|
72
|
+
call: ast.Call
|
|
73
|
+
at: int | str
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def takers(tree: ast.Module) -> dict[str, Takers]:
|
|
77
|
+
"""Find the module's top-level functions that declare a parameter a builtin container (`list[str]`).
|
|
78
|
+
|
|
79
|
+
A plain `def` or `async def`, defined once and undecorated, without `*args`: an argument's
|
|
80
|
+
place is its parameter's.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Each one's such parameters (see `Takers`), by its name; none for a function with none.
|
|
84
|
+
|
|
85
|
+
"""
|
|
86
|
+
counts: Counter[str] = Counter(
|
|
87
|
+
stmt.name
|
|
88
|
+
for stmt in tree.body
|
|
89
|
+
if isinstance(stmt, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef)
|
|
90
|
+
)
|
|
91
|
+
found: dict[str, Takers] = {}
|
|
92
|
+
stmt: ast.stmt
|
|
93
|
+
for stmt in tree.body:
|
|
94
|
+
if (
|
|
95
|
+
not isinstance(stmt, ast.FunctionDef | ast.AsyncFunctionDef)
|
|
96
|
+
or counts[stmt.name] != 1
|
|
97
|
+
or stmt.decorator_list
|
|
98
|
+
or stmt.args.vararg is not None
|
|
99
|
+
):
|
|
100
|
+
continue
|
|
101
|
+
positional: list[ast.arg] = [*stmt.args.posonlyargs, *stmt.args.args]
|
|
102
|
+
taken: dict[str, tuple[int | None, bool, str]] = {}
|
|
103
|
+
at: int
|
|
104
|
+
arg: ast.arg
|
|
105
|
+
for at, arg in enumerate([*positional, *stmt.args.kwonlyargs]):
|
|
106
|
+
text: str | None
|
|
107
|
+
if (text := _container(arg.annotation)) is not None:
|
|
108
|
+
place: int | None = at if at < len(positional) else None
|
|
109
|
+
taken[arg.arg] = (place, at >= len(stmt.args.posonlyargs), text)
|
|
110
|
+
if taken:
|
|
111
|
+
found[stmt.name] = taken
|
|
112
|
+
return found
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _container(annotation: ast.expr | None) -> str | None:
|
|
116
|
+
"""Read a parameter's annotation that is a builtin container of known elements: `list[str]`.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
It as text, or `None` for any other annotation, a vague one, or none.
|
|
120
|
+
|
|
121
|
+
"""
|
|
122
|
+
head: str
|
|
123
|
+
match annotation:
|
|
124
|
+
case ast.Subscript(value=ast.Name(id=head)) if head in _ADDERS and not is_vague(annotation):
|
|
125
|
+
return ast.unparse(annotation)
|
|
126
|
+
case _:
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def empty(value: ast.expr) -> str | None:
|
|
131
|
+
"""Recognise an empty container: `[]`, `{}`, `list()`, `dict()`, `set()`.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Its kind (`list`, `dict`, `set`), or `None` for anything else.
|
|
135
|
+
|
|
136
|
+
"""
|
|
137
|
+
name: str
|
|
138
|
+
match value:
|
|
139
|
+
case ast.List(elts=[]):
|
|
140
|
+
return _LIST
|
|
141
|
+
case ast.Dict(keys=[]):
|
|
142
|
+
return _DICT
|
|
143
|
+
case ast.Call(func=ast.Name(id=name), args=[], keywords=[]) if name in {_LIST, _DICT, _SET}:
|
|
144
|
+
return name
|
|
145
|
+
case _:
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class Uses(NamedTuple):
|
|
150
|
+
"""A function body's names as `filled` judges them, read once for all its empty containers.
|
|
151
|
+
|
|
152
|
+
`names`: each name's uses (not its bindings) outside nested scopes; `parents`: each node's parent,
|
|
153
|
+
by `id()`; `nested`: every name a nested function, lambda or class uses.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
names: Mapping[str, list[ast.Name]]
|
|
157
|
+
parents: Mapping[int, ast.AST]
|
|
158
|
+
nested: frozenset[str]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def uses(body: Sequence[ast.stmt]) -> Uses:
|
|
162
|
+
"""Read a function body's names for `filled`.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Them.
|
|
166
|
+
|
|
167
|
+
"""
|
|
168
|
+
names: dict[str, list[ast.Name]] = {}
|
|
169
|
+
parents: dict[int, ast.AST] = {}
|
|
170
|
+
nested: set[str] = set()
|
|
171
|
+
node: ast.AST
|
|
172
|
+
for node in own_nodes(body, parents):
|
|
173
|
+
if isinstance(node, NESTED_SCOPES):
|
|
174
|
+
nested.update(inner.id for inner in walk(node) if isinstance(inner, ast.Name))
|
|
175
|
+
elif isinstance(node, ast.Name) and not isinstance(node.ctx, ast.Store):
|
|
176
|
+
names.setdefault(node.id, []).append(node)
|
|
177
|
+
return Uses(names, parents, frozenset(nested))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def filled(
|
|
181
|
+
found: Uses,
|
|
182
|
+
name: str,
|
|
183
|
+
kind: str,
|
|
184
|
+
known: Known,
|
|
185
|
+
declared: Mapping[str, str],
|
|
186
|
+
) -> Inference | None:
|
|
187
|
+
"""Type an empty container `name` of `kind` from what its function's body (`found`) adds to it.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
`list[T]`, `set[T]` or `dict[K, V]`, or `None` if a use could add something unseen (a nested
|
|
191
|
+
scope sees it, out of this function's sight), nothing is added, or what's added isn't typed
|
|
192
|
+
alike.
|
|
193
|
+
|
|
194
|
+
"""
|
|
195
|
+
if name in found.nested:
|
|
196
|
+
return None
|
|
197
|
+
fills: list[Fill] = []
|
|
198
|
+
taken: list[str | None] = []
|
|
199
|
+
node: ast.Name
|
|
200
|
+
for node in found.names.get(name, []):
|
|
201
|
+
fill: Fill | Given | bool
|
|
202
|
+
if (fill := _use(node, kind, found.parents)) is False and not _only_read(node, kind, found):
|
|
203
|
+
return None
|
|
204
|
+
if isinstance(fill, Fill):
|
|
205
|
+
fills.append(fill)
|
|
206
|
+
elif isinstance(fill, Given):
|
|
207
|
+
taken.append(_taken(fill, kind, known, declared))
|
|
208
|
+
typed: Inference | None = _typed(fills, kind, known, declared) if fills else None
|
|
209
|
+
if (fills and typed is None) or None in taken or not (fills or taken):
|
|
210
|
+
return None
|
|
211
|
+
types: set[str | None] = {*taken, *([] if typed is None else [typed.annotation])}
|
|
212
|
+
passed: Inference = Inference(str(next(iter(types))), "what it's passed as", frozenset({"filled"}))
|
|
213
|
+
return None if len(types) != 1 else typed or passed
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _taken(given: Given, kind: str, known: Known, declared: Mapping[str, str]) -> str | None:
|
|
217
|
+
"""Type the container by the parameter a call it's passed to takes it as (see `Indirect.takers`).
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
The parameter's declared type, a container of its kind; `None` for a callee that declares
|
|
221
|
+
none there, one the scope binds itself, or a call that unpacks arguments before it.
|
|
222
|
+
|
|
223
|
+
"""
|
|
224
|
+
callee: str = dotted(given.call.func) or ""
|
|
225
|
+
unpacked: bool = any(isinstance(arg, ast.Starred) for arg in given.call.args)
|
|
226
|
+
if unpacked or callee.partition(".")[0] in declared:
|
|
227
|
+
return None
|
|
228
|
+
text: str | None = next(
|
|
229
|
+
(
|
|
230
|
+
text
|
|
231
|
+
for name, (position, named, text) in known.indirect.takers.get(callee, {}).items()
|
|
232
|
+
if (position == given.at) or (named and name == given.at)
|
|
233
|
+
),
|
|
234
|
+
None,
|
|
235
|
+
)
|
|
236
|
+
return text if text is not None and text.partition("[")[0] == kind else None
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def added_names(found: Uses, name: str, kind: str) -> frozenset[str]:
|
|
240
|
+
"""Name what the body (`found`) adds to container `name` that is a bare name: `d`, in `xs.append(d)`.
|
|
241
|
+
|
|
242
|
+
One in a display added too (`xs.append((key, d))`). Where the function tests one
|
|
243
|
+
(`isinstance(d, C)`), what's added is narrowed there: its declared type isn't the elements'.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
The names.
|
|
247
|
+
|
|
248
|
+
"""
|
|
249
|
+
fills: list[Fill] = [fill for fill in additions(found, name, kind) if not fill.spread]
|
|
250
|
+
return frozenset(
|
|
251
|
+
part.id for fill in fills for given in (fill.key, fill.value) for part in displayed(given)
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def additions(found: Uses, name: str, kind: str) -> list[Fill]:
|
|
256
|
+
"""List what the body (`found`) adds to container `name` of `kind`: its fills, in order.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Them.
|
|
260
|
+
|
|
261
|
+
"""
|
|
262
|
+
return [
|
|
263
|
+
fill
|
|
264
|
+
for fill in (_use(node, kind, found.parents) for node in found.names.get(name, []))
|
|
265
|
+
if isinstance(fill, Fill)
|
|
266
|
+
]
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def stored(body: Sequence[ast.stmt], kinds: Mapping[str, str]) -> Iterator[tuple[str, Fill | bool]]:
|
|
270
|
+
"""Judge each read of `self`'s attributes bound to an empty container, in one method's body.
|
|
271
|
+
|
|
272
|
+
`kinds`: each such attribute's kind. A nested function's or lambda's reads aren't judged.
|
|
273
|
+
|
|
274
|
+
Yields:
|
|
275
|
+
The attribute, and its use as `filled` judges a name's: a fill, `True` for one that adds
|
|
276
|
+
nothing, `False` for one that might add something unseen.
|
|
277
|
+
|
|
278
|
+
"""
|
|
279
|
+
parents: dict[int, ast.AST] = {}
|
|
280
|
+
found: Uses | None = None # read only for an attribute a local is bound to
|
|
281
|
+
node: ast.AST
|
|
282
|
+
attr: str
|
|
283
|
+
for node in own_nodes(body, parents):
|
|
284
|
+
match node:
|
|
285
|
+
case ast.Attribute(value=ast.Name(id="self"), attr=attr, ctx=ast.Load()) if attr in kinds:
|
|
286
|
+
judged: Fill | Given | bool = _use(node, kinds[attr], parents)
|
|
287
|
+
use: Fill | bool = False if isinstance(judged, Given) else judged # passed on: unseen
|
|
288
|
+
if use is False and isinstance(parents.get(id(node)), ast.Assign):
|
|
289
|
+
found = found or uses(body)
|
|
290
|
+
use = _only_read(node, kinds[attr], found._replace(parents={**found.parents, **parents}))
|
|
291
|
+
yield attr, use
|
|
292
|
+
case _:
|
|
293
|
+
pass
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _only_read(node: ast.expr, kind: str, found: Uses) -> bool:
|
|
297
|
+
"""Check whether a read binding the container to a local (`alias = x`) adds nothing to it.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Whether every use of that local, in no nested scope, only reads it.
|
|
301
|
+
|
|
302
|
+
"""
|
|
303
|
+
name: str
|
|
304
|
+
value: ast.expr
|
|
305
|
+
match found.parents.get(id(node)):
|
|
306
|
+
case ast.Assign(targets=[ast.Name(id=name)], value=value) if (
|
|
307
|
+
value is node and name not in found.nested
|
|
308
|
+
):
|
|
309
|
+
return all(_use(use, kind, found.parents) is True for use in found.names.get(name, []))
|
|
310
|
+
case _:
|
|
311
|
+
return False
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _use(node: ast.expr, kind: str, parents: Mapping[int, ast.AST]) -> Fill | Given | bool:
|
|
315
|
+
"""Judge one read of the container.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
A fill (what it adds), a pass to a call (whose parameter may say what it takes), `True` for
|
|
319
|
+
a use that adds nothing, `False` for one that might add something unseen.
|
|
320
|
+
|
|
321
|
+
"""
|
|
322
|
+
parent: ast.AST | None = parents.get(id(node))
|
|
323
|
+
grandparent: ast.AST | None = None if parent is None else parents.get(id(parent))
|
|
324
|
+
attr: str
|
|
325
|
+
keyword: str
|
|
326
|
+
match parent:
|
|
327
|
+
case ast.Attribute(attr=attr) if isinstance(grandparent, ast.Call) and grandparent.func is parent:
|
|
328
|
+
return _method(attr, grandparent, kind)
|
|
329
|
+
case ast.Subscript(ctx=ast.Store()):
|
|
330
|
+
return _stored(parent, grandparent, kind)
|
|
331
|
+
case _ if _reads(parent, grandparent):
|
|
332
|
+
return True
|
|
333
|
+
case ast.Call() if any(arg is node for arg in parent.args):
|
|
334
|
+
return Given(parent, next(at for at, arg in enumerate(parent.args) if arg is node))
|
|
335
|
+
case ast.keyword(arg=str() as keyword) if isinstance(grandparent, ast.Call):
|
|
336
|
+
return Given(grandparent, keyword)
|
|
337
|
+
case _:
|
|
338
|
+
return False
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _reads(parent: ast.AST | None, grandparent: ast.AST | None) -> bool:
|
|
342
|
+
"""Judge a use by the node it's in (and the one that's in): one that can't add to the container.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
Whether it only reads it: indexed, compared, returned (in a tuple too), iterated, tested,
|
|
346
|
+
formatted, an operand (`x + more`), unpacked (`[*x]`), or passed to a builtin that doesn't
|
|
347
|
+
change it (`len`, `sorted`, ...) or to a `join`.
|
|
348
|
+
|
|
349
|
+
"""
|
|
350
|
+
func: str
|
|
351
|
+
attr: str
|
|
352
|
+
match parent:
|
|
353
|
+
case ast.Subscript() | ast.Compare() | ast.Return() | ast.For() | ast.comprehension():
|
|
354
|
+
return True
|
|
355
|
+
case (
|
|
356
|
+
ast.If()
|
|
357
|
+
| ast.While()
|
|
358
|
+
| ast.IfExp()
|
|
359
|
+
| ast.BoolOp()
|
|
360
|
+
| ast.UnaryOp(op=ast.Not())
|
|
361
|
+
| ast.FormattedValue()
|
|
362
|
+
| ast.BinOp()
|
|
363
|
+
| ast.Starred()
|
|
364
|
+
| ast.YieldFrom()
|
|
365
|
+
):
|
|
366
|
+
return True
|
|
367
|
+
case ast.Call(func=ast.Name(id=func)):
|
|
368
|
+
return func in _PURE
|
|
369
|
+
case ast.Call(func=ast.Attribute(attr=attr)):
|
|
370
|
+
return attr == _JOIN
|
|
371
|
+
case ast.Tuple():
|
|
372
|
+
return isinstance(grandparent, ast.Return)
|
|
373
|
+
case _:
|
|
374
|
+
return False
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _method(attr: str, call: ast.Call, kind: str) -> Fill | bool:
|
|
378
|
+
"""Judge a method called on the container.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
A fill for an adder or a spreader (with the arguments it needs), `True` for a reader, else
|
|
382
|
+
`False`.
|
|
383
|
+
|
|
384
|
+
"""
|
|
385
|
+
position: int | None = _ADDERS[kind].get(attr)
|
|
386
|
+
if position is not None and not call.keywords and len(call.args) == position + 1:
|
|
387
|
+
return Fill(call.args[0] if kind == _DICT else None, call.args[position])
|
|
388
|
+
if attr == _SPREADERS[kind] and not call.keywords and len(call.args) == 1:
|
|
389
|
+
return Fill(None, call.args[0], spread=True)
|
|
390
|
+
return attr in _READERS
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _stored(target: ast.Subscript, statement: ast.AST | None, kind: str) -> Fill | bool:
|
|
394
|
+
"""Judge `x[k] = v`: a `dict`'s fill (key and value), a `list`'s element replaced.
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
The fill, or `False` for any other store (`x[k] += v`, `x[1:2] = ...`, a set's).
|
|
398
|
+
|
|
399
|
+
"""
|
|
400
|
+
if (
|
|
401
|
+
isinstance(statement, ast.Assign)
|
|
402
|
+
and statement.targets == [target]
|
|
403
|
+
and kind != _SET
|
|
404
|
+
and not isinstance(target.slice, ast.Slice)
|
|
405
|
+
):
|
|
406
|
+
return Fill(target.slice if kind == _DICT else None, statement.value)
|
|
407
|
+
return False
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _typed(fills: Sequence[Fill], kind: str, known: Known, declared: Mapping[str, str]) -> Inference | None:
|
|
411
|
+
"""Type the container from its fills, if they agree.
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
The inference, or `None` if a key or value isn't typed, or they differ.
|
|
415
|
+
|
|
416
|
+
"""
|
|
417
|
+
each: list[list[Inference | None]] = [added(fill, kind, known, declared) for fill in fills]
|
|
418
|
+
rows: list[list[Inference]] = [[part for part in row if part is not None] for row in each]
|
|
419
|
+
if [len(row) for row in rows] != [len(row) for row in each]:
|
|
420
|
+
return None
|
|
421
|
+
parts: list[Inference] = [part for row in rows for part in row]
|
|
422
|
+
# A `dict`'s keys, then its values; any other container's elements.
|
|
423
|
+
types: list[set[str]] = [{row[at].annotation for row in rows} for at in range(len(rows[0]))]
|
|
424
|
+
if any(len(found) != 1 for found in types):
|
|
425
|
+
return None
|
|
426
|
+
annotation: str = f"{kind}[{', '.join(next(iter(found)) for found in types)}]"
|
|
427
|
+
reason: str = f"what the function adds to it ({len(fills)} {'fill' if len(fills) == 1 else 'fills'})"
|
|
428
|
+
return Inference(annotation, reason, frozenset({"filled"}).union(*(part.kinds for part in parts)))
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def added(fill: Fill, kind: str, known: Known, declared: Mapping[str, str]) -> list[Inference | None]:
|
|
432
|
+
"""Type what one fill adds: a `dict`'s key and value, any other container's element.
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
Each one's inference (`None`: unknown).
|
|
436
|
+
|
|
437
|
+
"""
|
|
438
|
+
if not fill.spread:
|
|
439
|
+
return [inference(part, known, declared) for part in (fill.key, fill.value) if part is not None]
|
|
440
|
+
if kind != _DICT:
|
|
441
|
+
return [looped(fill.value, known, declared, inference)]
|
|
442
|
+
found: Inference | None = inference(fill.value, known, declared)
|
|
443
|
+
types: tuple[str, str] | None = None if found is None else dict_parts(found.annotation)
|
|
444
|
+
if found is None or types is None:
|
|
445
|
+
return [None, None]
|
|
446
|
+
return [found._replace(annotation=types[0]), found._replace(annotation=types[1])]
|
|
Binary file
|