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,351 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Decorators that give back the function they decorate, whose return still types its calls.
|
|
3
|
+
|
|
4
|
+
One the standard library has (`functools.cache`, `abc.abstractmethod`, `unittest.skipIf(...)`,
|
|
5
|
+
...), or a function whose own signature says so: it takes `F` and returns `F`, or (a factory, called
|
|
6
|
+
to decorate) returns a `Callable[[F], F]`, where `F` is a type variable, or a `Callable[P, T]`
|
|
7
|
+
returning one. Or, unannotated, one whose body says so: every `return` gives its one parameter back
|
|
8
|
+
(`def _id(obj): return obj`), or what such a decorator gives (`return impl_detail(**g)(test)`); a
|
|
9
|
+
factory's each gives such a decorator (`return unittest.skipUnless(ok, "why")`, `return _id`).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import ast
|
|
13
|
+
import re
|
|
14
|
+
from collections.abc import Mapping, Sequence
|
|
15
|
+
from typing import Final, NamedTuple, TypeAlias
|
|
16
|
+
|
|
17
|
+
from constricter.rules.syntax import child_statements, import_bindings
|
|
18
|
+
|
|
19
|
+
_CALLED: Final = "()" # after a decorator's name, as it's spelled when it's called to decorate
|
|
20
|
+
# How a pytest fixture's decorator is spelled (see `spelled`).
|
|
21
|
+
FIXTURES: Final = frozenset({"pytest.fixture", "pytest.fixture()", "fixture", "fixture()"})
|
|
22
|
+
_CALLABLE: Final = "Callable"
|
|
23
|
+
_DOTTED: Final = re.compile(r"[^\W\d]\w*(?:\.[^\W\d]\w*)*") # `name`, `pkg.util.name`
|
|
24
|
+
_Use: TypeAlias = tuple[bool, bool] # whether a decorator decorates bare, and called
|
|
25
|
+
_Uses: TypeAlias = dict[str, _Use] # each decorator's, by its name
|
|
26
|
+
# The standard library's, by where each is from.
|
|
27
|
+
_BARE: Final = (True, False)
|
|
28
|
+
_EITHER: Final = (True, True)
|
|
29
|
+
_FACTORY: Final = (False, True)
|
|
30
|
+
_LIBRARY: Final[dict[str, _Uses]] = {
|
|
31
|
+
"functools": {"cache": _BARE, "lru_cache": _EITHER, "wraps": _FACTORY},
|
|
32
|
+
"abc": {"abstractmethod": _BARE},
|
|
33
|
+
"typing": {"final": _BARE, "override": _BARE},
|
|
34
|
+
"typing_extensions": {"final": _BARE, "override": _BARE, "deprecated": _FACTORY},
|
|
35
|
+
"warnings": {"deprecated": _FACTORY},
|
|
36
|
+
"unittest": {"expectedFailure": _BARE, "skip": _FACTORY, "skipIf": _FACTORY, "skipUnless": _FACTORY},
|
|
37
|
+
}
|
|
38
|
+
_NO_TYPE_VARS: Final = frozenset[str]() # an unannotated pass's: its body, not a signature, says so
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Pass(NamedTuple):
|
|
42
|
+
"""A function that gives back what it decorates, by its signature.
|
|
43
|
+
|
|
44
|
+
`called`: whether it's a factory, called to decorate (`@named("x")`); `type_vars`: the names its
|
|
45
|
+
signature takes for type variables, which it is one only if they are.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
called: bool
|
|
49
|
+
type_vars: frozenset[str]
|
|
50
|
+
|
|
51
|
+
def spelled(self, name: str) -> str:
|
|
52
|
+
"""Spell its use as a decorator, as `spelled` reads one.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
`name`, with `()` after it for a factory.
|
|
56
|
+
|
|
57
|
+
"""
|
|
58
|
+
return f"{name}{_CALLED}" if self.called else name
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Held(NamedTuple):
|
|
62
|
+
"""A function's declared return, held back for decorators its module can't vouch for alone.
|
|
63
|
+
|
|
64
|
+
`decorators`: those, as `spelled` reads them.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
returns: str
|
|
68
|
+
decorators: tuple[str, ...]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def is_fixture(function: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
72
|
+
"""Check whether a function is a pytest fixture: decorated `pytest.fixture`, called or not.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Whether it is.
|
|
76
|
+
|
|
77
|
+
"""
|
|
78
|
+
return any(spelled(decorator) in FIXTURES for decorator in function.decorator_list)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def spelled(decorator: ast.expr) -> str | None:
|
|
82
|
+
"""Spell a decorator as written: `cache`, `functools.cache`, and `named()` for one that's called.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
It, or `None` for anything but a dotted name or a call of one.
|
|
86
|
+
|
|
87
|
+
"""
|
|
88
|
+
func: ast.expr
|
|
89
|
+
match decorator:
|
|
90
|
+
case ast.Call(func=func):
|
|
91
|
+
name: str | None = _dotted(func)
|
|
92
|
+
return None if name is None else f"{name}{_CALLED}"
|
|
93
|
+
case _:
|
|
94
|
+
return _dotted(decorator)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _dotted(node: ast.expr) -> str | None:
|
|
98
|
+
"""Spell a name, or a chain of attributes on one, as written.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
It, or `None` if `node` is anything else.
|
|
102
|
+
|
|
103
|
+
"""
|
|
104
|
+
text: str = ast.unparse(node)
|
|
105
|
+
return text if _DOTTED.fullmatch(text) else None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def passes(tree: ast.Module) -> dict[str, Pass]:
|
|
109
|
+
"""Find the module's own top-level functions that give back the function they decorate.
|
|
110
|
+
|
|
111
|
+
A plain `def`, defined once, undecorated: one parameter annotated as its return is (bare), or a
|
|
112
|
+
return of `Callable[[A], A]` (a factory), where `A` is a name or a `Callable[..., T]`. Or one
|
|
113
|
+
with no annotation at all whose every `return` says so (see `_inferred`), by the standard
|
|
114
|
+
library's decorators the module imports and its own found before it, until no more are.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Each one's name, and how it's used (see `Pass`).
|
|
118
|
+
|
|
119
|
+
"""
|
|
120
|
+
counts: dict[str, int] = {}
|
|
121
|
+
found: dict[str, Pass] = {}
|
|
122
|
+
plain: list[ast.FunctionDef] = []
|
|
123
|
+
stmt: ast.stmt
|
|
124
|
+
for stmt in tree.body:
|
|
125
|
+
if isinstance(stmt, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
126
|
+
counts[stmt.name] = counts.get(stmt.name, 0) + 1
|
|
127
|
+
read: Pass | None = _pass(stmt) if isinstance(stmt, ast.FunctionDef) else None
|
|
128
|
+
if read is not None and not stmt.decorator_list:
|
|
129
|
+
found[stmt.name] = read
|
|
130
|
+
elif isinstance(stmt, ast.FunctionDef) and not stmt.decorator_list and _unannotated(stmt):
|
|
131
|
+
plain.append(stmt)
|
|
132
|
+
found = {name: read for name, read in found.items() if counts[name] == 1}
|
|
133
|
+
library: frozenset[str] = _library(tree)
|
|
134
|
+
pending: list[ast.FunctionDef] = [func for func in plain if counts[func.name] == 1]
|
|
135
|
+
for _ in pending: # each pass reads at least one more, or ends: as many passes as functions at most
|
|
136
|
+
more: dict[str, Pass] = {}
|
|
137
|
+
func: ast.FunctionDef
|
|
138
|
+
for func in pending:
|
|
139
|
+
if func.name not in found and (read := _inferred(func, library, found)) is not None:
|
|
140
|
+
more[func.name] = read
|
|
141
|
+
if not more:
|
|
142
|
+
break
|
|
143
|
+
found.update(more)
|
|
144
|
+
return found
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _unannotated(func: ast.FunctionDef) -> bool:
|
|
148
|
+
"""Check whether a function annotates nothing: no parameter, and no return.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Whether it does.
|
|
152
|
+
|
|
153
|
+
"""
|
|
154
|
+
args: ast.arguments = func.args
|
|
155
|
+
stars: list[ast.arg] = [arg for arg in (args.vararg, args.kwarg) if arg is not None]
|
|
156
|
+
named: list[ast.arg] = [*args.posonlyargs, *args.args, *args.kwonlyargs, *stars]
|
|
157
|
+
return func.returns is None and all(arg.annotation is None for arg in named)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _inferred(func: ast.FunctionDef, library: frozenset[str], found: Mapping[str, Pass]) -> Pass | None:
|
|
161
|
+
"""Read an unannotated function's `return`s as a decorator's that gives back what it's given.
|
|
162
|
+
|
|
163
|
+
`library` spells the standard library's such decorators as the module imports them, `found` the
|
|
164
|
+
module's own read so far. A bare one has one parameter and returns it, or a factory's call's
|
|
165
|
+
call on it; a factory returns a factory's call, or a bare one's name. Every `return` of its own
|
|
166
|
+
(not a nested function's) must, and it must have one.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
How it's used, or `None` if its `return`s don't say so.
|
|
170
|
+
|
|
171
|
+
"""
|
|
172
|
+
returns: list[ast.Return] = _returns(func.body)
|
|
173
|
+
if not returns or any(stmt.value is None for stmt in returns):
|
|
174
|
+
return None
|
|
175
|
+
factories: frozenset[str] = frozenset(
|
|
176
|
+
{text for text in library if text.endswith(_CALLED)}
|
|
177
|
+
| {read.spelled(name) for name, read in found.items() if read.called},
|
|
178
|
+
)
|
|
179
|
+
bare: frozenset[str] = frozenset(name for name, read in found.items() if not read.called)
|
|
180
|
+
args: ast.arguments = func.args
|
|
181
|
+
named: list[ast.arg] = [*args.posonlyargs, *args.args]
|
|
182
|
+
alone: bool = len(named) == 1 and not (args.kwonlyargs or args.vararg or args.kwarg)
|
|
183
|
+
param: str | None = named[0].arg if alone else None
|
|
184
|
+
values: list[ast.expr] = [stmt.value for stmt in returns if stmt.value is not None]
|
|
185
|
+
if param is not None and all(_gives_back(value, param, factories) for value in values):
|
|
186
|
+
return Pass(called=False, type_vars=_NO_TYPE_VARS)
|
|
187
|
+
if all(_gives_decorator(value, factories, bare) for value in values):
|
|
188
|
+
return Pass(called=True, type_vars=_NO_TYPE_VARS)
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _returns(body: Sequence[ast.stmt]) -> list[ast.Return]:
|
|
193
|
+
"""Find a function's own `return` statements: not a nested function's or class's.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Them, in source order.
|
|
197
|
+
|
|
198
|
+
"""
|
|
199
|
+
found: list[ast.Return] = []
|
|
200
|
+
stmt: ast.stmt
|
|
201
|
+
for stmt in body:
|
|
202
|
+
if isinstance(stmt, ast.Return):
|
|
203
|
+
found.append(stmt)
|
|
204
|
+
elif not isinstance(stmt, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef):
|
|
205
|
+
found.extend(_returns(child_statements(stmt)))
|
|
206
|
+
return found
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _gives_back(value: ast.expr, param: str, factories: frozenset[str]) -> bool:
|
|
210
|
+
"""Check whether a `return`'s value is the parameter itself, or a factory's decorator applied to it.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Whether it is (`return test`, `return impl_detail(cpython=True)(test)`).
|
|
214
|
+
|
|
215
|
+
"""
|
|
216
|
+
name: str
|
|
217
|
+
inner: ast.Call
|
|
218
|
+
match value:
|
|
219
|
+
case ast.Name(id=name):
|
|
220
|
+
return name == param
|
|
221
|
+
case ast.Call(func=ast.Call() as inner, args=[ast.Name(id=name)], keywords=[]):
|
|
222
|
+
return name == param and spelled(inner) in factories
|
|
223
|
+
case _:
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _gives_decorator(value: ast.expr, factories: frozenset[str], bare: frozenset[str]) -> bool:
|
|
228
|
+
"""Check whether a `return`'s value is a decorator that gives a function back.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Whether it is: a factory's call (`unittest.skip("why")`), or a bare one's name (`_id`).
|
|
232
|
+
|
|
233
|
+
"""
|
|
234
|
+
name: str
|
|
235
|
+
match value:
|
|
236
|
+
case ast.Call():
|
|
237
|
+
return spelled(value) in factories
|
|
238
|
+
case ast.Name(id=name):
|
|
239
|
+
return name in bare
|
|
240
|
+
case _:
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _library(tree: ast.Module) -> frozenset[str]:
|
|
245
|
+
"""Spell the standard library's decorators that give a function back, as the module imports them.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Each one as `spelled` reads it: `cache`, `functools.lru_cache()`.
|
|
249
|
+
|
|
250
|
+
"""
|
|
251
|
+
found: set[str] = set()
|
|
252
|
+
bound: str
|
|
253
|
+
origin: str
|
|
254
|
+
for bound, origin, _ in import_bindings(tree.body):
|
|
255
|
+
module: str
|
|
256
|
+
name: str
|
|
257
|
+
module, _, name = origin.rpartition(".")
|
|
258
|
+
uses: _Uses = (
|
|
259
|
+
{f"{bound}.{known}": use for known, use in _LIBRARY[origin].items()}
|
|
260
|
+
if origin in _LIBRARY
|
|
261
|
+
else {bound: _LIBRARY[module][name]}
|
|
262
|
+
if name in _LIBRARY.get(module, {})
|
|
263
|
+
else {}
|
|
264
|
+
)
|
|
265
|
+
text: str
|
|
266
|
+
bare: bool
|
|
267
|
+
called: bool
|
|
268
|
+
for text, (bare, called) in uses.items():
|
|
269
|
+
found.update([text] * bare + [f"{text}{_CALLED}"] * called)
|
|
270
|
+
return frozenset(found)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _pass(func: ast.FunctionDef) -> Pass | None:
|
|
274
|
+
"""Read a function's signature as a decorator's that gives back what it's given.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
How it's used, or `None` if its signature doesn't say so.
|
|
278
|
+
|
|
279
|
+
"""
|
|
280
|
+
back: ast.expr | None = func.returns
|
|
281
|
+
taken: ast.expr | None = _only(func.args)
|
|
282
|
+
bare: bool = back is not None and taken is not None and ast.dump(taken) == ast.dump(back)
|
|
283
|
+
given: ast.expr | None = back if bare else _decorated(back)
|
|
284
|
+
names: frozenset[str] | None = None if given is None else _type_vars(given)
|
|
285
|
+
return None if names is None else Pass(called=not bare, type_vars=names)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _decorated(returns: ast.expr | None) -> ast.expr | None:
|
|
289
|
+
"""Read what a factory's declared return says its decorator takes and gives back.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
`Callable[[A], A]`'s `A`, or `None` for any other annotation.
|
|
293
|
+
|
|
294
|
+
"""
|
|
295
|
+
given: ast.expr
|
|
296
|
+
back: ast.expr
|
|
297
|
+
match returns:
|
|
298
|
+
case ast.Subscript(slice=ast.Tuple(elts=[ast.List(elts=[given]), back])) if (
|
|
299
|
+
_dotted(returns.value) or ""
|
|
300
|
+
).endswith(_CALLABLE) and ast.dump(given) == ast.dump(back):
|
|
301
|
+
return given
|
|
302
|
+
case _:
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _only(args: ast.arguments) -> ast.expr | None:
|
|
307
|
+
"""Read the annotation of a signature's one parameter.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
It, or `None` if it has more than one parameter, or the one isn't annotated.
|
|
311
|
+
|
|
312
|
+
"""
|
|
313
|
+
plain: bool = not (args.kwonlyargs or args.vararg or args.kwarg)
|
|
314
|
+
named: list[ast.arg] = [*args.posonlyargs, *args.args]
|
|
315
|
+
return named[0].annotation if plain and len(named) == 1 else None
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _type_vars(annotation: ast.expr) -> frozenset[str] | None:
|
|
319
|
+
"""Name what an annotation a decorator takes and gives back must have for type variables.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
The name itself (`F`); or, of a `Callable[P, T]`, `T` and (if it's a name) `P`; `None` for any
|
|
323
|
+
other annotation.
|
|
324
|
+
|
|
325
|
+
"""
|
|
326
|
+
name: str
|
|
327
|
+
taken: ast.expr
|
|
328
|
+
match annotation:
|
|
329
|
+
case ast.Name(id=name):
|
|
330
|
+
return frozenset({name})
|
|
331
|
+
case ast.Subscript(slice=ast.Tuple(elts=[taken, ast.Name(id=name)])) if (
|
|
332
|
+
_dotted(annotation.value) or ""
|
|
333
|
+
).endswith(_CALLABLE):
|
|
334
|
+
return frozenset({name, taken.id} if isinstance(taken, ast.Name) else {name})
|
|
335
|
+
case _:
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def passing(tree: ast.Module, type_vars: frozenset[str]) -> frozenset[str]:
|
|
340
|
+
"""Spell the decorators the module alone vouches for: its uses of them a declared return survives.
|
|
341
|
+
|
|
342
|
+
The standard library's, as it imports them, and its own (see `passes`) whose type variables are
|
|
343
|
+
all among `type_vars`, the module's.
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
Each one as `spelled` reads it: `cache`, `functools.lru_cache()`.
|
|
347
|
+
|
|
348
|
+
"""
|
|
349
|
+
return _library(tree) | {
|
|
350
|
+
read.spelled(name) for name, read in passes(tree).items() if read.type_vars <= type_vars
|
|
351
|
+
}
|
|
Binary file
|