python-constricter 0.4.1__cp314-cp314-win_amd64.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__.cp314-win_amd64.pyd +0 -0
- constricter/fix/__init__.py +2 -0
- constricter/fix/core/__init__.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/__init__.py +2 -0
- constricter/fix/core/asked.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/asked.py +56 -0
- constricter/fix/core/fixes.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/fixes.py +317 -0
- constricter/fix/core/imports.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/imports.py +580 -0
- constricter/fix/core/inherited.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/inherited.py +311 -0
- constricter/fix/core/known.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/known.py +730 -0
- constricter/fix/core/signatures.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/signatures.py +132 -0
- constricter/fix/index/__init__.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/__init__.py +2 -0
- constricter/fix/index/awaits.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/awaits.py +90 -0
- constricter/fix/index/beyond.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/beyond.py +149 -0
- constricter/fix/index/callers.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/callers.py +155 -0
- constricter/fix/index/crossed.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/crossed.py +327 -0
- constricter/fix/index/decorated.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/decorated.py +140 -0
- constricter/fix/index/fixtures.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/fixtures.py +176 -0
- constricter/fix/index/linked.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/linked.py +142 -0
- constricter/fix/index/loose.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/loose.py +103 -0
- constricter/fix/index/managed.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/managed.py +63 -0
- constricter/fix/index/modules.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/modules.py +741 -0
- constricter/fix/index/offers.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/offers.py +252 -0
- constricter/fix/index/order.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/order.py +155 -0
- constricter/fix/index/origins.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/origins.py +238 -0
- constricter/fix/index/own_methods.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/own_methods.py +91 -0
- constricter/fix/index/own_overloads.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/own_overloads.py +70 -0
- constricter/fix/index/own_types.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/own_types.py +330 -0
- constricter/fix/index/params.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/params.py +81 -0
- constricter/fix/index/plain.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/plain.py +221 -0
- constricter/fix/index/project.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/project.py +797 -0
- constricter/fix/index/sides.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/sides.py +160 -0
- constricter/fix/index/strings.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/strings.py +46 -0
- constricter/fix/index/stubs/__init__.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/__init__.py +2 -0
- constricter/fix/index/stubs/atoms.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/atoms.py +425 -0
- constricter/fix/index/stubs/chained.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/chained.py +69 -0
- constricter/fix/index/stubs/classnames.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/classnames.py +124 -0
- constricter/fix/index/stubs/declared.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/declared.py +508 -0
- constricter/fix/index/stubs/installed.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/installed.py +425 -0
- constricter/fix/index/stubs/names.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/names.py +200 -0
- constricter/fix/index/stubs/read.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/read.py +70 -0
- constricter/fix/index/stubs/signatures.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/signatures.py +656 -0
- constricter/fix/index/stubs/stubbed.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/stubbed.py +208 -0
- constricter/fix/index/taken.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/taken.py +45 -0
- constricter/fix/index/tuples.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/tuples.py +112 -0
- constricter/fix/libraries/__init__.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/__init__.py +2 -0
- constricter/fix/libraries/library.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/library.py +420 -0
- constricter/fix/libraries/opened.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/opened.py +147 -0
- constricter/fix/libraries/overloads.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/overloads.py +960 -0
- constricter/fix/libraries/receivers.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/receivers.py +274 -0
- constricter/fix/libraries/stdlib.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/stdlib.py +752 -0
- constricter/fix/libraries/walks.cp314-win_amd64.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__.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/__init__.py +2 -0
- constricter/fix/values/aliased.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/aliased.py +195 -0
- constricter/fix/values/bodies.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/bodies.py +76 -0
- constricter/fix/values/callables.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/callables.py +323 -0
- constricter/fix/values/called.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/called.py +194 -0
- constricter/fix/values/classvars.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/classvars.py +629 -0
- constricter/fix/values/decided.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/decided.py +429 -0
- constricter/fix/values/displays.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/displays.py +101 -0
- constricter/fix/values/doubts.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/doubts.py +677 -0
- constricter/fix/values/entered.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/entered.py +332 -0
- constricter/fix/values/fills.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/fills.py +446 -0
- constricter/fix/values/guesses.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/guesses.py +376 -0
- constricter/fix/values/hinted.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/hinted.py +811 -0
- constricter/fix/values/inference.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/inference.py +698 -0
- constricter/fix/values/loops.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/loops.py +335 -0
- constricter/fix/values/members.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/members.py +578 -0
- constricter/fix/values/narrowed.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/narrowed.py +220 -0
- constricter/fix/values/operated.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/operated.py +202 -0
- constricter/fix/values/ordered.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/ordered.py +74 -0
- constricter/fix/values/returned.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/returned.py +479 -0
- constricter/fix/values/returns.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/returns.py +237 -0
- constricter/fix/values/shapes.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/shapes.py +654 -0
- constricter/fix/values/stored.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/stored.py +564 -0
- constricter/fix/values/targets.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/targets.py +419 -0
- constricter/fix/values/unchecked.cp314-win_amd64.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__.cp314-win_amd64.pyd +0 -0
- constricter/rules/__init__.py +2 -0
- constricter/rules/annotations.cp314-win_amd64.pyd +0 -0
- constricter/rules/annotations.py +296 -0
- constricter/rules/binding.cp314-win_amd64.pyd +0 -0
- constricter/rules/binding.py +652 -0
- constricter/rules/calls.cp314-win_amd64.pyd +0 -0
- constricter/rules/calls.py +460 -0
- constricter/rules/checker.cp314-win_amd64.pyd +0 -0
- constricter/rules/checker.py +536 -0
- constricter/rules/declarations.cp314-win_amd64.pyd +0 -0
- constricter/rules/declarations.py +730 -0
- constricter/rules/decorators.cp314-win_amd64.pyd +0 -0
- constricter/rules/decorators.py +351 -0
- constricter/rules/flow.cp314-win_amd64.pyd +0 -0
- constricter/rules/flow.py +502 -0
- constricter/rules/keys.cp314-win_amd64.pyd +0 -0
- constricter/rules/keys.py +79 -0
- constricter/rules/late.cp314-win_amd64.pyd +0 -0
- constricter/rules/late.py +969 -0
- constricter/rules/narrowing.cp314-win_amd64.pyd +0 -0
- constricter/rules/narrowing.py +98 -0
- constricter/rules/parsed.cp314-win_amd64.pyd +0 -0
- constricter/rules/parsed.py +136 -0
- constricter/rules/quoted.cp314-win_amd64.pyd +0 -0
- constricter/rules/quoted.py +141 -0
- constricter/rules/rebinding.cp314-win_amd64.pyd +0 -0
- constricter/rules/rebinding.py +169 -0
- constricter/rules/recorded.cp314-win_amd64.pyd +0 -0
- constricter/rules/recorded.py +168 -0
- constricter/rules/redundant.cp314-win_amd64.pyd +0 -0
- constricter/rules/redundant.py +160 -0
- constricter/rules/scope.cp314-win_amd64.pyd +0 -0
- constricter/rules/scope.py +802 -0
- constricter/rules/scoped.cp314-win_amd64.pyd +0 -0
- constricter/rules/scoped.py +232 -0
- constricter/rules/scoping.cp314-win_amd64.pyd +0 -0
- constricter/rules/scoping.py +346 -0
- constricter/rules/settings.cp314-win_amd64.pyd +0 -0
- constricter/rules/settings.py +189 -0
- constricter/rules/syntax.cp314-win_amd64.pyd +0 -0
- constricter/rules/syntax.py +472 -0
- constricter/rules/tables.cp314-win_amd64.pyd +0 -0
- constricter/rules/tables.py +169 -0
- constricter/rules/walked.cp314-win_amd64.pyd +0 -0
- constricter/rules/walked.py +127 -0
- constricter/rules/widened.cp314-win_amd64.pyd +0 -0
- constricter/rules/widened.py +322 -0
- constricter/trace.py +374 -0
- e45ce06b2fdd5c43f782__mypyc.cp314-win_amd64.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,220 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Where a type checker narrows a value: the lines a test governs, and what it tests there.
|
|
3
|
+
|
|
4
|
+
An `if` or `while` narrows what its condition tests in its body (and, negated, in its `else`); a
|
|
5
|
+
`match` its subject in each case; an `assert`, and an `if` whose body always leaves (`return`,
|
|
6
|
+
`raise`, `continue`, `break`), what they test for the rest of their block. A copy of a narrowed
|
|
7
|
+
value there has the narrowed type, not its declared one: `--fix` offers it none (see `scope`). A
|
|
8
|
+
check (`isinstance`, a `TypeGuard`, a `match`) narrows any type; a truth test or a comparison only
|
|
9
|
+
a union (`int | None`).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import ast
|
|
13
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
14
|
+
from functools import lru_cache
|
|
15
|
+
from typing import Final, TypeAlias, cast
|
|
16
|
+
|
|
17
|
+
from constricter.rules.walked import walk
|
|
18
|
+
|
|
19
|
+
# The lines a test governs (first and last, from 1), and whether it's a check, which narrows any
|
|
20
|
+
# type (not a truth test or comparison, which narrows only a union); by each value it narrows.
|
|
21
|
+
Region: TypeAlias = tuple[int, int, bool]
|
|
22
|
+
Regions: TypeAlias = Mapping[str, tuple[Region, ...]]
|
|
23
|
+
_LEAVING: Final = (ast.Return, ast.Raise, ast.Continue, ast.Break)
|
|
24
|
+
_GUARDS: Final = frozenset({"isinstance", "issubclass", "callable", "hasattr"})
|
|
25
|
+
_READS: Final = (ast.Name, ast.Attribute, ast.Subscript)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def regions(tree: ast.Module) -> Regions:
|
|
29
|
+
"""Find every region a test narrows a value in, in a module.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Each value's regions.
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
found: dict[str, list[Region]] = {}
|
|
36
|
+
text: str
|
|
37
|
+
region: Region
|
|
38
|
+
for text, region in _block(tree.body):
|
|
39
|
+
found.setdefault(text, []).append(region)
|
|
40
|
+
return {text: tuple(spans) for text, spans in found.items()}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def narrowed_at(found: Regions, value: ast.expr, line: int, *, union: bool) -> bool:
|
|
44
|
+
"""Check whether a read (`x`, `self.x`, `d[k]`) is narrowed at `line` by a test around it.
|
|
45
|
+
|
|
46
|
+
`union`: whether its type is one, which a truth test or a comparison narrows too.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Whether it is.
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
return isinstance(value, _READS) and read_narrowed(found, ast.unparse(value), line, union=union)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def read_narrowed(found: Regions, read: str, line: int, *, union: bool) -> bool:
|
|
56
|
+
"""Check whether a read, as text, is narrowed at `line` (see `narrowed_at`).
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Whether it is.
|
|
60
|
+
|
|
61
|
+
"""
|
|
62
|
+
return any(start <= line <= end and (check or union) for start, end, check in found.get(read, ()))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@lru_cache(maxsize=64) # asked of each of a function's guessed unions
|
|
66
|
+
def tested_lines(function: ast.AST) -> Mapping[str, frozenset[int]]:
|
|
67
|
+
"""Find the lines on which a test of a function's own narrows a value it goes on to read.
|
|
68
|
+
|
|
69
|
+
A condition's own lines (`if opt and opt.cb:`), a conditional expression's
|
|
70
|
+
(`opt.cb if opt else None`), a filtered comprehension's, and those of an `and` or `or` anywhere
|
|
71
|
+
(`return opt and opt.cb`).
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Each value such a test narrows, as source text, and those lines.
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
found: dict[str, set[int]] = {}
|
|
78
|
+
node: ast.AST
|
|
79
|
+
for node in walk(function):
|
|
80
|
+
test: ast.expr
|
|
81
|
+
whole: ast.expr
|
|
82
|
+
for test, whole in _spanned(node):
|
|
83
|
+
lines: range = range(min(test.lineno, whole.lineno), (whole.end_lineno or whole.lineno) + 1)
|
|
84
|
+
text: str
|
|
85
|
+
for text, _ in _narrows(test):
|
|
86
|
+
found.setdefault(text, set()).update(lines)
|
|
87
|
+
return {text: frozenset(lines) for text, lines in found.items()}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _spanned(node: ast.AST) -> Iterator[tuple[ast.expr, ast.expr]]:
|
|
91
|
+
"""Find the tests a node holds, each with the expression whose lines it narrows on.
|
|
92
|
+
|
|
93
|
+
Yields:
|
|
94
|
+
Each test, and that expression: the test itself, or what it's part of.
|
|
95
|
+
|
|
96
|
+
"""
|
|
97
|
+
test: ast.expr
|
|
98
|
+
match node:
|
|
99
|
+
case ast.If(test=test) | ast.While(test=test) | ast.Assert(test=test):
|
|
100
|
+
yield test, test
|
|
101
|
+
case ast.IfExp(test=test):
|
|
102
|
+
yield test, node
|
|
103
|
+
case ast.BoolOp():
|
|
104
|
+
yield node, node
|
|
105
|
+
case ast.ListComp() | ast.SetComp() | ast.DictComp() | ast.GeneratorExp():
|
|
106
|
+
yield from ((test, node) for each in node.generators for test in each.ifs)
|
|
107
|
+
case _:
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _block(body: Sequence[ast.stmt]) -> Iterator[tuple[str, Region]]:
|
|
112
|
+
"""Walk a block's statements for the regions their tests narrow, inner blocks included.
|
|
113
|
+
|
|
114
|
+
Yields:
|
|
115
|
+
Each narrowed value, and a region.
|
|
116
|
+
|
|
117
|
+
"""
|
|
118
|
+
end: int = (body[-1].end_lineno or body[-1].lineno) if body else 0
|
|
119
|
+
stmt: ast.stmt
|
|
120
|
+
for stmt in body:
|
|
121
|
+
rest: tuple[int, int] = ((stmt.end_lineno or stmt.lineno) + 1, end)
|
|
122
|
+
text: str
|
|
123
|
+
check: bool
|
|
124
|
+
for text, check in _tested(stmt):
|
|
125
|
+
yield from ((text, (*span, check)) for span in _governed(stmt, rest))
|
|
126
|
+
inner: list[ast.stmt]
|
|
127
|
+
for inner in _bodies(stmt):
|
|
128
|
+
yield from _block(inner)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _tested(stmt: ast.stmt) -> frozenset[tuple[str, bool]]:
|
|
132
|
+
"""Name what a statement's test narrows.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Each value, as source text, and whether it's checked (see `Region`).
|
|
136
|
+
|
|
137
|
+
"""
|
|
138
|
+
test: ast.expr
|
|
139
|
+
subject: ast.expr
|
|
140
|
+
match stmt:
|
|
141
|
+
case ast.If(test=test) | ast.While(test=test) | ast.Assert(test=test):
|
|
142
|
+
return frozenset(_narrows(test))
|
|
143
|
+
case ast.Match(subject=subject) if isinstance(subject, _READS):
|
|
144
|
+
return frozenset({(ast.unparse(subject), True)})
|
|
145
|
+
case _:
|
|
146
|
+
return frozenset()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _narrows(test: ast.expr) -> Iterator[tuple[str, bool]]:
|
|
150
|
+
"""Walk a condition for the values it narrows: a value tested for truth, compared, or checked.
|
|
151
|
+
|
|
152
|
+
Yields:
|
|
153
|
+
Each, as source text (`x`, `self.x`), not what it's compared with or checked against; and
|
|
154
|
+
whether it's checked (a call's argument: `isinstance`, a `TypeGuard` function).
|
|
155
|
+
|
|
156
|
+
"""
|
|
157
|
+
operand: ast.expr
|
|
158
|
+
values: list[ast.expr]
|
|
159
|
+
left: ast.expr
|
|
160
|
+
args: list[ast.expr]
|
|
161
|
+
name: str
|
|
162
|
+
match test:
|
|
163
|
+
case ast.Name() | ast.Attribute() | ast.Subscript():
|
|
164
|
+
yield ast.unparse(test), False
|
|
165
|
+
case ast.UnaryOp(operand=operand):
|
|
166
|
+
yield from _narrows(operand)
|
|
167
|
+
case ast.BoolOp(values=values):
|
|
168
|
+
yield from (found for value in values for found in _narrows(value))
|
|
169
|
+
case ast.Compare(left=left) if isinstance(left, _READS):
|
|
170
|
+
yield ast.unparse(left), False
|
|
171
|
+
case ast.Call(func=ast.Name(id=name), args=args) if name in _GUARDS:
|
|
172
|
+
yield from ((ast.unparse(arg), True) for arg in args[:1] if isinstance(arg, _READS))
|
|
173
|
+
case ast.Call(args=args): # a `TypeGuard` or `TypeIs` function may narrow any it's passed
|
|
174
|
+
yield from ((ast.unparse(arg), True) for arg in args if isinstance(arg, _READS))
|
|
175
|
+
case _:
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _governed(stmt: ast.stmt, rest: tuple[int, int]) -> Iterator[tuple[int, int]]:
|
|
180
|
+
"""Find the lines a statement's test governs.
|
|
181
|
+
|
|
182
|
+
Yields:
|
|
183
|
+
Each region: its branches, and the rest of its block (`rest`) after an `assert`, or after an
|
|
184
|
+
`if` whose body always leaves.
|
|
185
|
+
|
|
186
|
+
"""
|
|
187
|
+
match stmt:
|
|
188
|
+
case ast.Assert():
|
|
189
|
+
yield rest
|
|
190
|
+
case ast.If() | ast.While():
|
|
191
|
+
yield from (_span(branch) for branch in (stmt.body, stmt.orelse) if branch)
|
|
192
|
+
if isinstance(stmt, ast.If) and isinstance(stmt.body[-1], _LEAVING):
|
|
193
|
+
yield rest
|
|
194
|
+
case _: # a `match`, the one other statement `_tested` finds a test in
|
|
195
|
+
yield from (_span(case.body) for case in cast("ast.Match", stmt).cases)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _span(body: Sequence[ast.stmt]) -> tuple[int, int]:
|
|
199
|
+
return body[0].lineno, body[-1].end_lineno or body[-1].lineno
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _bodies(stmt: ast.stmt) -> Iterator[list[ast.stmt]]:
|
|
203
|
+
"""Find the blocks inside a statement: a function's or class's body, a branch, a handler, a case.
|
|
204
|
+
|
|
205
|
+
Yields:
|
|
206
|
+
Each.
|
|
207
|
+
|
|
208
|
+
"""
|
|
209
|
+
match stmt:
|
|
210
|
+
case ast.FunctionDef() | ast.AsyncFunctionDef() | ast.ClassDef() | ast.With() | ast.AsyncWith():
|
|
211
|
+
yield stmt.body
|
|
212
|
+
case ast.If() | ast.For() | ast.AsyncFor() | ast.While():
|
|
213
|
+
yield from (block for block in (stmt.body, stmt.orelse) if block)
|
|
214
|
+
case ast.Try() | ast.TryStar():
|
|
215
|
+
yield from (block for block in (stmt.body, stmt.orelse, stmt.finalbody) if block)
|
|
216
|
+
yield from (handler.body for handler in stmt.handlers)
|
|
217
|
+
case ast.Match():
|
|
218
|
+
yield from (case.body for case in stmt.cases)
|
|
219
|
+
case _:
|
|
220
|
+
pass
|
|
Binary file
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Binary operators `--fix` types by their operands' builtin types, whose operators nothing can overload.
|
|
3
|
+
|
|
4
|
+
Numbers: `/` gives a `float`; `+`, `-`, `*`, `//` and `%` a `float` if either side is one, else an
|
|
5
|
+
`int`; `<<` and `>>` of integers an `int`, as their `&`, `|` and `^` are (of two `bool`s, a `bool`);
|
|
6
|
+
`**` an `int` by a literal exponent that isn't negative, and by any other integer a `float`, which
|
|
7
|
+
takes the `int` it may be as well as the `float` a negative one gives; a `float`'s by an integer a
|
|
8
|
+
`float`. `str` and `bytes`: `+` of two, `*` by an integer on either side, and `%`
|
|
9
|
+
formatting give the same type back; so do a `list[T]`'s `+` of another and `*` by an integer.
|
|
10
|
+
Tuples: `+` gives both sides' parts, and `*` by an integer a `tuple[T, ...]` of the one type they
|
|
11
|
+
have. A `set[T]`'s or a `frozenset[T]`'s `|`, `&`, `-` and `^` with another, and a `dict[K, V]`'s
|
|
12
|
+
`|`, give that type.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import ast
|
|
16
|
+
from typing import Final, cast
|
|
17
|
+
|
|
18
|
+
_INT: Final = "int"
|
|
19
|
+
_FLOAT: Final = "float"
|
|
20
|
+
_BOOL: Final = "bool"
|
|
21
|
+
_INTEGERS: Final = frozenset({_BOOL, _INT})
|
|
22
|
+
_NUMBERS: Final = _INTEGERS | {_FLOAT}
|
|
23
|
+
_TEXTS: Final = frozenset({"str", "bytes"})
|
|
24
|
+
_LIST: Final = "list"
|
|
25
|
+
_TUPLE: Final = "tuple"
|
|
26
|
+
_OPEN: Final = "[" # where a subscripted annotation's arguments start
|
|
27
|
+
_SETS: Final = frozenset({"set", "frozenset"})
|
|
28
|
+
_DICT: Final = "dict"
|
|
29
|
+
_SAME_TYPE: Final = (ast.Add, ast.Sub, ast.Mult, ast.FloorDiv, ast.Mod) # an `int`'s, or a `float`'s
|
|
30
|
+
_SHIFTS: Final = (ast.LShift, ast.RShift)
|
|
31
|
+
_BITWISE: Final = (ast.BitAnd, ast.BitOr, ast.BitXor)
|
|
32
|
+
_SET_OPERATORS: Final = (ast.BitOr, ast.BitAnd, ast.Sub, ast.BitXor)
|
|
33
|
+
_ANY_LENGTH: Final = 2 # `tuple[T, ...]`'s two parts: the element type and the ellipsis
|
|
34
|
+
_ELLIPSIS: Final = "..."
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def operated(value: ast.BinOp, left: str | None, right: str | None, max_length: int) -> str | None:
|
|
38
|
+
"""Type `value`, a binary operator between values typed `left` and `right` (see the module docstring).
|
|
39
|
+
|
|
40
|
+
`max_length`: the longest tuple written part by part.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The annotation, or `None` for any other operator or operand.
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
if left is None:
|
|
47
|
+
return None
|
|
48
|
+
if left in _NUMBERS and right in _NUMBERS:
|
|
49
|
+
return _number(value, left, right or "")
|
|
50
|
+
if left in _INTEGERS and isinstance(value.op, ast.Mult):
|
|
51
|
+
return _repeated(right or "", left) # `3 * "ab"`: the sequence's own `*`, reflected
|
|
52
|
+
if _root(left) == _TUPLE:
|
|
53
|
+
return _tuple(value.op, left, right or "", max_length)
|
|
54
|
+
if _root(left) in _SETS | {_DICT}:
|
|
55
|
+
return _same(value.op, left, right)
|
|
56
|
+
text: str | None = left if left in _TEXTS or _root(left) == _LIST else None
|
|
57
|
+
return text if text is not None and _keeps_text(value.op, text, right) else None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _root(annotation: str) -> str:
|
|
61
|
+
"""Name a subscripted annotation's outer type (`list` for `list[int]`).
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Its text before the `[`, or nothing for one that isn't subscripted: a union of them too.
|
|
65
|
+
|
|
66
|
+
"""
|
|
67
|
+
if _OPEN not in annotation: # an unknown operand's has no text at all
|
|
68
|
+
return ""
|
|
69
|
+
# `annotation` is always `ast.unparse`'s own output, so it's always valid Python to parse back.
|
|
70
|
+
whole: ast.expr = ast.parse(annotation, mode="eval").body
|
|
71
|
+
return ast.unparse(whole.value) if isinstance(whole, ast.Subscript) else ""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _number(value: ast.BinOp, left: str, right: str) -> str | None:
|
|
75
|
+
"""Type an operator between two builtin numbers.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
The annotation, or `None` for an operator whose result their types don't decide.
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
op: ast.operator = value.op
|
|
82
|
+
floating: bool = _FLOAT in {left, right}
|
|
83
|
+
if isinstance(op, ast.Div):
|
|
84
|
+
return _FLOAT
|
|
85
|
+
if isinstance(op, _SAME_TYPE):
|
|
86
|
+
return _FLOAT if floating else _INT
|
|
87
|
+
if isinstance(op, ast.Pow):
|
|
88
|
+
return _power(value.right, left, right)
|
|
89
|
+
if floating:
|
|
90
|
+
return None
|
|
91
|
+
if isinstance(op, _SHIFTS):
|
|
92
|
+
return _INT
|
|
93
|
+
both: bool = left == right == _BOOL
|
|
94
|
+
return (_BOOL if both else _INT) if isinstance(op, _BITWISE) else None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _power(exponent: ast.expr, left: str, right: str) -> str | None:
|
|
98
|
+
"""Type `left ** right`: an integer's by a literal that isn't negative, or by another integer.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
The annotation: `int` by such a literal; by another integer, which may be negative, a `float`,
|
|
102
|
+
which takes an `int` too; `None` where the exponent is a `float`: a negative number's gives a
|
|
103
|
+
`complex`.
|
|
104
|
+
|
|
105
|
+
"""
|
|
106
|
+
if right not in _INTEGERS:
|
|
107
|
+
return None
|
|
108
|
+
natural: bool = isinstance(exponent, ast.Constant) and isinstance(exponent.value, int)
|
|
109
|
+
return _INT if left in _INTEGERS and natural else _FLOAT
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _repeated(sequence: str, count: str) -> str | None:
|
|
113
|
+
"""Type a `str`, `bytes`, `list` or tuple repeated by a value typed `count`.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
The sequence's type (a tuple's as `tuple[T, ...]`), or `None` if it isn't one, or `count`
|
|
117
|
+
isn't an integer.
|
|
118
|
+
|
|
119
|
+
"""
|
|
120
|
+
if count not in _INTEGERS:
|
|
121
|
+
return None
|
|
122
|
+
if sequence in _TEXTS or _root(sequence) == _LIST:
|
|
123
|
+
return sequence
|
|
124
|
+
parts: list[str] | None = _parts(sequence)
|
|
125
|
+
return None if parts is None else _any_length(parts)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _keeps_text(op: ast.operator, text: str, right: str | None) -> bool:
|
|
129
|
+
"""Check whether `text op right` (`text` a `str`, a `bytes` or a `list[T]`) gives `text` back.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Whether it does: `+` of two, `*` by an integer, or a `str`'s or `bytes`'s `%` formatting.
|
|
133
|
+
|
|
134
|
+
"""
|
|
135
|
+
if isinstance(op, ast.Add):
|
|
136
|
+
return right == text
|
|
137
|
+
if isinstance(op, ast.Mult):
|
|
138
|
+
return right in _INTEGERS
|
|
139
|
+
return isinstance(op, ast.Mod) and text in _TEXTS
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _tuple(op: ast.operator, left: str, right: str, max_length: int) -> str | None:
|
|
143
|
+
"""Type a tuple's `+` of another, or its `*` by an integer.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Both tuples' parts, where each has a fixed length and they're no more than `max_length`; a
|
|
147
|
+
`tuple[T, ...]` where every part of both is a `T`; else `None`.
|
|
148
|
+
|
|
149
|
+
"""
|
|
150
|
+
if isinstance(op, ast.Mult):
|
|
151
|
+
return _repeated(left, right)
|
|
152
|
+
first: list[str] | None = _parts(left)
|
|
153
|
+
second: list[str] | None = _parts(right) if isinstance(op, ast.Add) else None
|
|
154
|
+
if first is None or second is None:
|
|
155
|
+
return None
|
|
156
|
+
fixed: bool = not (_is_any_length(first) or _is_any_length(second))
|
|
157
|
+
if fixed and len(first) + len(second) <= max_length:
|
|
158
|
+
return f"{_TUPLE}[{', '.join(first + second)}]"
|
|
159
|
+
return _any_length(first + second)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _parts(annotation: str) -> list[str] | None:
|
|
163
|
+
"""Read a tuple type's parts (`tuple[int, str]`, `tuple[int, ...]`).
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Them, as text, a `...` included; `None` for any other type, or a tuple that unpacks one.
|
|
167
|
+
|
|
168
|
+
"""
|
|
169
|
+
if _root(annotation) != _TUPLE:
|
|
170
|
+
return None
|
|
171
|
+
whole: ast.expr = cast("ast.Subscript", ast.parse(annotation, mode="eval").body).slice
|
|
172
|
+
elements: list[ast.expr] = whole.elts if isinstance(whole, ast.Tuple) else [whole]
|
|
173
|
+
starred: bool = any(isinstance(element, ast.Starred) for element in elements)
|
|
174
|
+
return None if starred or not elements else [ast.unparse(element) for element in elements]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _is_any_length(parts: list[str]) -> bool:
|
|
178
|
+
return len(parts) == _ANY_LENGTH and parts[1] == _ELLIPSIS
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _any_length(parts: list[str]) -> str | None:
|
|
182
|
+
"""Write the `tuple[T, ...]` of tuples' `parts`.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
It, or `None` where they aren't all one type.
|
|
186
|
+
|
|
187
|
+
"""
|
|
188
|
+
types: set[str] = set(parts) - {_ELLIPSIS}
|
|
189
|
+
return f"{_TUPLE}[{types.pop()}, ...]" if len(types) == 1 else None
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _same(op: ast.operator, left: str, right: str | None) -> str | None:
|
|
193
|
+
"""Type a set's operator with another of its type, or a `dict`'s `|` with another of its.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
That type, or `None` for any other operator, or a right side of another type.
|
|
197
|
+
|
|
198
|
+
"""
|
|
199
|
+
if left != right:
|
|
200
|
+
return None
|
|
201
|
+
merged: bool = isinstance(op, ast.BitOr) if _root(left) == _DICT else isinstance(op, _SET_OPERATORS)
|
|
202
|
+
return left if merged else None
|
|
Binary file
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""The order a module's functions are checked in: each after those it calls, whose `return`s then type it."""
|
|
3
|
+
|
|
4
|
+
import ast
|
|
5
|
+
from collections.abc import Iterator, Sequence
|
|
6
|
+
from typing import TypeAlias
|
|
7
|
+
|
|
8
|
+
from constricter.fix.values import returned
|
|
9
|
+
from constricter.rules.syntax import FunctionDef
|
|
10
|
+
|
|
11
|
+
# The functions by name: the module's (`[False]`), then the classes' methods (`[True]`).
|
|
12
|
+
_Named: TypeAlias = tuple[dict[str, list[FunctionDef]], dict[str, list[FunctionDef]]]
|
|
13
|
+
# One function to put in call order, and its callees still to put in before it.
|
|
14
|
+
_Visit: TypeAlias = tuple[FunctionDef, Iterator[FunctionDef]]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def in_call_order(module: ast.Module, functions: Sequence[FunctionDef]) -> list[FunctionDef]:
|
|
18
|
+
"""Order `functions` so that each comes after the functions it calls (a cycle's, in any order).
|
|
19
|
+
|
|
20
|
+
Its callees: the module's functions it calls by name, and every method named as it calls one
|
|
21
|
+
(`x.m()`: any class's `m`), as `called` counts them.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Them, callees first; otherwise in their own order.
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
named: _Named = ({}, {})
|
|
28
|
+
func: FunctionDef
|
|
29
|
+
for func in functions:
|
|
30
|
+
slot: returned.Slot | None
|
|
31
|
+
if (slot := returned.slots(module).get(id(func))) is not None:
|
|
32
|
+
named[slot.owner is not None].setdefault(slot.name, []).append(func)
|
|
33
|
+
ordered: list[FunctionDef] = []
|
|
34
|
+
seen: set[int] = set()
|
|
35
|
+
for func in functions:
|
|
36
|
+
if id(func) not in seen:
|
|
37
|
+
seen.add(id(func))
|
|
38
|
+
_after_callees(module, func, named, (ordered, seen))
|
|
39
|
+
return ordered
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _after_callees(
|
|
43
|
+
module: ast.Module,
|
|
44
|
+
func: FunctionDef,
|
|
45
|
+
named: _Named,
|
|
46
|
+
into: tuple[list[FunctionDef], set[int]],
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Put `func` in the order after its callees, depth first, with a stack: a call chain can be long."""
|
|
49
|
+
ordered: list[FunctionDef]
|
|
50
|
+
seen: set[int]
|
|
51
|
+
ordered, seen = into
|
|
52
|
+
# Each entry: a function, and its callees still to put in before it; `None` at the bottom ends it.
|
|
53
|
+
waiting: list[_Visit | None] = [None, (func, _callees(module, func, named))]
|
|
54
|
+
entry: _Visit
|
|
55
|
+
for entry in iter(waiting.pop, None):
|
|
56
|
+
callee: FunctionDef | None
|
|
57
|
+
if (callee := next((one for one in entry[1] if id(one) not in seen), None)) is None:
|
|
58
|
+
ordered.append(entry[0])
|
|
59
|
+
else:
|
|
60
|
+
seen.add(id(callee))
|
|
61
|
+
waiting.extend([entry, (callee, _callees(module, callee, named))])
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _callees(module: ast.Module, func: FunctionDef, named: _Named) -> Iterator[FunctionDef]:
|
|
65
|
+
"""Find the functions of `named` that `func` calls: the module's by name, methods by attribute.
|
|
66
|
+
|
|
67
|
+
Yields:
|
|
68
|
+
Each, in the order of its calls.
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
name: str | None
|
|
72
|
+
attr: str | None
|
|
73
|
+
for name, attr in returned.callees_in(module, func):
|
|
74
|
+
yield from named[False].get(name, []) if name is not None else named[True].get(attr or "", [])
|
|
Binary file
|