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,752 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Standard-library functions and classes whose types `--fix` knows, and how a module names them.
|
|
3
|
+
|
|
4
|
+
The tables are generated from typeshed's stubs into `tables/` when the package is built, one JSON
|
|
5
|
+
file each (see `stdlib_tables/generate.py`): `RETURNS` holds functions returning the same builtin type
|
|
6
|
+
whatever their arguments; `OVERLOADS` those whose arguments decide it (`str` in, `str` out;
|
|
7
|
+
`bytes` in, `bytes` out), so they're typed only when those are known; `CLASSES` holds
|
|
8
|
+
non-generic classes, and functions returning one; `library_member` types those classes' methods
|
|
9
|
+
and attributes. A call is matched by the module and name it resolves to through the module's imports
|
|
10
|
+
(`origins`), not by how it's spelled, so `import os as o` then `o.getpid()`, or `from os import
|
|
11
|
+
getpid`, are the same call, and a `getpid` from anywhere else isn't.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import ast
|
|
15
|
+
import builtins
|
|
16
|
+
import json
|
|
17
|
+
from collections.abc import Iterable, Mapping
|
|
18
|
+
from functools import cache, lru_cache
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from types import MappingProxyType
|
|
21
|
+
from typing import Final, NamedTuple, TypeAlias, cast
|
|
22
|
+
|
|
23
|
+
from constricter.fix.core.known import ImportPlan, Inference, Known
|
|
24
|
+
from constricter.fix.core.signatures import Variant
|
|
25
|
+
from constricter.rules.syntax import import_bindings
|
|
26
|
+
from constricter.rules.walked import classes
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _table(name: str) -> object:
|
|
30
|
+
"""Read one of the tables (`tables/<name>.json`), plain JSON as generated.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Its entries.
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
# Not through `jsonc`: its comment stripping took 20 ms of every run's start, for nothing.
|
|
37
|
+
return cast(
|
|
38
|
+
"object",
|
|
39
|
+
json.loads(Path(__file__).parents[1].joinpath("tables", f"{name}.json").read_bytes()),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# `os.environ`'s own method with a fixed type: a variable's, which the tables' functions don't hold.
|
|
44
|
+
_ENVIRON: Final = {"os.environ.copy": "dict[str, str]"}
|
|
45
|
+
# `tarfile.open` is `TarFile.open` (typeshed's `open = TarFile.open`): an alias the tables don't follow.
|
|
46
|
+
_ALIASED: Final = {"tarfile.open": "tarfile.TarFile"}
|
|
47
|
+
RETURNS: Final = {**cast("dict[str, str]", _table("returns")), **_ENVIRON, **_ALIASED}
|
|
48
|
+
# Functions whose arguments decide their type: each signature, as each configuration reads them
|
|
49
|
+
# (see `constricter.fix.libraries.overloads`).
|
|
50
|
+
OVERLOADS: Final = cast("dict[str, list[Variant]]", _table("overloads"))
|
|
51
|
+
# Classes, and functions (constructors, classmethods) returning one: typed by that class's dotted
|
|
52
|
+
# path, spelled (and imported, if it must be) the way the module can.
|
|
53
|
+
CLASSES: Final = cast("dict[str, str]", _table("classes"))
|
|
54
|
+
# The builtin iterators no Python can subscript at run time.
|
|
55
|
+
_UNSUBSCRIPTABLE: Final = frozenset({"zip", "map", "filter", "reversed"})
|
|
56
|
+
# The `pathlib` classes: each one's `/` joins another part onto it.
|
|
57
|
+
_PATHS: Final = frozenset(
|
|
58
|
+
f"pathlib.{name}"
|
|
59
|
+
for name in ("Path", "PosixPath", "PurePath", "PurePosixPath", "PureWindowsPath", "WindowsPath")
|
|
60
|
+
)
|
|
61
|
+
_ALIASES: Final = cast("Mapping[str, str]", _table("aliases")) # a class's other public paths, to its own
|
|
62
|
+
# Each class's methods' returns and attributes' types apart from its public ancestors' (`_BASES`),
|
|
63
|
+
# `None` where it hides one of theirs: `_member` resolves the rest through them.
|
|
64
|
+
_Own: TypeAlias = Mapping[str, Mapping[str, str | None]]
|
|
65
|
+
_METHODS: Final = cast("_Own", _table("methods"))
|
|
66
|
+
_ATTRIBUTES: Final = cast("_Own", _table("attributes"))
|
|
67
|
+
# What awaiting a call of each `async def` gives: a function's by its path, a method's by its
|
|
68
|
+
# class's path and its name (a class's own path: its instance).
|
|
69
|
+
_AWAITED: Final = cast("Mapping[str, str]", _table("awaited"))
|
|
70
|
+
# The `async def`s whose arguments decide what awaiting their call gives, as `OVERLOADS` holds a
|
|
71
|
+
# function's; `signatures.AWAIT` before a path names one's entry to `overloads.chosen`.
|
|
72
|
+
AWAITED_OVERLOADS: Final = cast("dict[str, list[Variant]]", _table("awaited_overloads"))
|
|
73
|
+
# The generic classes awaiting an instance of gives its last type argument.
|
|
74
|
+
_AWAITABLE: Final = frozenset({"asyncio.Future", "asyncio.Task"})
|
|
75
|
+
# Each class's public ancestors in the tables, nearest first, comma-separated.
|
|
76
|
+
_BASES: Final = cast("Mapping[str, str]", _table("bases"))
|
|
77
|
+
# Classes' methods whose arguments decide their type: each one's entry in `method_signatures` (its
|
|
78
|
+
# signatures without `self`, as `OVERLOADS`', under the class defining it: `module.Class.method`).
|
|
79
|
+
_METHOD_OVERLOADS: Final = cast("_Own", _table("method_overloads")) # by name, as `_METHODS`
|
|
80
|
+
# Each generic class's type parameters, in order, comma-separated: an instance's type binds them.
|
|
81
|
+
# One ending `=` has a default (PEP 696): a class all of whose have one may be written bare.
|
|
82
|
+
_TYPE_PARAMETERS: Final = cast("Mapping[str, str]", _table("type_parameters"))
|
|
83
|
+
# The generic classes every Python can subscript at run time (`itertools.chain[str]`, not
|
|
84
|
+
# `itertools.count[int]`), as a module's own annotations are evaluated.
|
|
85
|
+
_SUBSCRIPTABLE: Final = cast("Mapping[str, str]", _table("subscriptable"))
|
|
86
|
+
# Each generic class's own attributes and properties, as templates naming its type parameters
|
|
87
|
+
# (`re.Match`'s `string`: `AnyStr`), which its instance's type arguments bind; and a non-generic
|
|
88
|
+
# class's own that `_ATTRIBUTES` can't hold (`ast.Module`'s `body`: `list[ast.stmt]`).
|
|
89
|
+
_GENERIC_ATTRIBUTES: Final = cast("_Own", _table("generic_attributes"))
|
|
90
|
+
# What iterating each class's instance gives, as such a template (`io.TextIOWrapper`'s `str`).
|
|
91
|
+
_ELEMENTS: Final = cast("Mapping[str, str]", _table("elements"))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@cache
|
|
95
|
+
def method_signatures() -> dict[str, list[Variant]]:
|
|
96
|
+
"""Read the `method_signatures` table, the first time a call needs it.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Each method's signatures (see `OVERLOADS`), by its entry.
|
|
100
|
+
|
|
101
|
+
"""
|
|
102
|
+
return cast("dict[str, list[Variant]]", _table("method_signatures"))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@cache
|
|
106
|
+
def _scalars() -> Mapping[str, str]:
|
|
107
|
+
return cast("Mapping[str, str]", _table("scalars"))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@cache
|
|
111
|
+
def _scalar_members() -> Mapping[str, frozenset[str]]:
|
|
112
|
+
return {
|
|
113
|
+
scalar: frozenset(names)
|
|
114
|
+
for scalar, names in cast("Mapping[str, list[str]]", _table("scalar_members")).items()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def scalar_verdicts(path: str) -> str | None:
|
|
119
|
+
"""Look up which builtin scalars a standard-library class or alias takes (`typing.SupportsIndex`).
|
|
120
|
+
|
|
121
|
+
Read the first time an installed package's overloads need it (see `constricter.fix.index.stubs.stubbed`).
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Its verdict per `overloads.SCALARS` type, in order; or `None` if the tables don't have it.
|
|
125
|
+
|
|
126
|
+
"""
|
|
127
|
+
return _scalars().get(path)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def scalar_members(scalar: str) -> frozenset[str] | None:
|
|
131
|
+
"""Name what an argument of builtin scalar type `scalar` has, for a protocol to be checked against.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Its members, or `None` if the tables don't have them.
|
|
135
|
+
|
|
136
|
+
"""
|
|
137
|
+
return _scalar_members().get(scalar)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# An environment lookup: `os.environ.get(k)` is `str | None`, with a `str` default it's `str`
|
|
141
|
+
# (`os.environ`'s generic `Mapping.get` decides it by the arguments).
|
|
142
|
+
ENVIRONMENT: Final = "os.environ.get"
|
|
143
|
+
_KIND: Final = "stdlib" # the fix kind of what the tables type
|
|
144
|
+
_BUILTIN_NAMES: Final = frozenset({*dir(builtins), "None"})
|
|
145
|
+
_DOT: Final = "."
|
|
146
|
+
_ENTER: Final = "__enter__"
|
|
147
|
+
_SELF: Final = "Self" # in a template: the receiver's own type
|
|
148
|
+
ANY: Final = "Any" # a function's whole return in `RETURNS`, declared `typing.Any` (`json.loads`)
|
|
149
|
+
KNOWN: Final = frozenset({*RETURNS, *OVERLOADS, ENVIRONMENT, *CLASSES}) # every function the tables type
|
|
150
|
+
# Capitalised functions the tables don't type (`xml.etree.ElementTree.Comment`): no constructors.
|
|
151
|
+
FUNCTIONS: Final = frozenset(cast("Mapping[str, str]", _table("functions")))
|
|
152
|
+
# Module-level variables' types (`sys.path`: `list[str]`): builtin annotations, or classes' paths.
|
|
153
|
+
VARIABLES: Final = cast("dict[str, str]", _table("variables"))
|
|
154
|
+
_TABLE_MODULES: Final = frozenset(
|
|
155
|
+
name.rsplit(".", count)[0]
|
|
156
|
+
for name in (*KNOWN, *_ALIASES, *_METHODS, *_ATTRIBUTES, *_BASES, *VARIABLES)
|
|
157
|
+
for count in range(1, name.count(".") + 1)
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def origins(tree: ast.Module) -> dict[str, str]:
|
|
162
|
+
"""Map each top-level name the module imports from the standard library to what it is.
|
|
163
|
+
|
|
164
|
+
`import os` binds `os` to `os` (and `import os.path` binds `os` too); `import os.path as p`
|
|
165
|
+
binds `p` to `os.path`; `from os import getpid as pid` binds `pid` to `os.getpid`. Only the
|
|
166
|
+
modules the tables name are kept, and a relative import names no module here.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Each bound name, mapped to its dotted origin.
|
|
170
|
+
|
|
171
|
+
"""
|
|
172
|
+
return imported(tree.body)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def imported(body: Iterable[ast.stmt]) -> dict[str, str]:
|
|
176
|
+
"""Map the names the imports among `body` bind to what they are (see `origins`).
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Each bound name, mapped to its dotted origin.
|
|
180
|
+
|
|
181
|
+
"""
|
|
182
|
+
return {name: origin for name, origin, module in import_bindings(body) if module in _TABLE_MODULES}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def resolved(func: ast.expr, bound: Mapping[str, str]) -> str | None:
|
|
186
|
+
"""Resolve a callee (`o.path.join`, `getpid`) through the module's imports (`bound`, see `origins`).
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Its dotted origin (`os.path.join`), or `None` if its first name isn't one the module imports.
|
|
190
|
+
|
|
191
|
+
"""
|
|
192
|
+
name: str
|
|
193
|
+
owner: ast.expr
|
|
194
|
+
attr: str
|
|
195
|
+
match func:
|
|
196
|
+
case ast.Name(id=name) if name in bound:
|
|
197
|
+
return bound[name]
|
|
198
|
+
case ast.Attribute(value=owner, attr=attr):
|
|
199
|
+
found: str | None = resolved(owner, bound)
|
|
200
|
+
return None if found is None else f"{found}.{attr}"
|
|
201
|
+
case _:
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def library_member(
|
|
206
|
+
receiver: str,
|
|
207
|
+
name: str,
|
|
208
|
+
call: ast.Call | None,
|
|
209
|
+
known: Known,
|
|
210
|
+
*,
|
|
211
|
+
inherited: bool = False,
|
|
212
|
+
) -> Inference | None:
|
|
213
|
+
"""Type a standard-library class's attribute or property, or (`call`) its method's return.
|
|
214
|
+
|
|
215
|
+
`receiver` is the annotation of what it's looked up on, as the module spells it
|
|
216
|
+
(`ArgumentParser`, `argparse.ArgumentParser`), resolved through its imports and the ones `--fix`
|
|
217
|
+
is adding to it; a class the member gives is spelled (and imported, if it must be) as the
|
|
218
|
+
module can. The arguments of a call don't matter: the tables hold only what they can't change.
|
|
219
|
+
`inherited`: whether it's looked up for a class under `receiver`, where a member that is
|
|
220
|
+
`receiver` itself may be its `Self` (`Path.resolve()`), the inheriting class.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Its inference, or `None` if the class or its member isn't in the tables, or a class it gives
|
|
224
|
+
can't be named.
|
|
225
|
+
|
|
226
|
+
"""
|
|
227
|
+
path: str | None = _class_path(receiver, known)
|
|
228
|
+
found: str | None = (
|
|
229
|
+
None if path is None else _member(_ATTRIBUTES if call is None else _METHODS, path, name)
|
|
230
|
+
)
|
|
231
|
+
if found is not None and inherited and found == path:
|
|
232
|
+
return None
|
|
233
|
+
plan: ImportPlan | None = known.names.plan
|
|
234
|
+
if found is not None and is_class(found):
|
|
235
|
+
found = None if plan is None else plan.spell(found)
|
|
236
|
+
what: str = "annotation" if call is None else "return type"
|
|
237
|
+
return (
|
|
238
|
+
None
|
|
239
|
+
if found is None
|
|
240
|
+
else Inference(found, f"`{path}.{name}`'s {what} in typeshed", frozenset({_KIND}))
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def awaited_call(func: ast.expr, known: Known) -> Inference | None:
|
|
245
|
+
"""Type what awaiting a call of a standard-library `async def` gives (`await asyncio.start_server(...)`).
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Its inference, or `None` if `func` isn't one the tables hold, or its class can't be named.
|
|
249
|
+
|
|
250
|
+
"""
|
|
251
|
+
path: str | None = resolved(func, known.names.stdlib)
|
|
252
|
+
return None if path is None else _awaited(path, path, known)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def awaited_member(receiver: str, name: str, known: Known) -> Inference | None:
|
|
256
|
+
"""Type what awaiting a call of a standard-library class's `async def` method gives.
|
|
257
|
+
|
|
258
|
+
`await reader.readline()`, on a receiver typed `asyncio.StreamReader`; and what `async with`
|
|
259
|
+
binds, by `__aenter__`: the receiver's own type, where that returns `Self`.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Its inference, or `None` if the class or its method isn't in the tables, or a class it gives
|
|
263
|
+
can't be named.
|
|
264
|
+
|
|
265
|
+
"""
|
|
266
|
+
path: str | None = _class_path(receiver, known)
|
|
267
|
+
return None if path is None else _awaited(f"{path}.{name}", path, known, receiver)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _awaited(entry: str, path: str, known: Known, receiver: str | None = None) -> Inference | None:
|
|
271
|
+
found: str | None = _AWAITED.get(entry)
|
|
272
|
+
plan: ImportPlan | None = known.names.plan
|
|
273
|
+
if found is not None and found == path and receiver is not None:
|
|
274
|
+
found = receiver # its `Self`: the instance, as the module wrote its type
|
|
275
|
+
elif found is not None and is_class(found):
|
|
276
|
+
found = None if plan is None else plan.spell(found)
|
|
277
|
+
return (
|
|
278
|
+
None if found is None else Inference(found, f"`{entry}`'s return type, awaited", frozenset({_KIND}))
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def awaited_value(annotation: str, known: Known) -> str | None:
|
|
283
|
+
"""Find what awaiting a value typed `annotation` gives: `T`, of an `asyncio.Future[T]` or a `Task[T]`.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
It, as the annotation writes it; `None` for any other type.
|
|
287
|
+
|
|
288
|
+
"""
|
|
289
|
+
path: str | None
|
|
290
|
+
args: list[ast.expr]
|
|
291
|
+
path, args = _receiver(annotation, known)
|
|
292
|
+
return ast.unparse(args[-1]) if path in _AWAITABLE and args else None
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def bases(tree: ast.Module, bound: Mapping[str, str]) -> dict[str, str]:
|
|
296
|
+
"""Spell the standard-library classes the module's classes inherit from (`unittest.TestCase`).
|
|
297
|
+
|
|
298
|
+
Those the tables hold whole; and a generic one given all its arguments
|
|
299
|
+
(`collections.OrderedDict[str, int]`), which decide its members' types: written with builtin
|
|
300
|
+
types, dotted names and the module's own classes alone, not a type variable, which means
|
|
301
|
+
nothing outside its class.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Each, as the module's imports (`bound`, see `origins`) name it where it's a base, with its
|
|
305
|
+
path in the tables (`""`: a generic one's, whose members its arguments type).
|
|
306
|
+
|
|
307
|
+
"""
|
|
308
|
+
found: dict[str, str] = {}
|
|
309
|
+
plain: frozenset[str] = _BUILTIN_NAMES | {node.name for node in classes(tree)}
|
|
310
|
+
base: ast.expr
|
|
311
|
+
for base in (base for node in classes(tree) for base in node.bases):
|
|
312
|
+
head: ast.expr = base.value if isinstance(base, ast.Subscript) else base
|
|
313
|
+
path: str = resolved(head, bound) or ""
|
|
314
|
+
path = _ALIASES.get(path, path)
|
|
315
|
+
given: int = 0
|
|
316
|
+
if isinstance(base, ast.Subscript) and all(
|
|
317
|
+
part.id in plain for part in ast.walk(base.slice) if isinstance(part, ast.Name)
|
|
318
|
+
):
|
|
319
|
+
given = len(base.slice.elts) if isinstance(base.slice, ast.Tuple) else 1
|
|
320
|
+
if given and len(_TYPE_PARAMETERS.get(path, "").split(",")) == given and path in _TYPE_PARAMETERS:
|
|
321
|
+
found[ast.unparse(base)] = ""
|
|
322
|
+
if not given and CLASSES.get(path) == path:
|
|
323
|
+
found[ast.unparse(base)] = path
|
|
324
|
+
return found
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def members(path: str) -> frozenset[str]:
|
|
328
|
+
"""Name every member the tables hold of the class at `path`, its public ancestors' included.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
Them.
|
|
332
|
+
|
|
333
|
+
"""
|
|
334
|
+
own: set[str] = set()
|
|
335
|
+
table: _Own
|
|
336
|
+
for table in (_METHODS, _ATTRIBUTES, _METHOD_OVERLOADS, _GENERIC_ATTRIBUTES):
|
|
337
|
+
own.update(table.get(path, {}))
|
|
338
|
+
return frozenset(own).union(*(members(ancestor) for ancestor in _ancestors(path)))
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def attributes(path: str) -> dict[str, str]:
|
|
342
|
+
"""Type the attributes the tables hold of the class at `path`, its public ancestors' included.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
Each one's type, by name: the class's own, where an ancestor has one of its name.
|
|
346
|
+
|
|
347
|
+
"""
|
|
348
|
+
found: dict[str, str] = {}
|
|
349
|
+
ancestor: str
|
|
350
|
+
for ancestor in _ancestors(path):
|
|
351
|
+
found = attributes(ancestor) | found
|
|
352
|
+
own: Mapping[str, str | None] = _ATTRIBUTES.get(path, {})
|
|
353
|
+
return found | {name: typed for name, typed in own.items() if typed is not None}
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def lines(path: str) -> frozenset[str]:
|
|
357
|
+
"""Name the class at `path` and every public ancestor the tables give it.
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
Their paths: two classes sharing one have an order between them the tables don't say.
|
|
361
|
+
|
|
362
|
+
"""
|
|
363
|
+
return frozenset({path}).union(*(lines(ancestor) for ancestor in _ancestors(path)))
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def enters_itself(receiver: str, known: Known) -> bool:
|
|
367
|
+
"""Check whether `receiver` is a standard-library class whose `__enter__` returns the instance itself.
|
|
368
|
+
|
|
369
|
+
`receiver` is an annotation as the module spells it; its type arguments (`Popen[bytes]`) are the
|
|
370
|
+
instance's, which `with` then gives as it is.
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
Whether it is.
|
|
374
|
+
|
|
375
|
+
"""
|
|
376
|
+
path: str | None = _receiver(receiver, known)[0]
|
|
377
|
+
return path is not None and _member(_METHODS, path, _ENTER) == path
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _member(table: "_Own", path: str, name: str) -> str | None:
|
|
381
|
+
"""Look up a class's member in a table: its own entry, else its nearest public ancestor's.
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
Its entry (an attribute's type, a method's return, a `method_signatures` entry), or `None`.
|
|
385
|
+
|
|
386
|
+
"""
|
|
387
|
+
own: Mapping[str, str | None] = table.get(path, {})
|
|
388
|
+
if name in own:
|
|
389
|
+
return own[name]
|
|
390
|
+
ancestor: str
|
|
391
|
+
for ancestor in _ancestors(path):
|
|
392
|
+
found: str | None
|
|
393
|
+
if (found := _member(table, ancestor, name)) is not None:
|
|
394
|
+
return found
|
|
395
|
+
return None
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
@lru_cache(maxsize=1024)
|
|
399
|
+
def _ancestors(path: str) -> tuple[str, ...]:
|
|
400
|
+
return tuple(_BASES[path].split(",")) if path in _BASES else ()
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
class Method(NamedTuple):
|
|
404
|
+
"""A standard-library method whose arguments decide its type, on a receiver of a known type.
|
|
405
|
+
|
|
406
|
+
`entry`: its `method_signatures` entry; `instance`: the receiver's type arguments, if they're
|
|
407
|
+
builtins (`Pattern[str]`'s `str`), for a signature declaring `self`'s; `types`: its class's type
|
|
408
|
+
parameters, bound to the receiver's type arguments as the module spells them. For a receiver
|
|
409
|
+
typed through an installed class's alias (`npt.NDArray[np.float64]`): `templates`, the class's
|
|
410
|
+
type parameters as templates naming the alias's, and `matched`, the receiver's type as the class
|
|
411
|
+
it stands for, which a method's `self` is matched against (see `signatures.Expansion`).
|
|
412
|
+
"""
|
|
413
|
+
|
|
414
|
+
entry: str
|
|
415
|
+
instance: list[str] | None
|
|
416
|
+
types: dict[str, str]
|
|
417
|
+
templates: Mapping[str, str] = MappingProxyType({})
|
|
418
|
+
matched: str | None = None
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def for_receiver(method: Method, receiver: str) -> Method:
|
|
422
|
+
"""Bind a method's `Self` to `receiver`: a class under the one it was found on has it as its own.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
The method, its `Self` (if it binds one) the receiver's type.
|
|
426
|
+
|
|
427
|
+
"""
|
|
428
|
+
return method._replace(types={**method.types, _SELF: receiver}) if _SELF in method.types else method
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def overloaded_method(receiver: str, name: str, known: Known) -> Method | None:
|
|
432
|
+
"""Find a standard-library class's method whose arguments decide its type (see `method_signatures`).
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
It, or `None` if the receiver isn't such a class or the method isn't such a method.
|
|
436
|
+
|
|
437
|
+
"""
|
|
438
|
+
path: str | None
|
|
439
|
+
args: list[ast.expr]
|
|
440
|
+
path, args = _receiver(receiver, known)
|
|
441
|
+
entry: str | None = None if path is None else _member(_METHOD_OVERLOADS, path, name)
|
|
442
|
+
if path is None or entry is None:
|
|
443
|
+
return None
|
|
444
|
+
texts: list[str] = [ast.unparse(arg) for arg in args]
|
|
445
|
+
builtin: bool = all(
|
|
446
|
+
isinstance(node, ast.Name) and node.id in _BUILTIN_NAMES and known.is_builtin(node.id)
|
|
447
|
+
for arg in args
|
|
448
|
+
for node in ast.walk(arg)
|
|
449
|
+
if isinstance(node, ast.Name | ast.Attribute)
|
|
450
|
+
)
|
|
451
|
+
return Method(entry, texts if args and builtin else None, _bound(path, texts, receiver))
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def operand(annotation: str, known: Known) -> str | None:
|
|
455
|
+
"""Name an operand's type as an operator's `takes` does: a builtin's name, or a class's path.
|
|
456
|
+
|
|
457
|
+
Returns:
|
|
458
|
+
It, or `None` for any other type: a union, a subscript, a class the tables don't have.
|
|
459
|
+
|
|
460
|
+
"""
|
|
461
|
+
root: ast.expr = _parsed(annotation)
|
|
462
|
+
if isinstance(root, ast.Name) and root.id in _BUILTIN_NAMES:
|
|
463
|
+
return root.id if known.is_builtin(root.id) else None
|
|
464
|
+
return _path(root, known) if isinstance(root, ast.Name | ast.Attribute) else None
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def inherits(path: str, ancestor: str) -> bool:
|
|
468
|
+
"""Check whether the class at `path` is under `ancestor`, as far as the tables know its bases.
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
Whether it is.
|
|
472
|
+
|
|
473
|
+
"""
|
|
474
|
+
return any(base == ancestor or inherits(base, ancestor) for base in _ancestors(path))
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def generic_attribute(receiver: str, name: str, known: Known) -> tuple[str, str, dict[str, str]] | None:
|
|
478
|
+
"""Find a class's attribute or property held as a template, on a receiver of a known type.
|
|
479
|
+
|
|
480
|
+
A generic class's own (`string`, on an `re.Match[str]`), or one a class with no type parameters
|
|
481
|
+
has or inherits (`parameters`, on an `inspect.Signature`).
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
Its class's path, its template (see `_GENERIC_ATTRIBUTES`), and the class's type parameters
|
|
485
|
+
bound to the receiver's type arguments as the module spells them; or `None`.
|
|
486
|
+
|
|
487
|
+
"""
|
|
488
|
+
path: str | None
|
|
489
|
+
args: list[ast.expr]
|
|
490
|
+
path, args = _receiver(receiver, known)
|
|
491
|
+
template: str | None = None
|
|
492
|
+
if path is not None:
|
|
493
|
+
template = (
|
|
494
|
+
_GENERIC_ATTRIBUTES.get(path, {}).get(name)
|
|
495
|
+
if path in _TYPE_PARAMETERS
|
|
496
|
+
else _member(_GENERIC_ATTRIBUTES, path, name)
|
|
497
|
+
)
|
|
498
|
+
if path is None or template is None:
|
|
499
|
+
return None
|
|
500
|
+
return path, template, _bound(path, [ast.unparse(arg) for arg in args], receiver)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def element(receiver: str, known: Known) -> tuple[str, str, dict[str, str]] | None:
|
|
504
|
+
"""Find what iterating a standard-library class's instance gives (a `for` loop over a file: its lines).
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
Its class's path, the element's template (see `_ELEMENTS`), and the class's type parameters
|
|
508
|
+
bound to the receiver's type arguments as the module spells them; or `None`.
|
|
509
|
+
|
|
510
|
+
"""
|
|
511
|
+
path: str | None
|
|
512
|
+
args: list[ast.expr]
|
|
513
|
+
path, args = _receiver(receiver, known)
|
|
514
|
+
template: str | None = None if path is None else _ELEMENTS.get(path)
|
|
515
|
+
if path is None or template is None:
|
|
516
|
+
return None
|
|
517
|
+
return path, template, _bound(path, [ast.unparse(arg) for arg in args], receiver)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _receiver(receiver: str, known: Known) -> tuple[str | None, list[ast.expr]]:
|
|
521
|
+
"""Resolve a receiver's annotation to its class's path, and its type arguments (`re.Match[str]`'s `str`).
|
|
522
|
+
|
|
523
|
+
Returns:
|
|
524
|
+
The path (`None` if it's no standard-library class the module can name), and the arguments.
|
|
525
|
+
|
|
526
|
+
"""
|
|
527
|
+
root: ast.expr = _parsed(receiver)
|
|
528
|
+
args: list[ast.expr] = []
|
|
529
|
+
if isinstance(root, ast.Subscript):
|
|
530
|
+
args = list(root.slice.elts) if isinstance(root.slice, ast.Tuple) else [root.slice]
|
|
531
|
+
root = root.value
|
|
532
|
+
return _path(root, known), args
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _bound(path: str, texts: list[str], receiver: str) -> dict[str, str]:
|
|
536
|
+
"""Bind a generic class's type parameters to an instance's type arguments, if it gives them all.
|
|
537
|
+
|
|
538
|
+
And `Self` to `receiver`, the instance's type as the module spells it: not a generic class
|
|
539
|
+
named without its arguments.
|
|
540
|
+
|
|
541
|
+
Returns:
|
|
542
|
+
Each parameter's argument, by name; none if they don't match.
|
|
543
|
+
|
|
544
|
+
"""
|
|
545
|
+
params: list[str] = [param.rstrip("=") for param in _TYPE_PARAMETERS.get(path, "").split(",") if param]
|
|
546
|
+
if len(params) != len(texts):
|
|
547
|
+
return {}
|
|
548
|
+
return {**dict(zip(params, texts, strict=True)), _SELF: receiver}
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def generics(bound: Mapping[str, str]) -> frozenset[str]:
|
|
552
|
+
"""Spell the standard library's generic classes as a module's imports (`bound`, see `origins`) name them.
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
Each spelling (`StreamHandler`, `logging.StreamHandler`): written bare, one is missing its
|
|
556
|
+
type arguments.
|
|
557
|
+
|
|
558
|
+
"""
|
|
559
|
+
return frozenset(
|
|
560
|
+
f"{name}{path.removeprefix(origin)}"
|
|
561
|
+
for name, origin in bound.items()
|
|
562
|
+
for path in _generic_paths(origin)
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def held_whole(path: str) -> str | None:
|
|
567
|
+
"""Find the class at `path` (or the one `path` is another name of), if the tables hold it whole.
|
|
568
|
+
|
|
569
|
+
Returns:
|
|
570
|
+
Its own path; `None` for anything else, a generic class included (see `bases`).
|
|
571
|
+
|
|
572
|
+
"""
|
|
573
|
+
own: str = _ALIASES.get(path, path)
|
|
574
|
+
return own if CLASSES.get(own) == own else None
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def class_argument(node: ast.expr, known: Known) -> str | None:
|
|
578
|
+
"""Resolve a name or dotted path passed as an argument (`csv.Error`, `sqlite.OperationalError`).
|
|
579
|
+
|
|
580
|
+
Returns:
|
|
581
|
+
The standard-library class's own path, if the tables hold it whole and the module imports the
|
|
582
|
+
name it starts with; `None` otherwise.
|
|
583
|
+
|
|
584
|
+
"""
|
|
585
|
+
path: str | None = resolved(node, known.names.stdlib)
|
|
586
|
+
return None if path is None else held_whole(path)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def defines_class(path: str) -> bool:
|
|
590
|
+
"""Check whether `path` is a class the tables know: a plain one, or a generic one.
|
|
591
|
+
|
|
592
|
+
Returns:
|
|
593
|
+
Whether it is (`decimal.Decimal`, `operator.itemgetter`; not `os.path`, nor `logging.getLogger`).
|
|
594
|
+
|
|
595
|
+
"""
|
|
596
|
+
return CLASSES.get(path) == path or path in _TYPE_PARAMETERS
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def needs_arguments(path: str) -> bool:
|
|
600
|
+
"""Check whether `path` is a generic class of the standard library's that is missing arguments, bare.
|
|
601
|
+
|
|
602
|
+
Returns:
|
|
603
|
+
Whether it is (`operator.itemgetter`; not `io.BufferedReader`, whose parameter has a default).
|
|
604
|
+
|
|
605
|
+
"""
|
|
606
|
+
return path in _generic_paths(path)
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
@lru_cache(maxsize=1024)
|
|
610
|
+
def _generic_paths(origin: str) -> tuple[str, ...]:
|
|
611
|
+
"""Find the generic classes an import's origin names (itself, or those in its module) that need arguments.
|
|
612
|
+
|
|
613
|
+
Returns:
|
|
614
|
+
Their paths.
|
|
615
|
+
|
|
616
|
+
"""
|
|
617
|
+
return tuple(
|
|
618
|
+
path
|
|
619
|
+
for path, params in _TYPE_PARAMETERS.items()
|
|
620
|
+
if (path == origin or path.startswith(f"{origin}."))
|
|
621
|
+
and not all(param.endswith("=") for param in params.split(","))
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def evaluable(annotation: str, known: Known) -> bool:
|
|
626
|
+
"""Check that an annotation subscripts no standard-library class that can't be at run time.
|
|
627
|
+
|
|
628
|
+
Nor a builtin iterator that can't (`zip[tuple[int, str]]`).
|
|
629
|
+
|
|
630
|
+
Returns:
|
|
631
|
+
Whether it can be evaluated (as a module's annotations are) without that `TypeError`.
|
|
632
|
+
|
|
633
|
+
"""
|
|
634
|
+
node: ast.AST
|
|
635
|
+
for node in ast.walk(_parsed(annotation)):
|
|
636
|
+
if not isinstance(node, ast.Subscript):
|
|
637
|
+
continue
|
|
638
|
+
if isinstance(node.value, ast.Name) and node.value.id in _UNSUBSCRIPTABLE:
|
|
639
|
+
return False
|
|
640
|
+
path: str | None = _path(node.value, known)
|
|
641
|
+
if path in _TYPE_PARAMETERS and path not in _SUBSCRIPTABLE:
|
|
642
|
+
return False
|
|
643
|
+
return True
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def joins_path(left: str, right: str | None, known: Known) -> bool:
|
|
647
|
+
"""Check whether `left / right` joins a path: a `pathlib` class's `/` gives its own class back.
|
|
648
|
+
|
|
649
|
+
Returns:
|
|
650
|
+
Whether `left` is a `pathlib` path class's annotation, and `right` a `str`'s or the same.
|
|
651
|
+
|
|
652
|
+
"""
|
|
653
|
+
return right in {"str", left} and is_path(left, known)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def is_path(receiver: str, known: Known) -> bool:
|
|
657
|
+
"""Check whether `receiver` is a `pathlib` path class's annotation.
|
|
658
|
+
|
|
659
|
+
Returns:
|
|
660
|
+
Whether it is.
|
|
661
|
+
|
|
662
|
+
"""
|
|
663
|
+
return _class_path(receiver, known) in _PATHS
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def _class_path(receiver: str, known: Known) -> str | None:
|
|
667
|
+
"""Resolve a receiver's annotation to a class's path in the tables (its own, not an alias).
|
|
668
|
+
|
|
669
|
+
Returns:
|
|
670
|
+
The path, or `None` if the annotation doesn't name a standard-library class the module imports
|
|
671
|
+
(or `--fix` is importing).
|
|
672
|
+
|
|
673
|
+
"""
|
|
674
|
+
return _path(_parsed(receiver), known)
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def _path(root: ast.expr, known: Known) -> str | None:
|
|
678
|
+
"""Resolve a class's name or dotted path to its path in the tables (see `_class_path`).
|
|
679
|
+
|
|
680
|
+
Returns:
|
|
681
|
+
The path, or `None`.
|
|
682
|
+
|
|
683
|
+
"""
|
|
684
|
+
path: str | None = resolved(root, known.names.stdlib)
|
|
685
|
+
plan: ImportPlan | None = known.names.plan
|
|
686
|
+
if path is None and plan is not None and plan.added:
|
|
687
|
+
path = resolved(root, _added(tuple(plan.added.values())))
|
|
688
|
+
if path is None and plan is not None and plan.guarded:
|
|
689
|
+
path = _guarded(root, plan)
|
|
690
|
+
if path is None and plan is not None and plan.checking.bound:
|
|
691
|
+
path = resolved(root, plan.checking.bound)
|
|
692
|
+
# A class's own path, where the module binds no name it starts with: a library base out of its
|
|
693
|
+
# sight, as the index names it (see `constricter.fix.index.beyond`).
|
|
694
|
+
if path is None and isinstance(root, ast.Attribute): # a dotted path alone can be a class's own
|
|
695
|
+
written: str = ast.unparse(root)
|
|
696
|
+
if held_whole(written) and (plan is None or written.partition(".")[0] not in plan.taken):
|
|
697
|
+
path = written
|
|
698
|
+
return None if path is None else _ALIASES.get(path, path)
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def _guarded(root: ast.expr, plan: ImportPlan) -> str | None:
|
|
702
|
+
"""Resolve a class's name or dotted path through an import for type checking (`ImportPlan.guarded`).
|
|
703
|
+
|
|
704
|
+
One the module has, or `--fix` is adding for another file's type: the next run would resolve it
|
|
705
|
+
by the import, so this one does.
|
|
706
|
+
|
|
707
|
+
Returns:
|
|
708
|
+
Its dotted origin, or `None` if its first name isn't such an import's.
|
|
709
|
+
|
|
710
|
+
"""
|
|
711
|
+
first: str | None = next(
|
|
712
|
+
(node.id for node in ast.walk(root) if isinstance(node, ast.Name) and node.id in plan.guarded),
|
|
713
|
+
None,
|
|
714
|
+
)
|
|
715
|
+
if first is None:
|
|
716
|
+
return None
|
|
717
|
+
return resolved(root, {first: ".".join(part for part in plan.guarded[first].origin if part)})
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
@lru_cache(maxsize=256)
|
|
721
|
+
def _added(statements: tuple[str, ...]) -> Mapping[str, str]:
|
|
722
|
+
"""Map the names the imports `--fix` is adding bind (see `imported`), read once for all its lookups.
|
|
723
|
+
|
|
724
|
+
Returns:
|
|
725
|
+
Each bound name, mapped to its dotted origin: shared, so only read it.
|
|
726
|
+
|
|
727
|
+
"""
|
|
728
|
+
return imported(ast.parse("\n".join(statements)).body)
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
@lru_cache(maxsize=4096)
|
|
732
|
+
def _parsed(annotation: str) -> ast.expr:
|
|
733
|
+
"""Parse an annotation, once for all its lookups.
|
|
734
|
+
|
|
735
|
+
`annotation` is always `ast.unparse`'s own output, so it's always valid Python. The tree is
|
|
736
|
+
shared: only read it.
|
|
737
|
+
|
|
738
|
+
Returns:
|
|
739
|
+
Its expression.
|
|
740
|
+
|
|
741
|
+
"""
|
|
742
|
+
return ast.parse(annotation, mode="eval").body
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def is_class(annotation: str) -> bool:
|
|
746
|
+
"""Check whether a table's annotation is a class's dotted path (`io.BytesIO`), not a builtin one.
|
|
747
|
+
|
|
748
|
+
Returns:
|
|
749
|
+
Whether it is.
|
|
750
|
+
|
|
751
|
+
"""
|
|
752
|
+
return _DOT in annotation and all(part.isidentifier() for part in annotation.split(_DOT))
|