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,429 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Builtins and operators `--fix` types by their operands' types, as typeshed's `builtins.pyi` has them.
|
|
3
|
+
|
|
4
|
+
`abs`, `round`, `divmod` and `sum` of builtin numbers; `min` and `max` of values of one type (or of
|
|
5
|
+
numbers, the wider one), or of something's elements; `next` of what yields a known type; `dict` of a
|
|
6
|
+
mapping, of pairs or of keywords, and `dict.fromkeys`; `enumerate`, `zip`, `map` and `reversed`, of
|
|
7
|
+
what they yield; the builtin classes' classmethods with a fixed return (`bytes.fromhex`); and `-n`,
|
|
8
|
+
`+n` and `~n`. `infer` types a value as
|
|
9
|
+
`constricter.fix.values.inference` does, and `loop` what iterating it gives.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import ast
|
|
13
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
14
|
+
from typing import Final, NamedTuple, TypeAlias
|
|
15
|
+
|
|
16
|
+
from constricter.fix.core.known import ImportPlan, Inference, Known
|
|
17
|
+
from constricter.fix.values.shapes import Infer, is_none, or_none, typed
|
|
18
|
+
from constricter.fix.values.targets import dict_parts, iterator_call
|
|
19
|
+
from constricter.rules.annotations import dotted
|
|
20
|
+
from constricter.rules.flow import members
|
|
21
|
+
|
|
22
|
+
_KIND: Final = "builtin" # the fix kind
|
|
23
|
+
_INT: Final = "int"
|
|
24
|
+
_FLOAT: Final = "float"
|
|
25
|
+
_COMPLEX: Final = "complex"
|
|
26
|
+
_INTEGERS: Final = frozenset({"bool", _INT})
|
|
27
|
+
_REALS: Final = _INTEGERS | {_FLOAT}
|
|
28
|
+
_KEY: Final = "key" # `min`'s and `max`'s keyword that only orders
|
|
29
|
+
_DEFAULT: Final = "default" # theirs for an empty iterable
|
|
30
|
+
_START: Final = "start" # `sum`'s
|
|
31
|
+
# What `sum` doesn't add as another class's instances: the builtin numbers (see `_sum`), the text
|
|
32
|
+
# Types it refuses, and `None`.
|
|
33
|
+
_LIST: Final = "list"
|
|
34
|
+
_TUPLE: Final = "tuple"
|
|
35
|
+
_UNSUMMED: Final = _REALS | {"str", "bytes", "None"}
|
|
36
|
+
_PAIR: Final = 2 # `divmod`'s arguments, and the most `round` and `next` take
|
|
37
|
+
_ANY_LENGTH: Final = "..." # a `tuple[T, ...]`'s second part
|
|
38
|
+
_ENUMERATE: Final = "enumerate"
|
|
39
|
+
_ZIP: Final = "zip"
|
|
40
|
+
_ZIPPED: Final = 5 # the most iterables typeshed's `zip` overloads type part by part
|
|
41
|
+
_ITERATOR: Final = "collections.abc.Iterator" # what `iter` gives
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class _Call(NamedTuple):
|
|
45
|
+
"""One call to a builtin: its name, positional arguments and keywords, and how to type their parts."""
|
|
46
|
+
|
|
47
|
+
name: str
|
|
48
|
+
args: Sequence[ast.expr]
|
|
49
|
+
keywords: Mapping[str, ast.expr]
|
|
50
|
+
infer: Infer
|
|
51
|
+
loop: Infer
|
|
52
|
+
plan: ImportPlan | None # how the module names a library class, one it's still to import too
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_Rule: TypeAlias = Callable[[_Call], Inference | None]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _made(call: _Call, annotation: str, *parts: Inference) -> Inference:
|
|
59
|
+
"""Write a call's inference, resting on its `parts`'.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
It.
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
return Inference(
|
|
66
|
+
annotation,
|
|
67
|
+
f"`{call.name}` of {parts[0].reason}",
|
|
68
|
+
frozenset({_KIND}).union(*(part.kinds for part in parts)),
|
|
69
|
+
tuple(read for part in parts for read in part.reads),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _numbers(call: _Call, count: int) -> list[Inference] | None:
|
|
74
|
+
"""Type a call's arguments, `count` of them and no keyword, each a builtin number.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Their inferences, or `None` if it's called otherwise or one isn't an `int`, `bool` or `float`.
|
|
78
|
+
|
|
79
|
+
"""
|
|
80
|
+
if call.keywords or len(call.args) != count:
|
|
81
|
+
return None
|
|
82
|
+
found: list[Inference | None] = [call.infer(arg) for arg in call.args]
|
|
83
|
+
numbers: list[Inference] = [part for part in found if part is not None and part.annotation in _REALS]
|
|
84
|
+
return numbers if len(numbers) == count else None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _wider(parts: Sequence[Inference]) -> str:
|
|
88
|
+
"""Name the type arithmetic on builtin numbers gives.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
`float` if any of them is one, else `int`.
|
|
92
|
+
|
|
93
|
+
"""
|
|
94
|
+
return _FLOAT if any(part.annotation == _FLOAT for part in parts) else _INT
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _abs(call: _Call) -> Inference | None:
|
|
98
|
+
"""Type `abs(x)`: an `int`'s is an `int`, a `float`'s or a `complex`'s a `float`.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
The inference, or `None`.
|
|
102
|
+
|
|
103
|
+
"""
|
|
104
|
+
found: Inference | None = call.infer(call.args[0]) if len(call.args) == 1 and not call.keywords else None
|
|
105
|
+
if found is None or found.annotation not in _REALS | {_COMPLEX}:
|
|
106
|
+
return None
|
|
107
|
+
return _made(call, _INT if found.annotation in _INTEGERS else _FLOAT, found)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _round(call: _Call) -> Inference | None:
|
|
111
|
+
"""Type `round(x)`, an `int`, and `round(x, digits)`, a number of `x`'s type.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
The inference, or `None`.
|
|
115
|
+
|
|
116
|
+
"""
|
|
117
|
+
digits: bool = len(call.args) == _PAIR and not is_none(call.args[1])
|
|
118
|
+
found: list[Inference] | None = _numbers(call._replace(args=call.args[:1]), 1)
|
|
119
|
+
if found is None or len(call.args) > _PAIR or call.keywords:
|
|
120
|
+
return None
|
|
121
|
+
return _made(call, _wider(found) if digits else _INT, *found)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _divmod(call: _Call) -> Inference | None:
|
|
125
|
+
"""Type `divmod(a, b)` of builtin numbers: a pair of `int`s, or of `float`s if either is one.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
The inference, or `None`.
|
|
129
|
+
|
|
130
|
+
"""
|
|
131
|
+
found: list[Inference] | None = _numbers(call, _PAIR)
|
|
132
|
+
return None if found is None else _made(call, f"tuple[{_wider(found)}, {_wider(found)}]", *found)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _sum(call: _Call) -> Inference | None:
|
|
136
|
+
"""Type `sum(xs)` of builtin numbers, with a start of them or none: an `int`, or a `float`.
|
|
137
|
+
|
|
138
|
+
Of another one type `T`: with no start, a `T | int` (the `0` it starts from, for no elements);
|
|
139
|
+
with a start of `T`, or an empty display of `T`'s builtin class (`sum(lists, [])`), a `T`.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
The inference, or `None`.
|
|
143
|
+
|
|
144
|
+
"""
|
|
145
|
+
starts: list[ast.expr] = [*call.args[1:], *call.keywords.values()]
|
|
146
|
+
if not call.args or len(starts) > 1 or call.keywords.keys() - {_START}:
|
|
147
|
+
return None
|
|
148
|
+
element: Inference | None = call.loop(call.args[0])
|
|
149
|
+
parts: list[Inference | None] = [element, *(call.infer(start) for start in starts)]
|
|
150
|
+
found: list[Inference] = [part for part in parts if part is not None and part.annotation in _REALS]
|
|
151
|
+
if len(found) == len(parts):
|
|
152
|
+
return _made(call, _wider(found), *found)
|
|
153
|
+
if element is None or element.annotation in _UNSUMMED or len(members(element.annotation) or ()) != 1:
|
|
154
|
+
return None
|
|
155
|
+
if not starts:
|
|
156
|
+
return _made(call, f"{element.annotation} | {_INT}", element)
|
|
157
|
+
start: Inference | None = parts[1]
|
|
158
|
+
same: bool = start is not None and start.annotation == element.annotation
|
|
159
|
+
emptied: bool = _empty_of(starts[0], element.annotation)
|
|
160
|
+
return _made(call, element.annotation, element) if same or emptied else None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _empty_of(start: ast.expr, annotation: str) -> bool:
|
|
164
|
+
"""Check whether `start` is an empty `list` or `tuple` display, and `annotation` that builtin's type.
|
|
165
|
+
|
|
166
|
+
A `tuple` of any length (`tuple[T, ...]`): a fixed one's sum is longer than it.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Whether it is: adding it to one changes nothing.
|
|
170
|
+
|
|
171
|
+
"""
|
|
172
|
+
head: str = annotation.partition("[")[0]
|
|
173
|
+
return (isinstance(start, ast.List) and not start.elts and head == _LIST) or (
|
|
174
|
+
isinstance(start, ast.Tuple) and not start.elts and head == _TUPLE and annotation.endswith(", ...]")
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _extreme(call: _Call) -> Inference | None:
|
|
179
|
+
"""Type `min` and `max`: of several values of one type, that type; of one argument, its elements'.
|
|
180
|
+
|
|
181
|
+
Of an `int` and a `float`, a `float`: either fits one.
|
|
182
|
+
|
|
183
|
+
With a `default`, one of the elements' type, or `None` (`T | None`).
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
The inference, or `None`.
|
|
187
|
+
|
|
188
|
+
"""
|
|
189
|
+
if not call.args or call.keywords.keys() - {_KEY, _DEFAULT}:
|
|
190
|
+
return None
|
|
191
|
+
default: ast.expr | None = call.keywords.get(_DEFAULT)
|
|
192
|
+
if len(call.args) > 1:
|
|
193
|
+
parts: list[Inference | None] = [typed(arg, call.infer) for arg in call.args]
|
|
194
|
+
found: list[Inference] = [part for part in parts if part is not None]
|
|
195
|
+
if len(found) != len(parts) or default is not None:
|
|
196
|
+
return None
|
|
197
|
+
if len({part.annotation for part in found}) > 1:
|
|
198
|
+
# Numbers of two types: the wider one holds whichever it is.
|
|
199
|
+
mixed: bool = all(part.annotation in _REALS for part in found)
|
|
200
|
+
return _made(call, _wider(found), *found) if mixed else None
|
|
201
|
+
single: bool = len(members(found[0].annotation) or ()) == 1
|
|
202
|
+
return _made(call, found[0].annotation, *found) if single else None
|
|
203
|
+
element: Inference | None = call.loop(call.args[0])
|
|
204
|
+
return None if element is None else _defaulted(call, element, default)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _next(call: _Call) -> Inference | None:
|
|
208
|
+
"""Type `next(it)`: what `it` yields; with a default, of that type, or `None` (`T | None`).
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
The inference, or `None`.
|
|
212
|
+
|
|
213
|
+
"""
|
|
214
|
+
if call.keywords or not 1 <= len(call.args) <= _PAIR:
|
|
215
|
+
return None
|
|
216
|
+
element: Inference | None = call.loop(call.args[0])
|
|
217
|
+
return None if element is None else _defaulted(call, element, (*call.args, None)[1])
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _defaulted(call: _Call, element: Inference, default: ast.expr | None) -> Inference | None:
|
|
221
|
+
"""Type a call that gives an `element`, or else its `default` (if any).
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
The element's type, a default of it, or with `None` for a `None` one; `None` for any other.
|
|
225
|
+
|
|
226
|
+
"""
|
|
227
|
+
if default is None:
|
|
228
|
+
return _made(call, element.annotation, element)
|
|
229
|
+
if is_none(default):
|
|
230
|
+
union: str | None = or_none(element.annotation)
|
|
231
|
+
return None if union is None else _made(call, union, element)
|
|
232
|
+
other: Inference | None = call.infer(default)
|
|
233
|
+
same: bool = other is not None and other.annotation == element.annotation
|
|
234
|
+
return _made(call, element.annotation, element, other) if other is not None and same else None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _dict(call: _Call) -> Inference | None:
|
|
238
|
+
"""Type `dict(d)`, by `d`'s keys and values; `dict(pairs)`, by their parts; `dict(a=1)`, a `dict[str, T]`.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
The inference, or `None` for both arguments and keywords, or parts that don't agree.
|
|
242
|
+
|
|
243
|
+
"""
|
|
244
|
+
if call.keywords and not call.args:
|
|
245
|
+
values: list[Inference | None] = [call.infer(value) for value in call.keywords.values()]
|
|
246
|
+
found: list[Inference] = [value for value in values if value is not None]
|
|
247
|
+
same: bool = len({value.annotation for value in found}) == 1 and len(found) == len(values)
|
|
248
|
+
return _made(call, f"dict[str, {found[0].annotation}]", *found) if same else None
|
|
249
|
+
if call.keywords or len(call.args) != 1:
|
|
250
|
+
return None
|
|
251
|
+
copied: Inference | None = typed(call.args[0], call.infer)
|
|
252
|
+
parts: tuple[str, str] | None = None if copied is None else dict_parts(copied.annotation)
|
|
253
|
+
if copied is not None and parts is not None:
|
|
254
|
+
return _made(call, f"dict[{parts[0]}, {parts[1]}]", copied)
|
|
255
|
+
item: Inference | None = call.loop(call.args[0])
|
|
256
|
+
pair: tuple[str, str] | None = None if item is None else _pair(item.annotation)
|
|
257
|
+
return None if item is None or pair is None else _made(call, f"dict[{pair[0]}, {pair[1]}]", item)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _pair(annotation: str) -> tuple[str, str] | None:
|
|
261
|
+
"""Read a `tuple[K, V]`'s two parts.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Them, as text; `None` for any other annotation, a `tuple[T, ...]` included.
|
|
265
|
+
|
|
266
|
+
"""
|
|
267
|
+
key: ast.expr
|
|
268
|
+
value: ast.expr
|
|
269
|
+
# `annotation` is always `ast.unparse`'s own output, so it's always valid Python to parse back.
|
|
270
|
+
match ast.parse(annotation, mode="eval").body:
|
|
271
|
+
case ast.Subscript(value=ast.Name(id="tuple" | "Tuple"), slice=ast.Tuple(elts=[key, value])) if (
|
|
272
|
+
ast.unparse(value) != _ANY_LENGTH
|
|
273
|
+
):
|
|
274
|
+
return ast.unparse(key), ast.unparse(value)
|
|
275
|
+
case _:
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _fromkeys(call: _Call) -> Inference | None:
|
|
280
|
+
"""Type `dict.fromkeys(keys, value)`: a `dict` of the keys' elements to the value's type.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
The inference, or `None` without a value (each is then `None`, or anything), or a part unknown.
|
|
284
|
+
|
|
285
|
+
"""
|
|
286
|
+
if call.keywords or len(call.args) != _PAIR:
|
|
287
|
+
return None
|
|
288
|
+
key: Inference | None = call.loop(call.args[0])
|
|
289
|
+
value: Inference | None = call.infer(call.args[1])
|
|
290
|
+
if key is None or value is None:
|
|
291
|
+
return None
|
|
292
|
+
return _made(call, f"dict[{key.annotation}, {value.annotation}]", key, value)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _fixed(call: _Call) -> Inference:
|
|
296
|
+
"""Type a builtin class's classmethod whose return is fixed, whatever it's given.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
The inference.
|
|
300
|
+
|
|
301
|
+
"""
|
|
302
|
+
return Inference(_FIXED[call.name], f"`{call.name}`'s fixed return type", frozenset({_KIND}))
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# The builtin classes' classmethods and staticmethods whose return type is fixed, as called on the class.
|
|
306
|
+
_FIXED: Final = {
|
|
307
|
+
"bytearray.fromhex": "bytearray",
|
|
308
|
+
"bytes.fromhex": "bytes",
|
|
309
|
+
"bytes.maketrans": "bytes",
|
|
310
|
+
"float.fromhex": "float",
|
|
311
|
+
"int.from_bytes": "int",
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _iterating(call: _Call) -> Inference | None:
|
|
316
|
+
"""Type `enumerate(xs)`, `zip(xs, ys)`, `map(f, xs)`, `filter(f, xs)` or `reversed(xs)`: the iterator.
|
|
317
|
+
|
|
318
|
+
Of what it yields, as a loop over it is typed (`call.loop`): `zip[tuple[str, int]]`, `map[str]`,
|
|
319
|
+
`filter[str]`; `enumerate[T]` by the `T` it counts.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
The inference, or `None` where a loop's target has none, or `zip` takes more than typeshed
|
|
323
|
+
has overloads for.
|
|
324
|
+
|
|
325
|
+
"""
|
|
326
|
+
if not call.args or (call.name == _ZIP and len(call.args) > _ZIPPED):
|
|
327
|
+
return None
|
|
328
|
+
whole: ast.Call = ast.copy_location(
|
|
329
|
+
ast.Call(
|
|
330
|
+
ast.Name(call.name, ast.Load()),
|
|
331
|
+
[*call.args],
|
|
332
|
+
[ast.keyword(arg=name, value=value) for name, value in call.keywords.items()],
|
|
333
|
+
),
|
|
334
|
+
call.args[0],
|
|
335
|
+
)
|
|
336
|
+
if iterator_call(whole) is None: # a keyword that may change what it yields
|
|
337
|
+
return None
|
|
338
|
+
element: Inference | None = call.loop(call.args[0] if call.name == _ENUMERATE else whole)
|
|
339
|
+
if element is None or (call.name == _ENUMERATE and call.loop(whole) is None):
|
|
340
|
+
return None
|
|
341
|
+
return _made(call, f"{call.name}[{element.annotation}]", element)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _iter(call: _Call) -> Inference | None:
|
|
345
|
+
"""Type `iter(xs)`: an `Iterator` of what a loop over `xs` binds.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
The inference, or `None`: with a sentinel, where a loop's target has no type, or where the
|
|
349
|
+
module can't name `Iterator`.
|
|
350
|
+
|
|
351
|
+
"""
|
|
352
|
+
element: Inference | None = None if call.keywords or len(call.args) != 1 else call.loop(call.args[0])
|
|
353
|
+
spelled: str | None = None if element is None or call.plan is None else call.plan.spell(_ITERATOR)
|
|
354
|
+
return (
|
|
355
|
+
None
|
|
356
|
+
if element is None or spelled is None
|
|
357
|
+
else _made(call, f"{spelled}[{element.annotation}]", element)
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
_CALLS: Final[Mapping[str, _Rule]] = {
|
|
362
|
+
"abs": _abs,
|
|
363
|
+
**dict.fromkeys(("enumerate", "filter", "map", "reversed", "zip"), _iterating),
|
|
364
|
+
"dict": _dict,
|
|
365
|
+
"dict.fromkeys": _fromkeys,
|
|
366
|
+
"divmod": _divmod,
|
|
367
|
+
"iter": _iter,
|
|
368
|
+
"max": _extreme,
|
|
369
|
+
"min": _extreme,
|
|
370
|
+
"next": _next,
|
|
371
|
+
"round": _round,
|
|
372
|
+
"sum": _sum,
|
|
373
|
+
**dict.fromkeys(_FIXED, _fixed),
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def decides(func: ast.expr, known: Known) -> bool:
|
|
378
|
+
"""Check whether `func` is a builtin, or a builtin class's method, this module types the calls of.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
Whether it is, and the module doesn't bind the name itself.
|
|
382
|
+
|
|
383
|
+
"""
|
|
384
|
+
spelled: str | None = dotted(func)
|
|
385
|
+
return spelled is not None and spelled in _CALLS and known.is_builtin(spelled.partition(".")[0])
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def builtin(value: ast.expr, known: Known, infer: Infer, loop: Infer) -> Inference | None:
|
|
389
|
+
"""Infer a call to a builtin its arguments' types decide (see the module docstring).
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
The inference, or `None` for any other call, one that unpacks its arguments, a module that
|
|
393
|
+
binds the name itself, or arguments that don't decide it.
|
|
394
|
+
|
|
395
|
+
"""
|
|
396
|
+
func: ast.expr
|
|
397
|
+
args: list[ast.expr]
|
|
398
|
+
keywords: list[ast.keyword]
|
|
399
|
+
match value:
|
|
400
|
+
case ast.Call(func=func, args=args, keywords=keywords) if (
|
|
401
|
+
decides(func, known)
|
|
402
|
+
and not any(isinstance(arg, ast.Starred) for arg in args)
|
|
403
|
+
and all(keyword.arg for keyword in keywords)
|
|
404
|
+
):
|
|
405
|
+
named: dict[str, ast.expr] = {keyword.arg or "": keyword.value for keyword in keywords}
|
|
406
|
+
name: str = ast.unparse(func)
|
|
407
|
+
return _CALLS[name](_Call(name, args, named, infer, loop, known.names.plan))
|
|
408
|
+
case _:
|
|
409
|
+
return None
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def unary(value: ast.UnaryOp, infer: Infer) -> Inference | None:
|
|
413
|
+
"""Infer `-x` and `+x` of a builtin number (its own type, a `bool`'s an `int`), and `~n` of an `int`.
|
|
414
|
+
|
|
415
|
+
Not `not x`, a `bool` whatever `x` is.
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
The inference, or `None` for any other operand.
|
|
419
|
+
|
|
420
|
+
"""
|
|
421
|
+
found: Inference | None = infer(value.operand)
|
|
422
|
+
signed: bool = isinstance(value.op, ast.USub | ast.UAdd)
|
|
423
|
+
if found is None or found.annotation not in (_REALS | {_COMPLEX} if signed else _INTEGERS):
|
|
424
|
+
return None
|
|
425
|
+
return Inference(
|
|
426
|
+
_INT if found.annotation in _INTEGERS else found.annotation,
|
|
427
|
+
"arithmetic on builtin types",
|
|
428
|
+
found.kinds | {"arithmetic"},
|
|
429
|
+
)
|
|
Binary file
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""A list, set, tuple or dict display's type, from its elements' types.
|
|
3
|
+
|
|
4
|
+
Where a list's, set's or dict's differ, they're joined into a union (`[1, "a"]` is a
|
|
5
|
+
`list[int | str]`): a guess, since a checker joins them too, but not always to the same union (mypy
|
|
6
|
+
to their common base).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
import re
|
|
11
|
+
from collections.abc import Sequence
|
|
12
|
+
from typing import Final
|
|
13
|
+
|
|
14
|
+
from constricter.fix.core.known import Inference
|
|
15
|
+
|
|
16
|
+
JOINED: Final = "joined" # the fix kind (and guessing mechanism) of a display's joined element types
|
|
17
|
+
_MOST_JOINED: Final = 3 # the most types a display's elements are joined from
|
|
18
|
+
_PLAIN: Final = re.compile(r"[A-Za-z_][\w.]*") # a type that's a name alone
|
|
19
|
+
_UNION_BAR: Final = " | "
|
|
20
|
+
_NONE: Final = "None"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def element_type(
|
|
24
|
+
elements: Sequence[ast.expr | None],
|
|
25
|
+
parts: Sequence[Inference | None],
|
|
26
|
+
) -> tuple[str | None, bool]:
|
|
27
|
+
"""Type a display's `elements` as one: the type they all have (`parts`), or their types joined.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
The type (`None` if there's none to write), and whether it's joined from several.
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
uniform: str | None = _uniform(parts)
|
|
34
|
+
return (uniform, False) if uniform is not None else (_joint(elements, parts), True)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def tuple_type(parts: Sequence[Inference | None], max_length: int) -> str | None:
|
|
38
|
+
"""Type a tuple display from its elements' types.
|
|
39
|
+
|
|
40
|
+
One type per element (`tuple[int, str]`), up to `max_length` of them; a longer one (LVA011's)
|
|
41
|
+
is `tuple[T, ...]` when every element is a `T`, and nothing when they differ: its fields need
|
|
42
|
+
names, not a list of types. One that unpacks (`max_length` below 0) is always the longer kind.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
The annotation, or `None` if an element's type isn't known or a long tuple's differ.
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
known_parts: list[Inference] = [part for part in parts if part is not None]
|
|
49
|
+
if len(known_parts) != len(parts):
|
|
50
|
+
return None
|
|
51
|
+
if len(parts) <= max_length:
|
|
52
|
+
return f"tuple[{', '.join(part.annotation for part in known_parts)}]"
|
|
53
|
+
element: str | None = _uniform(parts)
|
|
54
|
+
return None if element is None else f"tuple[{element}, ...]"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _uniform(parts: Sequence[Inference | None]) -> str | None:
|
|
58
|
+
"""Find the one type every element has.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
That type, or `None` if they differ or any is unknown.
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
types: set[str | None] = {None if part is None else part.annotation for part in parts}
|
|
65
|
+
return next(iter(types)) if len(types) == 1 else None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _joint(elements: Sequence[ast.expr | None], parts: Sequence[Inference | None]) -> str | None:
|
|
69
|
+
"""Join a display's `elements`' types (`parts`) where they differ: `int | str`, `None` last.
|
|
70
|
+
|
|
71
|
+
Up to `_MOST_JOINED` of them, each a plain name (a builtin's, a class's): a union of unions or
|
|
72
|
+
of containers is better left to the author. A literal `None` among them is `None`.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
The union, or `None` if any is unknown, or they're too many or not plain.
|
|
76
|
+
|
|
77
|
+
"""
|
|
78
|
+
found: list[str | None] = [
|
|
79
|
+
_NONE
|
|
80
|
+
if isinstance(element, ast.Constant) and element.value is None
|
|
81
|
+
else None
|
|
82
|
+
if part is None
|
|
83
|
+
else part.annotation
|
|
84
|
+
for element, part in zip(elements, parts, strict=True)
|
|
85
|
+
]
|
|
86
|
+
return None if None in found else union(list(dict.fromkeys(each for each in found if each is not None)))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def union(types: Sequence[str]) -> str | None:
|
|
90
|
+
"""Join several types as one: `int | str`, `None` last.
|
|
91
|
+
|
|
92
|
+
Up to `_MOST_JOINED` of them, each a plain name (a builtin's, a class's): a union of unions or of
|
|
93
|
+
containers is better left to the author.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
The union, or `None` if they're too many, too few, or not plain.
|
|
97
|
+
|
|
98
|
+
"""
|
|
99
|
+
if not 1 < len(types) <= _MOST_JOINED or not all(_PLAIN.fullmatch(each) for each in types):
|
|
100
|
+
return None
|
|
101
|
+
return _UNION_BAR.join(sorted(types, key=lambda each: each == _NONE))
|
|
Binary file
|