shakeout 0.1.0__py3-none-any.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.
- shakeout/__init__.py +3 -0
- shakeout/__main__.py +5 -0
- shakeout/_compat.py +19 -0
- shakeout/analysis/__init__.py +1 -0
- shakeout/analysis/classes.py +115 -0
- shakeout/analysis/effects.py +168 -0
- shakeout/analysis/registry.py +220 -0
- shakeout/analysis/scopes.py +515 -0
- shakeout/api.py +65 -0
- shakeout/cli.py +250 -0
- shakeout/config.py +88 -0
- shakeout/emit/__init__.py +188 -0
- shakeout/emit/archive.py +141 -0
- shakeout/emit/deps.py +98 -0
- shakeout/emit/minify.py +105 -0
- shakeout/emit/single.py +63 -0
- shakeout/emit/tree.py +59 -0
- shakeout/graph.py +249 -0
- shakeout/model.py +136 -0
- shakeout/options.py +58 -0
- shakeout/project.py +545 -0
- shakeout/py.typed +0 -0
- shakeout/report.py +48 -0
- shakeout/resolve.py +135 -0
- shakeout/result.py +185 -0
- shakeout/runtime.py +234 -0
- shakeout/trace.py +94 -0
- shakeout/uvstage.py +95 -0
- shakeout-0.1.0.dist-info/METADATA +72 -0
- shakeout-0.1.0.dist-info/RECORD +33 -0
- shakeout-0.1.0.dist-info/WHEEL +4 -0
- shakeout-0.1.0.dist-info/entry_points.txt +2 -0
- shakeout-0.1.0.dist-info/licenses/LICENSE +21 -0
shakeout/__init__.py
ADDED
shakeout/__main__.py
ADDED
shakeout/_compat.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Small shims for older supported Python versions (no extra runtime dependencies)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if sys.version_info >= (3, 12):
|
|
9
|
+
from typing import override
|
|
10
|
+
elif TYPE_CHECKING: # type checkers understand typing_extensions; nothing is imported at runtime
|
|
11
|
+
from typing_extensions import override
|
|
12
|
+
else: # pragma: no cover - exercised on 3.10/3.11 only
|
|
13
|
+
|
|
14
|
+
def override(func, /): # noqa: ANN001, ANN202 - typed via typing_extensions above
|
|
15
|
+
"""Mark a method as overriding its base (typing.override before 3.12)."""
|
|
16
|
+
return func
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__all__ = ["override"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Per-statement analysis: scopes, bindings and side effects."""
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Side effects of class definitions that come from their bases.
|
|
2
|
+
|
|
3
|
+
Subclassing runs ``__init_subclass__`` and metaclass code of every base, which is how plugin
|
|
4
|
+
registries work. A class is *registering* if it defines ``__init_subclass__``, has a metaclass,
|
|
5
|
+
or inherits from a registering class; defining a subclass of a registering class is a side effect.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from shakeout.analysis.effects import EffectRules
|
|
14
|
+
from shakeout.analysis.registry import PURE_BASE_MODULES, PURE_BASES
|
|
15
|
+
from shakeout.analysis.scopes import attribute_chain
|
|
16
|
+
from shakeout.model import NameTarget
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
|
|
21
|
+
from shakeout.graph import Resolver, StmtKey
|
|
22
|
+
from shakeout.model import Module
|
|
23
|
+
|
|
24
|
+
Verdict = tuple[str | None, bool] # (impurity reason from bases, is registering)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ClassEffects:
|
|
28
|
+
"""Classifies class statements by what their bases do when subclassed."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
modules: dict[str, Module],
|
|
33
|
+
resolver: Resolver,
|
|
34
|
+
qualify: Callable[[Module, ast.expr], str | None],
|
|
35
|
+
*,
|
|
36
|
+
pure_calls: frozenset[str],
|
|
37
|
+
assume_pure_bases: bool,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Create an analyzer over ``modules``; ``qualify`` names expressions per module."""
|
|
40
|
+
self.modules = modules
|
|
41
|
+
self.resolver = resolver
|
|
42
|
+
self.qualify = qualify
|
|
43
|
+
self.pure_calls = pure_calls
|
|
44
|
+
self.assume_pure_bases = assume_pure_bases
|
|
45
|
+
self._memo: dict[StmtKey, Verdict] = {}
|
|
46
|
+
|
|
47
|
+
def classify_all(self) -> None:
|
|
48
|
+
"""Set ``impure`` on every class statement whose bases make defining it observable."""
|
|
49
|
+
for mod in self.modules.values():
|
|
50
|
+
for st in mod.statements:
|
|
51
|
+
if st.kind == "class" and not st.impure:
|
|
52
|
+
st.impure, _ = self.verdict((mod.name, st.index), frozenset())
|
|
53
|
+
|
|
54
|
+
def verdict(self, key: StmtKey, active: frozenset[StmtKey]) -> Verdict:
|
|
55
|
+
"""Impurity reason and registering flag for the class statement ``key``."""
|
|
56
|
+
if key in self._memo:
|
|
57
|
+
return self._memo[key]
|
|
58
|
+
if key in active:
|
|
59
|
+
return ("cyclic class hierarchy", True)
|
|
60
|
+
mod = self.modules[key[0]]
|
|
61
|
+
node = mod.statements[key[1]].nodes[0]
|
|
62
|
+
if not isinstance(node, ast.ClassDef): # pragma: no cover - callers check kind
|
|
63
|
+
return (None, False)
|
|
64
|
+
registering = _declares_registration(node)
|
|
65
|
+
impure: str | None = None
|
|
66
|
+
for base in node.bases:
|
|
67
|
+
reason = self._base_reason(mod, base, active | {key})
|
|
68
|
+
if reason:
|
|
69
|
+
impure = impure or reason
|
|
70
|
+
registering = True
|
|
71
|
+
self._memo[key] = (impure, registering)
|
|
72
|
+
return self._memo[key]
|
|
73
|
+
|
|
74
|
+
def _base_reason(self, mod: Module, base: ast.expr, active: frozenset[StmtKey]) -> str | None:
|
|
75
|
+
text = ast.unparse(base)
|
|
76
|
+
expr = base.value if isinstance(base, ast.Subscript) else base
|
|
77
|
+
chain = attribute_chain(expr)
|
|
78
|
+
if chain is None:
|
|
79
|
+
rules = EffectRules(lambda e: self.qualify(mod, e), self.pure_calls)
|
|
80
|
+
return None if rules.pure_expr(base) else f"base {text!r} may have side effects"
|
|
81
|
+
qualified = self.qualify(mod, expr)
|
|
82
|
+
if qualified is not None and self._is_external(qualified):
|
|
83
|
+
return self._external_base_reason(qualified, text)
|
|
84
|
+
res = self.resolver.resolve(mod.name, NameTarget(chain[0]), chain[1])
|
|
85
|
+
if not res.terminal:
|
|
86
|
+
return None if self.assume_pure_bases else f"base {text!r} cannot be resolved"
|
|
87
|
+
for tkey in sorted(res.terminal):
|
|
88
|
+
tst = self.modules[tkey[0]].statements[tkey[1]]
|
|
89
|
+
if tst.kind == "class" and isinstance(tst.nodes[0], ast.ClassDef):
|
|
90
|
+
if self.verdict(tkey, active)[1]:
|
|
91
|
+
return f"base {text!r} registers subclasses (__init_subclass__/metaclass)"
|
|
92
|
+
elif not self.assume_pure_bases:
|
|
93
|
+
return f"base {text!r} is not a plain class definition"
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
def _external_base_reason(self, qualified: str, text: str) -> str | None:
|
|
97
|
+
known_pure = (
|
|
98
|
+
qualified.startswith("builtins.")
|
|
99
|
+
or qualified in PURE_BASES
|
|
100
|
+
or qualified.rpartition(".")[0] in PURE_BASE_MODULES
|
|
101
|
+
)
|
|
102
|
+
if known_pure or self.assume_pure_bases:
|
|
103
|
+
return None
|
|
104
|
+
return f"base {text!r} comes from outside the bundle and may register subclasses"
|
|
105
|
+
|
|
106
|
+
def _is_external(self, qualified: str) -> bool:
|
|
107
|
+
parts = qualified.split(".")
|
|
108
|
+
return not any(".".join(parts[:i]) in self.modules for i in range(len(parts), 0, -1))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _declares_registration(node: ast.ClassDef) -> bool:
|
|
112
|
+
return any(k.arg == "metaclass" for k in node.keywords) or any(
|
|
113
|
+
isinstance(b, (ast.FunctionDef, ast.AsyncFunctionDef)) and b.name == "__init_subclass__"
|
|
114
|
+
for b in node.body
|
|
115
|
+
)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Side-effect classification of top-level statements.
|
|
2
|
+
|
|
3
|
+
A statement is *pure* when executing it only creates bindings in its own module: it cannot print,
|
|
4
|
+
mutate shared state, register anything, or fail in a way that matters. Pure statements may be
|
|
5
|
+
removed when nothing references what they bind. Everything else is impure and always kept.
|
|
6
|
+
|
|
7
|
+
The rules are deliberately conservative. Known assumptions (documented in docs/PLAN.md):
|
|
8
|
+
operators, attribute reads and subscripts on module-level values do not call user code with side
|
|
9
|
+
effects, and the listed pure callables do not call back into user code.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import ast
|
|
15
|
+
from typing import TYPE_CHECKING, cast
|
|
16
|
+
|
|
17
|
+
from shakeout.analysis.registry import PURE_CALLABLES, PURE_IF_CHILDREN_PURE
|
|
18
|
+
from shakeout.model import ImportTarget, ModuleTarget
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from collections.abc import Callable
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EffectRules:
|
|
25
|
+
"""Purity rules for one module. ``qualify`` maps an expression to a dotted external name."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
qualify: Callable[[ast.expr], str | None],
|
|
30
|
+
extra_pure: frozenset[str] = frozenset(),
|
|
31
|
+
) -> None:
|
|
32
|
+
self.qualify = qualify
|
|
33
|
+
self.pure_callables = PURE_CALLABLES | extra_pure
|
|
34
|
+
|
|
35
|
+
# -- expressions -------------------------------------------------------------------------
|
|
36
|
+
def pure_callable(self, func: ast.expr) -> bool:
|
|
37
|
+
"""Whether calling ``func`` is known to have no side effects."""
|
|
38
|
+
q = self.qualify(func)
|
|
39
|
+
return q is not None and q in self.pure_callables
|
|
40
|
+
|
|
41
|
+
def pure_expr(self, node: ast.expr | None) -> bool:
|
|
42
|
+
"""Whether evaluating ``node`` at module level has no observable side effects."""
|
|
43
|
+
if node is None or isinstance(node, (ast.Constant, ast.Name, ast.Lambda)):
|
|
44
|
+
return True
|
|
45
|
+
if isinstance(node, ast.Call):
|
|
46
|
+
return (
|
|
47
|
+
self.pure_callable(node.func)
|
|
48
|
+
and all(self.pure_expr(a) for a in node.args)
|
|
49
|
+
and all(self.pure_expr(k.value) for k in node.keywords)
|
|
50
|
+
)
|
|
51
|
+
children = PURE_IF_CHILDREN_PURE.get(type(node))
|
|
52
|
+
# Anything else (comprehensions may call user __iter__, walrus binds, await...) is impure.
|
|
53
|
+
return children is not None and all(self.pure_expr(c) for c in children(node))
|
|
54
|
+
|
|
55
|
+
def pure_decorator(self, node: ast.expr) -> bool:
|
|
56
|
+
"""Whether applying the decorator ``node`` has no side effects."""
|
|
57
|
+
if isinstance(node, ast.Call):
|
|
58
|
+
return self.pure_expr(node)
|
|
59
|
+
return self.pure_callable(node)
|
|
60
|
+
|
|
61
|
+
# -- statements --------------------------------------------------------------------------
|
|
62
|
+
def function_effect(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> str | None:
|
|
63
|
+
"""Why defining the function has side effects (decorators, defaults), or None."""
|
|
64
|
+
for deco in node.decorator_list:
|
|
65
|
+
if not self.pure_decorator(deco):
|
|
66
|
+
return f"decorator {ast.unparse(deco)!r} may have side effects"
|
|
67
|
+
for default in (*node.args.defaults, *node.args.kw_defaults):
|
|
68
|
+
if not self.pure_expr(default):
|
|
69
|
+
return "default argument value may have side effects"
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
def class_shell_effect(self, node: ast.ClassDef) -> str | None:
|
|
73
|
+
"""Effects from decorators, keywords and body; bases are checked by the linker."""
|
|
74
|
+
for deco in node.decorator_list:
|
|
75
|
+
if not self.pure_decorator(deco):
|
|
76
|
+
return f"class decorator {ast.unparse(deco)!r} may have side effects"
|
|
77
|
+
for kw in node.keywords:
|
|
78
|
+
return f"class keyword {kw.arg or '**'!s}= is passed to the metaclass/__init_subclass__"
|
|
79
|
+
for base in node.bases:
|
|
80
|
+
if isinstance(base, ast.Starred):
|
|
81
|
+
return "starred class bases"
|
|
82
|
+
for stmt in node.body:
|
|
83
|
+
reason = self.class_body_effect(stmt)
|
|
84
|
+
if reason:
|
|
85
|
+
return reason
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
def class_body_effect(self, stmt: ast.stmt) -> str | None:
|
|
89
|
+
"""Why executing ``stmt`` inside a class body has side effects, or None."""
|
|
90
|
+
match stmt:
|
|
91
|
+
case ast.FunctionDef() | ast.AsyncFunctionDef():
|
|
92
|
+
return self.function_effect(stmt)
|
|
93
|
+
case ast.ClassDef():
|
|
94
|
+
return self.class_shell_effect(stmt) or (
|
|
95
|
+
None if all(self.pure_expr(b) for b in stmt.bases) else "nested class base"
|
|
96
|
+
)
|
|
97
|
+
case ast.Assign(targets=targets, value=value):
|
|
98
|
+
if all(isinstance(t, ast.Name) for t in targets) and self.pure_expr(value):
|
|
99
|
+
return None
|
|
100
|
+
return "class attribute initializer may have side effects"
|
|
101
|
+
case ast.AnnAssign(target=ast.Name(), value=value):
|
|
102
|
+
return (
|
|
103
|
+
None
|
|
104
|
+
if self.pure_expr(value)
|
|
105
|
+
else "class attribute initializer may have side effects"
|
|
106
|
+
)
|
|
107
|
+
case ast.Expr(value=ast.Constant()) | ast.Pass():
|
|
108
|
+
return None
|
|
109
|
+
case _:
|
|
110
|
+
return f"class body contains {type(stmt).__name__}"
|
|
111
|
+
|
|
112
|
+
def assignment_effect(self, stmt: ast.Assign | ast.AnnAssign) -> str | None:
|
|
113
|
+
"""Why a module-level assignment has side effects, or None."""
|
|
114
|
+
value: ast.expr | None
|
|
115
|
+
if isinstance(stmt, ast.Assign):
|
|
116
|
+
targets: list[ast.expr] = list(stmt.targets)
|
|
117
|
+
value = stmt.value
|
|
118
|
+
else:
|
|
119
|
+
if stmt.value is None and isinstance(stmt.target, ast.Name):
|
|
120
|
+
return None
|
|
121
|
+
targets, value = [cast("ast.expr", stmt.target)], stmt.value
|
|
122
|
+
for t in targets:
|
|
123
|
+
for sub in ast.walk(t):
|
|
124
|
+
if isinstance(sub, (ast.Attribute, ast.Subscript)):
|
|
125
|
+
return "assignment stores into an attribute or item"
|
|
126
|
+
if not self.pure_expr(value):
|
|
127
|
+
return f"initializer {_short(value)} may have side effects"
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
_SHORT = 40
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _short(node: ast.expr | None) -> str:
|
|
135
|
+
if node is None:
|
|
136
|
+
return "''"
|
|
137
|
+
text = ast.unparse(node)
|
|
138
|
+
return repr(text if len(text) <= _SHORT else text[: _SHORT - 3] + "...")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def is_main_guard(node: ast.stmt) -> bool:
|
|
142
|
+
"""Whether ``node`` is ``if __name__ == '__main__':``."""
|
|
143
|
+
if not isinstance(node, ast.If):
|
|
144
|
+
return False
|
|
145
|
+
test = node.test
|
|
146
|
+
if isinstance(test, ast.Compare) and len(test.ops) == 1 and isinstance(test.ops[0], ast.Eq):
|
|
147
|
+
sides = [test.left, test.comparators[0]]
|
|
148
|
+
names = [s for s in sides if isinstance(s, ast.Name) and s.id == "__name__"]
|
|
149
|
+
consts = [s for s in sides if isinstance(s, ast.Constant) and s.value == "__main__"]
|
|
150
|
+
return bool(names and consts)
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def is_type_checking_guard(node: ast.stmt, qualify: Callable[[ast.expr], str | None]) -> bool:
|
|
155
|
+
"""Whether ``node`` is ``if TYPE_CHECKING:`` without an ``else``."""
|
|
156
|
+
if not isinstance(node, ast.If) or node.orelse:
|
|
157
|
+
return False
|
|
158
|
+
q = qualify(node.test)
|
|
159
|
+
if q in {"typing.TYPE_CHECKING", "typing_extensions.TYPE_CHECKING"}:
|
|
160
|
+
return True
|
|
161
|
+
return isinstance(node.test, ast.Name) and node.test.id == "TYPE_CHECKING"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def qualify_target(target: ImportTarget, attrs: tuple[str, ...]) -> str:
|
|
165
|
+
"""Dotted name of ``attrs`` reached through an import target."""
|
|
166
|
+
if isinstance(target, ModuleTarget):
|
|
167
|
+
return ".".join((target.module, *attrs))
|
|
168
|
+
return ".".join((target.module, target.attr, *attrs))
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Static knowledge used by the effects rules.
|
|
2
|
+
|
|
3
|
+
Known-pure callables, safe base classes, and which expressions are pure when their parts are.
|
|
4
|
+
Pure data built once at import, so it is kept out of mutation testing (mutants of import-time
|
|
5
|
+
code can never be observed).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
from typing import TYPE_CHECKING, cast
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
|
|
16
|
+
_TYPING = ("typing", "typing_extensions")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _qualified(*names: str) -> frozenset[str]:
|
|
20
|
+
return frozenset(names)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
PURE_CALLABLES = _qualified(
|
|
24
|
+
# builtins that construct values and never call user callbacks
|
|
25
|
+
*(
|
|
26
|
+
f"builtins.{n}"
|
|
27
|
+
for n in (
|
|
28
|
+
"object",
|
|
29
|
+
"int",
|
|
30
|
+
"float",
|
|
31
|
+
"complex",
|
|
32
|
+
"str",
|
|
33
|
+
"bytes",
|
|
34
|
+
"bytearray",
|
|
35
|
+
"bool",
|
|
36
|
+
"tuple",
|
|
37
|
+
"list",
|
|
38
|
+
"dict",
|
|
39
|
+
"set",
|
|
40
|
+
"frozenset",
|
|
41
|
+
"range",
|
|
42
|
+
"slice",
|
|
43
|
+
"len",
|
|
44
|
+
"repr",
|
|
45
|
+
"ord",
|
|
46
|
+
"chr",
|
|
47
|
+
"hex",
|
|
48
|
+
"oct",
|
|
49
|
+
"bin",
|
|
50
|
+
"abs",
|
|
51
|
+
"round",
|
|
52
|
+
"property",
|
|
53
|
+
"staticmethod",
|
|
54
|
+
"classmethod",
|
|
55
|
+
"isinstance",
|
|
56
|
+
"issubclass",
|
|
57
|
+
"callable",
|
|
58
|
+
"id",
|
|
59
|
+
"memoryview",
|
|
60
|
+
)
|
|
61
|
+
),
|
|
62
|
+
*(
|
|
63
|
+
f"{m}.{n}"
|
|
64
|
+
for m in _TYPING
|
|
65
|
+
for n in (
|
|
66
|
+
"TypeVar",
|
|
67
|
+
"NewType",
|
|
68
|
+
"ParamSpec",
|
|
69
|
+
"TypeVarTuple",
|
|
70
|
+
"NamedTuple",
|
|
71
|
+
"TypedDict",
|
|
72
|
+
"cast",
|
|
73
|
+
"overload",
|
|
74
|
+
"final",
|
|
75
|
+
"runtime_checkable",
|
|
76
|
+
"no_type_check",
|
|
77
|
+
"dataclass_transform",
|
|
78
|
+
"override",
|
|
79
|
+
"TypeAliasType",
|
|
80
|
+
"get_type_hints",
|
|
81
|
+
"deprecated",
|
|
82
|
+
)
|
|
83
|
+
),
|
|
84
|
+
"collections.namedtuple",
|
|
85
|
+
"collections.OrderedDict",
|
|
86
|
+
"collections.defaultdict",
|
|
87
|
+
"collections.deque",
|
|
88
|
+
"collections.Counter",
|
|
89
|
+
"collections.ChainMap",
|
|
90
|
+
"re.compile",
|
|
91
|
+
"dataclasses.field",
|
|
92
|
+
"dataclasses.dataclass",
|
|
93
|
+
"dataclasses.make_dataclass",
|
|
94
|
+
"functools.partial",
|
|
95
|
+
"functools.lru_cache",
|
|
96
|
+
"functools.cache",
|
|
97
|
+
"functools.wraps",
|
|
98
|
+
"functools.cached_property",
|
|
99
|
+
"functools.total_ordering",
|
|
100
|
+
"functools.singledispatch",
|
|
101
|
+
"functools.singledispatchmethod",
|
|
102
|
+
"functools.partialmethod",
|
|
103
|
+
"contextlib.contextmanager",
|
|
104
|
+
"contextlib.asynccontextmanager",
|
|
105
|
+
"abc.abstractmethod",
|
|
106
|
+
"abc.abstractproperty",
|
|
107
|
+
"abc.abstractclassmethod",
|
|
108
|
+
"abc.abstractstaticmethod",
|
|
109
|
+
"enum.auto",
|
|
110
|
+
"enum.unique",
|
|
111
|
+
"enum.verify",
|
|
112
|
+
"enum.member",
|
|
113
|
+
"enum.nonmember",
|
|
114
|
+
"logging.getLogger",
|
|
115
|
+
"operator.itemgetter",
|
|
116
|
+
"operator.attrgetter",
|
|
117
|
+
"operator.methodcaller",
|
|
118
|
+
"pathlib.Path",
|
|
119
|
+
"pathlib.PurePath",
|
|
120
|
+
"pathlib.PurePosixPath",
|
|
121
|
+
"pathlib.PureWindowsPath",
|
|
122
|
+
"decimal.Decimal",
|
|
123
|
+
"fractions.Fraction",
|
|
124
|
+
"datetime.timedelta",
|
|
125
|
+
"datetime.date",
|
|
126
|
+
"datetime.datetime",
|
|
127
|
+
"datetime.time",
|
|
128
|
+
"datetime.timezone",
|
|
129
|
+
"types.MappingProxyType",
|
|
130
|
+
"types.SimpleNamespace",
|
|
131
|
+
"struct.Struct",
|
|
132
|
+
"string.Template",
|
|
133
|
+
"threading.Lock",
|
|
134
|
+
"threading.RLock",
|
|
135
|
+
"threading.local",
|
|
136
|
+
"threading.Event",
|
|
137
|
+
"weakref.WeakValueDictionary",
|
|
138
|
+
"weakref.WeakKeyDictionary",
|
|
139
|
+
"weakref.WeakSet",
|
|
140
|
+
"weakref.ref",
|
|
141
|
+
"contextvars.ContextVar",
|
|
142
|
+
"uuid.UUID",
|
|
143
|
+
"ipaddress.ip_address",
|
|
144
|
+
"ipaddress.ip_network",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Bases whose subclassing has no global side effects.
|
|
148
|
+
PURE_BASES = _qualified(
|
|
149
|
+
*(
|
|
150
|
+
f"{m}.{n}"
|
|
151
|
+
for m in _TYPING
|
|
152
|
+
for n in (
|
|
153
|
+
"NamedTuple",
|
|
154
|
+
"TypedDict",
|
|
155
|
+
"Protocol",
|
|
156
|
+
"Generic",
|
|
157
|
+
"Any",
|
|
158
|
+
)
|
|
159
|
+
),
|
|
160
|
+
"abc.ABC",
|
|
161
|
+
"enum.Enum",
|
|
162
|
+
"enum.IntEnum",
|
|
163
|
+
"enum.Flag",
|
|
164
|
+
"enum.IntFlag",
|
|
165
|
+
"enum.StrEnum",
|
|
166
|
+
"enum.ReprEnum",
|
|
167
|
+
"collections.OrderedDict",
|
|
168
|
+
"collections.defaultdict",
|
|
169
|
+
"collections.UserDict",
|
|
170
|
+
"collections.UserList",
|
|
171
|
+
"collections.UserString",
|
|
172
|
+
"collections.namedtuple",
|
|
173
|
+
"threading.Thread",
|
|
174
|
+
"threading.local",
|
|
175
|
+
"json.JSONEncoder",
|
|
176
|
+
"json.JSONDecoder",
|
|
177
|
+
"argparse.Action",
|
|
178
|
+
"argparse.ArgumentParser",
|
|
179
|
+
"string.Formatter",
|
|
180
|
+
"io.StringIO",
|
|
181
|
+
"io.BytesIO",
|
|
182
|
+
"contextlib.AbstractContextManager",
|
|
183
|
+
"contextlib.ContextDecorator",
|
|
184
|
+
"html.parser.HTMLParser",
|
|
185
|
+
)
|
|
186
|
+
PURE_BASE_MODULES = frozenset({"collections.abc", "numbers", "typing", "typing_extensions"})
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _children(*fields: str) -> Callable[[ast.AST], list[ast.expr | None]]:
|
|
190
|
+
def get(node: ast.AST) -> list[ast.expr | None]:
|
|
191
|
+
out: list[ast.expr | None] = []
|
|
192
|
+
for name in fields:
|
|
193
|
+
value: object = getattr(node, name)
|
|
194
|
+
if isinstance(value, list):
|
|
195
|
+
out.extend(cast("list[ast.expr | None]", value))
|
|
196
|
+
else:
|
|
197
|
+
out.append(cast("ast.expr | None", value))
|
|
198
|
+
return out
|
|
199
|
+
|
|
200
|
+
return get
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# Expressions that are pure exactly when all of their sub-expressions are pure.
|
|
204
|
+
PURE_IF_CHILDREN_PURE: dict[type[ast.AST], Callable[[ast.AST], list[ast.expr | None]]] = {
|
|
205
|
+
ast.Attribute: _children("value"),
|
|
206
|
+
ast.Subscript: _children("value", "slice"),
|
|
207
|
+
ast.Tuple: _children("elts"),
|
|
208
|
+
ast.List: _children("elts"),
|
|
209
|
+
ast.Set: _children("elts"),
|
|
210
|
+
ast.Starred: _children("value"),
|
|
211
|
+
ast.Dict: _children("keys", "values"),
|
|
212
|
+
ast.BinOp: _children("left", "right"),
|
|
213
|
+
ast.UnaryOp: _children("operand"),
|
|
214
|
+
ast.BoolOp: _children("values"),
|
|
215
|
+
ast.Compare: _children("left", "comparators"),
|
|
216
|
+
ast.IfExp: _children("test", "body", "orelse"),
|
|
217
|
+
ast.JoinedStr: _children("values"),
|
|
218
|
+
ast.FormattedValue: _children("value", "format_spec"),
|
|
219
|
+
ast.Slice: _children("lower", "upper", "step"),
|
|
220
|
+
}
|