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
|
Binary file
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Cross-module `--fix` for a classmethod or staticmethod called on its class: `Row.make()`.
|
|
3
|
+
|
|
4
|
+
A class another checked file defines, found through the calling file's imports and the re-exports
|
|
5
|
+
between them (`MultiIndex.from_arrays(...)`, `pd.MultiIndex.from_arrays(...)`); the method's declared
|
|
6
|
+
return types the call where the file can write it (see `project.portable`). And one of the file's
|
|
7
|
+
own classes that takes the method from such a base (`Sub.make()`): a `Self` return is that class.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from collections.abc import Callable, Iterator, Mapping
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Final
|
|
13
|
+
|
|
14
|
+
from constricter.fix.core.known import Guarded, Origin
|
|
15
|
+
from constricter.fix.index import project
|
|
16
|
+
from constricter.fix.index.modules import SUFFIX, Index, Module, module_name
|
|
17
|
+
|
|
18
|
+
_DOT: Final = "."
|
|
19
|
+
_DEPTH: Final = 8 # how many of the file's own classes a line of bases is followed through
|
|
20
|
+
_HOPS: Final = 5 # how many re-exports a call is followed through
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def calls(
|
|
24
|
+
catalog: Index,
|
|
25
|
+
path: Path,
|
|
26
|
+
guarded: Mapping[str, Guarded],
|
|
27
|
+
) -> tuple[dict[str, str], dict[str, Guarded]]:
|
|
28
|
+
"""Type the calls the file at `path` makes to other checked files' classes' class-side methods.
|
|
29
|
+
|
|
30
|
+
`guarded`: the names its other imported types need imported for type checking (see `Guarded`).
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Each call's name as written (`Row.make`, `m.Row.make`), mapped to its type; and `guarded`,
|
|
34
|
+
with the names these types need too. No call for a file `catalog` doesn't have.
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
needed: dict[str, Guarded] = dict(guarded)
|
|
38
|
+
found: dict[str, str] = {}
|
|
39
|
+
target: Module | None
|
|
40
|
+
if path.suffix != SUFFIX or (target := catalog.modules.get(module_name(path))) is None:
|
|
41
|
+
return found, needed
|
|
42
|
+
spelled: str
|
|
43
|
+
for spelled in target.called:
|
|
44
|
+
receiver: str
|
|
45
|
+
method: str
|
|
46
|
+
receiver, _, method = spelled.rpartition(_DOT)
|
|
47
|
+
base: str | None = _base(target, receiver, method, _DEPTH)
|
|
48
|
+
defined: tuple[Module, str] | None = _class_named(catalog.modules, target, base or receiver)
|
|
49
|
+
if defined is None or method not in defined[0].sides.get(defined[1], {}):
|
|
50
|
+
continue
|
|
51
|
+
if base is not None and method in defined[0].selfish.get(defined[1], ()):
|
|
52
|
+
found[spelled] = receiver # its `Self`: the file's own class
|
|
53
|
+
continue
|
|
54
|
+
declared: dict[str, str] = {spelled: defined[0].sides[defined[1]][method]}
|
|
55
|
+
found.update(project.portable(catalog.modules, (target, defined), base or receiver, declared, needed))
|
|
56
|
+
return found, needed
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _base(target: Module, receiver: str, method: str, depth: int) -> str | None:
|
|
60
|
+
"""Find the base another module defines that one of `target`'s own classes takes `method` from.
|
|
61
|
+
|
|
62
|
+
Its first base, through the file's own classes, that the file doesn't define: `None` for a
|
|
63
|
+
class that isn't the file's, one that binds `method` itself, or a name the file binds as a
|
|
64
|
+
value too.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
The base, as the file writes it.
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
if (
|
|
71
|
+
receiver not in target.bases
|
|
72
|
+
or receiver in target.shadowed
|
|
73
|
+
or method in target.bound.get(receiver, frozenset[str]())
|
|
74
|
+
):
|
|
75
|
+
return None
|
|
76
|
+
written: str
|
|
77
|
+
for written in target.bases[receiver] if depth else ():
|
|
78
|
+
if written not in target.bases:
|
|
79
|
+
return written
|
|
80
|
+
if method in target.bound.get(written, frozenset[str]()):
|
|
81
|
+
return None # the file's own: its tables have it
|
|
82
|
+
found: str | None
|
|
83
|
+
if (found := _base(target, written, method, depth - 1)) is not None:
|
|
84
|
+
return found
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _class_named(modules: Mapping[str, Module], target: Module, spelled: str) -> tuple[Module, str] | None:
|
|
89
|
+
"""Find the class another module defines that `target` writes as `spelled` (`Row`, `m.Row`, `pkg.Row`).
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
The module defining it and its name there, through imports and re-exports; or `None`.
|
|
93
|
+
|
|
94
|
+
"""
|
|
95
|
+
origin: Origin | None = named(target, spelled)
|
|
96
|
+
return None if origin is None else project.definition(modules, origin, project.CLASS)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def named(target: Module, spelled: str) -> Origin | None:
|
|
100
|
+
"""Resolve what `target` writes as `spelled` (`Row`, `m.Row`, `pkg.m.load`) through its imports.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
The module it's an attribute of, and its name there; `None` for a name the file doesn't
|
|
104
|
+
import, binds as a value too, or imports from itself.
|
|
105
|
+
|
|
106
|
+
"""
|
|
107
|
+
first: str
|
|
108
|
+
rest: str
|
|
109
|
+
first, _, rest = spelled.partition(_DOT)
|
|
110
|
+
origin: Origin | None = target.names.get(first)
|
|
111
|
+
if origin is None or origin[0] == target.name or first in target.shadowed:
|
|
112
|
+
return None
|
|
113
|
+
path: list[str] = [origin[0], *([origin[1]] if origin[1] else []), *(rest.split(_DOT) if rest else [])]
|
|
114
|
+
return _DOT.join(path[:-1]), path[-1]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def called(
|
|
118
|
+
modules: Mapping[str, Module],
|
|
119
|
+
target: Module,
|
|
120
|
+
has: Callable[[Module, str], bool],
|
|
121
|
+
) -> Iterator[tuple[str, tuple[Module, str]]]:
|
|
122
|
+
"""Find the calls `target` makes to what another module defines, as `has` says of a module's name.
|
|
123
|
+
|
|
124
|
+
Through its imports, and the re-exports between the modules (`_HOPS` of them).
|
|
125
|
+
|
|
126
|
+
Yields:
|
|
127
|
+
Each call's name as written (`load`, `client.load`), and the module defining what it calls
|
|
128
|
+
and its name there.
|
|
129
|
+
|
|
130
|
+
"""
|
|
131
|
+
spelled: str
|
|
132
|
+
for spelled in target.called:
|
|
133
|
+
origin: Origin | None = named(target, spelled)
|
|
134
|
+
defined: tuple[Module, str] | None = (
|
|
135
|
+
None if origin is None else _followed(modules, origin, has, _HOPS)
|
|
136
|
+
)
|
|
137
|
+
if defined is not None:
|
|
138
|
+
yield spelled, defined
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _followed(
|
|
142
|
+
modules: Mapping[str, Module],
|
|
143
|
+
origin: Origin,
|
|
144
|
+
has: Callable[[Module, str], bool],
|
|
145
|
+
hops: int,
|
|
146
|
+
) -> tuple[Module, str] | None:
|
|
147
|
+
"""Follow `origin` (through re-exports) to the module that `has` it.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
That module and the name, or `None`.
|
|
151
|
+
|
|
152
|
+
"""
|
|
153
|
+
module: Module | None = modules.get(origin[0])
|
|
154
|
+
name: str | None = origin[1]
|
|
155
|
+
if module is None or name is None or not hops:
|
|
156
|
+
return None
|
|
157
|
+
if has(module, name):
|
|
158
|
+
return module, name
|
|
159
|
+
onward: Origin | None = module.names.get(name)
|
|
160
|
+
return _followed(modules, onward, has, hops - 1) if onward and onward[0] != module.name else None
|
|
Binary file
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""The strings an annotation leaves inside it (an `Annotated`'s metadata), and the names in them."""
|
|
3
|
+
|
|
4
|
+
import ast
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
from typing import Final
|
|
7
|
+
|
|
8
|
+
from constricter.rules.annotations import node_name
|
|
9
|
+
|
|
10
|
+
_LITERAL: Final = "Literal"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@lru_cache(maxsize=4096)
|
|
14
|
+
def quoted(annotation: str) -> frozenset[str]:
|
|
15
|
+
"""Find the names in the strings left inside an annotation: `meta` in `Annotated[int, 'meta']`.
|
|
16
|
+
|
|
17
|
+
A quoted type is read as its text before it gets here (see `annotations.written`). Not a
|
|
18
|
+
`Literal`'s strings, nor a whole annotation's own quotes (see `roots`).
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
The names.
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
tree: ast.expr = ast.parse(annotation, mode="eval").body
|
|
25
|
+
found: set[str] = set()
|
|
26
|
+
waiting: list[ast.AST | None] = [None, tree] # `None` at its bottom ends it
|
|
27
|
+
node: ast.AST
|
|
28
|
+
for node in iter(waiting.pop, None):
|
|
29
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str) and node is not tree:
|
|
30
|
+
found.update(_names_in(node.value))
|
|
31
|
+
elif not (isinstance(node, ast.Subscript) and node_name(node.value) == _LITERAL):
|
|
32
|
+
waiting.extend(ast.iter_child_nodes(node))
|
|
33
|
+
return frozenset(found)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _names_in(text: str) -> set[str]:
|
|
37
|
+
"""Read the names a quoted part of an annotation is written with.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Them; none if it isn't an expression.
|
|
41
|
+
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
return {node.id for node in ast.walk(ast.parse(text, mode="eval")) if isinstance(node, ast.Name)}
|
|
45
|
+
except SyntaxError:
|
|
46
|
+
return set()
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""What an installed package's annotations are made of, and which arguments each part takes.
|
|
3
|
+
|
|
4
|
+
An annotation is read (by `constricter.fix.index.stubs.signatures`) into its union's members (`Atom`s):
|
|
5
|
+
`None`, a `Literal`'s values, a type variable (with its bound's members), a `type[...]`, a class, or something
|
|
6
|
+
unread. Each takes a builtin scalar or container argument, or a class passed as one, by the verdicts
|
|
7
|
+
here: a standard-library class by the `scalars` table, an installed protocol by its members, and no
|
|
8
|
+
other installed class.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import ast
|
|
12
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
13
|
+
from functools import lru_cache
|
|
14
|
+
from types import EllipsisType
|
|
15
|
+
from typing import Final, NamedTuple, TypeAlias
|
|
16
|
+
|
|
17
|
+
from constricter.fix.core.known import Origin
|
|
18
|
+
from constricter.fix.core.signatures import Constant
|
|
19
|
+
from constricter.fix.index.modules import Module
|
|
20
|
+
from constricter.fix.index.stubs.declared import Alias, Declarations, Protocol, Variable
|
|
21
|
+
from constricter.fix.libraries import stdlib
|
|
22
|
+
from constricter.fix.libraries.overloads import CONTAINERS, SCALARS
|
|
23
|
+
|
|
24
|
+
YES: Final = "y"
|
|
25
|
+
NO: Final = "n"
|
|
26
|
+
MAYBE: Final = "?"
|
|
27
|
+
NONE: Final = "None"
|
|
28
|
+
LITERAL_STRING: Final = "LiteralString"
|
|
29
|
+
VAR: Final = "var"
|
|
30
|
+
TYPE: Final = "type"
|
|
31
|
+
CLASS: Final = "class"
|
|
32
|
+
UNKNOWN_ATOM: Final = "unknown"
|
|
33
|
+
STR: Final = "str" # `project.definition`'s kind for a function `Declarations` has
|
|
34
|
+
BUILTINS_MODULE: Final = "builtins"
|
|
35
|
+
TYPING: Final = frozenset({"typing", "typing_extensions"})
|
|
36
|
+
ANYTHING: Final = frozenset(
|
|
37
|
+
{"typing.Any", "typing_extensions.Any", "_typeshed.Incomplete", "builtins.object"},
|
|
38
|
+
)
|
|
39
|
+
TYPES: Final = frozenset({"builtins.type", "typing.Type", "typing_extensions.Type"})
|
|
40
|
+
REFUSING: Final = frozenset({"Callable", "Type"}) # no scalar is a callable or a class
|
|
41
|
+
PARAM_SPEC: Final = "*" # a `ParamSpec`'s or `TypeVarTuple`'s bound (see `declared`)
|
|
42
|
+
BUILTIN_TYPES: Final = (*SCALARS, *CONTAINERS) # the `scalars` table's columns
|
|
43
|
+
ELEMENT_OF: Final = frozenset({"list", "set", "frozenset"}) # one type argument: their elements'
|
|
44
|
+
TUPLE: Final = "tuple"
|
|
45
|
+
HOMOGENEOUS: Final = 2 # `tuple[int, ...]`'s arguments
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Scope(NamedTuple):
|
|
49
|
+
"""Where an annotation is read: its module, and the type parameters in scope.
|
|
50
|
+
|
|
51
|
+
Each parameter is its bound's text (`None`: unbounded; `"*"`: a `ParamSpec`'s), or an
|
|
52
|
+
argument an alias was subscripted with: its annotation and the scope that's read in.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
module: Module
|
|
56
|
+
params: Mapping[str, "str | tuple[ast.expr, Scope] | None"]
|
|
57
|
+
owner: str # the module whose function is read: only its type variables are bound
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Atom(NamedTuple):
|
|
61
|
+
"""One member of an annotation's union.
|
|
62
|
+
|
|
63
|
+
`kind`: `none`, `literal` (`values`), `any`, `var` (a type variable `name`, bounded by `bound`
|
|
64
|
+
read in `scope`), `type` (a `type[...]`, whose own atoms are `inner`), `class` (`origin`, and
|
|
65
|
+
the installed module defining it, if one does), or `unknown`.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
kind: str
|
|
69
|
+
values: tuple[Constant, ...] = ()
|
|
70
|
+
name: str = ""
|
|
71
|
+
bound: str | None = None
|
|
72
|
+
scope: Scope | None = None
|
|
73
|
+
inner: tuple["Atom", ...] = ()
|
|
74
|
+
origin: Origin = ("", None)
|
|
75
|
+
module: Module | None = None
|
|
76
|
+
subscripted: bool = False
|
|
77
|
+
args: tuple[ast.expr, ...] = () # a class's type arguments, read in `scope`
|
|
78
|
+
constrained: bool = False # a type variable's `bound` is its constraints: it binds none of them
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
UNKNOWN: Final = Atom(UNKNOWN_ATOM)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# Whether an atom takes a container argument, and what that container's elements must fit, if it says.
|
|
85
|
+
Taken: TypeAlias = tuple[str, tuple[Atom, ...] | None]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def atom_path(atom: Atom) -> str:
|
|
89
|
+
"""Name a class atom by its dotted path (`typing.SupportsIndex`).
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
It.
|
|
93
|
+
|
|
94
|
+
"""
|
|
95
|
+
return f"{atom.origin[0]}.{atom.origin[1]}"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def owned(atom: Atom, owner: str) -> bool:
|
|
99
|
+
"""Check that a type variable is the function's own: one of its type parameters, or its module's.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Whether it is.
|
|
103
|
+
|
|
104
|
+
"""
|
|
105
|
+
return atom.scope is not None and atom.scope.module.name == owner
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def argument_of(atom: Atom) -> tuple[ast.expr, Scope]:
|
|
109
|
+
"""Read back the argument an alias's parameter was bound to (an `arg` atom).
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Its annotation, and the scope it's read in.
|
|
113
|
+
|
|
114
|
+
"""
|
|
115
|
+
scope: Scope | None = atom.scope
|
|
116
|
+
assert scope is not None # an `arg` atom always has one # ruff: ignore[assert]
|
|
117
|
+
return parse_text(atom.name), scope
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def bound_alias(alias: Alias, where: Scope, args: Sequence[ast.expr], caller: Scope) -> Scope:
|
|
121
|
+
"""Scope an alias's value: its type parameters bound to the arguments it's subscripted with.
|
|
122
|
+
|
|
123
|
+
Its own (`type X[T] = ...`), or else the type variables its value names, in order; one without
|
|
124
|
+
an argument stays unbound (any type).
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
The scope.
|
|
128
|
+
|
|
129
|
+
"""
|
|
130
|
+
params: tuple[str, ...] = alias.params or free_variables(alias.value, where)
|
|
131
|
+
bound: dict[str, str | tuple[ast.expr, Scope] | None] = dict.fromkeys(params)
|
|
132
|
+
name: str
|
|
133
|
+
arg: ast.expr
|
|
134
|
+
for name, arg in zip(params, args, strict=False):
|
|
135
|
+
bound[name] = (arg, caller)
|
|
136
|
+
return Scope(where.module, bound, caller.owner)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def free_variables(value: str, scope: Scope) -> tuple[str, ...]:
|
|
140
|
+
"""Name the type variables an old-style alias's value names (its parameters), in order.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Them.
|
|
144
|
+
|
|
145
|
+
"""
|
|
146
|
+
declared: Declarations | None = scope.module.typing
|
|
147
|
+
variables: Mapping[str, Variable] = {} if declared is None else declared.variables
|
|
148
|
+
return tuple(
|
|
149
|
+
dict.fromkeys(
|
|
150
|
+
node.id
|
|
151
|
+
for node in ast.walk(parse_text(value))
|
|
152
|
+
if isinstance(node, ast.Name) and (node.id in variables or node.id in scope.module.type_vars)
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def verdict_of(atoms: Sequence[Atom], scalar: str, *, constant: bool) -> str:
|
|
158
|
+
"""Decide whether an annotation's atoms take an argument of type `scalar` (a literal one if `constant`).
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
`y` if one certainly does, `n` if none does, else `?`.
|
|
162
|
+
|
|
163
|
+
"""
|
|
164
|
+
return combined({scalar_verdict(atom, scalar, constant=constant) for atom in atoms})
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def combined(verdicts: set[str]) -> str:
|
|
168
|
+
"""Combine the verdicts of a union's members: `y` if one takes it, `n` if none does, else `?`.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
The verdict.
|
|
172
|
+
|
|
173
|
+
"""
|
|
174
|
+
if YES in verdicts:
|
|
175
|
+
return YES
|
|
176
|
+
return NO if verdicts == {NO} else MAYBE
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def scalar_verdict(atom: Atom, scalar: str, *, constant: bool) -> str:
|
|
180
|
+
"""Decide whether one atom takes an argument of type `scalar`.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
The verdict.
|
|
184
|
+
|
|
185
|
+
"""
|
|
186
|
+
match atom.kind:
|
|
187
|
+
case "none":
|
|
188
|
+
return YES if scalar == NONE else NO
|
|
189
|
+
case "literal":
|
|
190
|
+
types: set[str] = {type(value).__name__ if value is not None else NONE for value in atom.values}
|
|
191
|
+
matches: bool = scalar in types or (scalar == LITERAL_STRING and STR in types)
|
|
192
|
+
return NO if constant or not matches else MAYBE
|
|
193
|
+
case "var":
|
|
194
|
+
return bound_verdict(atom, scalar)
|
|
195
|
+
case "type":
|
|
196
|
+
return NO
|
|
197
|
+
case "class":
|
|
198
|
+
return class_takes(atom, scalar)
|
|
199
|
+
case _:
|
|
200
|
+
return MAYBE
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def bound_verdict(atom: Atom, scalar: str) -> str:
|
|
204
|
+
"""Decide whether a type variable takes an argument of type `scalar`, by its bound (its `inner` atoms).
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
The verdict (`y` without one).
|
|
208
|
+
|
|
209
|
+
"""
|
|
210
|
+
if atom.bound is None:
|
|
211
|
+
return YES
|
|
212
|
+
return MAYBE if atom.bound == PARAM_SPEC else verdict_of(atom.inner, scalar, constant=False)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def class_takes(atom: Atom, scalar: str) -> str:
|
|
216
|
+
"""Decide whether a class takes an argument of type `scalar`.
|
|
217
|
+
|
|
218
|
+
A standard-library one by the `scalars` table; an installed protocol by its members (a generic
|
|
219
|
+
one's parameters unread); no other installed class.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
The verdict.
|
|
223
|
+
|
|
224
|
+
"""
|
|
225
|
+
path: str = atom_path(atom)
|
|
226
|
+
if atom.module is None:
|
|
227
|
+
return external_takes(path, scalar)
|
|
228
|
+
declared: Declarations | None = atom.module.typing
|
|
229
|
+
protocol: Protocol | None = None if declared is None else declared.protocols.get(path.rpartition(".")[2])
|
|
230
|
+
members: frozenset[str] | None = stdlib.scalar_members(scalar)
|
|
231
|
+
if protocol is None or (members is not None and not protocol.members <= members):
|
|
232
|
+
return NO
|
|
233
|
+
return (
|
|
234
|
+
MAYBE
|
|
235
|
+
if members is None or atom.subscripted or path.rpartition(".")[2] in atom.module.generics
|
|
236
|
+
else YES
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def element_of(container: str, args: Sequence[ast.expr]) -> ast.expr | None:
|
|
241
|
+
"""Find what a builtin container's type arguments say its elements are (`list[int]`, `tuple[int, ...]`).
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Their annotation, or `None` for any other (`tuple[int, str]`, a `dict`'s, none at all).
|
|
245
|
+
|
|
246
|
+
"""
|
|
247
|
+
if container in ELEMENT_OF and len(args) == 1:
|
|
248
|
+
return args[0]
|
|
249
|
+
ellipsis: bool = (
|
|
250
|
+
len(args) == HOMOGENEOUS
|
|
251
|
+
and isinstance(args[1], ast.Constant)
|
|
252
|
+
and isinstance(args[1].value, EllipsisType)
|
|
253
|
+
)
|
|
254
|
+
return args[0] if container == TUPLE and ellipsis else None
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@lru_cache(maxsize=8192) # asked of each parameter's classes, for each scalar
|
|
258
|
+
def external_takes(path: str, scalar: str) -> str:
|
|
259
|
+
"""Decide whether a class outside the installed packages (`typing.SupportsIndex`) takes a `scalar`.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
The verdict: the `scalars` table's, or `?` where it hasn't one.
|
|
263
|
+
|
|
264
|
+
"""
|
|
265
|
+
module: str
|
|
266
|
+
name: str
|
|
267
|
+
module, _, name = path.rpartition(".")
|
|
268
|
+
verdicts: str | None = stdlib.scalar_verdicts(path)
|
|
269
|
+
if path in ANYTHING:
|
|
270
|
+
verdicts = YES * len(BUILTIN_TYPES)
|
|
271
|
+
elif module in TYPING and name == LITERAL_STRING:
|
|
272
|
+
verdicts = "".join(YES if each == LITERAL_STRING else NO for each in BUILTIN_TYPES)
|
|
273
|
+
elif module in TYPING and name in REFUSING:
|
|
274
|
+
verdicts = NO * len(BUILTIN_TYPES)
|
|
275
|
+
return MAYBE if verdicts is None else verdicts[BUILTIN_TYPES.index(scalar)]
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def class_verdict(atom: Atom) -> str:
|
|
279
|
+
"""Decide whether one atom takes a class passed as an argument (`dtype=np.float64`).
|
|
280
|
+
|
|
281
|
+
A class is an instance of `type` alone: `type[...]`, `type` and `object` take it; `None`, a
|
|
282
|
+
literal, any other class, or a protocol with an attribute or property (which a class has as a
|
|
283
|
+
descriptor, not a value) don't.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
The verdict.
|
|
287
|
+
|
|
288
|
+
"""
|
|
289
|
+
found: str = MAYBE
|
|
290
|
+
match atom.kind:
|
|
291
|
+
case "none" | "literal":
|
|
292
|
+
found = NO
|
|
293
|
+
case "type": # a `type[T]` takes any class (bounds unchecked: a call outside one is an error)
|
|
294
|
+
found = YES if all(inner.kind == VAR or is_anything(inner) for inner in atom.inner) else MAYBE
|
|
295
|
+
case "var" if atom.bound is None:
|
|
296
|
+
found = YES
|
|
297
|
+
case "var" if atom.bound != PARAM_SPEC:
|
|
298
|
+
found = combined({class_verdict(inner) for inner in atom.inner})
|
|
299
|
+
case "class":
|
|
300
|
+
found = class_takes_class(atom)
|
|
301
|
+
case _:
|
|
302
|
+
pass
|
|
303
|
+
return found
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def is_anything(atom: Atom) -> bool:
|
|
307
|
+
"""Check whether an atom is `Any`, `object` or `typeshed`'s `Incomplete`.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
Whether it is.
|
|
311
|
+
|
|
312
|
+
"""
|
|
313
|
+
return atom.kind == CLASS and atom_path(atom) in ANYTHING
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def class_takes_class(atom: Atom) -> str:
|
|
317
|
+
"""Decide whether a class atom takes a class passed as an argument (see `class_verdict`).
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
The verdict.
|
|
321
|
+
|
|
322
|
+
"""
|
|
323
|
+
origin: Origin = atom.origin
|
|
324
|
+
if atom_path(atom) in ANYTHING | TYPES:
|
|
325
|
+
return YES
|
|
326
|
+
if atom.module is None:
|
|
327
|
+
return NO if origin[0] == BUILTINS_MODULE else MAYBE
|
|
328
|
+
declared: Declarations | None = atom.module.typing
|
|
329
|
+
protocol: Protocol | None = None if declared is None else declared.protocols.get(origin[1] or "")
|
|
330
|
+
return NO if protocol is None or protocol.properties else MAYBE
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def scalar_binds(atoms: Sequence[Atom], scalar: str, owner: str) -> tuple[str, str] | None:
|
|
334
|
+
"""Find the type variable an argument of type `scalar` binds: the one member that takes it.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Its name, and the argument's type (a literal's widened to `str`); or `None`.
|
|
338
|
+
|
|
339
|
+
"""
|
|
340
|
+
taking: list[Atom] = [atom for atom in atoms if scalar_verdict(atom, scalar, constant=False) != NO]
|
|
341
|
+
if len(taking) != 1 or taking[0].kind != VAR or taking[0].constrained or not owned(taking[0], owner):
|
|
342
|
+
return None
|
|
343
|
+
if scalar_verdict(taking[0], scalar, constant=False) != YES:
|
|
344
|
+
return None
|
|
345
|
+
return taking[0].name, STR if scalar == LITERAL_STRING else scalar
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def class_binds(atoms: Sequence[Atom], owner: str) -> str | None:
|
|
349
|
+
"""Find the type variable a class argument binds: a `type[T]`, all else refusing a class.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
Its name, or `None`.
|
|
353
|
+
|
|
354
|
+
"""
|
|
355
|
+
types: list[Atom] = [atom for atom in atoms if atom.kind == TYPE]
|
|
356
|
+
if len(types) != 1 or any(class_verdict(atom) != NO for atom in atoms if atom.kind != TYPE):
|
|
357
|
+
return None
|
|
358
|
+
inner: tuple[Atom, ...] = types[0].inner
|
|
359
|
+
variable: Atom | None = inner[0] if len(inner) == 1 and inner[0].kind == VAR else None
|
|
360
|
+
return None if variable is None or variable.constrained or not owned(variable, owner) else variable.name
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def class_bounded(atoms: Sequence[Atom]) -> bool:
|
|
364
|
+
"""Check whether the type variable a class argument binds (see `class_binds`) is bounded.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Whether a `type[T]` among `atoms` has a `T` with a bound.
|
|
368
|
+
|
|
369
|
+
"""
|
|
370
|
+
return any(
|
|
371
|
+
inner.kind == VAR and inner.bound not in {None, PARAM_SPEC}
|
|
372
|
+
for atom in atoms
|
|
373
|
+
if atom.kind == TYPE
|
|
374
|
+
for inner in atom.inner
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def literal_values(args: Sequence[ast.expr]) -> Iterator[Constant]:
|
|
379
|
+
"""Read a `Literal[...]`'s values (a name, which isn't one, left out).
|
|
380
|
+
|
|
381
|
+
Yields:
|
|
382
|
+
Each.
|
|
383
|
+
|
|
384
|
+
"""
|
|
385
|
+
arg: ast.expr
|
|
386
|
+
for arg in args:
|
|
387
|
+
if isinstance(arg, ast.Constant) and isinstance(
|
|
388
|
+
arg.value,
|
|
389
|
+
bool | int | float | complex | str | bytes | None,
|
|
390
|
+
):
|
|
391
|
+
yield arg.value
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def joined(separator: str, parts: Sequence[str | None]) -> str | None:
|
|
395
|
+
"""Join a template's parts, none of which may be missing.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
The text, or `None` if a part is.
|
|
399
|
+
|
|
400
|
+
"""
|
|
401
|
+
return None if None in parts else separator.join(part for part in parts if part is not None)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
@lru_cache(maxsize=8192) # a package's signatures name the same few thousand types over and over
|
|
405
|
+
def parse_text(text: str) -> ast.expr:
|
|
406
|
+
"""Parse an annotation's text; one that doesn't parse (`"int["`) names nothing.
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
Its tree, shared by everyone who asks: to read, never to change.
|
|
410
|
+
|
|
411
|
+
"""
|
|
412
|
+
try:
|
|
413
|
+
return ast.parse(text, mode="eval").body
|
|
414
|
+
except SyntaxError:
|
|
415
|
+
return ast.Constant(value=...) # read as nothing it names
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def private_name(name: str | None) -> bool:
|
|
419
|
+
"""Check whether a dotted name (`numpy._typing`) has a private part, or is missing.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
Whether it has, or is.
|
|
423
|
+
|
|
424
|
+
"""
|
|
425
|
+
return name is None or any(part.startswith("_") for part in name.split("."))
|
|
Binary file
|