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,652 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Binding the names a statement binds: each target typed, or declared before the statement.
|
|
3
|
+
|
|
4
|
+
`bind` is what `checker` calls for every statement: a plain `name = value` goes through the scope
|
|
5
|
+
(`Scope.assign`); a loop's or an unpacking's targets are split from the value's type and declared on a
|
|
6
|
+
line of their own before it; a loop typed only by its `# type:` comment (LVA003) declares that type
|
|
7
|
+
instead; `with manager as name` declares `name` first; and a `match`'s captures and an augmented
|
|
8
|
+
assignment are bound as they are. `walruses` binds a statement's `:=` targets, each declared before
|
|
9
|
+
it too.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import ast
|
|
13
|
+
from collections.abc import Iterator, Mapping
|
|
14
|
+
from typing import Final, TypeAlias, cast
|
|
15
|
+
|
|
16
|
+
from constricter.fix.core.known import Inference, Known
|
|
17
|
+
from constricter.fix.libraries.opened import opened
|
|
18
|
+
from constricter.fix.values import shapes
|
|
19
|
+
from constricter.fix.values.doubts import bare
|
|
20
|
+
from constricter.fix.values.entered import Entered, entered, entered_async, entering
|
|
21
|
+
from constricter.fix.values.inference import inference
|
|
22
|
+
from constricter.fix.values.loops import LoopPart, looped, looped_parts
|
|
23
|
+
from constricter.fix.values.targets import iterated, unpacked
|
|
24
|
+
from constricter.offences import COMMENT_TYPED_TARGET, UNTYPED_TARGET, VAGUE_TYPE, Edit, Fix, Offence, at
|
|
25
|
+
from constricter.rules.flow import augmented, members
|
|
26
|
+
from constricter.rules.scope import Scope, certain_type, guessed_type, guesses_in
|
|
27
|
+
from constricter.rules.scoped import Bare, Late
|
|
28
|
+
from constricter.rules.syntax import captures, comment_type, target_names, type_comment_span
|
|
29
|
+
|
|
30
|
+
_COMMENT: Final = "comment" # the fix kind of LVA003's declaration
|
|
31
|
+
_UNPACK: Final = frozenset({"unpack"}) # the fix kind of an unpacking's split
|
|
32
|
+
_TUPLE: Final = ("tuple[", "Tuple[") # how a tuple's type starts
|
|
33
|
+
_ANY: Final = ", ...]" # and how one of any length ends
|
|
34
|
+
# A name's inference (`None`: unknown), whether it's a guess, and what the guess rests on.
|
|
35
|
+
# What `Scope.valued` gives.
|
|
36
|
+
_Valued: TypeAlias = tuple[Inference | None, bool, frozenset[str]]
|
|
37
|
+
_Named: TypeAlias = tuple[ast.Name, _Valued]
|
|
38
|
+
_COMPREHENSIONS: Final = (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)
|
|
39
|
+
# Statements a declaration can't go before: a decorator's line is its definition's.
|
|
40
|
+
_DEFINITIONS: Final = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def bind(scope: Scope, stmt: ast.stmt) -> None:
|
|
44
|
+
"""Bind the names `stmt` binds that need typing, reporting the untyped ones."""
|
|
45
|
+
target: ast.expr
|
|
46
|
+
items: list[ast.withitem]
|
|
47
|
+
comment: str | None
|
|
48
|
+
name: str
|
|
49
|
+
single: ast.Name
|
|
50
|
+
value: ast.expr
|
|
51
|
+
cases: list[ast.match_case]
|
|
52
|
+
op: ast.operator
|
|
53
|
+
match stmt:
|
|
54
|
+
case ast.Assign(targets=[ast.Name() as single], value=value, type_comment=comment):
|
|
55
|
+
scope.assign(single, scope.unannotated(comment), value)
|
|
56
|
+
case ast.Assign(targets=[ast.Tuple() | ast.List() as target], value=value):
|
|
57
|
+
_bind_unpacked(scope, stmt, target, value)
|
|
58
|
+
case ast.Assign():
|
|
59
|
+
_bind_assigned(scope, stmt)
|
|
60
|
+
case ast.With(items=items, type_comment=comment) | ast.AsyncWith(items=items, type_comment=comment):
|
|
61
|
+
_bind_with(scope, stmt, items, scope.unannotated(comment))
|
|
62
|
+
case (
|
|
63
|
+
ast.For(target=target, iter=value, type_comment=None)
|
|
64
|
+
| ast.AsyncFor(
|
|
65
|
+
target=target,
|
|
66
|
+
iter=value,
|
|
67
|
+
type_comment=None,
|
|
68
|
+
)
|
|
69
|
+
):
|
|
70
|
+
_bind_loop(scope, stmt, target, value)
|
|
71
|
+
case (
|
|
72
|
+
ast.For(target=target, type_comment=str() as comment)
|
|
73
|
+
| ast.AsyncFor(
|
|
74
|
+
target=target,
|
|
75
|
+
type_comment=str() as comment,
|
|
76
|
+
)
|
|
77
|
+
):
|
|
78
|
+
_bind_commented(scope, stmt, target, comment)
|
|
79
|
+
case ast.Match(cases=cases):
|
|
80
|
+
_bind_captures(scope, cases)
|
|
81
|
+
case ast.AugAssign(target=ast.Name(id=name) as single, op=op, value=value):
|
|
82
|
+
own: str | None = None if name in scope.inferred.guesses else scope.inferred.types.get(name)
|
|
83
|
+
bound: str | None = augmented(op, certain_type(scope, value), own)
|
|
84
|
+
scope.lifetime(name).bind(at(single), bound, None if bound else _augmented_guess(scope, stmt))
|
|
85
|
+
scope.inferred.rebound(name, bound)
|
|
86
|
+
case _:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def walruses(scope: Scope, part: ast.AST, before: ast.stmt) -> None:
|
|
91
|
+
"""Bind the `:=` targets in `part`, an expression of a statement, each offered a declaration.
|
|
92
|
+
|
|
93
|
+
Comprehensions included, lambdas excluded. A name can't be annotated where `:=` binds it: it's
|
|
94
|
+
declared on a line of its own before `before` (its statement, or the `if` an `elif` belongs to),
|
|
95
|
+
typed as a plain assignment's is. Not one inside a comprehension, whose value may read the
|
|
96
|
+
comprehension's own names, nor where a line can't go before the statement: a definition (its
|
|
97
|
+
decorators' lines are its own), or one that doesn't start its line (`else: x = (y := 1)`).
|
|
98
|
+
Asked only in a module with a `:=` (`Settings.walruses`).
|
|
99
|
+
"""
|
|
100
|
+
hidden: set[int] = _inside(part, ast.Lambda)
|
|
101
|
+
line: bytes = scope.settings.lines[before.lineno - 1].encode() if scope.settings.lines else b""
|
|
102
|
+
plain: bool = isinstance(before, _DEFINITIONS) or bool(line[: before.col_offset].strip())
|
|
103
|
+
comprehended: set[int] = set() if plain else _inside(part, *_COMPREHENSIONS)
|
|
104
|
+
code: str = scope.kind.unannotated
|
|
105
|
+
node: ast.AST
|
|
106
|
+
for node in ast.walk(part):
|
|
107
|
+
if not isinstance(node, ast.NamedExpr) or id(node) in hidden:
|
|
108
|
+
continue
|
|
109
|
+
if plain or id(node) in comprehended:
|
|
110
|
+
scope.bind(node.target.id, at(node.target), code)
|
|
111
|
+
else:
|
|
112
|
+
_bind_declaration(scope, before, node.target, code, scope.valued(node.value, node.lineno))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _inside(part: ast.AST, *kinds: type[ast.AST]) -> set[int]:
|
|
116
|
+
"""Find every node inside a node of one of `kinds`, in `part`.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Their `id()`s.
|
|
120
|
+
|
|
121
|
+
"""
|
|
122
|
+
return {id(inner) for outer in ast.walk(part) if isinstance(outer, kinds) for inner in ast.walk(outer)}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _bind_assigned(scope: Scope, stmt: ast.Assign) -> None:
|
|
126
|
+
"""Bind a chained assignment's names (`a = b = 0`), each offered a declaration before it, or others'."""
|
|
127
|
+
code: str | None = scope.unannotated(stmt.type_comment)
|
|
128
|
+
if not all(isinstance(target, ast.Name) for target in stmt.targets):
|
|
129
|
+
_bind_targets(scope, stmt.targets, code)
|
|
130
|
+
return
|
|
131
|
+
target: ast.expr
|
|
132
|
+
for target in stmt.targets:
|
|
133
|
+
scope.assign(cast("ast.Name", target), code, stmt.value, stmt)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _bind_loop(scope: Scope, stmt: ast.For | ast.AsyncFor, target: ast.expr, value: ast.expr) -> None:
|
|
137
|
+
"""Bind a loop's target (see `_bind_declared`), over `enumerate` or `zip` one part at a time.
|
|
138
|
+
|
|
139
|
+
`for i, x in enumerate(xs)` declares `i: int` even when `xs`'s elements aren't known, and a
|
|
140
|
+
guess about them makes only `x`'s fix one.
|
|
141
|
+
"""
|
|
142
|
+
parts: list[LoopPart] | None = looped_parts(
|
|
143
|
+
value,
|
|
144
|
+
scope.settings.known,
|
|
145
|
+
scope.inferred.types,
|
|
146
|
+
inference,
|
|
147
|
+
)
|
|
148
|
+
elements: list[ast.expr]
|
|
149
|
+
match target:
|
|
150
|
+
case ast.Tuple(elts=elements) | ast.List(elts=elements) if (
|
|
151
|
+
parts is not None
|
|
152
|
+
and len(elements) == len(parts)
|
|
153
|
+
and not any(isinstance(element, ast.Starred) for element in elements)
|
|
154
|
+
):
|
|
155
|
+
element: ast.expr
|
|
156
|
+
typed: Inference | None
|
|
157
|
+
bases: list[ast.expr]
|
|
158
|
+
for element, (typed, bases) in zip(elements, parts, strict=True):
|
|
159
|
+
_bind_declared(scope, stmt, element, typed, bases)
|
|
160
|
+
case _:
|
|
161
|
+
_bind_declared(
|
|
162
|
+
scope,
|
|
163
|
+
stmt,
|
|
164
|
+
target,
|
|
165
|
+
looped(value, scope.settings.known, scope.inferred.types, inference),
|
|
166
|
+
iterated(value),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _bind_unpacked(scope: Scope, stmt: ast.Assign, target: ast.expr, value: ast.expr) -> None:
|
|
171
|
+
"""Bind an unpacking's names, offering to declare each before `stmt` (see `_unpacked`).
|
|
172
|
+
|
|
173
|
+
Every name is typed before any is bound, as its value is evaluated: `a, b = b, a`.
|
|
174
|
+
"""
|
|
175
|
+
code: str | None = scope.unannotated(stmt.type_comment)
|
|
176
|
+
name: ast.Name
|
|
177
|
+
typed: _Valued
|
|
178
|
+
for name, typed in list(_unpacked(scope, stmt, target, value)):
|
|
179
|
+
_bind_declaration(scope, stmt, name, code, typed)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _source(stmt: ast.stmt, name: ast.Name) -> ast.expr | None:
|
|
183
|
+
"""Find what a loop iterates, an unpacking splits or a `with` enters to bind `name`.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
It, or `None` for a name `stmt` binds otherwise (a `:=`'s, a chained assignment's).
|
|
187
|
+
|
|
188
|
+
"""
|
|
189
|
+
target: ast.expr
|
|
190
|
+
value: ast.expr
|
|
191
|
+
items: list[ast.withitem]
|
|
192
|
+
pairs: Iterator[tuple[ast.Name, ast.expr]]
|
|
193
|
+
match stmt:
|
|
194
|
+
case ast.For(target=target, iter=value) | ast.AsyncFor(target=target, iter=value):
|
|
195
|
+
pairs = ((each, value) for each in target_names(target))
|
|
196
|
+
case ast.Assign(targets=[target], value=value):
|
|
197
|
+
pairs = _sources(target, value)
|
|
198
|
+
case ast.With(items=items) | ast.AsyncWith(items=items):
|
|
199
|
+
pairs = (
|
|
200
|
+
(each, item.context_expr)
|
|
201
|
+
for item in items
|
|
202
|
+
if item.optional_vars is not None
|
|
203
|
+
for each in target_names(item.optional_vars)
|
|
204
|
+
)
|
|
205
|
+
case _:
|
|
206
|
+
return None
|
|
207
|
+
return next((taken for each, taken in pairs if each is name), None)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _sources(target: ast.expr, value: ast.expr) -> Iterator[tuple[ast.Name, ast.expr]]:
|
|
211
|
+
"""Pair each name an unpacking of `value` into `target` binds with what it's taken from.
|
|
212
|
+
|
|
213
|
+
Yields:
|
|
214
|
+
Each name, with its own value where a display of as many is split with the target (see
|
|
215
|
+
`_unpacked`), else with `value`.
|
|
216
|
+
|
|
217
|
+
"""
|
|
218
|
+
targets: list[ast.expr]
|
|
219
|
+
values: list[ast.expr]
|
|
220
|
+
match (target, value):
|
|
221
|
+
case (
|
|
222
|
+
ast.Tuple(elts=targets) | ast.List(elts=targets),
|
|
223
|
+
ast.Tuple(elts=values) | ast.List(elts=values),
|
|
224
|
+
) if len(targets) == len(values) and not any(
|
|
225
|
+
isinstance(part, ast.Starred) for part in (*targets, *values)
|
|
226
|
+
):
|
|
227
|
+
part: ast.expr
|
|
228
|
+
item: ast.expr
|
|
229
|
+
for part, item in zip(targets, values, strict=True):
|
|
230
|
+
yield from _sources(part, item)
|
|
231
|
+
case _:
|
|
232
|
+
name: ast.Name
|
|
233
|
+
for name in target_names(target):
|
|
234
|
+
yield name, value
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _unpacked(scope: Scope, stmt: ast.Assign, target: ast.expr, value: ast.expr) -> Iterator[_Named]:
|
|
238
|
+
"""Type each name an unpacking of `value` into `target` binds.
|
|
239
|
+
|
|
240
|
+
A display of as many values is split with its target, each name typed by its own value as a
|
|
241
|
+
plain assignment's is (`a, b = x, 1` declares `b: int` whatever `x` is); any other value's type
|
|
242
|
+
is split over the names (see `unpacked`), or its elements' is (`a, b = range(2)`).
|
|
243
|
+
|
|
244
|
+
Yields:
|
|
245
|
+
Each name, with its inference, whether that's a guess, and what the guess rests on.
|
|
246
|
+
|
|
247
|
+
"""
|
|
248
|
+
targets: list[ast.expr]
|
|
249
|
+
values: list[ast.expr]
|
|
250
|
+
name: ast.Name
|
|
251
|
+
match (target, value):
|
|
252
|
+
case (
|
|
253
|
+
ast.Tuple(elts=targets) | ast.List(elts=targets),
|
|
254
|
+
ast.Tuple(elts=values) | ast.List(elts=values),
|
|
255
|
+
) if len(targets) == len(values) and not any(
|
|
256
|
+
isinstance(part, ast.Starred) for part in (*targets, *values)
|
|
257
|
+
):
|
|
258
|
+
part: ast.expr
|
|
259
|
+
item: ast.expr
|
|
260
|
+
for part, item in zip(targets, values, strict=True):
|
|
261
|
+
yield from _unpacked(scope, stmt, part, item)
|
|
262
|
+
case (ast.Name() as name, _):
|
|
263
|
+
fix: Inference | None
|
|
264
|
+
unsafe: bool
|
|
265
|
+
origins: frozenset[str]
|
|
266
|
+
fix, unsafe, origins = scope.valued(value, stmt.lineno)
|
|
267
|
+
yield name, (None if fix is None else fix._replace(kinds=fix.kinds | _UNPACK), unsafe, origins)
|
|
268
|
+
case _:
|
|
269
|
+
yield from _unpacked_whole(scope, stmt, target, value)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _unpacked_whole(scope: Scope, stmt: ast.Assign, target: ast.expr, value: ast.expr) -> Iterator[_Named]:
|
|
273
|
+
"""Type each name an unpacking of `value` binds by `value`'s own type, split over them.
|
|
274
|
+
|
|
275
|
+
Its type as any value's is inferred (a call's declared return with a vague part too: a part
|
|
276
|
+
vaguer than `vague` allows is its name's type, and no fix); else what iterating it gives, each
|
|
277
|
+
name an element.
|
|
278
|
+
|
|
279
|
+
Yields:
|
|
280
|
+
Each name, with its inference, whether that's a guess, and what the guess rests on.
|
|
281
|
+
|
|
282
|
+
"""
|
|
283
|
+
known: Known = scope.settings.known
|
|
284
|
+
typed: Inference | None = inference(value, known, scope.inferred.types)
|
|
285
|
+
element: Inference | None
|
|
286
|
+
if (element := None if typed else looped(value, known, scope.inferred.types, inference)) is not None:
|
|
287
|
+
typed = element._replace(annotation=f"tuple[{element.annotation}, ...]")
|
|
288
|
+
yield from _split(scope, stmt, target, typed, iterated(value))
|
|
289
|
+
return
|
|
290
|
+
yield from _split(scope, stmt, target, typed, [value])
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _bind_declared(
|
|
294
|
+
scope: Scope,
|
|
295
|
+
stmt: ast.For | ast.AsyncFor,
|
|
296
|
+
target: ast.expr,
|
|
297
|
+
typed: Inference | None,
|
|
298
|
+
bases: list[ast.expr],
|
|
299
|
+
) -> None:
|
|
300
|
+
"""Bind each name in a loop's `target`, offering to declare each before `stmt` (see `_split`)."""
|
|
301
|
+
name: ast.Name
|
|
302
|
+
part: _Valued
|
|
303
|
+
for name, part in _split(scope, stmt, target, typed, bases):
|
|
304
|
+
_bind_declaration(scope, stmt, name, UNTYPED_TARGET, part)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _split(
|
|
308
|
+
scope: Scope,
|
|
309
|
+
stmt: ast.stmt,
|
|
310
|
+
target: ast.expr,
|
|
311
|
+
typed: Inference | None,
|
|
312
|
+
bases: list[ast.expr],
|
|
313
|
+
) -> Iterator[_Named]:
|
|
314
|
+
"""Type each name in `target` (a loop's, or an unpacking's).
|
|
315
|
+
|
|
316
|
+
`typed` is what the whole target gets (a loop's element, an unpacked value's type), split over
|
|
317
|
+
its names (see `unpacked`); a name whose part isn't known gets no fix. The fixes are guesses if
|
|
318
|
+
any of `bases`, the values `typed` came from, is.
|
|
319
|
+
|
|
320
|
+
Yields:
|
|
321
|
+
Each name, with its inference, whether that's a guess, and what the guess rests on.
|
|
322
|
+
|
|
323
|
+
"""
|
|
324
|
+
unsafe: bool
|
|
325
|
+
origins: frozenset[str]
|
|
326
|
+
unsafe, origins = (False, frozenset()) if typed is None else guesses_in(scope, bases)
|
|
327
|
+
# An unpacking's names are split from the value's type; a loop's are what it iterates (`loop`).
|
|
328
|
+
split: frozenset[str] = frozenset() if isinstance(stmt, ast.For | ast.AsyncFor) else _UNPACK
|
|
329
|
+
name: ast.Name
|
|
330
|
+
annotation: str | None
|
|
331
|
+
whole: str | None = None if typed is None else typed.annotation
|
|
332
|
+
tuples: Mapping[str, str] = scope.settings.known.indirect.tuples
|
|
333
|
+
parts: list[tuple[ast.Name, str | None]] = unpacked(target, whole, tuples)
|
|
334
|
+
mixed: bool = _by_position(parts, whole or "", tuples)
|
|
335
|
+
for name, annotation in parts:
|
|
336
|
+
part: Inference | None = (
|
|
337
|
+
None
|
|
338
|
+
if typed is None or annotation is None or mixed
|
|
339
|
+
else Inference(annotation, typed.reason, typed.kinds | split)
|
|
340
|
+
)
|
|
341
|
+
yield name, (part, unsafe, origins)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _by_position(parts: list[tuple[ast.Name, str | None]], whole: str, tuples: Mapping[str, str]) -> bool:
|
|
345
|
+
"""Whether `parts`, split from `whole`, share a union that may be by position.
|
|
346
|
+
|
|
347
|
+
A union over several names, of anything but a tuple of that many, is by position as often as
|
|
348
|
+
not (`name, length`, of `[["prefix", 24], ...]`): each has no fix.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Whether each name's part is the same union, of a `whole` that isn't a tuple of that many.
|
|
352
|
+
|
|
353
|
+
"""
|
|
354
|
+
fixed: bool = tuples.get(whole, whole).startswith(_TUPLE) and not whole.endswith(_ANY)
|
|
355
|
+
return (
|
|
356
|
+
not fixed
|
|
357
|
+
and len(parts) > 1
|
|
358
|
+
and len({annotation for _, annotation in parts}) == 1
|
|
359
|
+
and len(members(parts[0][1] or "") or ()) > 1
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _bind_declaration(
|
|
364
|
+
scope: Scope,
|
|
365
|
+
stmt: ast.stmt,
|
|
366
|
+
name: ast.Name,
|
|
367
|
+
code: str | None,
|
|
368
|
+
typed: _Valued,
|
|
369
|
+
) -> None:
|
|
370
|
+
"""Bind one name a statement binds, offering to declare it before `stmt` as `typed` has it.
|
|
371
|
+
|
|
372
|
+
`typed`: its inference (`None`: unknown, when the type checker's hint is asked), whether that's a
|
|
373
|
+
guess, and what the guess rests on. A loop's, an unpacking's or a `with`'s name left with no fix
|
|
374
|
+
is noted with what it's taken from (see `_source`), for `late.widens`, whose wider type is
|
|
375
|
+
declared before `stmt` too.
|
|
376
|
+
"""
|
|
377
|
+
found: Inference | None
|
|
378
|
+
unsafe: bool
|
|
379
|
+
origins: frozenset[str]
|
|
380
|
+
found, unsafe, origins = typed
|
|
381
|
+
hint: Inference | None
|
|
382
|
+
if (found is None or not scope.writable(found)) and (hint := scope.hint(name)) is not None:
|
|
383
|
+
found, unsafe, origins = hint, True, hint.kinds
|
|
384
|
+
fix: Fix | None = None
|
|
385
|
+
noted: Bare | None = scope.assignments.bare.get(name.id)
|
|
386
|
+
if found is not None:
|
|
387
|
+
fix = scope.offer(
|
|
388
|
+
found,
|
|
389
|
+
origins,
|
|
390
|
+
unsafe=unsafe,
|
|
391
|
+
edit=Edit.DECLARE,
|
|
392
|
+
span=(stmt.lineno, stmt.col_offset),
|
|
393
|
+
)
|
|
394
|
+
# What the rest of the scope infers from `name` knows its type, as for `name = value`.
|
|
395
|
+
scope.inferred.learn(
|
|
396
|
+
name.id,
|
|
397
|
+
found.annotation,
|
|
398
|
+
origins if unsafe else None,
|
|
399
|
+
# A marked declaration with no value bound it to nothing.
|
|
400
|
+
again=name.id in scope.declared and noted is None,
|
|
401
|
+
)
|
|
402
|
+
if found is not None and noted is not None and noted[1] is None:
|
|
403
|
+
scope.assignments.bare[name.id] = (noted[0], found)
|
|
404
|
+
source: ast.expr | None
|
|
405
|
+
if (source := None if fix or name.id in scope.declared else _source(stmt, name)) is not None:
|
|
406
|
+
scope.assignments.plain[name.id] = (name, source, found)
|
|
407
|
+
scope.assignments.chained[name.id] = (stmt.lineno, stmt.col_offset)
|
|
408
|
+
typed_as: str | Late | None = None
|
|
409
|
+
if found is not None and scope.writable(found):
|
|
410
|
+
typed_as = (found.annotation, origins) if unsafe else found.annotation
|
|
411
|
+
scope.bind(name.id, at(name), code, fix, typed_as)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _bind_commented(scope: Scope, stmt: ast.For | ast.AsyncFor, target: ast.expr, comment: str) -> None:
|
|
415
|
+
"""Bind a loop's target typed only by its `# type:` comment (LVA003), offering to declare it instead.
|
|
416
|
+
|
|
417
|
+
The comment's type (`int`, or `int, str` for a tuple target) is split over the target's names as
|
|
418
|
+
an unpacking's is; each is declared before the loop, and the comment dropped, since a type checker
|
|
419
|
+
would see the name declared twice. Only for a loop whose header is on one line, where the comment
|
|
420
|
+
is found after its iterable.
|
|
421
|
+
"""
|
|
422
|
+
drop: tuple[int, int] | None = type_comment_span(scope.settings.lines, stmt)
|
|
423
|
+
annotation: str | None = comment_type(comment) if drop is not None else None
|
|
424
|
+
name: ast.Name
|
|
425
|
+
part: str | None
|
|
426
|
+
for name, part in unpacked(target, annotation):
|
|
427
|
+
fix: Fix | None = None
|
|
428
|
+
if part is not None and drop is not None:
|
|
429
|
+
fix = scope.offer(
|
|
430
|
+
Inference(part, "its `# type:` comment", frozenset({_COMMENT})),
|
|
431
|
+
frozenset(),
|
|
432
|
+
unsafe=False,
|
|
433
|
+
edit=Edit.DECLARE,
|
|
434
|
+
span=(stmt.lineno, stmt.col_offset),
|
|
435
|
+
)
|
|
436
|
+
fix = None if fix is None else fix._replace(drop=drop)
|
|
437
|
+
scope.bind(name.id, at(name), COMMENT_TYPED_TARGET, fix)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _bind_with(scope: Scope, stmt: ast.stmt, items: list[ast.withitem], code: str | None) -> None:
|
|
441
|
+
"""Bind each `with` item's target, offering to declare `with manager as name`'s `name` first.
|
|
442
|
+
|
|
443
|
+
As what the manager's `__enter__` returns (see `constricter.fix.values.entered`); the file object
|
|
444
|
+
`open` gives, which is its own context manager, by its literal mode. A target that unpacks takes
|
|
445
|
+
that type split over its names, as an unpacking's are (but for a vague part). An `async with`'s
|
|
446
|
+
is typed by a standard-library manager's `__aenter__` alone, and not split.
|
|
447
|
+
"""
|
|
448
|
+
item: ast.withitem
|
|
449
|
+
name: ast.Name
|
|
450
|
+
target: ast.expr
|
|
451
|
+
for item in items:
|
|
452
|
+
typed: _Valued = _entered(scope, item.context_expr, asynchronous=isinstance(stmt, ast.AsyncWith))
|
|
453
|
+
match item.optional_vars:
|
|
454
|
+
case ast.Name() as name:
|
|
455
|
+
_bind_declaration(scope, stmt, name, code, typed)
|
|
456
|
+
case None:
|
|
457
|
+
pass
|
|
458
|
+
case target if isinstance(stmt, ast.With):
|
|
459
|
+
named: _Named
|
|
460
|
+
for named in _entered_parts(scope, target, item.context_expr, typed):
|
|
461
|
+
_bind_declaration(scope, stmt, named[0], code, named[1])
|
|
462
|
+
case target:
|
|
463
|
+
_bind_targets(scope, [target], code)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _entered_parts(scope: Scope, target: ast.expr, manager: ast.expr, typed: _Valued) -> Iterator[_Named]:
|
|
467
|
+
"""Type each name a `with` statement's unpacking `target` binds, entering `manager`.
|
|
468
|
+
|
|
469
|
+
`typed`: what the whole target gets (see `_entered`), split over its names as an unpacking's
|
|
470
|
+
value is; where that's unknown, an `__enter__` declared to return a vague part is split instead
|
|
471
|
+
(see `shapes.partly`). A part vaguer than `vague` allows is its name's type, and no fix.
|
|
472
|
+
|
|
473
|
+
Yields:
|
|
474
|
+
Each name, with its inference, whether that's a guess, and what the guess rests on.
|
|
475
|
+
|
|
476
|
+
"""
|
|
477
|
+
known: Known = scope.settings.known
|
|
478
|
+
whole: Inference | None = typed[0]
|
|
479
|
+
doubt: tuple[bool, frozenset[str]] = typed[1:]
|
|
480
|
+
if whole is None:
|
|
481
|
+
whole = shapes.partly(
|
|
482
|
+
entering(manager),
|
|
483
|
+
known,
|
|
484
|
+
lambda part: inference(part, known, scope.inferred.types),
|
|
485
|
+
)
|
|
486
|
+
doubt = guesses_in(scope, [manager])
|
|
487
|
+
name: ast.Name
|
|
488
|
+
part: str | None
|
|
489
|
+
for name, part in unpacked(target, None if whole is None else whole.annotation, known.indirect.tuples):
|
|
490
|
+
split: Inference | None = (
|
|
491
|
+
None if whole is None or part is None else Inference(part, whole.reason, whole.kinds | _UNPACK)
|
|
492
|
+
)
|
|
493
|
+
yield name, (split, *doubt)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _entered(scope: Scope, manager: ast.expr, *, asynchronous: bool = False) -> _Valued:
|
|
497
|
+
"""Infer what a `with` statement binds its target to, entering `manager`.
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
The inference, whether it's a guess (the manager's type is one), and what the guess rests on;
|
|
501
|
+
none for a generic class written without its arguments.
|
|
502
|
+
|
|
503
|
+
"""
|
|
504
|
+
known: Known = scope.settings.known
|
|
505
|
+
file: Inference | None
|
|
506
|
+
if not asynchronous and (file := opened(manager, known)) is not None:
|
|
507
|
+
return file, False, frozenset()
|
|
508
|
+
found: Entered | None = (
|
|
509
|
+
entered_async(manager, known, scope.inferred.types)
|
|
510
|
+
if asynchronous
|
|
511
|
+
else entered(manager, known, scope.inferred.types, scope.settings.facts.managers)
|
|
512
|
+
)
|
|
513
|
+
if found is None or bare(found[0].annotation, scope.settings.facts.generics):
|
|
514
|
+
return None, False, frozenset()
|
|
515
|
+
unsafe: bool
|
|
516
|
+
origins: frozenset[str]
|
|
517
|
+
unsafe, origins = guesses_in(scope, found[1])
|
|
518
|
+
return found[0], unsafe or bool(found[2]), origins | found[2]
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _bind_targets(scope: Scope, targets: list[ast.expr], code: str | None) -> None:
|
|
522
|
+
"""Bind every name in `targets`, reporting `code` for each first binding."""
|
|
523
|
+
target: ast.expr
|
|
524
|
+
name: ast.Name
|
|
525
|
+
for target in targets:
|
|
526
|
+
for name in target_names(target):
|
|
527
|
+
scope.bind(name.id, at(name), code)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _bind_captures(scope: Scope, cases: list[ast.match_case]) -> None:
|
|
531
|
+
"""Bind every name the `case` patterns capture: LVA002 unless declared first."""
|
|
532
|
+
case: ast.match_case
|
|
533
|
+
name: str
|
|
534
|
+
where: tuple[int, int]
|
|
535
|
+
for case in cases:
|
|
536
|
+
for name, where in captures(case.pattern, scope.settings.lines):
|
|
537
|
+
scope.bind(name, where, UNTYPED_TARGET)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _augmented_guess(scope: Scope, stmt: ast.AugAssign) -> Late | None:
|
|
541
|
+
"""Guess the type `name op= value` binds, where the name's type or the value's is only a guess.
|
|
542
|
+
|
|
543
|
+
As the statement binds it once `--fix` has written those guesses, and they're declared.
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
The type and what the guess rests on, or `None` if it's unknown still.
|
|
547
|
+
|
|
548
|
+
"""
|
|
549
|
+
name: str = cast("ast.Name", stmt.target).id
|
|
550
|
+
operand: Late | None = guessed_type(scope, stmt.value)
|
|
551
|
+
certain: str | None = None if operand else certain_type(scope, stmt.value)
|
|
552
|
+
bound: str | None = augmented(stmt.op, operand[0] if operand else certain, scope.inferred.types.get(name))
|
|
553
|
+
none: frozenset[str] = frozenset()
|
|
554
|
+
own: frozenset[str] = scope.inferred.origins.get(name, none) if name in scope.inferred.guesses else none
|
|
555
|
+
origins: frozenset[str] = own | (operand[1] if operand else none)
|
|
556
|
+
return (bound, origins) if bound is not None and origins else None
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def annotated(scope: Scope, stmt: ast.AnnAssign, target: ast.Name) -> None:
|
|
560
|
+
"""Bind the name an annotated assignment declares (`name: T`, `name: T = value`).
|
|
561
|
+
|
|
562
|
+
One a widening annotated (its statement's last line is marked: see `widened`) is typed by its
|
|
563
|
+
value where `--fix` now knows it (see `_rewidened`), and has no type otherwise: a second pass
|
|
564
|
+
then finds nothing the first didn't.
|
|
565
|
+
"""
|
|
566
|
+
name: str = target.id
|
|
567
|
+
annotation: ast.expr = stmt.annotation
|
|
568
|
+
fresh: bool = name not in scope.declared
|
|
569
|
+
marked: bool = stmt.end_lineno in scope.settings.known.limits.marks
|
|
570
|
+
scope.declare(name)
|
|
571
|
+
scope.annotation(name, annotation, marked=marked)
|
|
572
|
+
if fresh and marked and _rewidened(scope, stmt, target):
|
|
573
|
+
return
|
|
574
|
+
held: str | None = None if stmt.value is None else certain_type(scope, stmt.value)
|
|
575
|
+
if not marked: # a widening's type says nothing of the value: what's read of the name has none
|
|
576
|
+
scope.inferred.declare(name, annotation, held)
|
|
577
|
+
scope.lifetime(name).declare(ast.unparse(annotation), at(target), _span(annotation, target))
|
|
578
|
+
if stmt.value is not None:
|
|
579
|
+
scope.lifetime(name).bind(at(target), held)
|
|
580
|
+
scope.assigned(name, at(target))
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _rewidened(scope: Scope, stmt: ast.AnnAssign, target: ast.Name) -> bool:
|
|
584
|
+
"""Offer a marked annotation's value's own type in its place (LVA005), where `--fix` now knows it.
|
|
585
|
+
|
|
586
|
+
The annotation is a widening's (see `widened`): the name is first bound here, as it was before
|
|
587
|
+
it was written. Its fix writes over the annotation and deletes the mark; the name is then what
|
|
588
|
+
its value makes it, to the rest of the scope.
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
Whether it's offered; not for an annotation over several lines, a value `--fix` still can't
|
|
592
|
+
type, or a scope it leaves alone. One with no value is noted, for `redeclared`.
|
|
593
|
+
|
|
594
|
+
"""
|
|
595
|
+
name: str = target.id
|
|
596
|
+
span: tuple[int, int] | None = _span(stmt.annotation, target)
|
|
597
|
+
scope.assignments.widened.add(name)
|
|
598
|
+
if stmt.value is None and span is not None and scope.kind.fixable:
|
|
599
|
+
scope.assignments.bare[name] = (stmt, None)
|
|
600
|
+
if stmt.value is None or span is None or not scope.kind.fixable:
|
|
601
|
+
return False
|
|
602
|
+
found: Inference | None
|
|
603
|
+
unsafe: bool
|
|
604
|
+
origins: frozenset[str]
|
|
605
|
+
found, unsafe, origins = scope.valued(stmt.value, target.lineno)
|
|
606
|
+
fix: Fix | None = (
|
|
607
|
+
None if found is None else scope.offer(found, origins, unsafe=unsafe, edit=Edit.REPLACE, span=span)
|
|
608
|
+
)
|
|
609
|
+
if found is None or fix is None:
|
|
610
|
+
return False
|
|
611
|
+
last: int = stmt.end_lineno or target.lineno
|
|
612
|
+
marked: Fix = fix._replace(drop=scope.settings.known.limits.marks[last], mark=last)
|
|
613
|
+
scope.offences.append(Offence(*at(stmt.annotation), name, VAGUE_TYPE, marked))
|
|
614
|
+
scope.lifetime(name).bind(at(target), None if unsafe else found.annotation)
|
|
615
|
+
scope.assigned(name, at(target))
|
|
616
|
+
scope.inferred.learn(name, found.annotation, origins if unsafe else None)
|
|
617
|
+
return True
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def redeclared(scope: Scope) -> None:
|
|
621
|
+
"""Offer each marked declaration with no value its name's type in its place (LVA005), once known.
|
|
622
|
+
|
|
623
|
+
`x: Any`, marked, before `for x in rows:`: where the loop, the unpacking or the `with` that
|
|
624
|
+
binds the name types it now, and nothing binds it again. The fix writes over the annotation and
|
|
625
|
+
deletes the mark, as `_rewidened`'s does; run once the scope is checked, and again without effect.
|
|
626
|
+
"""
|
|
627
|
+
name: str
|
|
628
|
+
stmt: ast.AnnAssign
|
|
629
|
+
found: Inference | None
|
|
630
|
+
for name, (stmt, found) in list(scope.assignments.bare.items()):
|
|
631
|
+
del scope.assignments.bare[name]
|
|
632
|
+
span: tuple[int, int] | None = _span(stmt.annotation, stmt.target)
|
|
633
|
+
if found is None or span is None or scope.flow[name].escaped or len(scope.flow[name].bindings) != 1:
|
|
634
|
+
continue
|
|
635
|
+
guessed: bool = name in scope.inferred.guesses
|
|
636
|
+
origins: frozenset[str] = scope.inferred.origins.get(name, frozenset()) if guessed else frozenset()
|
|
637
|
+
fix: Fix | None
|
|
638
|
+
if (fix := scope.offer(found, origins, unsafe=guessed, edit=Edit.REPLACE, span=span)) is not None:
|
|
639
|
+
last: int = stmt.end_lineno or stmt.lineno
|
|
640
|
+
marked: Fix = fix._replace(drop=scope.settings.known.limits.marks[last], mark=last)
|
|
641
|
+
scope.offences.append(Offence(*at(stmt.annotation), name, VAGUE_TYPE, marked))
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def _span(annotation: ast.expr, target: ast.expr) -> tuple[int, int] | None:
|
|
645
|
+
"""Find an annotation's columns, if it's all on its target's line (so `--fix` can rewrite it).
|
|
646
|
+
|
|
647
|
+
Returns:
|
|
648
|
+
Its start and end columns (UTF-8 bytes, as `ast` counts), or `None`.
|
|
649
|
+
|
|
650
|
+
"""
|
|
651
|
+
one_line: bool = annotation.lineno == annotation.end_lineno == target.lineno
|
|
652
|
+
return (annotation.col_offset, annotation.end_col_offset or 0) if one_line else None
|
|
Binary file
|