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,730 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""What a module declares, for `--fix`.
|
|
3
|
+
|
|
4
|
+
Its functions' and methods' return types, its classes' attributes, its type variables, and the
|
|
5
|
+
factories and casts it imports (see `constricter.rules.annotations` for what an annotation says).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import ast
|
|
9
|
+
from collections.abc import Iterator, Sequence
|
|
10
|
+
from functools import lru_cache
|
|
11
|
+
from typing import Final, TypeAlias, cast
|
|
12
|
+
from weakref import WeakKeyDictionary
|
|
13
|
+
|
|
14
|
+
from constricter.rules.annotations import is_vague, node_name, words
|
|
15
|
+
from constricter.rules.decorators import Held, passing, spelled
|
|
16
|
+
from constricter.rules.keys import key, keys
|
|
17
|
+
from constricter.rules.quoted import written
|
|
18
|
+
from constricter.rules.syntax import child_statements, declared_return, top_level
|
|
19
|
+
|
|
20
|
+
_TYPE_VARS: Final = frozenset({"TypeVar", "ParamSpec", "TypeVarTuple"})
|
|
21
|
+
# Each module's classes and type variables, for as long as its tree lives: every class table
|
|
22
|
+
# reads them, for the index and then for the check.
|
|
23
|
+
_Classes: TypeAlias = tuple[ast.ClassDef, ...]
|
|
24
|
+
_Names: TypeAlias = frozenset[str]
|
|
25
|
+
_CLASS_NODES: Final[WeakKeyDictionary[ast.Module, _Classes]] = WeakKeyDictionary()
|
|
26
|
+
_TYPE_VARS_DEFINED: Final[WeakKeyDictionary[ast.Module, _Names]] = WeakKeyDictionary()
|
|
27
|
+
_TYPING_MODULES: Final = frozenset({"typing", "typing_extensions"})
|
|
28
|
+
_TYPING_VARS: Final = frozenset({"AnyStr"}) # the type variables `typing` itself defines
|
|
29
|
+
# Modules `_FACTORIES`' names are imported from (so an aliased or re-exported import is still found).
|
|
30
|
+
_FACTORY_MODULES: Final = frozenset({"enum", "typing", "typing_extensions"})
|
|
31
|
+
# A method returning `Self` returns its receiver's own class.
|
|
32
|
+
_SELF: Final = "Self"
|
|
33
|
+
# Decorators that make a method an attribute of its instances, or callable on its class.
|
|
34
|
+
_PROPERTIES: Final = frozenset({"property", "cached_property"})
|
|
35
|
+
_CLASS_SIDE: Final = frozenset({"classmethod", "staticmethod"})
|
|
36
|
+
_ACCESSORS: Final = frozenset({"setter", "deleter"}) # `@name.setter`: the same property, not a redefinition
|
|
37
|
+
_CLASS_VAR: Final = "ClassVar"
|
|
38
|
+
_TYPED_DICT: Final = "TypedDict"
|
|
39
|
+
_CAST: Final = "cast"
|
|
40
|
+
_UNDECORATED: Final[frozenset[str]] = frozenset() # no decorators: a plain method
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def imported_from(tree: ast.Module, modules: frozenset[str]) -> frozenset[str]:
|
|
44
|
+
"""Find the top-level names this module imports (`from module import name`) from one of `modules`.
|
|
45
|
+
|
|
46
|
+
Only absolute imports are resolved; a relative one (`from . import x`) names no module here.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Those names, as they're bound here (their alias, if importing gave them one).
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
names: set[str] = set()
|
|
53
|
+
stmt: ast.stmt
|
|
54
|
+
module: str
|
|
55
|
+
for stmt in tree.body:
|
|
56
|
+
match stmt:
|
|
57
|
+
case ast.ImportFrom(module=str() as module, level=0) if module in modules:
|
|
58
|
+
names.update(alias.asname or alias.name for alias in stmt.names)
|
|
59
|
+
case _:
|
|
60
|
+
pass
|
|
61
|
+
return frozenset(names)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def factories(tree: ast.Module) -> frozenset[str]:
|
|
65
|
+
"""Find names this module imports that are known to build a class or special form.
|
|
66
|
+
|
|
67
|
+
Not an instance of what they're named (`Enum`, `NamedTuple`, `TypeVar`, ... from `enum`,
|
|
68
|
+
`typing` or `typing_extensions`), however they're aliased.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Those names, as they're bound here.
|
|
72
|
+
|
|
73
|
+
"""
|
|
74
|
+
return imported_from(tree, _FACTORY_MODULES)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def casts(tree: ast.Module) -> frozenset[str]:
|
|
78
|
+
"""Find how this module can name `typing.cast`: `cast` (or its alias), `typing.cast`, `t.cast`.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Each spelling of a call to it, as `ast.unparse` writes the callee.
|
|
82
|
+
|
|
83
|
+
"""
|
|
84
|
+
names: set[str] = set()
|
|
85
|
+
stmt: ast.stmt
|
|
86
|
+
module: str
|
|
87
|
+
for stmt in tree.body:
|
|
88
|
+
match stmt:
|
|
89
|
+
case ast.ImportFrom(module=str() as module, level=0) if module in _FACTORY_MODULES - {"enum"}:
|
|
90
|
+
names.update(alias.asname or alias.name for alias in stmt.names if alias.name == _CAST)
|
|
91
|
+
case ast.Import():
|
|
92
|
+
names.update(
|
|
93
|
+
f"{alias.asname or alias.name}.{_CAST}"
|
|
94
|
+
for alias in stmt.names
|
|
95
|
+
if alias.name in _FACTORY_MODULES - {"enum"}
|
|
96
|
+
)
|
|
97
|
+
case _:
|
|
98
|
+
pass
|
|
99
|
+
return frozenset(names)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _class_nodes(tree: ast.Module) -> tuple[ast.ClassDef, ...]:
|
|
103
|
+
"""Find every class the module defines, however deep.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Them, in source order.
|
|
107
|
+
|
|
108
|
+
"""
|
|
109
|
+
found: tuple[ast.ClassDef, ...] | None
|
|
110
|
+
if (found := _CLASS_NODES.get(tree)) is None:
|
|
111
|
+
found = _CLASS_NODES[tree] = _read_class_nodes(tree)
|
|
112
|
+
return found
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _read_class_nodes(tree: ast.Module) -> tuple[ast.ClassDef, ...]:
|
|
116
|
+
return tuple(node for node in _statements(tree.body) if isinstance(node, ast.ClassDef))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _statements(body: Sequence[ast.stmt]) -> Iterator[ast.stmt]:
|
|
120
|
+
"""Walk statements only, into every nested block (a function's and a class's too), not expressions.
|
|
121
|
+
|
|
122
|
+
Much less than `ast.walk` visits, for what only a statement can be: a class, an annotated
|
|
123
|
+
assignment.
|
|
124
|
+
|
|
125
|
+
Yields:
|
|
126
|
+
Each statement, before those inside it (depth first, in source order).
|
|
127
|
+
|
|
128
|
+
"""
|
|
129
|
+
# A stack, not a recursion: a nested generator passes each statement up through every level.
|
|
130
|
+
# `None` at its bottom ends it: popped, the walk's done.
|
|
131
|
+
waiting: list[ast.stmt | None] = [None, *reversed(body)]
|
|
132
|
+
stmt: ast.stmt
|
|
133
|
+
for stmt in iter(waiting.pop, None):
|
|
134
|
+
yield stmt
|
|
135
|
+
waiting.extend(reversed(_blocks(stmt)))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _blocks(stmt: ast.stmt) -> list[ast.stmt]:
|
|
139
|
+
"""Collect the statements directly inside `stmt`, a function's or class's body included.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Them, in source order.
|
|
143
|
+
|
|
144
|
+
"""
|
|
145
|
+
return (
|
|
146
|
+
stmt.body
|
|
147
|
+
if isinstance(stmt, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef)
|
|
148
|
+
else child_statements(stmt)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def classes(tree: ast.Module) -> dict[str, dict[str, str]]:
|
|
153
|
+
"""Map each class defined in the module to its instances' annotated attributes.
|
|
154
|
+
|
|
155
|
+
A class-body annotation (`class C: x: int`), a `self.x: int = ...` annotated assignment anywhere
|
|
156
|
+
in one of its methods, and a `@property`'s declared return (as `method_returns` reads a method's,
|
|
157
|
+
and a vague one too, as an annotation's is kept) all count; a name that names more than one class
|
|
158
|
+
in the module (however unlikely) gets the last one's attributes. A `TypedDict` class (under
|
|
159
|
+
`TypedDict`, or one of the module's defined before it; not a generic one) has its keys instead,
|
|
160
|
+
each spelled as `keys.key` does.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Each class's name, mapped to its attributes' names and annotation text.
|
|
164
|
+
|
|
165
|
+
"""
|
|
166
|
+
properties: dict[str, dict[str, str]] = _class_returns(tree, _PROPERTIES)
|
|
167
|
+
vague: dict[str, dict[str, str]] = _class_returns(tree, _PROPERTIES, partly=True)
|
|
168
|
+
found: dict[str, dict[str, str]] = {}
|
|
169
|
+
keyed: set[str] = set()
|
|
170
|
+
node: ast.ClassDef
|
|
171
|
+
called: dict[str, str] | None
|
|
172
|
+
for node in _class_nodes(tree):
|
|
173
|
+
if (called := _called_keys(node)) is not None:
|
|
174
|
+
keyed.add(node.name)
|
|
175
|
+
found[node.name] = called
|
|
176
|
+
elif not _generic(node) and any(
|
|
177
|
+
node_name(base) == _TYPED_DICT or (isinstance(base, ast.Name) and base.id in keyed)
|
|
178
|
+
for base in node.bases
|
|
179
|
+
):
|
|
180
|
+
keyed.add(node.name)
|
|
181
|
+
found[node.name] = keys(node)
|
|
182
|
+
else:
|
|
183
|
+
found[node.name] = {
|
|
184
|
+
**properties.get(node.name, {}),
|
|
185
|
+
**vague.get(node.name, {}),
|
|
186
|
+
**_attributes(node),
|
|
187
|
+
}
|
|
188
|
+
return found
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _called_keys(node: ast.ClassDef) -> dict[str, str] | None:
|
|
192
|
+
"""Read the keys of a class whose one base is a `TypedDict("D", {"a": int, ...})` call.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Each key's type as written; `None` for a class under anything else.
|
|
196
|
+
|
|
197
|
+
"""
|
|
198
|
+
func: ast.expr
|
|
199
|
+
names: list[ast.expr | None]
|
|
200
|
+
values: list[ast.expr]
|
|
201
|
+
match node.bases:
|
|
202
|
+
case [
|
|
203
|
+
ast.Call(
|
|
204
|
+
func=func,
|
|
205
|
+
args=[ast.Constant(value=str()), ast.Dict(keys=names, values=values)],
|
|
206
|
+
),
|
|
207
|
+
] if node_name(func) == _TYPED_DICT and all(
|
|
208
|
+
isinstance(name, ast.Constant) and isinstance(name.value, str) for name in names
|
|
209
|
+
):
|
|
210
|
+
return {
|
|
211
|
+
key(str(cast("ast.Constant", name).value)): written(value)
|
|
212
|
+
for name, value in zip(names, values, strict=True)
|
|
213
|
+
}
|
|
214
|
+
case _:
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def class_attributes(tree: ast.Module) -> dict[str, dict[str, str]]:
|
|
219
|
+
"""Map each non-generic class defined in the module to the attributes its class itself has.
|
|
220
|
+
|
|
221
|
+
A class-body annotation with a value (`limit: int = 3`), or a `ClassVar[T]` (as `T`): what `cls.x`
|
|
222
|
+
reads in a classmethod. A bare annotation (`x: int`) only declares an instance attribute (a
|
|
223
|
+
dataclass field), so it doesn't count.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Each class's name, mapped to its class attributes' names and annotation text.
|
|
227
|
+
|
|
228
|
+
"""
|
|
229
|
+
type_vars: frozenset[str] = defined_type_vars(tree)
|
|
230
|
+
found: dict[str, dict[str, str]] = {}
|
|
231
|
+
node: ast.AST
|
|
232
|
+
stmt: ast.stmt
|
|
233
|
+
name: str
|
|
234
|
+
annotation: ast.expr
|
|
235
|
+
value: ast.expr | None
|
|
236
|
+
for node in _class_nodes(tree):
|
|
237
|
+
if not _generic(node):
|
|
238
|
+
attrs: dict[str, str] = {}
|
|
239
|
+
for stmt in node.body:
|
|
240
|
+
match stmt:
|
|
241
|
+
case ast.AnnAssign(target=ast.Name(id=name), annotation=annotation, value=value):
|
|
242
|
+
text: str | None = _class_var(annotation)
|
|
243
|
+
if text is None and value is not None:
|
|
244
|
+
text = written(annotation)
|
|
245
|
+
if text and not type_vars & set(words(text)):
|
|
246
|
+
attrs[name] = text
|
|
247
|
+
case _:
|
|
248
|
+
pass
|
|
249
|
+
found[node.name] = attrs
|
|
250
|
+
return found
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _class_var(annotation: ast.expr) -> str | None:
|
|
254
|
+
"""Unwrap `ClassVar[T]`.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
`T` as text, or `None` if `annotation` isn't a parameterised `ClassVar`.
|
|
258
|
+
|
|
259
|
+
"""
|
|
260
|
+
outer: ast.expr
|
|
261
|
+
inner: ast.expr
|
|
262
|
+
match annotation:
|
|
263
|
+
case ast.Subscript(value=outer, slice=inner) if node_name(outer) == _CLASS_VAR:
|
|
264
|
+
return written(inner)
|
|
265
|
+
case _:
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def class_methods(tree: ast.Module) -> dict[str, dict[str, str]]:
|
|
270
|
+
"""Map each non-generic class defined in the module to its classmethods' and staticmethods' returns.
|
|
271
|
+
|
|
272
|
+
What `cls.method()` gives in a classmethod, under `method_returns`' rules.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Each class's name, mapped to those methods' names and return annotation text.
|
|
276
|
+
|
|
277
|
+
"""
|
|
278
|
+
return _class_returns(tree, _CLASS_SIDE)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def held_class_methods(tree: ast.Module) -> dict[str, dict[str, Held]]:
|
|
282
|
+
"""Find the classmethods and staticmethods declaring a return under decorators the module can't vouch for.
|
|
283
|
+
|
|
284
|
+
As `class_methods` reads them, but for those decorators: another module's may give the method
|
|
285
|
+
back (see `held`).
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Each class's name, mapped to those methods' names, return annotations and decorators.
|
|
289
|
+
|
|
290
|
+
"""
|
|
291
|
+
known: frozenset[str] = _passing(tree)
|
|
292
|
+
declared: dict[str, dict[str, str]] = _class_returns(tree, _CLASS_SIDE, anything=True)
|
|
293
|
+
own: dict[str, dict[str, str]] = class_methods(tree)
|
|
294
|
+
found: dict[str, dict[str, Held]] = {}
|
|
295
|
+
node: ast.ClassDef
|
|
296
|
+
for node in _class_nodes(tree):
|
|
297
|
+
methods: dict[str, Held] = {
|
|
298
|
+
stmt.name: Held(
|
|
299
|
+
declared[node.name][stmt.name],
|
|
300
|
+
tuple(
|
|
301
|
+
sorted(
|
|
302
|
+
{spelled(d) or "" for d in stmt.decorator_list if node_name(d) not in _CLASS_SIDE}
|
|
303
|
+
- known,
|
|
304
|
+
),
|
|
305
|
+
),
|
|
306
|
+
)
|
|
307
|
+
for stmt in node.body
|
|
308
|
+
if isinstance(stmt, ast.FunctionDef)
|
|
309
|
+
and stmt.name in declared.get(node.name, {})
|
|
310
|
+
and stmt.name not in own[node.name]
|
|
311
|
+
}
|
|
312
|
+
if methods:
|
|
313
|
+
found[node.name] = methods
|
|
314
|
+
return found
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _attributes(node: ast.ClassDef) -> dict[str, str]:
|
|
318
|
+
attrs: dict[str, str] = {}
|
|
319
|
+
stmt: ast.stmt
|
|
320
|
+
name: str
|
|
321
|
+
annotation: ast.expr
|
|
322
|
+
for stmt in node.body:
|
|
323
|
+
match stmt:
|
|
324
|
+
case ast.AnnAssign(target=ast.Name(id=name), annotation=annotation):
|
|
325
|
+
attrs[name] = written(annotation)
|
|
326
|
+
case ast.FunctionDef() | ast.AsyncFunctionDef():
|
|
327
|
+
attrs.update(_self_attributes(stmt))
|
|
328
|
+
case _:
|
|
329
|
+
pass
|
|
330
|
+
return attrs
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _self_attributes(func: ast.FunctionDef | ast.AsyncFunctionDef) -> Iterator[tuple[str, str]]:
|
|
334
|
+
"""Find `self.attr: T = ...` annotated assignments anywhere in a method's body.
|
|
335
|
+
|
|
336
|
+
Yields:
|
|
337
|
+
Each attribute's name and annotation text.
|
|
338
|
+
|
|
339
|
+
"""
|
|
340
|
+
node: ast.stmt
|
|
341
|
+
name: str
|
|
342
|
+
annotation: ast.expr
|
|
343
|
+
for node in _statements(func.body): # an annotated assignment is always a statement
|
|
344
|
+
match node:
|
|
345
|
+
case ast.AnnAssign(
|
|
346
|
+
target=ast.Attribute(value=ast.Name(id="self"), attr=name),
|
|
347
|
+
annotation=annotation,
|
|
348
|
+
):
|
|
349
|
+
yield name, written(annotation)
|
|
350
|
+
case _:
|
|
351
|
+
pass
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def returns(tree: ast.Module) -> dict[str, str]:
|
|
355
|
+
"""Return the declared return type of each plain top-level function whose calls `--fix` can annotate.
|
|
356
|
+
|
|
357
|
+
Skipped: generic, async and redefined functions, one decorated by anything but a decorator that
|
|
358
|
+
gives it back (see `decorators.passing`), and returns that are `None`, vague, or mention a
|
|
359
|
+
module-level `TypeVar` (a call's type then depends on its arguments).
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
Each such function's name, and its return annotation as source text.
|
|
363
|
+
|
|
364
|
+
"""
|
|
365
|
+
return _declared_returns(tree.body, defined_type_vars(tree), vouched=_passing(tree))
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
@lru_cache(maxsize=4) # asked for a module's functions, then its classes' methods
|
|
369
|
+
def _passing(tree: ast.Module) -> frozenset[str]:
|
|
370
|
+
"""Spell the decorators the module alone vouches for (see `decorators.passing`).
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
Them.
|
|
374
|
+
|
|
375
|
+
"""
|
|
376
|
+
return passing(tree, defined_type_vars(tree))
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def held(tree: ast.Module) -> dict[str, Held]:
|
|
380
|
+
"""Find the plain top-level functions declaring a return under decorators the module can't vouch for.
|
|
381
|
+
|
|
382
|
+
As `returns` reads them, but for those decorators: another module's may give the function back.
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
Each one's name, its return annotation and those decorators (see `Held`).
|
|
386
|
+
|
|
387
|
+
"""
|
|
388
|
+
known: frozenset[str] = _passing(tree)
|
|
389
|
+
declared: dict[str, str] = _declared_returns(tree.body, defined_type_vars(tree), vouched=None)
|
|
390
|
+
return {
|
|
391
|
+
stmt.name: Held(
|
|
392
|
+
declared[stmt.name],
|
|
393
|
+
tuple(sorted({spelled(d) or "" for d in stmt.decorator_list} - known)),
|
|
394
|
+
)
|
|
395
|
+
for stmt in tree.body
|
|
396
|
+
if isinstance(stmt, ast.FunctionDef) and stmt.name in declared and stmt.name not in returns(tree)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def method_returns(tree: ast.Module, *, awaited: bool = False) -> dict[str, dict[str, str]]:
|
|
401
|
+
"""Map each non-generic class defined in the module to its methods' (`awaited`: `async` ones') returns.
|
|
402
|
+
|
|
403
|
+
For `--fix` to type `obj.method()` on a local already typed as that class. A method counts under
|
|
404
|
+
the same rules as `returns`' functions: a plain `def` directly in the class body, not decorated
|
|
405
|
+
(so no `staticmethod`, `classmethod` or `property`) or redefined, whose return isn't `None`,
|
|
406
|
+
vague, or a `TypeVar`. A generic class (`class C[T]`, or any subscripted base like `Generic[T]`)
|
|
407
|
+
is skipped whole: its methods' returns depend on how it's parameterised. A bare `Self` return is
|
|
408
|
+
the class itself; one that only mentions `Self` (`list[Self]`) is skipped.
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
Each class's name, mapped to its methods' names and return annotation text.
|
|
412
|
+
|
|
413
|
+
"""
|
|
414
|
+
return _class_returns(tree, _UNDECORATED, awaited=awaited)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def partial_returns(tree: ast.Module) -> dict[str, str]:
|
|
418
|
+
"""Return the declared return of each function `returns` leaves out for a vague part.
|
|
419
|
+
|
|
420
|
+
`tuple[Row, dict[str, Any]]` types a call only as far as `vague` allows (see `vague_fits`), and
|
|
421
|
+
an unpacking's names part by part.
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
Each such function's name, and its return annotation as source text.
|
|
425
|
+
|
|
426
|
+
"""
|
|
427
|
+
return _partial_returns(tree.body, defined_type_vars(tree), vouched=_passing(tree))
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def partial_method_returns(tree: ast.Module) -> dict[str, dict[str, str]]:
|
|
431
|
+
"""Map each non-generic class to its methods' declared returns that have a vague part.
|
|
432
|
+
|
|
433
|
+
As `partial_returns` reads functions': its plain methods', and its classmethods' and staticmethods'.
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
Each class's name, mapped to those methods' names and return annotation text.
|
|
437
|
+
|
|
438
|
+
"""
|
|
439
|
+
sides: dict[str, dict[str, str]] = _class_returns(tree, _CLASS_SIDE, partly=True)
|
|
440
|
+
return {
|
|
441
|
+
owner: {**sides[owner], **methods}
|
|
442
|
+
for owner, methods in _class_returns(tree, _UNDECORATED, partly=True).items()
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _class_returns(
|
|
447
|
+
tree: ast.Module,
|
|
448
|
+
decorators: frozenset[str],
|
|
449
|
+
*,
|
|
450
|
+
anything: bool = False,
|
|
451
|
+
partly: bool = False,
|
|
452
|
+
awaited: bool = False,
|
|
453
|
+
) -> dict[str, dict[str, str]]:
|
|
454
|
+
"""Map each non-generic class to the declared returns of its methods decorated by one of `decorators`.
|
|
455
|
+
|
|
456
|
+
None of them (an empty set) means plain methods; see `method_returns` for the rules. Any other
|
|
457
|
+
decorator must be one the module vouches for (see `decorators.passing`), or (`anything`) one
|
|
458
|
+
`decorators.spelled` reads. `partly`: those left out for a vague part, instead.
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
Each class's name, mapped to those methods' names and return annotation text.
|
|
462
|
+
|
|
463
|
+
"""
|
|
464
|
+
type_vars: frozenset[str] = defined_type_vars(tree)
|
|
465
|
+
found: dict[str, dict[str, str]] = {}
|
|
466
|
+
node: ast.ClassDef
|
|
467
|
+
for node in _class_nodes(tree):
|
|
468
|
+
if not _generic(node):
|
|
469
|
+
found[node.name] = {
|
|
470
|
+
name: node.name if _is_self(annotation) else annotation
|
|
471
|
+
for name, annotation in (_partial_returns if partly else _declared_returns)(
|
|
472
|
+
node.body,
|
|
473
|
+
type_vars,
|
|
474
|
+
awaited=awaited,
|
|
475
|
+
decorators=decorators,
|
|
476
|
+
vouched=None if anything else _passing(tree),
|
|
477
|
+
).items()
|
|
478
|
+
if _is_self(annotation) or _SELF not in words(annotation)
|
|
479
|
+
}
|
|
480
|
+
return found
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def self_returns(tree: ast.Module) -> dict[str, frozenset[str]]:
|
|
484
|
+
"""Map each class to its methods (classmethods and staticmethods too) declared to return a bare `Self`.
|
|
485
|
+
|
|
486
|
+
`method_returns` types such a call as the class; but called on `self` or `cls` it's `Self`, which
|
|
487
|
+
a subclass's is too, so `--fix` writes that instead (see `constricter.fix.values.doubts`).
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
Each class's name, and those methods' names.
|
|
491
|
+
|
|
492
|
+
"""
|
|
493
|
+
return {
|
|
494
|
+
node.name: frozenset(
|
|
495
|
+
stmt.name
|
|
496
|
+
for stmt in node.body
|
|
497
|
+
if isinstance(stmt, ast.FunctionDef | ast.AsyncFunctionDef)
|
|
498
|
+
and stmt.returns is not None
|
|
499
|
+
and _is_self(written(stmt.returns))
|
|
500
|
+
)
|
|
501
|
+
for node in _class_nodes(tree)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def generic_classes(tree: ast.Module) -> frozenset[str]:
|
|
506
|
+
"""Find the generic classes the module defines (see `_generic`).
|
|
507
|
+
|
|
508
|
+
Returns:
|
|
509
|
+
Their names: written bare, as `--fix` would write `self`'s type, each is missing its arguments.
|
|
510
|
+
|
|
511
|
+
"""
|
|
512
|
+
return frozenset(node.name for node in _class_nodes(tree) if _generic(node))
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _generic(node: ast.ClassDef) -> bool:
|
|
516
|
+
"""Check whether a class is generic: `class C[T]`, or any subscripted base like `Generic[T]`.
|
|
517
|
+
|
|
518
|
+
Returns:
|
|
519
|
+
Whether it is: its members' types then depend on how it's parameterised.
|
|
520
|
+
|
|
521
|
+
"""
|
|
522
|
+
return bool(cast("object", getattr(node, "type_params", ()))) or any( # Python 3.12+'s `class C[T]`
|
|
523
|
+
isinstance(base, ast.Subscript) for base in node.bases
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _is_self(annotation: str) -> bool:
|
|
528
|
+
"""Check whether an annotation is exactly `Self` (bare, or `typing.Self` and the like).
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
Whether it is.
|
|
532
|
+
|
|
533
|
+
"""
|
|
534
|
+
# `annotation` is always `ast.unparse`'s own output, so it's always valid Python to parse back.
|
|
535
|
+
return node_name(ast.parse(annotation, mode="eval").body) == _SELF
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def defined_type_vars(tree: ast.Module) -> frozenset[str]:
|
|
539
|
+
"""Find the module-level names bound to a `TypeVar`, `ParamSpec` or `TypeVarTuple`, or `typing.AnyStr`.
|
|
540
|
+
|
|
541
|
+
Returns:
|
|
542
|
+
Those names.
|
|
543
|
+
|
|
544
|
+
"""
|
|
545
|
+
found: frozenset[str] | None
|
|
546
|
+
if (found := _TYPE_VARS_DEFINED.get(tree)) is None:
|
|
547
|
+
found = _TYPE_VARS_DEFINED[tree] = _read_defined_type_vars(tree)
|
|
548
|
+
return found
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _read_defined_type_vars(tree: ast.Module) -> frozenset[str]:
|
|
552
|
+
names: set[str] = set()
|
|
553
|
+
stmt: ast.stmt
|
|
554
|
+
name: str
|
|
555
|
+
func: ast.expr
|
|
556
|
+
module: str
|
|
557
|
+
for stmt in top_level(tree.body): # under an `if TYPE_CHECKING:` too
|
|
558
|
+
match stmt:
|
|
559
|
+
case ast.Assign(targets=[ast.Name(id=name)], value=ast.Call(func=func)) if (
|
|
560
|
+
node_name(func) in _TYPE_VARS
|
|
561
|
+
):
|
|
562
|
+
names.add(name)
|
|
563
|
+
case ast.ImportFrom(module=str() as module, level=0) if module in _TYPING_MODULES:
|
|
564
|
+
names.update(alias.asname or alias.name for alias in stmt.names if alias.name in _TYPING_VARS)
|
|
565
|
+
case ast.Import(names=[*_]) if any(alias.name in _TYPING_MODULES for alias in stmt.names):
|
|
566
|
+
names.update(_TYPING_VARS) # `typing.AnyStr`: the word `AnyStr`
|
|
567
|
+
case _:
|
|
568
|
+
pass
|
|
569
|
+
return frozenset(names)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def awaited_returns(tree: ast.Module) -> dict[str, str]:
|
|
573
|
+
"""Return the declared return type of each plain top-level `async def`: what awaiting a call gives.
|
|
574
|
+
|
|
575
|
+
The same rules as `returns`' functions, for `async def` instead of `def`.
|
|
576
|
+
|
|
577
|
+
Returns:
|
|
578
|
+
Each such function's name, and its return annotation as source text.
|
|
579
|
+
|
|
580
|
+
"""
|
|
581
|
+
return _declared_returns(tree.body, defined_type_vars(tree), awaited=True)
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def _declared_returns(
|
|
585
|
+
body: Sequence[ast.stmt],
|
|
586
|
+
type_vars: frozenset[str],
|
|
587
|
+
*,
|
|
588
|
+
awaited: bool = False,
|
|
589
|
+
decorators: frozenset[str] = _UNDECORATED,
|
|
590
|
+
vouched: frozenset[str] | None = _UNDECORATED,
|
|
591
|
+
) -> dict[str, str]:
|
|
592
|
+
"""Find the plain functions (`async` ones if `awaited`) directly in `body` `--fix` can annotate.
|
|
593
|
+
|
|
594
|
+
Plain means decorated only as `vouched` spells (`None`: by anything `decorators.spelled`
|
|
595
|
+
reads), and with `decorators`, by exactly one of them too. A property's `@name.setter` or
|
|
596
|
+
`@name.deleter` is the same property, not a redefinition. Not one whose return has a vague
|
|
597
|
+
part (see `_partial_returns`).
|
|
598
|
+
|
|
599
|
+
Returns:
|
|
600
|
+
Each such function's name, and its return annotation as source text.
|
|
601
|
+
|
|
602
|
+
"""
|
|
603
|
+
found: dict[str, tuple[str, bool]] = _every_return(
|
|
604
|
+
body,
|
|
605
|
+
type_vars,
|
|
606
|
+
awaited=awaited,
|
|
607
|
+
decorators=decorators,
|
|
608
|
+
vouched=vouched,
|
|
609
|
+
)
|
|
610
|
+
return {name: annotation for name, (annotation, partial) in found.items() if not partial}
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _partial_returns(
|
|
614
|
+
body: Sequence[ast.stmt],
|
|
615
|
+
type_vars: frozenset[str],
|
|
616
|
+
*,
|
|
617
|
+
awaited: bool = False,
|
|
618
|
+
decorators: frozenset[str] = _UNDECORATED,
|
|
619
|
+
vouched: frozenset[str] | None = _UNDECORATED,
|
|
620
|
+
) -> dict[str, str]:
|
|
621
|
+
"""Find the plain functions directly in `body` whose declared return has a vague part.
|
|
622
|
+
|
|
623
|
+
As `_declared_returns` finds the others.
|
|
624
|
+
|
|
625
|
+
Returns:
|
|
626
|
+
Each such function's name, and its return annotation as source text.
|
|
627
|
+
|
|
628
|
+
"""
|
|
629
|
+
found: dict[str, tuple[str, bool]] = _every_return(
|
|
630
|
+
body,
|
|
631
|
+
type_vars,
|
|
632
|
+
awaited=awaited,
|
|
633
|
+
decorators=decorators,
|
|
634
|
+
vouched=vouched,
|
|
635
|
+
)
|
|
636
|
+
return {name: annotation for name, (annotation, partial) in found.items() if partial}
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def _every_return(
|
|
640
|
+
body: Sequence[ast.stmt],
|
|
641
|
+
type_vars: frozenset[str],
|
|
642
|
+
*,
|
|
643
|
+
awaited: bool,
|
|
644
|
+
decorators: frozenset[str],
|
|
645
|
+
vouched: frozenset[str] | None,
|
|
646
|
+
) -> dict[str, tuple[str, bool]]:
|
|
647
|
+
"""Read the declared return of each plain function directly in `body` (see `_declared_returns`).
|
|
648
|
+
|
|
649
|
+
Returns:
|
|
650
|
+
Each one's name, its return annotation as source text, and whether that has a vague
|
|
651
|
+
part.
|
|
652
|
+
|
|
653
|
+
"""
|
|
654
|
+
counts: dict[str, int] = {}
|
|
655
|
+
found: dict[str, tuple[str, bool]] = {}
|
|
656
|
+
stmt: ast.stmt
|
|
657
|
+
name: str
|
|
658
|
+
for stmt in body:
|
|
659
|
+
match stmt:
|
|
660
|
+
case ast.FunctionDef(name=name) | ast.AsyncFunctionDef(name=name) if not _accessor(stmt):
|
|
661
|
+
counts[name] = counts.get(name, 0) + 1
|
|
662
|
+
declared: tuple[str, bool] | None = _usable_return(stmt)
|
|
663
|
+
if (
|
|
664
|
+
declared is not None
|
|
665
|
+
and isinstance(stmt, ast.AsyncFunctionDef) == awaited
|
|
666
|
+
and _plain(stmt, decorators, vouched)
|
|
667
|
+
):
|
|
668
|
+
found[name] = declared
|
|
669
|
+
case _:
|
|
670
|
+
pass
|
|
671
|
+
return {
|
|
672
|
+
name: read
|
|
673
|
+
for name, read in found.items()
|
|
674
|
+
if counts[name] == 1 and not type_vars & set(words(read[0]))
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def _accessor(func: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
679
|
+
"""Check whether `func` is a property's setter or deleter (`@name.setter`).
|
|
680
|
+
|
|
681
|
+
Returns:
|
|
682
|
+
Whether it is.
|
|
683
|
+
|
|
684
|
+
"""
|
|
685
|
+
return any(
|
|
686
|
+
isinstance(decorator, ast.Attribute) and decorator.attr in _ACCESSORS
|
|
687
|
+
for decorator in func.decorator_list
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def _plain(
|
|
692
|
+
func: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
693
|
+
decorators: frozenset[str],
|
|
694
|
+
vouched: frozenset[str] | None,
|
|
695
|
+
) -> bool:
|
|
696
|
+
"""Check that `func` is decorated as asked, for its declared return to type its calls.
|
|
697
|
+
|
|
698
|
+
Returns:
|
|
699
|
+
Whether it is: decorated by exactly one of `decorators` (if any), and otherwise only as
|
|
700
|
+
`vouched` spells (`None`: by anything spelled).
|
|
701
|
+
|
|
702
|
+
"""
|
|
703
|
+
others: list[ast.expr] = [d for d in func.decorator_list if node_name(d) not in decorators]
|
|
704
|
+
spellings: list[str | None] = [spelled(decorator) for decorator in others]
|
|
705
|
+
return (
|
|
706
|
+
len(func.decorator_list) - len(others) == bool(decorators)
|
|
707
|
+
and None not in spellings
|
|
708
|
+
and (vouched is None or vouched.issuperset(spellings))
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
# By the function alone: each class's body is read for its methods, its properties, its
|
|
713
|
+
# classmethods and those held back, and a function's declared return is the same to them all.
|
|
714
|
+
@lru_cache(maxsize=4096)
|
|
715
|
+
def _usable_return(func: ast.FunctionDef | ast.AsyncFunctionDef) -> tuple[str, bool] | None:
|
|
716
|
+
"""Read the return type `func` declares, if its calls always have it.
|
|
717
|
+
|
|
718
|
+
Returns:
|
|
719
|
+
Its annotation as source text, and whether it has a vague part (`tuple[Row, dict[str, Any]]`,
|
|
720
|
+
`Any`); `None` for a generic function, no return, or `None`.
|
|
721
|
+
|
|
722
|
+
"""
|
|
723
|
+
declared: ast.expr | None = declared_return(func)
|
|
724
|
+
if (
|
|
725
|
+
cast("object", getattr(func, "type_params", ())) # Python 3.12+'s `def f[T]()`
|
|
726
|
+
or declared is None
|
|
727
|
+
or (isinstance(declared, ast.Constant) and declared.value is None)
|
|
728
|
+
):
|
|
729
|
+
return None
|
|
730
|
+
return written(declared), is_vague(declared)
|