python-constricter 0.4.1__cp313-cp313-win32.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- constricter/__init__.py +55 -0
- constricter/__main__.py +26 -0
- constricter/_version.py +4 -0
- constricter/cli/__init__.py +2 -0
- constricter/cli/baseline.py +107 -0
- constricter/cli/collecting.py +54 -0
- constricter/cli/command.py +547 -0
- constricter/cli/config.py +322 -0
- constricter/cli/explain.py +104 -0
- constricter/cli/lsp/__init__.py +2 -0
- constricter/cli/lsp/connection.py +475 -0
- constricter/cli/lsp/edits.py +188 -0
- constricter/cli/lsp/guard.py +133 -0
- constricter/cli/lsp/hints.py +391 -0
- constricter/cli/lsp/protocol.py +192 -0
- constricter/cli/lsp/remembered.py +81 -0
- constricter/cli/lsp/stored.py +200 -0
- constricter/cli/options.py +799 -0
- constricter/cli/paths.py +151 -0
- constricter/cli/reading.py +36 -0
- constricter/cli/report.py +397 -0
- constricter/cli/rounds.py +328 -0
- constricter/cli/runs.py +90 -0
- constricter/cli/schedule.py +463 -0
- constricter/cli/traced.py +401 -0
- constricter/cli/workers.py +286 -0
- constricter/empty.py +66 -0
- constricter/fix/__init__.cp313-win32.pyd +0 -0
- constricter/fix/__init__.py +2 -0
- constricter/fix/core/__init__.cp313-win32.pyd +0 -0
- constricter/fix/core/__init__.py +2 -0
- constricter/fix/core/asked.cp313-win32.pyd +0 -0
- constricter/fix/core/asked.py +56 -0
- constricter/fix/core/fixes.cp313-win32.pyd +0 -0
- constricter/fix/core/fixes.py +317 -0
- constricter/fix/core/imports.cp313-win32.pyd +0 -0
- constricter/fix/core/imports.py +580 -0
- constricter/fix/core/inherited.cp313-win32.pyd +0 -0
- constricter/fix/core/inherited.py +311 -0
- constricter/fix/core/known.cp313-win32.pyd +0 -0
- constricter/fix/core/known.py +730 -0
- constricter/fix/core/signatures.cp313-win32.pyd +0 -0
- constricter/fix/core/signatures.py +132 -0
- constricter/fix/index/__init__.cp313-win32.pyd +0 -0
- constricter/fix/index/__init__.py +2 -0
- constricter/fix/index/awaits.cp313-win32.pyd +0 -0
- constricter/fix/index/awaits.py +90 -0
- constricter/fix/index/beyond.cp313-win32.pyd +0 -0
- constricter/fix/index/beyond.py +149 -0
- constricter/fix/index/callers.cp313-win32.pyd +0 -0
- constricter/fix/index/callers.py +155 -0
- constricter/fix/index/crossed.cp313-win32.pyd +0 -0
- constricter/fix/index/crossed.py +327 -0
- constricter/fix/index/decorated.cp313-win32.pyd +0 -0
- constricter/fix/index/decorated.py +140 -0
- constricter/fix/index/fixtures.cp313-win32.pyd +0 -0
- constricter/fix/index/fixtures.py +176 -0
- constricter/fix/index/linked.cp313-win32.pyd +0 -0
- constricter/fix/index/linked.py +142 -0
- constricter/fix/index/loose.cp313-win32.pyd +0 -0
- constricter/fix/index/loose.py +103 -0
- constricter/fix/index/managed.cp313-win32.pyd +0 -0
- constricter/fix/index/managed.py +63 -0
- constricter/fix/index/modules.cp313-win32.pyd +0 -0
- constricter/fix/index/modules.py +741 -0
- constricter/fix/index/offers.cp313-win32.pyd +0 -0
- constricter/fix/index/offers.py +252 -0
- constricter/fix/index/order.cp313-win32.pyd +0 -0
- constricter/fix/index/order.py +155 -0
- constricter/fix/index/origins.cp313-win32.pyd +0 -0
- constricter/fix/index/origins.py +238 -0
- constricter/fix/index/own_methods.cp313-win32.pyd +0 -0
- constricter/fix/index/own_methods.py +91 -0
- constricter/fix/index/own_overloads.cp313-win32.pyd +0 -0
- constricter/fix/index/own_overloads.py +70 -0
- constricter/fix/index/own_types.cp313-win32.pyd +0 -0
- constricter/fix/index/own_types.py +330 -0
- constricter/fix/index/params.cp313-win32.pyd +0 -0
- constricter/fix/index/params.py +81 -0
- constricter/fix/index/plain.cp313-win32.pyd +0 -0
- constricter/fix/index/plain.py +221 -0
- constricter/fix/index/project.cp313-win32.pyd +0 -0
- constricter/fix/index/project.py +797 -0
- constricter/fix/index/sides.cp313-win32.pyd +0 -0
- constricter/fix/index/sides.py +160 -0
- constricter/fix/index/strings.cp313-win32.pyd +0 -0
- constricter/fix/index/strings.py +46 -0
- constricter/fix/index/stubs/__init__.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/__init__.py +2 -0
- constricter/fix/index/stubs/atoms.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/atoms.py +425 -0
- constricter/fix/index/stubs/chained.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/chained.py +69 -0
- constricter/fix/index/stubs/classnames.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/classnames.py +124 -0
- constricter/fix/index/stubs/declared.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/declared.py +508 -0
- constricter/fix/index/stubs/installed.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/installed.py +425 -0
- constricter/fix/index/stubs/names.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/names.py +200 -0
- constricter/fix/index/stubs/read.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/read.py +70 -0
- constricter/fix/index/stubs/signatures.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/signatures.py +656 -0
- constricter/fix/index/stubs/stubbed.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/stubbed.py +208 -0
- constricter/fix/index/taken.cp313-win32.pyd +0 -0
- constricter/fix/index/taken.py +45 -0
- constricter/fix/index/tuples.cp313-win32.pyd +0 -0
- constricter/fix/index/tuples.py +112 -0
- constricter/fix/libraries/__init__.cp313-win32.pyd +0 -0
- constricter/fix/libraries/__init__.py +2 -0
- constricter/fix/libraries/library.cp313-win32.pyd +0 -0
- constricter/fix/libraries/library.py +420 -0
- constricter/fix/libraries/opened.cp313-win32.pyd +0 -0
- constricter/fix/libraries/opened.py +147 -0
- constricter/fix/libraries/overloads.cp313-win32.pyd +0 -0
- constricter/fix/libraries/overloads.py +960 -0
- constricter/fix/libraries/receivers.cp313-win32.pyd +0 -0
- constricter/fix/libraries/receivers.py +274 -0
- constricter/fix/libraries/stdlib.cp313-win32.pyd +0 -0
- constricter/fix/libraries/stdlib.py +752 -0
- constricter/fix/libraries/walks.cp313-win32.pyd +0 -0
- constricter/fix/libraries/walks.py +49 -0
- constricter/fix/tables/aliases.json +147 -0
- constricter/fix/tables/attributes.json +700 -0
- constricter/fix/tables/awaited.json +69 -0
- constricter/fix/tables/awaited_overloads.json +14 -0
- constricter/fix/tables/bases.json +888 -0
- constricter/fix/tables/classes.json +1941 -0
- constricter/fix/tables/elements.json +156 -0
- constricter/fix/tables/functions.json +58 -0
- constricter/fix/tables/generic_attributes.json +254 -0
- constricter/fix/tables/method_overloads.json +461 -0
- constricter/fix/tables/method_signatures.json +954 -0
- constricter/fix/tables/methods.json +954 -0
- constricter/fix/tables/overloads.json +540 -0
- constricter/fix/tables/platform.json +37 -0
- constricter/fix/tables/returns.json +1362 -0
- constricter/fix/tables/scalar_members.json +16 -0
- constricter/fix/tables/scalars.json +2707 -0
- constricter/fix/tables/source.json +28 -0
- constricter/fix/tables/subscriptable.json +77 -0
- constricter/fix/tables/type_parameters.json +178 -0
- constricter/fix/tables/variables.json +2962 -0
- constricter/fix/values/__init__.cp313-win32.pyd +0 -0
- constricter/fix/values/__init__.py +2 -0
- constricter/fix/values/aliased.cp313-win32.pyd +0 -0
- constricter/fix/values/aliased.py +195 -0
- constricter/fix/values/bodies.cp313-win32.pyd +0 -0
- constricter/fix/values/bodies.py +76 -0
- constricter/fix/values/callables.cp313-win32.pyd +0 -0
- constricter/fix/values/callables.py +323 -0
- constricter/fix/values/called.cp313-win32.pyd +0 -0
- constricter/fix/values/called.py +194 -0
- constricter/fix/values/classvars.cp313-win32.pyd +0 -0
- constricter/fix/values/classvars.py +629 -0
- constricter/fix/values/decided.cp313-win32.pyd +0 -0
- constricter/fix/values/decided.py +429 -0
- constricter/fix/values/displays.cp313-win32.pyd +0 -0
- constricter/fix/values/displays.py +101 -0
- constricter/fix/values/doubts.cp313-win32.pyd +0 -0
- constricter/fix/values/doubts.py +677 -0
- constricter/fix/values/entered.cp313-win32.pyd +0 -0
- constricter/fix/values/entered.py +332 -0
- constricter/fix/values/fills.cp313-win32.pyd +0 -0
- constricter/fix/values/fills.py +446 -0
- constricter/fix/values/guesses.cp313-win32.pyd +0 -0
- constricter/fix/values/guesses.py +376 -0
- constricter/fix/values/hinted.cp313-win32.pyd +0 -0
- constricter/fix/values/hinted.py +811 -0
- constricter/fix/values/inference.cp313-win32.pyd +0 -0
- constricter/fix/values/inference.py +698 -0
- constricter/fix/values/loops.cp313-win32.pyd +0 -0
- constricter/fix/values/loops.py +335 -0
- constricter/fix/values/members.cp313-win32.pyd +0 -0
- constricter/fix/values/members.py +578 -0
- constricter/fix/values/narrowed.cp313-win32.pyd +0 -0
- constricter/fix/values/narrowed.py +220 -0
- constricter/fix/values/operated.cp313-win32.pyd +0 -0
- constricter/fix/values/operated.py +202 -0
- constricter/fix/values/ordered.cp313-win32.pyd +0 -0
- constricter/fix/values/ordered.py +74 -0
- constricter/fix/values/returned.cp313-win32.pyd +0 -0
- constricter/fix/values/returned.py +479 -0
- constricter/fix/values/returns.cp313-win32.pyd +0 -0
- constricter/fix/values/returns.py +237 -0
- constricter/fix/values/shapes.cp313-win32.pyd +0 -0
- constricter/fix/values/shapes.py +654 -0
- constricter/fix/values/stored.cp313-win32.pyd +0 -0
- constricter/fix/values/stored.py +564 -0
- constricter/fix/values/targets.cp313-win32.pyd +0 -0
- constricter/fix/values/targets.py +419 -0
- constricter/fix/values/unchecked.cp313-win32.pyd +0 -0
- constricter/fix/values/unchecked.py +278 -0
- constricter/jsonc.py +50 -0
- constricter/noqa.py +47 -0
- constricter/notebook.py +115 -0
- constricter/offences.py +436 -0
- constricter/overrides.py +24 -0
- constricter/plugins/__init__.py +2 -0
- constricter/plugins/flake8.py +133 -0
- constricter/plugins/pylint.py +186 -0
- constricter/py.typed +0 -0
- constricter/recording.py +660 -0
- constricter/rules/__init__.cp313-win32.pyd +0 -0
- constricter/rules/__init__.py +2 -0
- constricter/rules/annotations.cp313-win32.pyd +0 -0
- constricter/rules/annotations.py +296 -0
- constricter/rules/binding.cp313-win32.pyd +0 -0
- constricter/rules/binding.py +652 -0
- constricter/rules/calls.cp313-win32.pyd +0 -0
- constricter/rules/calls.py +460 -0
- constricter/rules/checker.cp313-win32.pyd +0 -0
- constricter/rules/checker.py +536 -0
- constricter/rules/declarations.cp313-win32.pyd +0 -0
- constricter/rules/declarations.py +730 -0
- constricter/rules/decorators.cp313-win32.pyd +0 -0
- constricter/rules/decorators.py +351 -0
- constricter/rules/flow.cp313-win32.pyd +0 -0
- constricter/rules/flow.py +502 -0
- constricter/rules/keys.cp313-win32.pyd +0 -0
- constricter/rules/keys.py +79 -0
- constricter/rules/late.cp313-win32.pyd +0 -0
- constricter/rules/late.py +969 -0
- constricter/rules/narrowing.cp313-win32.pyd +0 -0
- constricter/rules/narrowing.py +98 -0
- constricter/rules/parsed.cp313-win32.pyd +0 -0
- constricter/rules/parsed.py +136 -0
- constricter/rules/quoted.cp313-win32.pyd +0 -0
- constricter/rules/quoted.py +141 -0
- constricter/rules/rebinding.cp313-win32.pyd +0 -0
- constricter/rules/rebinding.py +169 -0
- constricter/rules/recorded.cp313-win32.pyd +0 -0
- constricter/rules/recorded.py +168 -0
- constricter/rules/redundant.cp313-win32.pyd +0 -0
- constricter/rules/redundant.py +160 -0
- constricter/rules/scope.cp313-win32.pyd +0 -0
- constricter/rules/scope.py +802 -0
- constricter/rules/scoped.cp313-win32.pyd +0 -0
- constricter/rules/scoped.py +232 -0
- constricter/rules/scoping.cp313-win32.pyd +0 -0
- constricter/rules/scoping.py +346 -0
- constricter/rules/settings.cp313-win32.pyd +0 -0
- constricter/rules/settings.py +189 -0
- constricter/rules/syntax.cp313-win32.pyd +0 -0
- constricter/rules/syntax.py +472 -0
- constricter/rules/tables.cp313-win32.pyd +0 -0
- constricter/rules/tables.py +169 -0
- constricter/rules/walked.cp313-win32.pyd +0 -0
- constricter/rules/walked.py +127 -0
- constricter/rules/widened.cp313-win32.pyd +0 -0
- constricter/rules/widened.py +322 -0
- constricter/trace.py +374 -0
- e45ce06b2fdd5c43f782__mypyc.cp313-win32.pyd +0 -0
- python_constricter-0.4.1.dist-info/METADATA +400 -0
- python_constricter-0.4.1.dist-info/RECORD +261 -0
- python_constricter-0.4.1.dist-info/WHEEL +4 -0
- python_constricter-0.4.1.dist-info/entry_points.txt +5 -0
- python_constricter-0.4.1.dist-info/licenses/LICENSE.md +21 -0
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""`--infer-with`: a type checker's inferred type (an inlay hint) as a fix, when the file can use it.
|
|
3
|
+
|
|
4
|
+
Always a guess: a hint is the type the checker gave the value where the name is bound, so it can be
|
|
5
|
+
too narrow for a later binding, or too wide for what the code means. It's judged here as text, as
|
|
6
|
+
the checker printed it:
|
|
7
|
+
|
|
8
|
+
- a class object printed `<class 'Point'>` (ty's way) is `type[Point]`, a type variable printed with
|
|
9
|
+
its scope (`Model@create_model`) the variable, and an intersection with a truthiness
|
|
10
|
+
(`str & ~AlwaysFalsy`) its other member;
|
|
11
|
+
- an unpacked tuple (`tuple[str, *tuple[str, ...]]`, Python 3.11's syntax) is kept where the
|
|
12
|
+
project's oldest Python parses it (`min-python`); else written with `Unpack` where the module
|
|
13
|
+
imports it (one level deeper, to LVA006), and dropped where it doesn't;
|
|
14
|
+
- a `Literal` is widened to its values' types (`Literal[1, 2]` is `int`, `Literal[Color.RED]` is
|
|
15
|
+
`Color`), and `LiteralString` to `str`, then a union's repeated members dropped;
|
|
16
|
+
- anything that isn't an annotation (`Module("os")`, a callable's signature, `Self@C`) is dropped,
|
|
17
|
+
as is a vague one (`Any`, `list[Unknown]`), a bare `None`, one with `Never` or `NoReturn` in it
|
|
18
|
+
(an empty display's `list[Never]`, or code the checker takes not to run), one nested as deep as
|
|
19
|
+
LVA006 reports, or a tuple as long as LVA011 does;
|
|
20
|
+
- a special form alone (`type[Generic]`) is dropped, as is a union inside the hint
|
|
21
|
+
(`dict[str, int | bytes]`), and a `Literal` in a class's own arguments, which may be bound to it
|
|
22
|
+
(a union for a name its function doesn't narrow, a name it returns as `Self` and one bound again
|
|
23
|
+
are `constricter.rules`' to drop);
|
|
24
|
+
- `TypeAlias`, hinted for an alias's assignment, declares a module's alias written as a subscript or
|
|
25
|
+
a union (see `type_alias`), and is dropped anywhere else;
|
|
26
|
+
- a generator's, coroutine's or async generator's class as `types` has it at run time (ty's
|
|
27
|
+
`GeneratorType[str, None, None]`) is `collections.abc`'s, as every checker types the expression
|
|
28
|
+
that makes one (`Generator[str, None, None]`);
|
|
29
|
+
- every name in it must be one the file can use: a builtin, a name the module binds at its top
|
|
30
|
+
level (before the binding, in a module body, where the annotation is evaluated), a class or
|
|
31
|
+
alias it imports under `if TYPE_CHECKING:` or the hint's own edits import (`Offered`: imported
|
|
32
|
+
for type checking alone, as the checker says from where; see `constricter.fix.index.offers` for which
|
|
33
|
+
are taken), or a class the checker prints by its bare name from a module `ImportPlan.spell` can
|
|
34
|
+
import (`Callable`, `Iterator`, `Path`, `deque`, ...). Anything else, it can't be sure what the
|
|
35
|
+
name means. Nor where the file binds a name to another thing than the checker prints it for: one
|
|
36
|
+
of those well-known classes bound to its own class or a module (`class Match`, `import datetime`),
|
|
37
|
+
a builtin that takes no type arguments given some (numpy's `bool[bool]`, printed bare), a dotted
|
|
38
|
+
name with a private part (`np._NBitIntP`), or a class a function defines (`Parser.read.Dialect`).
|
|
39
|
+
|
|
40
|
+
A hint with edits is judged as it shows first (`Thing[str]`), then as they'd write it
|
|
41
|
+
(`things.Thing[str]`, through a module the file imports).
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
import ast
|
|
45
|
+
import builtins
|
|
46
|
+
import re
|
|
47
|
+
import sys
|
|
48
|
+
from functools import lru_cache
|
|
49
|
+
from typing import Final, TypeAlias
|
|
50
|
+
|
|
51
|
+
from constricter.fix.core.known import Hints, ImportPlan, Inference, Known, Offered
|
|
52
|
+
from constricter.fix.libraries import stdlib
|
|
53
|
+
from constricter.rules.annotations import ABSTRACT, depth, dotted, length, vague_fits
|
|
54
|
+
|
|
55
|
+
KIND: Final = "checker" # the fix kind, and what the guess rests on
|
|
56
|
+
TRACED: Final = "traced" # the same, for a traced run's types (`--infer-from`)
|
|
57
|
+
KINDS: Final = frozenset({KIND, TRACED})
|
|
58
|
+
_LITERAL: Final = "Literal"
|
|
59
|
+
_NONE: Final = "None"
|
|
60
|
+
_BUILTINS: Final = frozenset(dir(builtins))
|
|
61
|
+
ALIAS: Final = "TypeAlias" # what a checker hints an alias's assignment as
|
|
62
|
+
_ALIAS_ORIGINS: Final = ("typing.TypeAlias", "typing_extensions.TypeAlias")
|
|
63
|
+
_CLASS: Final = "type["
|
|
64
|
+
_UNPACK: Final = "Unpack" # what an unpacked tuple (`*tuple[str, ...]`) is written with
|
|
65
|
+
_UNPACK_ORIGINS: Final = frozenset({"typing.Unpack", "typing_extensions.Unpack"})
|
|
66
|
+
# `collections.abc`'s classes, which a checker prints bare (not `Set`: that's `AbstractSet` to it).
|
|
67
|
+
_ABSTRACT: Final = sorted(ABSTRACT | {"Hashable", "MappingView", "Sized"})
|
|
68
|
+
# Classes a checker prints by their bare names, and where each is from.
|
|
69
|
+
WELL_KNOWN: Final = {
|
|
70
|
+
**{name: f"collections.abc.{name}" for name in _ABSTRACT},
|
|
71
|
+
**{
|
|
72
|
+
name: f"collections.{name}" for name in ("ChainMap", "Counter", "OrderedDict", "defaultdict", "deque")
|
|
73
|
+
},
|
|
74
|
+
**{name: f"re.{name}" for name in ("Match", "Pattern")},
|
|
75
|
+
**{name: f"pathlib.{name}" for name in ("Path", "PosixPath", "PurePath", "WindowsPath")},
|
|
76
|
+
**{
|
|
77
|
+
name: f"io.{name}"
|
|
78
|
+
for name in ("BufferedRandom", "BufferedReader", "BufferedWriter", "BytesIO", "FileIO", "StringIO")
|
|
79
|
+
},
|
|
80
|
+
**{name: f"datetime.{name}" for name in ("date", "datetime", "time", "timedelta", "timezone")},
|
|
81
|
+
**{name: f"subprocess.{name}" for name in ("CompletedProcess", "Popen")},
|
|
82
|
+
"ArgumentParser": "argparse.ArgumentParser",
|
|
83
|
+
"Decimal": "decimal.Decimal",
|
|
84
|
+
"Fraction": "fractions.Fraction",
|
|
85
|
+
"Logger": "logging.Logger",
|
|
86
|
+
"Namespace": "argparse.Namespace",
|
|
87
|
+
"UUID": "uuid.UUID",
|
|
88
|
+
"stat_result": "os.stat_result",
|
|
89
|
+
}
|
|
90
|
+
# Nodes an annotation is made of: names, attributes, subscripts and their parts, and unions.
|
|
91
|
+
_ANNOTATION_NODES: Final = (
|
|
92
|
+
ast.Name,
|
|
93
|
+
ast.Attribute,
|
|
94
|
+
ast.Subscript,
|
|
95
|
+
ast.Tuple,
|
|
96
|
+
ast.List,
|
|
97
|
+
ast.Constant,
|
|
98
|
+
ast.BinOp,
|
|
99
|
+
ast.BitOr,
|
|
100
|
+
ast.Load,
|
|
101
|
+
ast.Starred, # `tuple[str, *tuple[str, ...]]`
|
|
102
|
+
)
|
|
103
|
+
_WIDER: Final = {"LiteralString": "str"}
|
|
104
|
+
# `typing`'s special forms, which a checker prints for the form itself (`type[Generic]`, `Annotated`).
|
|
105
|
+
_FORMS: Final = frozenset(
|
|
106
|
+
{
|
|
107
|
+
"Annotated",
|
|
108
|
+
"ClassVar",
|
|
109
|
+
"Concatenate",
|
|
110
|
+
"Final",
|
|
111
|
+
"Generic",
|
|
112
|
+
"Literal",
|
|
113
|
+
"NotRequired",
|
|
114
|
+
"Optional",
|
|
115
|
+
"Protocol",
|
|
116
|
+
"ReadOnly",
|
|
117
|
+
"Required",
|
|
118
|
+
"TypeGuard",
|
|
119
|
+
"TypeIs",
|
|
120
|
+
"Union",
|
|
121
|
+
"Unpack",
|
|
122
|
+
},
|
|
123
|
+
)
|
|
124
|
+
# ty prints a class object as `<class 'Point'>`: that's `type[Point]`.
|
|
125
|
+
_CLASS_OBJECT: Final = re.compile(r"<class '(\w+)'>")
|
|
126
|
+
# ty prints a type variable with the function or class that declares it: `Model@create_model`.
|
|
127
|
+
_SCOPED: Final = re.compile(r"\b(?!Self@)(\w+)@\w+")
|
|
128
|
+
# What ty intersects a type with where a truth test narrowed it: `str & ~AlwaysFalsy`.
|
|
129
|
+
_TRUTHINESS: Final = frozenset({"AlwaysFalsy", "AlwaysTruthy"})
|
|
130
|
+
_DISCARD: Final = "_"
|
|
131
|
+
# Each name a hint's edits import: its statement, module and name there (see `_origins`).
|
|
132
|
+
_Origins: TypeAlias = dict[str, tuple[str, str, str]]
|
|
133
|
+
_STDLIB: Final = sys.stdlib_module_names | {"_typeshed"} # and the module its stubs alone have
|
|
134
|
+
_RELATIVE: Final = "."
|
|
135
|
+
_DOT: Final = "."
|
|
136
|
+
# How one unbound name of a hint can be written: through a guarded import, or `ImportPlan.spell`.
|
|
137
|
+
_OWN: Final = "own" # the module imports it under `if TYPE_CHECKING:`
|
|
138
|
+
_OFFERED: Final = "offered" # the hint's edits import it
|
|
139
|
+
_KNOWN: Final = "known" # `WELL_KNOWN` says where it's from
|
|
140
|
+
# The builtins that take type arguments: another of a builtin's names given some is another class.
|
|
141
|
+
_GENERIC_BUILTINS: Final = frozenset(
|
|
142
|
+
{
|
|
143
|
+
"BaseExceptionGroup",
|
|
144
|
+
"ExceptionGroup",
|
|
145
|
+
"classmethod",
|
|
146
|
+
"dict",
|
|
147
|
+
"enumerate",
|
|
148
|
+
"filter",
|
|
149
|
+
"frozenset",
|
|
150
|
+
"list",
|
|
151
|
+
"map",
|
|
152
|
+
"memoryview",
|
|
153
|
+
"reversed",
|
|
154
|
+
"set",
|
|
155
|
+
"slice",
|
|
156
|
+
"staticmethod",
|
|
157
|
+
"tuple",
|
|
158
|
+
"type",
|
|
159
|
+
"zip",
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
# `types`' classes for what a generator, a coroutine and an async generator make, and how checkers
|
|
163
|
+
# type those: as `collections.abc`'s classes.
|
|
164
|
+
_RUNTIME: Final = {
|
|
165
|
+
"GeneratorType": "Generator",
|
|
166
|
+
"CoroutineType": "Coroutine",
|
|
167
|
+
"AsyncGeneratorType": "AsyncGenerator",
|
|
168
|
+
}
|
|
169
|
+
_TYPES: Final = "types"
|
|
170
|
+
_BOTTOM: Final = frozenset({"Never", "NoReturn"}) # the type of no value: never a variable's
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def hinted(
|
|
174
|
+
text: str,
|
|
175
|
+
known: Known,
|
|
176
|
+
*,
|
|
177
|
+
nesting: int,
|
|
178
|
+
before: int | None,
|
|
179
|
+
offered: Offered | None = None,
|
|
180
|
+
) -> str | None:
|
|
181
|
+
"""Judge a checker's hint `text` as a fix for a binding (in a module body, on line `before`).
|
|
182
|
+
|
|
183
|
+
`offered`: what the hint's edits would write, judged if `text` is no fix.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
The annotation, spelled as the file can (an import added if it must be), or `None`.
|
|
187
|
+
|
|
188
|
+
"""
|
|
189
|
+
imports: tuple[str, ...] = () if offered is None else offered.imports
|
|
190
|
+
candidates: list[str] = [text]
|
|
191
|
+
if offered is not None and not _renamed(text, offered.text, known.names.plan, _origins(imports)):
|
|
192
|
+
candidates.append(offered.text)
|
|
193
|
+
candidate: str
|
|
194
|
+
for candidate in dict.fromkeys(candidates):
|
|
195
|
+
annotation: str | None
|
|
196
|
+
if (annotation := _spelled(candidate, known, imports, nesting=nesting, before=before)) is not None:
|
|
197
|
+
return annotation
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def inference(annotation: str, found: Hints) -> Inference:
|
|
202
|
+
"""Make the fix a hint of `found` gives, as `hinted` wrote it.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
It.
|
|
206
|
+
|
|
207
|
+
"""
|
|
208
|
+
reason: str = (
|
|
209
|
+
"what a traced run bound it to" if found.kind == TRACED else f"{found.checker}'s inferred type"
|
|
210
|
+
)
|
|
211
|
+
return Inference(annotation, reason, frozenset({found.kind}))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def type_alias(known: Known, before: int) -> str | None:
|
|
215
|
+
"""Name `TypeAlias`, to declare the alias a module body binds on line `before`.
|
|
216
|
+
|
|
217
|
+
As an import the module has names it (`typing`'s, or `typing_extensions`'s), if it runs by then,
|
|
218
|
+
the name itself before one through its module (`TypeAlias`, then `typing.TypeAlias`); else by a
|
|
219
|
+
new import from `typing`, where every Python the module runs on has it there (`aliased`).
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
The name, or `None` if the module can't name it there.
|
|
223
|
+
|
|
224
|
+
"""
|
|
225
|
+
plan: ImportPlan | None
|
|
226
|
+
if (plan := known.names.plan) is None:
|
|
227
|
+
return None
|
|
228
|
+
name: str | None
|
|
229
|
+
named: list[str]
|
|
230
|
+
if not (named := [name for origin in _ALIAS_ORIGINS if (name := plan.named(origin)) is not None]):
|
|
231
|
+
return plan.spell(_ALIAS_ORIGINS[0]) if known.names.aliased else None
|
|
232
|
+
bound: list[str] = [each for each in named if plan.defined.get(each.partition(".")[0], before) < before]
|
|
233
|
+
return min(bound, key=lambda each: _DOT in each) if bound else None
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def renames(name: str, annotation: str, *, local: bool) -> bool:
|
|
237
|
+
"""Check whether `annotation`, a hint's, says `name` is another name for a class: a `type[C]`.
|
|
238
|
+
|
|
239
|
+
An alias annotations are written with, which declared (a variable) it would no longer be. A
|
|
240
|
+
module's name is taken for one, and a function's (`local`) if it's written as a class's is
|
|
241
|
+
(`Pair = tuple[int, str]`, not `cls = type(self)`).
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Whether it does.
|
|
245
|
+
|
|
246
|
+
"""
|
|
247
|
+
return annotation.startswith(_CLASS) and not (local and name[:1].islower())
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _renamed(shown: str, written: str, plan: ImportPlan | None, origins: _Origins) -> bool:
|
|
251
|
+
"""Check whether a hint's edit writes a generic `shown` under one name under another.
|
|
252
|
+
|
|
253
|
+
ty shows a generic alias (`NDArray[bool]`) and writes the class it stands for with the alias's
|
|
254
|
+
own arguments (`np.ndarray[np.bool]`), which aren't the class's. A name the file or the edit's
|
|
255
|
+
imports bind to the one shown (`from shapes import Shape as S`) is the same one.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Whether it does.
|
|
259
|
+
|
|
260
|
+
"""
|
|
261
|
+
try:
|
|
262
|
+
pairs: list[tuple[ast.expr, ast.expr]] = list(zip(_heads(shown), _heads(written), strict=False))
|
|
263
|
+
except SyntaxError:
|
|
264
|
+
return False
|
|
265
|
+
was: ast.expr
|
|
266
|
+
now: ast.expr
|
|
267
|
+
for was, now in pairs:
|
|
268
|
+
name: str = now.attr if isinstance(now, ast.Attribute) else ast.unparse(now)
|
|
269
|
+
if name in origins and isinstance(now, ast.Name):
|
|
270
|
+
name = origins[name][2]
|
|
271
|
+
elif isinstance(now, ast.Name):
|
|
272
|
+
name = (name if plan is None else plan.bound.get(name, name)).rpartition(_DOT)[2]
|
|
273
|
+
if name != (was.attr if isinstance(was, ast.Attribute) else ast.unparse(was)):
|
|
274
|
+
return True
|
|
275
|
+
return False
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _heads(text: str) -> list[ast.expr]:
|
|
279
|
+
"""List what a hint's `text` subscripts (`dict` and `list` in `dict[str, list[int]]`), outermost first.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Them.
|
|
283
|
+
|
|
284
|
+
"""
|
|
285
|
+
root: ast.expr = ast.parse(_plain(text), mode="eval").body
|
|
286
|
+
return [node.value for node in ast.walk(root) if isinstance(node, ast.Subscript)]
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _spelled(
|
|
290
|
+
text: str,
|
|
291
|
+
known: Known,
|
|
292
|
+
imports: tuple[str, ...],
|
|
293
|
+
*,
|
|
294
|
+
nesting: int,
|
|
295
|
+
before: int | None,
|
|
296
|
+
) -> str | None:
|
|
297
|
+
"""Write a hint's `text` as an annotation the file can hold, with `imports` for its names.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
The annotation, or `None` if it isn't one, says too little, or names what the file can't.
|
|
301
|
+
|
|
302
|
+
"""
|
|
303
|
+
plan: ImportPlan | None = known.names.plan
|
|
304
|
+
try:
|
|
305
|
+
parsed: ast.expr = ast.parse(_plain(text), mode="eval").body
|
|
306
|
+
except SyntaxError:
|
|
307
|
+
return None
|
|
308
|
+
root: ast.expr | None = _widened(parsed)
|
|
309
|
+
if plan is not None and root is not None and not known.names.starred:
|
|
310
|
+
root = _unstarred(root, plan)
|
|
311
|
+
if plan is None or root is None or not _annotation(root) or not _fits(root, nesting, known):
|
|
312
|
+
return None
|
|
313
|
+
origins: _Origins = _origins(imports)
|
|
314
|
+
root = _abstract(root, plan, origins)
|
|
315
|
+
lone: set[str] = _lone(root)
|
|
316
|
+
# `TypeAlias` is `type_alias`'s to write. A special form alone isn't a type.
|
|
317
|
+
if _aliases(root) or lone & _FORMS or _misnamed(root, plan) or _bottom(root):
|
|
318
|
+
return None
|
|
319
|
+
names: list[str] = sorted(
|
|
320
|
+
{
|
|
321
|
+
node.id
|
|
322
|
+
for node in ast.walk(root)
|
|
323
|
+
if isinstance(node, ast.Name) and not _usable(node.id, plan, before)
|
|
324
|
+
},
|
|
325
|
+
)
|
|
326
|
+
ways: dict[str, str | None] = {name: _way(name, plan, origins, lone=name in lone) for name in names}
|
|
327
|
+
if None in ways.values():
|
|
328
|
+
return None
|
|
329
|
+
spelled: dict[str, str] = {}
|
|
330
|
+
name: str
|
|
331
|
+
for name in names:
|
|
332
|
+
found: str | None
|
|
333
|
+
if (found := _named(name, ways[name], plan, origins)) is None:
|
|
334
|
+
return None
|
|
335
|
+
spelled[name] = found
|
|
336
|
+
# The annotation holds no strings (its only constants are `None` and `...`): a name is a word
|
|
337
|
+
# that isn't an attribute's.
|
|
338
|
+
return re.sub(
|
|
339
|
+
r"(?<![\w.])(\w+)",
|
|
340
|
+
lambda word: spelled.get(word[1], word[1]),
|
|
341
|
+
ast.unparse(root),
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _unstarred(root: ast.expr, plan: ImportPlan) -> ast.expr | None:
|
|
346
|
+
"""Write an annotation's unpacked tuples as every Python 3 parses them: `Unpack[tuple[str, ...]]`.
|
|
347
|
+
|
|
348
|
+
`*tuple[str, ...]` in a subscript is Python 3.11's syntax: for a module that may run on an
|
|
349
|
+
older one (see `LibraryNames.starred`).
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
The annotation; `None` for one that unpacks, where the module doesn't import `Unpack`.
|
|
353
|
+
|
|
354
|
+
"""
|
|
355
|
+
nodes: list[ast.AST] = list(ast.walk(root))
|
|
356
|
+
if not any(isinstance(node, ast.Starred) for node in nodes):
|
|
357
|
+
return root
|
|
358
|
+
if plan.bound.get(_UNPACK) not in _UNPACK_ORIGINS:
|
|
359
|
+
return None
|
|
360
|
+
lone: list[ast.expr] = [] # each tuple of one unpacked element: `tuple[*Ts]`'s, its one argument
|
|
361
|
+
node: ast.AST
|
|
362
|
+
for node in nodes:
|
|
363
|
+
if isinstance(node, ast.Tuple | ast.List):
|
|
364
|
+
if isinstance(node, ast.Tuple) and len(node.elts) == 1 and isinstance(node.elts[0], ast.Starred):
|
|
365
|
+
lone.append(node)
|
|
366
|
+
node.elts = [
|
|
367
|
+
ast.Subscript(ast.Name(_UNPACK), each.value) if isinstance(each, ast.Starred) else each
|
|
368
|
+
for each in node.elts
|
|
369
|
+
]
|
|
370
|
+
for node in nodes:
|
|
371
|
+
if isinstance(node, ast.Subscript) and isinstance(node.slice, ast.Tuple) and node.slice in lone:
|
|
372
|
+
node.slice = node.slice.elts[0]
|
|
373
|
+
return root
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _abstract(root: ast.expr, plan: ImportPlan, origins: _Origins) -> ast.expr:
|
|
377
|
+
"""Write `types`' generator, coroutine and async generator classes as `collections.abc`'s.
|
|
378
|
+
|
|
379
|
+
Where the name is `types`' (`GeneratorType` that the module or the hint's edits import from it,
|
|
380
|
+
`types.GeneratorType` through the module): see `_RUNTIME`.
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
The annotation.
|
|
384
|
+
|
|
385
|
+
"""
|
|
386
|
+
node: ast.AST
|
|
387
|
+
abstract: str | None
|
|
388
|
+
for node in ast.walk(root):
|
|
389
|
+
if isinstance(node, ast.Subscript) and (abstract := _runtime(node.value, plan, origins)) is not None:
|
|
390
|
+
node.value = ast.Name(abstract)
|
|
391
|
+
return root if (abstract := _runtime(root, plan, origins)) is None else ast.Name(abstract)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _runtime(node: ast.expr, plan: ImportPlan, origins: _Origins) -> str | None:
|
|
395
|
+
"""Name the `collections.abc` class for `types`' class `node` names, if it names one of `_RUNTIME`'s.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
It, or `None`.
|
|
399
|
+
|
|
400
|
+
"""
|
|
401
|
+
name: str
|
|
402
|
+
module: str
|
|
403
|
+
match node:
|
|
404
|
+
case ast.Name(id=name) if name in _RUNTIME and _origin(name, plan, origins) == f"{_TYPES}.{name}":
|
|
405
|
+
return _RUNTIME[name]
|
|
406
|
+
case ast.Attribute(value=ast.Name(id=module), attr=name) if (
|
|
407
|
+
name in _RUNTIME and plan.bound.get(module) == _TYPES
|
|
408
|
+
):
|
|
409
|
+
return _RUNTIME[name]
|
|
410
|
+
case _:
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _bottom(root: ast.expr) -> bool:
|
|
415
|
+
"""Check whether an annotation has `Never` or `NoReturn` in it, however it's spelled.
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
Whether it does.
|
|
419
|
+
|
|
420
|
+
"""
|
|
421
|
+
return any(
|
|
422
|
+
(isinstance(node, ast.Name) and node.id in _BOTTOM)
|
|
423
|
+
or (isinstance(node, ast.Attribute) and node.attr in _BOTTOM)
|
|
424
|
+
for node in ast.walk(root)
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _misnamed(root: ast.expr, plan: ImportPlan) -> bool:
|
|
429
|
+
"""Check whether an annotation names what the file binds to another thing than the checker means.
|
|
430
|
+
|
|
431
|
+
A module, written bare (`import socket`, then `socket`: the checker's is a class), a well-known
|
|
432
|
+
class's name (`Match`) the module binds to its own class, a builtin it doesn't rebind given type
|
|
433
|
+
arguments it takes none of (numpy's `bool`, printed bare), a private part of a dotted name
|
|
434
|
+
(`np._NBitIntP`), or a class a function defines, named through it (`Parser.read.Dialect`).
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
Whether it does.
|
|
438
|
+
|
|
439
|
+
"""
|
|
440
|
+
node: ast.AST
|
|
441
|
+
name: str
|
|
442
|
+
# A dotted name's root (`datetime` in `datetime.date`) is a module's, rightly.
|
|
443
|
+
roots: set[int] = {id(node.value) for node in ast.walk(root) if isinstance(node, ast.Attribute)}
|
|
444
|
+
for node in ast.walk(root):
|
|
445
|
+
match node:
|
|
446
|
+
case ast.Name(id=name) if id(node) not in roots and (
|
|
447
|
+
plan.bound.get(name) == name
|
|
448
|
+
or (name in WELL_KNOWN and name in plan.defined and not _imported(name, plan))
|
|
449
|
+
):
|
|
450
|
+
return True
|
|
451
|
+
case ast.Subscript(value=ast.Name(id=name)) if (
|
|
452
|
+
name in _BUILTINS and name not in plan.defined and name not in _GENERIC_BUILTINS
|
|
453
|
+
):
|
|
454
|
+
return True
|
|
455
|
+
case ast.Attribute():
|
|
456
|
+
parts: list[str] = _dotted(node)
|
|
457
|
+
if parts and (
|
|
458
|
+
any(part.startswith("_") for part in parts[1:])
|
|
459
|
+
or (parts[0] not in plan.bound and any(part[:1].islower() for part in parts[1:-1]))
|
|
460
|
+
):
|
|
461
|
+
return True
|
|
462
|
+
case _:
|
|
463
|
+
pass
|
|
464
|
+
return False
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _origin(name: str, plan: ImportPlan, origins: _Origins) -> str | None:
|
|
468
|
+
"""Find what a hint's bare `name` is: what the module's import binds, else the hint's edits'.
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
Its qualified name; `None` where the module binds it otherwise, or nothing says.
|
|
472
|
+
|
|
473
|
+
"""
|
|
474
|
+
if name in plan.bound:
|
|
475
|
+
return plan.bound[name]
|
|
476
|
+
if name in plan.defined or name not in origins:
|
|
477
|
+
return None
|
|
478
|
+
return f"{origins[name][1]}.{origins[name][2]}"
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _imported(name: str, plan: ImportPlan) -> bool:
|
|
482
|
+
"""Check whether the module binds `name` by importing a class of that name (`from re import Match`).
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
Whether it does: not by `import name` (a module), nor under another name, nor otherwise.
|
|
486
|
+
|
|
487
|
+
"""
|
|
488
|
+
bound: str | None = plan.bound.get(name)
|
|
489
|
+
return bound is not None and _DOT in bound and bound.rpartition(_DOT)[2] == name
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _dotted(node: ast.expr) -> list[str]:
|
|
493
|
+
"""Split a dotted name into its parts: `np.ma.MaskedArray`'s `np`, `ma` and `MaskedArray`.
|
|
494
|
+
|
|
495
|
+
Returns:
|
|
496
|
+
Them, the root first; empty for an attribute of anything but a name.
|
|
497
|
+
|
|
498
|
+
"""
|
|
499
|
+
spelled: str | None = dotted(node)
|
|
500
|
+
return [] if spelled is None else spelled.split(_DOT)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _plain(text: str) -> str:
|
|
504
|
+
"""Write a hint's text as Python reads it: ty's class objects and scoped type variables as names.
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
It: `<class 'Point'>` as `type[Point]`, `Model@create_model` as `Model`.
|
|
508
|
+
|
|
509
|
+
"""
|
|
510
|
+
return _SCOPED.sub(r"\1", _CLASS_OBJECT.sub(r"type[\1]", text))
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _lone(root: ast.expr) -> set[str]:
|
|
514
|
+
"""Name the names an annotation writes without type arguments (`Box` in `list[Box]`, not `list`).
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
Them.
|
|
518
|
+
|
|
519
|
+
"""
|
|
520
|
+
subscripted: set[int] = {id(node.value) for node in ast.walk(root) if isinstance(node, ast.Subscript)}
|
|
521
|
+
return {node.id for node in ast.walk(root) if isinstance(node, ast.Name) and id(node) not in subscripted}
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _aliases(root: ast.expr) -> bool:
|
|
525
|
+
"""Check whether an annotation is (or has in it) `TypeAlias`, however it's spelled.
|
|
526
|
+
|
|
527
|
+
Returns:
|
|
528
|
+
Whether it does.
|
|
529
|
+
|
|
530
|
+
"""
|
|
531
|
+
return any(
|
|
532
|
+
(isinstance(node, ast.Name) and node.id == ALIAS)
|
|
533
|
+
or (isinstance(node, ast.Attribute) and node.attr == ALIAS)
|
|
534
|
+
for node in ast.walk(root)
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
@lru_cache(maxsize=1024)
|
|
539
|
+
def _origins(imports: tuple[str, ...]) -> _Origins:
|
|
540
|
+
"""Read the `from` imports a hint's edits add (see `Offered`), each a statement binding one name.
|
|
541
|
+
|
|
542
|
+
An `import m` is passed over: it names no class.
|
|
543
|
+
|
|
544
|
+
Returns:
|
|
545
|
+
Each name bound, with its statement, the module (a relative one with its dots) and its name there.
|
|
546
|
+
|
|
547
|
+
"""
|
|
548
|
+
found: _Origins = {}
|
|
549
|
+
statement: str
|
|
550
|
+
for statement in imports:
|
|
551
|
+
node: ast.stmt = ast.parse(statement).body[0]
|
|
552
|
+
if isinstance(node, ast.ImportFrom):
|
|
553
|
+
alias: ast.alias = node.names[0]
|
|
554
|
+
module: str = _RELATIVE * node.level + (node.module or "")
|
|
555
|
+
found[alias.asname or alias.name] = (statement, module, alias.name)
|
|
556
|
+
return found
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _way(name: str, plan: ImportPlan, origins: _Origins, *, lone: bool) -> str | None:
|
|
560
|
+
"""Choose how a name the module doesn't bind where annotations run can be written.
|
|
561
|
+
|
|
562
|
+
`lone`: whether the annotation writes it without type arguments somewhere.
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
`_OWN`, `_OFFERED` or `_KNOWN`; `None` if nothing says what the name means. From the standard
|
|
566
|
+
library, a hint's edits must import a class its tables know, from a public module, and never a
|
|
567
|
+
generic one written bare (a checker prints one so when it doesn't know its arguments).
|
|
568
|
+
|
|
569
|
+
"""
|
|
570
|
+
if name == _DISCARD:
|
|
571
|
+
return None
|
|
572
|
+
if name in plan.guarded and plan.guarded[name].statement is None and name not in plan.values:
|
|
573
|
+
return _OWN
|
|
574
|
+
module: str
|
|
575
|
+
attribute: str
|
|
576
|
+
_, module, attribute = origins.get(name, ("", "", ""))
|
|
577
|
+
# A standard-library class a checker prints bare is imported to run, as `--fix`'s own are.
|
|
578
|
+
standard: bool = name not in origins or module.partition(_RELATIVE)[0] in _STDLIB
|
|
579
|
+
if name in WELL_KNOWN and standard:
|
|
580
|
+
return _KNOWN
|
|
581
|
+
if name not in origins:
|
|
582
|
+
return None
|
|
583
|
+
path: str = f"{module}.{attribute}"
|
|
584
|
+
writable: bool = (
|
|
585
|
+
not any(part.startswith("_") for part in module.split(_RELATIVE))
|
|
586
|
+
and stdlib.defines_class(path)
|
|
587
|
+
and not (lone and stdlib.needs_arguments(path))
|
|
588
|
+
)
|
|
589
|
+
return _OFFERED if writable or not standard else None
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def _named(name: str, way: str | None, plan: ImportPlan, origins: _Origins) -> str | None:
|
|
593
|
+
"""Write `name` the `way` chosen for it, adding the import it takes.
|
|
594
|
+
|
|
595
|
+
Returns:
|
|
596
|
+
The name as the file is to write it, or `None` if the import can't bind it.
|
|
597
|
+
|
|
598
|
+
"""
|
|
599
|
+
if way == _KNOWN:
|
|
600
|
+
return plan.spell(WELL_KNOWN[name])
|
|
601
|
+
if way == _OWN:
|
|
602
|
+
return name
|
|
603
|
+
statement: str
|
|
604
|
+
module: str
|
|
605
|
+
attribute: str
|
|
606
|
+
statement, module, attribute = origins[name]
|
|
607
|
+
# Through an import that runs, if the module has one of what the hint's would bind.
|
|
608
|
+
found: str | None
|
|
609
|
+
if not module.startswith(_RELATIVE) and (found := plan.named(f"{module}.{attribute}")) is not None:
|
|
610
|
+
return found
|
|
611
|
+
return name if plan.guard(name, (module, attribute), statement) else None
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def _annotation(root: ast.expr) -> bool:
|
|
615
|
+
"""Check that an expression is an annotation, and says something: not a bare `None`.
|
|
616
|
+
|
|
617
|
+
Names, attributes, subscripts and unions, whose only constants are `None` and `...`: not a call
|
|
618
|
+
(`Module("os")`), a signature, or a string.
|
|
619
|
+
|
|
620
|
+
Returns:
|
|
621
|
+
Whether it is.
|
|
622
|
+
|
|
623
|
+
"""
|
|
624
|
+
return (
|
|
625
|
+
all(isinstance(node, _ANNOTATION_NODES) for node in ast.walk(root))
|
|
626
|
+
and all(node.value in {None, Ellipsis} for node in ast.walk(root) if isinstance(node, ast.Constant))
|
|
627
|
+
and not (isinstance(root, ast.Constant) and root.value is None)
|
|
628
|
+
and not _mixes(root)
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _mixes(root: ast.expr) -> bool:
|
|
633
|
+
"""Check whether an annotation has a union inside it (`dict[str, int | bytes]`): a mixed container's.
|
|
634
|
+
|
|
635
|
+
Its elements are read as one member or another, which no test of the name narrows.
|
|
636
|
+
|
|
637
|
+
Returns:
|
|
638
|
+
Whether it does.
|
|
639
|
+
|
|
640
|
+
"""
|
|
641
|
+
chain: set[int] = _chain(root)
|
|
642
|
+
return any(isinstance(node, ast.BinOp) and id(node) not in chain for node in ast.walk(root))
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _chain(root: ast.expr) -> set[int]:
|
|
646
|
+
"""Find the nodes joining an annotation's outermost union: `A | B` in `A | B | C`.
|
|
647
|
+
|
|
648
|
+
Returns:
|
|
649
|
+
Their identities.
|
|
650
|
+
|
|
651
|
+
"""
|
|
652
|
+
return {id(root)} | _chain(root.left) if isinstance(root, ast.BinOp) else set()
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def _fits(root: ast.expr, nesting: int, known: Known) -> bool:
|
|
656
|
+
"""Check the rules accept an annotation: not vague (LVA005), too deep (LVA006) or long (LVA011).
|
|
657
|
+
|
|
658
|
+
Returns:
|
|
659
|
+
Whether it is.
|
|
660
|
+
|
|
661
|
+
"""
|
|
662
|
+
return (
|
|
663
|
+
vague_fits(root, known.limits.vague)
|
|
664
|
+
and depth(root) < nesting
|
|
665
|
+
and length(root) <= known.limits.max_length
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def _usable(name: str, plan: ImportPlan, before: int | None) -> bool:
|
|
670
|
+
"""Check whether the annotation can use `name` as it is: the module binds it, or it's a builtin.
|
|
671
|
+
|
|
672
|
+
In a module body (`before`, the binding's line), the module must bind it earlier: the
|
|
673
|
+
annotation is evaluated there.
|
|
674
|
+
|
|
675
|
+
Returns:
|
|
676
|
+
Whether it can.
|
|
677
|
+
|
|
678
|
+
"""
|
|
679
|
+
if name == _DISCARD: # gettext's, or a throwaway: never a class the checker means
|
|
680
|
+
return False
|
|
681
|
+
if name in plan.defined:
|
|
682
|
+
return before is None or plan.defined[name] < before
|
|
683
|
+
return name in _BUILTINS
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def _widened(node: ast.expr) -> ast.expr | None:
|
|
687
|
+
"""Widen every `Literal` in an annotation to its values' types, and a union's repeats away.
|
|
688
|
+
|
|
689
|
+
Returns:
|
|
690
|
+
The annotation, or `None` if a `Literal` holds something that isn't a plain value.
|
|
691
|
+
|
|
692
|
+
"""
|
|
693
|
+
name: str
|
|
694
|
+
value: ast.expr
|
|
695
|
+
inner: ast.expr
|
|
696
|
+
elements: list[ast.expr]
|
|
697
|
+
match node:
|
|
698
|
+
case ast.Subscript(value=ast.Name(id=name), slice=inner) if name == _LITERAL:
|
|
699
|
+
members: list[ast.expr] = inner.elts if isinstance(inner, ast.Tuple) else [inner]
|
|
700
|
+
return _union([_literal_type(member) for member in members])
|
|
701
|
+
case ast.BinOp(op=ast.BitOr() | ast.BitAnd()):
|
|
702
|
+
return _joined(node)
|
|
703
|
+
case ast.Subscript(value=value, slice=inner):
|
|
704
|
+
widened: ast.expr | None = _widened(inner)
|
|
705
|
+
# A class's own argument may be bound to the literals (`Reader[Literal["frame"]]`).
|
|
706
|
+
# Compared as written: a node built here has no `ctx` before Python 3.13, so its dump differs.
|
|
707
|
+
same: bool = widened is not None and (_holds(value) or ast.unparse(widened) == ast.unparse(inner))
|
|
708
|
+
return ast.Subscript(value, widened) if widened is not None and same else None
|
|
709
|
+
case ast.Tuple(elts=elements) | ast.List(elts=elements):
|
|
710
|
+
parts: list[ast.expr | None] = [_widened(element) for element in elements]
|
|
711
|
+
kept: list[ast.expr] = [part for part in parts if part is not None]
|
|
712
|
+
rebuilt: ast.expr = ast.Tuple(kept) if isinstance(node, ast.Tuple) else ast.List(kept)
|
|
713
|
+
return rebuilt if len(kept) == len(parts) else None
|
|
714
|
+
case ast.Name(id=name):
|
|
715
|
+
return ast.Name(_WIDER.get(name, name))
|
|
716
|
+
case _:
|
|
717
|
+
return node
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def _holds(head: ast.expr) -> bool:
|
|
721
|
+
"""Check whether a subscript's head is a container of any type: a builtin, or `collections.abc`'s.
|
|
722
|
+
|
|
723
|
+
Returns:
|
|
724
|
+
Whether it is.
|
|
725
|
+
|
|
726
|
+
"""
|
|
727
|
+
return isinstance(head, ast.Name) and (head.id in _BUILTINS or head.id in WELL_KNOWN)
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def _joined(node: ast.BinOp) -> ast.expr | None:
|
|
731
|
+
"""Widen a union's members; or read an intersection with a truthiness as its one other member.
|
|
732
|
+
|
|
733
|
+
Returns:
|
|
734
|
+
The annotation, or `None` for a member that widens to none, or any other intersection.
|
|
735
|
+
|
|
736
|
+
"""
|
|
737
|
+
if isinstance(node.op, ast.BitOr):
|
|
738
|
+
return _union([_widened(member) for member in _members(node)])
|
|
739
|
+
types: list[ast.expr] = [part for part in _members(node, ast.BitAnd) if not _truthiness(part)]
|
|
740
|
+
return _widened(types[0]) if len(types) == 1 else None
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def _literal_type(member: ast.expr) -> ast.expr | None:
|
|
744
|
+
"""Type one `Literal` value: `1` is `int`, `-1` too, `"a"` is `str`, `Color.RED` is `Color`.
|
|
745
|
+
|
|
746
|
+
Returns:
|
|
747
|
+
Its type, or `None` for anything else.
|
|
748
|
+
|
|
749
|
+
"""
|
|
750
|
+
owner: ast.expr
|
|
751
|
+
value: bool | int | str | bytes
|
|
752
|
+
match member:
|
|
753
|
+
case ast.Constant(value=None):
|
|
754
|
+
return member
|
|
755
|
+
case ast.Constant(value=bool() | int() | str() | bytes() as value):
|
|
756
|
+
return ast.Name(type(value).__name__)
|
|
757
|
+
case ast.UnaryOp(op=ast.USub(), operand=ast.Constant(value=int())):
|
|
758
|
+
return ast.Name("int")
|
|
759
|
+
case ast.Attribute(value=owner):
|
|
760
|
+
return owner
|
|
761
|
+
case _:
|
|
762
|
+
return None
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
def _members(node: ast.expr, joined: type[ast.operator] = ast.BitOr) -> list[ast.expr]:
|
|
766
|
+
"""Flatten a union (`A | B | C`) into its members; or, `joined` by `&`, an intersection.
|
|
767
|
+
|
|
768
|
+
Returns:
|
|
769
|
+
Them, in order.
|
|
770
|
+
|
|
771
|
+
"""
|
|
772
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, joined):
|
|
773
|
+
return [*_members(node.left, joined), *_members(node.right, joined)]
|
|
774
|
+
return [node]
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def _truthiness(node: ast.expr) -> bool:
|
|
778
|
+
"""Check whether an intersection's member only says its value is true, or false (`~AlwaysFalsy`).
|
|
779
|
+
|
|
780
|
+
Returns:
|
|
781
|
+
Whether it does.
|
|
782
|
+
|
|
783
|
+
"""
|
|
784
|
+
name: str
|
|
785
|
+
match node:
|
|
786
|
+
case ast.UnaryOp(op=ast.Invert(), operand=ast.Name(id=name)):
|
|
787
|
+
return name in _TRUTHINESS
|
|
788
|
+
case _:
|
|
789
|
+
return False
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def _union(members: list[ast.expr | None]) -> ast.expr | None:
|
|
793
|
+
"""Join members into a union, each once, in order (`None` last, as it's conventionally written).
|
|
794
|
+
|
|
795
|
+
Returns:
|
|
796
|
+
The union (a single member alone), or `None` if any member is.
|
|
797
|
+
|
|
798
|
+
"""
|
|
799
|
+
seen: dict[str, ast.expr] = {}
|
|
800
|
+
member: ast.expr | None
|
|
801
|
+
for member in members:
|
|
802
|
+
if member is None:
|
|
803
|
+
return None
|
|
804
|
+
part: ast.expr
|
|
805
|
+
for part in _members(member):
|
|
806
|
+
_ = seen.setdefault(ast.unparse(part), part)
|
|
807
|
+
ordered: list[ast.expr] = sorted(seen.values(), key=lambda part: ast.unparse(part) == _NONE)
|
|
808
|
+
union: ast.expr = ordered[0]
|
|
809
|
+
for member in ordered[1:]:
|
|
810
|
+
union = ast.BinOp(union, ast.BitOr(), member)
|
|
811
|
+
return union
|