python-constricter 0.4.1__cp311-cp311-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__.cp311-win32.pyd +0 -0
- constricter/fix/__init__.py +2 -0
- constricter/fix/core/__init__.cp311-win32.pyd +0 -0
- constricter/fix/core/__init__.py +2 -0
- constricter/fix/core/asked.cp311-win32.pyd +0 -0
- constricter/fix/core/asked.py +56 -0
- constricter/fix/core/fixes.cp311-win32.pyd +0 -0
- constricter/fix/core/fixes.py +317 -0
- constricter/fix/core/imports.cp311-win32.pyd +0 -0
- constricter/fix/core/imports.py +580 -0
- constricter/fix/core/inherited.cp311-win32.pyd +0 -0
- constricter/fix/core/inherited.py +311 -0
- constricter/fix/core/known.cp311-win32.pyd +0 -0
- constricter/fix/core/known.py +730 -0
- constricter/fix/core/signatures.cp311-win32.pyd +0 -0
- constricter/fix/core/signatures.py +132 -0
- constricter/fix/index/__init__.cp311-win32.pyd +0 -0
- constricter/fix/index/__init__.py +2 -0
- constricter/fix/index/awaits.cp311-win32.pyd +0 -0
- constricter/fix/index/awaits.py +90 -0
- constricter/fix/index/beyond.cp311-win32.pyd +0 -0
- constricter/fix/index/beyond.py +149 -0
- constricter/fix/index/callers.cp311-win32.pyd +0 -0
- constricter/fix/index/callers.py +155 -0
- constricter/fix/index/crossed.cp311-win32.pyd +0 -0
- constricter/fix/index/crossed.py +327 -0
- constricter/fix/index/decorated.cp311-win32.pyd +0 -0
- constricter/fix/index/decorated.py +140 -0
- constricter/fix/index/fixtures.cp311-win32.pyd +0 -0
- constricter/fix/index/fixtures.py +176 -0
- constricter/fix/index/linked.cp311-win32.pyd +0 -0
- constricter/fix/index/linked.py +142 -0
- constricter/fix/index/loose.cp311-win32.pyd +0 -0
- constricter/fix/index/loose.py +103 -0
- constricter/fix/index/managed.cp311-win32.pyd +0 -0
- constricter/fix/index/managed.py +63 -0
- constricter/fix/index/modules.cp311-win32.pyd +0 -0
- constricter/fix/index/modules.py +741 -0
- constricter/fix/index/offers.cp311-win32.pyd +0 -0
- constricter/fix/index/offers.py +252 -0
- constricter/fix/index/order.cp311-win32.pyd +0 -0
- constricter/fix/index/order.py +155 -0
- constricter/fix/index/origins.cp311-win32.pyd +0 -0
- constricter/fix/index/origins.py +238 -0
- constricter/fix/index/own_methods.cp311-win32.pyd +0 -0
- constricter/fix/index/own_methods.py +91 -0
- constricter/fix/index/own_overloads.cp311-win32.pyd +0 -0
- constricter/fix/index/own_overloads.py +70 -0
- constricter/fix/index/own_types.cp311-win32.pyd +0 -0
- constricter/fix/index/own_types.py +330 -0
- constricter/fix/index/params.cp311-win32.pyd +0 -0
- constricter/fix/index/params.py +81 -0
- constricter/fix/index/plain.cp311-win32.pyd +0 -0
- constricter/fix/index/plain.py +221 -0
- constricter/fix/index/project.cp311-win32.pyd +0 -0
- constricter/fix/index/project.py +797 -0
- constricter/fix/index/sides.cp311-win32.pyd +0 -0
- constricter/fix/index/sides.py +160 -0
- constricter/fix/index/strings.cp311-win32.pyd +0 -0
- constricter/fix/index/strings.py +46 -0
- constricter/fix/index/stubs/__init__.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/__init__.py +2 -0
- constricter/fix/index/stubs/atoms.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/atoms.py +425 -0
- constricter/fix/index/stubs/chained.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/chained.py +69 -0
- constricter/fix/index/stubs/classnames.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/classnames.py +124 -0
- constricter/fix/index/stubs/declared.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/declared.py +508 -0
- constricter/fix/index/stubs/installed.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/installed.py +425 -0
- constricter/fix/index/stubs/names.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/names.py +200 -0
- constricter/fix/index/stubs/read.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/read.py +70 -0
- constricter/fix/index/stubs/signatures.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/signatures.py +656 -0
- constricter/fix/index/stubs/stubbed.cp311-win32.pyd +0 -0
- constricter/fix/index/stubs/stubbed.py +208 -0
- constricter/fix/index/taken.cp311-win32.pyd +0 -0
- constricter/fix/index/taken.py +45 -0
- constricter/fix/index/tuples.cp311-win32.pyd +0 -0
- constricter/fix/index/tuples.py +112 -0
- constricter/fix/libraries/__init__.cp311-win32.pyd +0 -0
- constricter/fix/libraries/__init__.py +2 -0
- constricter/fix/libraries/library.cp311-win32.pyd +0 -0
- constricter/fix/libraries/library.py +420 -0
- constricter/fix/libraries/opened.cp311-win32.pyd +0 -0
- constricter/fix/libraries/opened.py +147 -0
- constricter/fix/libraries/overloads.cp311-win32.pyd +0 -0
- constricter/fix/libraries/overloads.py +960 -0
- constricter/fix/libraries/receivers.cp311-win32.pyd +0 -0
- constricter/fix/libraries/receivers.py +274 -0
- constricter/fix/libraries/stdlib.cp311-win32.pyd +0 -0
- constricter/fix/libraries/stdlib.py +752 -0
- constricter/fix/libraries/walks.cp311-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__.cp311-win32.pyd +0 -0
- constricter/fix/values/__init__.py +2 -0
- constricter/fix/values/aliased.cp311-win32.pyd +0 -0
- constricter/fix/values/aliased.py +195 -0
- constricter/fix/values/bodies.cp311-win32.pyd +0 -0
- constricter/fix/values/bodies.py +76 -0
- constricter/fix/values/callables.cp311-win32.pyd +0 -0
- constricter/fix/values/callables.py +323 -0
- constricter/fix/values/called.cp311-win32.pyd +0 -0
- constricter/fix/values/called.py +194 -0
- constricter/fix/values/classvars.cp311-win32.pyd +0 -0
- constricter/fix/values/classvars.py +629 -0
- constricter/fix/values/decided.cp311-win32.pyd +0 -0
- constricter/fix/values/decided.py +429 -0
- constricter/fix/values/displays.cp311-win32.pyd +0 -0
- constricter/fix/values/displays.py +101 -0
- constricter/fix/values/doubts.cp311-win32.pyd +0 -0
- constricter/fix/values/doubts.py +677 -0
- constricter/fix/values/entered.cp311-win32.pyd +0 -0
- constricter/fix/values/entered.py +332 -0
- constricter/fix/values/fills.cp311-win32.pyd +0 -0
- constricter/fix/values/fills.py +446 -0
- constricter/fix/values/guesses.cp311-win32.pyd +0 -0
- constricter/fix/values/guesses.py +376 -0
- constricter/fix/values/hinted.cp311-win32.pyd +0 -0
- constricter/fix/values/hinted.py +811 -0
- constricter/fix/values/inference.cp311-win32.pyd +0 -0
- constricter/fix/values/inference.py +698 -0
- constricter/fix/values/loops.cp311-win32.pyd +0 -0
- constricter/fix/values/loops.py +335 -0
- constricter/fix/values/members.cp311-win32.pyd +0 -0
- constricter/fix/values/members.py +578 -0
- constricter/fix/values/narrowed.cp311-win32.pyd +0 -0
- constricter/fix/values/narrowed.py +220 -0
- constricter/fix/values/operated.cp311-win32.pyd +0 -0
- constricter/fix/values/operated.py +202 -0
- constricter/fix/values/ordered.cp311-win32.pyd +0 -0
- constricter/fix/values/ordered.py +74 -0
- constricter/fix/values/returned.cp311-win32.pyd +0 -0
- constricter/fix/values/returned.py +479 -0
- constricter/fix/values/returns.cp311-win32.pyd +0 -0
- constricter/fix/values/returns.py +237 -0
- constricter/fix/values/shapes.cp311-win32.pyd +0 -0
- constricter/fix/values/shapes.py +654 -0
- constricter/fix/values/stored.cp311-win32.pyd +0 -0
- constricter/fix/values/stored.py +564 -0
- constricter/fix/values/targets.cp311-win32.pyd +0 -0
- constricter/fix/values/targets.py +419 -0
- constricter/fix/values/unchecked.cp311-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__.cp311-win32.pyd +0 -0
- constricter/rules/__init__.py +2 -0
- constricter/rules/annotations.cp311-win32.pyd +0 -0
- constricter/rules/annotations.py +296 -0
- constricter/rules/binding.cp311-win32.pyd +0 -0
- constricter/rules/binding.py +652 -0
- constricter/rules/calls.cp311-win32.pyd +0 -0
- constricter/rules/calls.py +460 -0
- constricter/rules/checker.cp311-win32.pyd +0 -0
- constricter/rules/checker.py +536 -0
- constricter/rules/declarations.cp311-win32.pyd +0 -0
- constricter/rules/declarations.py +730 -0
- constricter/rules/decorators.cp311-win32.pyd +0 -0
- constricter/rules/decorators.py +351 -0
- constricter/rules/flow.cp311-win32.pyd +0 -0
- constricter/rules/flow.py +502 -0
- constricter/rules/keys.cp311-win32.pyd +0 -0
- constricter/rules/keys.py +79 -0
- constricter/rules/late.cp311-win32.pyd +0 -0
- constricter/rules/late.py +969 -0
- constricter/rules/narrowing.cp311-win32.pyd +0 -0
- constricter/rules/narrowing.py +98 -0
- constricter/rules/parsed.cp311-win32.pyd +0 -0
- constricter/rules/parsed.py +136 -0
- constricter/rules/quoted.cp311-win32.pyd +0 -0
- constricter/rules/quoted.py +141 -0
- constricter/rules/rebinding.cp311-win32.pyd +0 -0
- constricter/rules/rebinding.py +169 -0
- constricter/rules/recorded.cp311-win32.pyd +0 -0
- constricter/rules/recorded.py +168 -0
- constricter/rules/redundant.cp311-win32.pyd +0 -0
- constricter/rules/redundant.py +160 -0
- constricter/rules/scope.cp311-win32.pyd +0 -0
- constricter/rules/scope.py +802 -0
- constricter/rules/scoped.cp311-win32.pyd +0 -0
- constricter/rules/scoped.py +232 -0
- constricter/rules/scoping.cp311-win32.pyd +0 -0
- constricter/rules/scoping.py +346 -0
- constricter/rules/settings.cp311-win32.pyd +0 -0
- constricter/rules/settings.py +189 -0
- constricter/rules/syntax.cp311-win32.pyd +0 -0
- constricter/rules/syntax.py +472 -0
- constricter/rules/tables.cp311-win32.pyd +0 -0
- constricter/rules/tables.py +169 -0
- constricter/rules/walked.cp311-win32.pyd +0 -0
- constricter/rules/walked.py +127 -0
- constricter/rules/widened.cp311-win32.pyd +0 -0
- constricter/rules/widened.py +322 -0
- constricter/trace.py +374 -0
- e45ce06b2fdd5c43f782__mypyc.cp311-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,629 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""`--fix` for a plain class's variables: `limit = 3` in a class body, typed by its literal value.
|
|
3
|
+
|
|
4
|
+
An annotation in a class body can be more than a type: a dataclass's, a `NamedTuple`'s or a model's
|
|
5
|
+
makes the variable a field. So only a plain class's body is fixed: one defined once in its module,
|
|
6
|
+
with no decorator or metaclass, every base of which is `object`, a `unittest` test case, a builtin
|
|
7
|
+
exception or value class (`ValueError`, `str`: see `BUILTIN_BASES`), `Generic[T]` or `abc.ABC`
|
|
8
|
+
(`PASSED_OVER`), a listed one (`listed`: a framework's that reads no annotations, as
|
|
9
|
+
`fix-plain-bases` says), or another plain class, subscripted or not (`Box[int]`); and no class that
|
|
10
|
+
isn't plain may inherit from it (a mixin of a model's). What the module alone sees is
|
|
11
|
+
`plain`; with the CLI, the index of checked files says which bases other files define are plain too
|
|
12
|
+
(see `constricter.fix.index.project`).
|
|
13
|
+
|
|
14
|
+
A variable counts when it's bound once, by a plain `name = value` directly in the body, to a value
|
|
15
|
+
whose type its own text decides (a literal, or a display of them) or the standard library does, by
|
|
16
|
+
the module's imports (`re.compile("x")`, `os.sep`, `len(NAMES)`), and nothing in the module stores
|
|
17
|
+
the attribute as anything else (`self.limit = 5` and `self.limit += 1` keep an `int`;
|
|
18
|
+
`self.limit = size` may not), nor is it one a builtin base has itself (`errno` under `OSError`) or a
|
|
19
|
+
class above it annotates as another type (`limit: int | None`): a type checker holds a variable to
|
|
20
|
+
what its base declares. Its type is that value's, whatever the module
|
|
21
|
+
declares: the same read from the file alone, so the files reading the attribute (`self.limit`,
|
|
22
|
+
`cls.limit`) are typed in the same run as the class is fixed. But one a test case declares itself
|
|
23
|
+
(`maxDiff`, an `int | None`) is that type, under any base no checked file defines but a builtin
|
|
24
|
+
one, where its value is of it (see `HELD`). A guess (`member`): a subclass, or code elsewhere, may
|
|
25
|
+
bind it to another type.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import ast
|
|
29
|
+
import builtins
|
|
30
|
+
import re
|
|
31
|
+
from collections import Counter
|
|
32
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
33
|
+
from typing import Final, TypeAlias, cast
|
|
34
|
+
from weakref import WeakKeyDictionary
|
|
35
|
+
|
|
36
|
+
from constricter.fix.core.imports import plan, taken_names
|
|
37
|
+
from constricter.fix.core.known import Inference, Known, LibraryNames, Outside
|
|
38
|
+
from constricter.fix.libraries import stdlib
|
|
39
|
+
from constricter.fix.values.inference import inference
|
|
40
|
+
from constricter.fix.values.operated import operated
|
|
41
|
+
from constricter.offences import MAX_LENGTH
|
|
42
|
+
from constricter.rules.annotations import dotted
|
|
43
|
+
from constricter.rules.declarations import classes as annotated
|
|
44
|
+
from constricter.rules.flow import members as union
|
|
45
|
+
from constricter.rules.syntax import import_bindings
|
|
46
|
+
from constricter.rules.walked import classes, of_type
|
|
47
|
+
|
|
48
|
+
OBJECT: Final = "object"
|
|
49
|
+
# The standard library's test cases: their subclasses' variables are only ever their own.
|
|
50
|
+
TEST_CASES: Final = frozenset(
|
|
51
|
+
{
|
|
52
|
+
"unittest.TestCase",
|
|
53
|
+
"unittest.IsolatedAsyncioTestCase",
|
|
54
|
+
"unittest.case.TestCase",
|
|
55
|
+
"unittest.async_case.IsolatedAsyncioTestCase",
|
|
56
|
+
},
|
|
57
|
+
)
|
|
58
|
+
# The builtin classes of values whose subclasses' variables are their own, as an exception's are.
|
|
59
|
+
_VALUES: Final = frozenset(
|
|
60
|
+
{"bytearray", "bytes", "complex", "dict", "float", "frozenset", "int", "list", "set", "str", "tuple"},
|
|
61
|
+
)
|
|
62
|
+
# The standard library's bases that read no annotation in a class body, and bind nothing there.
|
|
63
|
+
PASSED_OVER: Final = frozenset({"abc.ABC", "typing.Generic", "typing_extensions.Generic"})
|
|
64
|
+
# The functions a call to which, as a base, makes a class of the call's fields: its body is plain.
|
|
65
|
+
# `bases` spells such a base with `CALLED` after the function (`collections.namedtuple()`): the class
|
|
66
|
+
# under `NamedTuple` itself has fields in its body, and isn't plain.
|
|
67
|
+
FUNCTIONAL: Final = frozenset({"namedtuple", "NamedTuple", "TypedDict"})
|
|
68
|
+
CALLED: Final = "()"
|
|
69
|
+
FUNCTIONAL_BASES: Final = frozenset(
|
|
70
|
+
{
|
|
71
|
+
f"collections.namedtuple{CALLED}",
|
|
72
|
+
*(
|
|
73
|
+
f"{module}.{name}{CALLED}"
|
|
74
|
+
for module in ("typing", "typing_extensions")
|
|
75
|
+
for name in ("NamedTuple", "TypedDict")
|
|
76
|
+
),
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
Bases: TypeAlias = Mapping[str, tuple[str, ...]] # each class's bases as written (see `bases`)
|
|
80
|
+
SPECIAL: Final = "" # a base no class has: what makes a class one that can't be plain
|
|
81
|
+
# Each module's classes' bases and variables, for as long as its tree lives: read for the index,
|
|
82
|
+
# then for the check.
|
|
83
|
+
_Bases: TypeAlias = dict[str, tuple[str, ...]]
|
|
84
|
+
_Members: TypeAlias = dict[str, dict[str, str]]
|
|
85
|
+
_BASES: Final[WeakKeyDictionary[ast.Module, _Bases]] = WeakKeyDictionary()
|
|
86
|
+
_MEMBERS: Final[WeakKeyDictionary[ast.Module, _Members]] = WeakKeyDictionary()
|
|
87
|
+
# What decides a value's type from its own text, with what the module imports of the standard
|
|
88
|
+
# library: nothing the module declares.
|
|
89
|
+
_OWN_KINDS: Final = frozenset(
|
|
90
|
+
{
|
|
91
|
+
"literal",
|
|
92
|
+
"container",
|
|
93
|
+
"arithmetic",
|
|
94
|
+
"compare",
|
|
95
|
+
"stdlib",
|
|
96
|
+
"builtin",
|
|
97
|
+
"builder",
|
|
98
|
+
"loop",
|
|
99
|
+
"method",
|
|
100
|
+
"conditional",
|
|
101
|
+
"boolean",
|
|
102
|
+
"comprehension",
|
|
103
|
+
"open",
|
|
104
|
+
},
|
|
105
|
+
)
|
|
106
|
+
_NONE: Final = "None"
|
|
107
|
+
_NOT: Final = "!" # an entry that leaves a base out (see `listed`)
|
|
108
|
+
_DEFINITIONS: Final = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)
|
|
109
|
+
# One store of an attribute: its value, and (augmented) its operator.
|
|
110
|
+
_Store: TypeAlias = tuple[ast.operator | None, ast.expr]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _builtin_bases() -> dict[str, frozenset[str]]:
|
|
114
|
+
"""Find the builtin classes a plain class may inherit from: the exceptions, and `_VALUES`.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Each one's name, and the names of what it has itself.
|
|
118
|
+
|
|
119
|
+
"""
|
|
120
|
+
return {
|
|
121
|
+
name: frozenset(dir(cast("object", getattr(builtins, name))))
|
|
122
|
+
for name in dir(builtins)
|
|
123
|
+
if _is_base(name, cast("object", getattr(builtins, name)))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _is_base(name: str, value: object) -> bool:
|
|
128
|
+
"""Check whether a builtin is a class a plain class may inherit from.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Whether it is.
|
|
132
|
+
|
|
133
|
+
"""
|
|
134
|
+
return isinstance(value, type) and (name in _VALUES or issubclass(value, BaseException))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
BUILTIN_BASES: Final = _builtin_bases()
|
|
138
|
+
_WORD: Final = re.compile(r"[A-Za-z_][\w.]*")
|
|
139
|
+
# What a test case declares of itself, where builtins alone spell it: a subclass's variable of the
|
|
140
|
+
# name has that type, to a type checker, whatever its value's is.
|
|
141
|
+
HELD: Final = {
|
|
142
|
+
name: typed
|
|
143
|
+
for name, typed in stdlib.attributes("unittest.TestCase").items()
|
|
144
|
+
if all(word == _NONE or hasattr(builtins, word) for word in cast("list[str]", _WORD.findall(typed)))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def member_type(value: ast.expr, known: Known) -> str | None:
|
|
149
|
+
"""Type a class variable's value by its own text: a literal, or a display of them.
|
|
150
|
+
|
|
151
|
+
Or, `known` naming the standard library as the module does (see `library`), by what it gives:
|
|
152
|
+
a function's or a class's with a fixed type, an attribute, a builtin's.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
The annotation, or `None` for a value that names anything else (a class, a copy), or `None`
|
|
156
|
+
itself.
|
|
157
|
+
|
|
158
|
+
"""
|
|
159
|
+
found: Inference | None = inference(value, known, {})
|
|
160
|
+
if found is None or found.annotation == _NONE or not found.kinds <= _OWN_KINDS:
|
|
161
|
+
return None
|
|
162
|
+
return found.annotation
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def library(tree: ast.Module) -> Known:
|
|
166
|
+
"""Know what a module names of the standard library, and nothing it declares itself.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
What `member_type` types a value by, the same for every file that reads the module.
|
|
170
|
+
|
|
171
|
+
"""
|
|
172
|
+
return Known({}, frozenset(), {}, {}, names=LibraryNames(frozenset(), stdlib.origins(tree), plan(tree)))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def bases(tree: ast.Module) -> dict[str, tuple[str, ...]]:
|
|
176
|
+
"""Map each class the module defines once to its bases, as written.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Each one's bases (`object` left out, a subscripted one by its name, a call to a functional
|
|
180
|
+
`namedtuple`, `NamedTuple` or `TypedDict` by the function's: a class with those fields); with
|
|
181
|
+
`SPECIAL` among them for a class that can't be plain: decorated, given a metaclass or another
|
|
182
|
+
keyword, or with a base that isn't a name (`make()`) or is a builtin's the module binds itself.
|
|
183
|
+
|
|
184
|
+
"""
|
|
185
|
+
found: dict[str, tuple[str, ...]] | None
|
|
186
|
+
if (found := _BASES.get(tree)) is None:
|
|
187
|
+
found = _BASES[tree] = _read_bases(tree)
|
|
188
|
+
return found
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _read_bases(tree: ast.Module) -> dict[str, tuple[str, ...]]:
|
|
192
|
+
taken: frozenset[str] = taken_names(tree)[0]
|
|
193
|
+
counts: dict[str, int] = {}
|
|
194
|
+
node: ast.ClassDef
|
|
195
|
+
for node in classes(tree):
|
|
196
|
+
counts[node.name] = counts.get(node.name, 0) + 1
|
|
197
|
+
found: dict[str, tuple[str, ...]] = {}
|
|
198
|
+
for node in classes(tree):
|
|
199
|
+
if counts[node.name] == 1:
|
|
200
|
+
names: list[str | None] = [_base_name(base) for base in node.bases]
|
|
201
|
+
shadowed: bool = any(name in BUILTIN_BASES and name in taken for name in names)
|
|
202
|
+
special: bool = bool(node.keywords or node.decorator_list) or None in names or shadowed
|
|
203
|
+
found[node.name] = (
|
|
204
|
+
*([SPECIAL] if special else []),
|
|
205
|
+
*(name for name in names if name and name != OBJECT),
|
|
206
|
+
)
|
|
207
|
+
return found
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _base_name(base: ast.expr) -> str | None:
|
|
211
|
+
"""Spell a base: a name, a subscripted one's, or a functional `namedtuple`-like call's function.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
It, or `None` for a base that's anything else (`make()`).
|
|
215
|
+
|
|
216
|
+
"""
|
|
217
|
+
name: str | None
|
|
218
|
+
value: ast.expr
|
|
219
|
+
func: ast.expr
|
|
220
|
+
match base:
|
|
221
|
+
case ast.Subscript(value=value):
|
|
222
|
+
return dotted(value)
|
|
223
|
+
case ast.Call(func=func) if (name := dotted(func)) is not None and name.rpartition(".")[
|
|
224
|
+
2
|
|
225
|
+
] in FUNCTIONAL:
|
|
226
|
+
return f"{name}{CALLED}"
|
|
227
|
+
case _:
|
|
228
|
+
return dotted(base)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def imported(tree: ast.Module) -> dict[str, str]:
|
|
232
|
+
"""Map each top-level name the module's absolute imports bind to what it is (`models.Model`'s `models`).
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Each bound name's dotted origin, to resolve a base by (see `plain`).
|
|
236
|
+
|
|
237
|
+
"""
|
|
238
|
+
return {name: origin for name, origin, _ in import_bindings(tree.body)}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def plain(tree: ast.Module, origins: Mapping[str, str], entries: Sequence[str] = ()) -> frozenset[str]:
|
|
242
|
+
"""Find the module's plain classes, as far as the module alone sees (see the module docstring).
|
|
243
|
+
|
|
244
|
+
`origins`: what each name its imports bind refers to (see `imported`), to know a test case;
|
|
245
|
+
`entries`: the listed bases (see `listed`).
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Their names.
|
|
249
|
+
|
|
250
|
+
"""
|
|
251
|
+
return settled(bases(tree), lambda base: allowed(_resolved(base, origins), entries))
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def allowed(base: str, entries: Sequence[str] = ()) -> bool:
|
|
255
|
+
"""Check whether a base no checked file defines is one a plain class may have.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Whether it's a test case's (`TEST_CASES`), a builtin's (`BUILTIN_BASES`), one passed over
|
|
259
|
+
(`PASSED_OVER`) or a listed one (`listed`), by its origin.
|
|
260
|
+
|
|
261
|
+
"""
|
|
262
|
+
return (
|
|
263
|
+
base in TEST_CASES
|
|
264
|
+
or base in BUILTIN_BASES
|
|
265
|
+
or base in PASSED_OVER
|
|
266
|
+
or base in FUNCTIONAL_BASES
|
|
267
|
+
or listed(base, entries)
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def listed(base: str, entries: Sequence[str]) -> bool:
|
|
272
|
+
"""Check whether `entries` list a base, by its dotted origin (`django.db.models.Model`).
|
|
273
|
+
|
|
274
|
+
An entry names a class, or a package for every class in it (`django`); one starting `!` leaves
|
|
275
|
+
out what it names, whatever else lists it.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
Whether one lists it, and none leaves it out.
|
|
279
|
+
|
|
280
|
+
"""
|
|
281
|
+
left_out: list[str] = [entry[1:] for entry in entries if entry.startswith(_NOT)]
|
|
282
|
+
kept: list[str] = [entry for entry in entries if not entry.startswith(_NOT)]
|
|
283
|
+
return _under(base, kept) and not _under(base, left_out)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _under(base: str, paths: Sequence[str]) -> bool:
|
|
287
|
+
return any(base == path or base.startswith(f"{path}.") for path in paths)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def ancestors(name: str, found: Bases) -> list[str]:
|
|
291
|
+
"""Name every class of `found` that `name` inherits from, however far.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Them, sorted.
|
|
295
|
+
|
|
296
|
+
"""
|
|
297
|
+
return sorted(_ancestors(name, found, frozenset({name})))
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def reserved(name: str, found: Bases) -> frozenset[str]:
|
|
301
|
+
"""Name what the builtin classes `name` inherits from have themselves, as far as `found` sees.
|
|
302
|
+
|
|
303
|
+
A variable hiding one would be declared another type than its base declares it.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
Those names.
|
|
307
|
+
|
|
308
|
+
"""
|
|
309
|
+
owners: list[str] = [name, *ancestors(name, found)]
|
|
310
|
+
return frozenset[str]().union(
|
|
311
|
+
*(BUILTIN_BASES.get(base, frozenset[str]()) for owner in owners for base in found[owner]),
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def settled(found: Bases, outside: Callable[[str], bool]) -> frozenset[str]:
|
|
316
|
+
"""Settle which classes are plain, given each one's bases (see `bases`).
|
|
317
|
+
|
|
318
|
+
`outside`: whether a base that isn't one of `found` is one a plain class may have.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
The names of those whose every base is plain, and that no class that isn't inherits from.
|
|
322
|
+
|
|
323
|
+
"""
|
|
324
|
+
above: dict[str, set[str]] = {name: _ancestors(name, found, frozenset({name})) for name in found}
|
|
325
|
+
rooted: set[str] = {name for name in found if all(base in found or outside(base) for base in found[name])}
|
|
326
|
+
kept: set[str] = {name for name in rooted if above[name] <= rooted}
|
|
327
|
+
# A class that isn't plain makes what it inherits from not plain either: a model's mixin.
|
|
328
|
+
mixed: set[str] = {base for name in found.keys() - kept for base in above[name]}
|
|
329
|
+
return frozenset(kept - mixed)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _ancestors(name: str, found: Bases, seen: frozenset[str]) -> set[str]:
|
|
333
|
+
"""Name every class of `found` that `name` inherits from, however far.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Them.
|
|
337
|
+
|
|
338
|
+
"""
|
|
339
|
+
above: set[str] = {base for base in found[name] if base in found and base not in seen}
|
|
340
|
+
base: str
|
|
341
|
+
for base in sorted(above):
|
|
342
|
+
above |= _ancestors(base, found, seen | above)
|
|
343
|
+
return above
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _resolved(base: str, origins: Mapping[str, str]) -> str:
|
|
347
|
+
"""Resolve a base as written through the module's imports (`TestCase`, `unittest.TestCase`).
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Its dotted origin; itself, if its first name isn't one the module imports.
|
|
351
|
+
|
|
352
|
+
"""
|
|
353
|
+
first: str
|
|
354
|
+
rest: str
|
|
355
|
+
called: str = CALLED if base.endswith(CALLED) else ""
|
|
356
|
+
first, _, rest = base.removesuffix(CALLED).partition(".")
|
|
357
|
+
origin: str = origins.get(first, first)
|
|
358
|
+
return f"{origin}.{rest}{called}" if rest else f"{origin}{called}"
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def variables(
|
|
362
|
+
tree: ast.Module,
|
|
363
|
+
origins: Mapping[str, str],
|
|
364
|
+
outside: Outside | None,
|
|
365
|
+
entries: Sequence[str] = (),
|
|
366
|
+
) -> dict[str, Mapping[str, str]]:
|
|
367
|
+
"""Type the plain classes' variables a module reads: its own, and those it imports.
|
|
368
|
+
|
|
369
|
+
Which of its own are plain is the index's to say (`Outside.plain`), or else the module's alone,
|
|
370
|
+
with `entries` the listed bases (see `listed`).
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
Each class's variables' types, as the module spells the class.
|
|
374
|
+
|
|
375
|
+
"""
|
|
376
|
+
own: frozenset[str] = own_plain(tree, origins, outside, entries)
|
|
377
|
+
typed: dict[str, dict[str, str]] = members(tree) if own else {}
|
|
378
|
+
# The index's of its own: what the file alone types, and what it types by other files' classes.
|
|
379
|
+
indexed: Mapping[str, Mapping[str, str]] = {} if outside is None else outside.members
|
|
380
|
+
return {
|
|
381
|
+
**indexed,
|
|
382
|
+
**{
|
|
383
|
+
name: {**typed.get(name, {}), **indexed.get(name, {})}
|
|
384
|
+
for name in own
|
|
385
|
+
if name in typed or name in indexed
|
|
386
|
+
},
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def own_plain(
|
|
391
|
+
tree: ast.Module,
|
|
392
|
+
origins: Mapping[str, str],
|
|
393
|
+
outside: Outside | None,
|
|
394
|
+
entries: Sequence[str] = (),
|
|
395
|
+
) -> frozenset[str]:
|
|
396
|
+
"""Name the module's own plain classes: the index's to say (`Outside.plain`), or else the module's alone.
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
Them.
|
|
400
|
+
|
|
401
|
+
"""
|
|
402
|
+
indexed: frozenset[str] | None = None if outside is None else outside.plain
|
|
403
|
+
return plain(tree, origins, entries) if indexed is None else indexed
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def members(tree: ast.Module) -> dict[str, dict[str, str]]:
|
|
407
|
+
"""Type the variables of each class the module defines once (see the module docstring).
|
|
408
|
+
|
|
409
|
+
Of every such class, plain or not: which are plain is the caller's to say.
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
Each class's variables' types, by name; a class with none is left out.
|
|
413
|
+
|
|
414
|
+
"""
|
|
415
|
+
found: dict[str, dict[str, str]] | None
|
|
416
|
+
if (found := _MEMBERS.get(tree)) is None:
|
|
417
|
+
found = _MEMBERS[tree] = _read_members(tree)
|
|
418
|
+
return found
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _read_members(tree: ast.Module) -> dict[str, dict[str, str]]:
|
|
422
|
+
single: dict[str, tuple[str, ...]]
|
|
423
|
+
if not (single := bases(tree)):
|
|
424
|
+
return {}
|
|
425
|
+
stored: dict[str, list[_Store | None]] = _stores(tree)
|
|
426
|
+
origins: dict[str, str] = imported(tree)
|
|
427
|
+
known: Known = library(tree)
|
|
428
|
+
typed: dict[str, dict[str, str]] = {
|
|
429
|
+
node.name: _typed(
|
|
430
|
+
node.body,
|
|
431
|
+
reserved(node.name, single),
|
|
432
|
+
stored,
|
|
433
|
+
(HELD if _may_test(node.name, single, origins) else {}, known),
|
|
434
|
+
)
|
|
435
|
+
for node in classes(tree)
|
|
436
|
+
if node.name in single
|
|
437
|
+
}
|
|
438
|
+
if not any(typed.values()):
|
|
439
|
+
return {}
|
|
440
|
+
declared: Mapping[str, Mapping[str, str]] = annotated(tree)
|
|
441
|
+
kept: dict[str, dict[str, str]] = {
|
|
442
|
+
name: agreeing(each, [declared[above] for above in ancestors(name, single)])
|
|
443
|
+
for name, each in typed.items()
|
|
444
|
+
}
|
|
445
|
+
return {name: each for name, each in kept.items() if each}
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def named(tree: ast.Module) -> dict[str, dict[str, str]]:
|
|
449
|
+
"""Find the variables of each class the module defines once that are bound to a bare name.
|
|
450
|
+
|
|
451
|
+
`form_class = PasswordChangeForm`: a `type[PasswordChangeForm]`, where the index finds a checked
|
|
452
|
+
file's class by that name and no class above or under this one binds the variable
|
|
453
|
+
(`plain.settled`). Bound once, directly in the body, and not one the module stores
|
|
454
|
+
(`self.form_class = ...`), a builtin base has, or its value's own text types already.
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
Each class's such variables, with the name each is bound to; a class with none left out.
|
|
458
|
+
|
|
459
|
+
"""
|
|
460
|
+
single: dict[str, tuple[str, ...]]
|
|
461
|
+
if not (single := bases(tree)):
|
|
462
|
+
return {}
|
|
463
|
+
stored: dict[str, list[_Store | None]] = _stores(tree)
|
|
464
|
+
typed: dict[str, dict[str, str]] = members(tree)
|
|
465
|
+
found: dict[str, dict[str, str]] = {}
|
|
466
|
+
node: ast.ClassDef
|
|
467
|
+
for node in (each for each in classes(tree) if each.name in single):
|
|
468
|
+
counts: Counter[str] = Counter(name for stmt in node.body for name in _bound(stmt))
|
|
469
|
+
kept: frozenset[str] = reserved(node.name, single).union(typed.get(node.name, {}), stored)
|
|
470
|
+
bound: dict[str, str] = {}
|
|
471
|
+
stmt: ast.stmt
|
|
472
|
+
for stmt in node.body:
|
|
473
|
+
variable: str
|
|
474
|
+
value: str
|
|
475
|
+
match stmt:
|
|
476
|
+
case ast.Assign(targets=[ast.Name(id=variable)], value=ast.Name(id=value)) if (
|
|
477
|
+
counts[variable] == 1 and variable not in kept
|
|
478
|
+
):
|
|
479
|
+
bound[variable] = value
|
|
480
|
+
case _:
|
|
481
|
+
pass
|
|
482
|
+
if bound:
|
|
483
|
+
found[node.name] = bound
|
|
484
|
+
return found
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _may_test(name: str, found: Bases, origins: Mapping[str, str]) -> bool:
|
|
488
|
+
"""Check whether a class may be a test case: it has a base no class of the module, nor a builtin, is.
|
|
489
|
+
|
|
490
|
+
A test case's own, or a framework's (which this file can't see the bases of).
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
Whether it may.
|
|
494
|
+
|
|
495
|
+
"""
|
|
496
|
+
return any(
|
|
497
|
+
base not in found and _resolved(base, origins) not in BUILTIN_BASES.keys() | PASSED_OVER
|
|
498
|
+
for owner in (name, *ancestors(name, found))
|
|
499
|
+
for base in found[owner]
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def agreeing(typed: Mapping[str, str], declared: Sequence[Mapping[str, str]]) -> dict[str, str]:
|
|
504
|
+
"""Keep a class's variables that no class above it annotates as another type.
|
|
505
|
+
|
|
506
|
+
`declared`: each such class's annotated attributes.
|
|
507
|
+
|
|
508
|
+
Returns:
|
|
509
|
+
Those variables' types, by name.
|
|
510
|
+
|
|
511
|
+
"""
|
|
512
|
+
return {
|
|
513
|
+
name: annotation
|
|
514
|
+
for name, annotation in typed.items()
|
|
515
|
+
if all(above.get(name, annotation) == annotation for above in declared)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _stores(tree: ast.Module) -> dict[str, list[_Store | None]]:
|
|
520
|
+
"""Find how the module stores each attribute, whatever it's an attribute of.
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
Each stored attribute's name, with its every store: an assignment's value (`self.x = 1`), with
|
|
524
|
+
an augmented one's operator (`self.x += 1`), or `None` for any other (an unpacking, a loop's
|
|
525
|
+
target, a `del`, an annotated one).
|
|
526
|
+
|
|
527
|
+
"""
|
|
528
|
+
assigned: dict[ast.expr, _Store] = {
|
|
529
|
+
target: (None, stmt.value)
|
|
530
|
+
for stmt in cast("list[ast.Assign]", of_type(tree, ast.Assign))
|
|
531
|
+
for target in stmt.targets
|
|
532
|
+
}
|
|
533
|
+
stmt: ast.AugAssign
|
|
534
|
+
for stmt in cast("list[ast.AugAssign]", of_type(tree, ast.AugAssign)):
|
|
535
|
+
assigned[stmt.target] = (stmt.op, stmt.value)
|
|
536
|
+
found: dict[str, list[_Store | None]] = {}
|
|
537
|
+
node: ast.Attribute
|
|
538
|
+
for node in cast("list[ast.Attribute]", of_type(tree, ast.Attribute)):
|
|
539
|
+
if not isinstance(node.ctx, ast.Load):
|
|
540
|
+
found.setdefault(node.attr, []).append(assigned.get(node))
|
|
541
|
+
return found
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _keeps(store: _Store | None, annotation: str, known: Known) -> bool:
|
|
545
|
+
"""Check whether a store of an attribute keeps it the type `annotation`.
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
Whether its value's own text types it so (see `member_type`), or (augmented) the operator
|
|
549
|
+
gives that type back.
|
|
550
|
+
|
|
551
|
+
"""
|
|
552
|
+
if store is None:
|
|
553
|
+
return False
|
|
554
|
+
own: str | None = _of(store[1], annotation, known)
|
|
555
|
+
if store[0] is None:
|
|
556
|
+
return own == annotation
|
|
557
|
+
given: ast.BinOp = ast.BinOp(store[1], store[0], store[1])
|
|
558
|
+
return own is not None and operated(given, annotation, own, MAX_LENGTH) == annotation
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _typed(
|
|
562
|
+
body: Sequence[ast.stmt],
|
|
563
|
+
kept: frozenset[str],
|
|
564
|
+
stored: Mapping[str, Sequence[_Store | None]],
|
|
565
|
+
typing: tuple[Mapping[str, str], Known],
|
|
566
|
+
) -> dict[str, str]:
|
|
567
|
+
"""Type a class body's variables: each bound once there, to a value its text types.
|
|
568
|
+
|
|
569
|
+
`kept`: the names left alone (a builtin base's own); `stored`: how the module stores each
|
|
570
|
+
attribute (see `_stores`), which leaves one alone unless every store keeps its type; `typing`:
|
|
571
|
+
the types a base declares some names (see `HELD`), each its variable's where the value is of
|
|
572
|
+
it, and what the module names of the standard library (see `library`).
|
|
573
|
+
|
|
574
|
+
Returns:
|
|
575
|
+
Their types, by name.
|
|
576
|
+
|
|
577
|
+
"""
|
|
578
|
+
bound: dict[str, int] = {}
|
|
579
|
+
stmt: ast.stmt
|
|
580
|
+
name: str
|
|
581
|
+
for stmt in body:
|
|
582
|
+
for name in _bound(stmt):
|
|
583
|
+
bound[name] = bound.get(name, 0) + 1
|
|
584
|
+
found: dict[str, str] = {}
|
|
585
|
+
value: ast.expr
|
|
586
|
+
annotation: str | None
|
|
587
|
+
for stmt in body:
|
|
588
|
+
match stmt:
|
|
589
|
+
case ast.Assign(targets=[ast.Name(id=name)], value=value) if (
|
|
590
|
+
bound[name] == 1
|
|
591
|
+
and name not in kept
|
|
592
|
+
and (annotation := _of(value, typing[0].get(name), typing[1])) is not None
|
|
593
|
+
and all(_keeps(store, annotation, typing[1]) for store in stored.get(name, ()))
|
|
594
|
+
):
|
|
595
|
+
found[name] = annotation
|
|
596
|
+
case _:
|
|
597
|
+
pass
|
|
598
|
+
return found
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def _of(value: ast.expr, held: str | None, known: Known) -> str | None:
|
|
602
|
+
"""Type a variable by its `value`, under a base that declares it `held` (`None`: none does).
|
|
603
|
+
|
|
604
|
+
Returns:
|
|
605
|
+
`held`, where its value is of it, `None` too (`maxDiff = None`); its value's own type (see
|
|
606
|
+
`member_type`) where no base declares it; else `None`.
|
|
607
|
+
|
|
608
|
+
"""
|
|
609
|
+
none: bool = isinstance(value, ast.Constant) and value.value is None
|
|
610
|
+
own: str | None = _NONE if none and held is not None else member_type(value, known)
|
|
611
|
+
if own is None or held is None:
|
|
612
|
+
return own
|
|
613
|
+
return held if own in (union(held) or ()) else None
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _bound(stmt: ast.stmt) -> list[str]:
|
|
617
|
+
"""Name what one statement of a class's body binds: a definition's name, or every name it stores.
|
|
618
|
+
|
|
619
|
+
Returns:
|
|
620
|
+
Them, each as many times as it's bound.
|
|
621
|
+
|
|
622
|
+
"""
|
|
623
|
+
if isinstance(stmt, _DEFINITIONS):
|
|
624
|
+
return [stmt.name]
|
|
625
|
+
return [
|
|
626
|
+
node.id
|
|
627
|
+
for node in ast.walk(stmt)
|
|
628
|
+
if isinstance(node, ast.Name) and not isinstance(node.ctx, ast.Load)
|
|
629
|
+
]
|
|
Binary file
|