-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathast_transform_hook.py
More file actions
120 lines (93 loc) · 3.67 KB
/
ast_transform_hook.py
File metadata and controls
120 lines (93 loc) · 3.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
"""Plug user logic into paker's pack pipeline via ``ast_transform`` / ``code_transform``.
paker itself doesn't ship an obfuscation pipeline. The hooks are the contract:
- ``ast_transform(tree, module_name) -> ast.AST``
Runs after ``ast.parse`` and before ``compile``. Rewrite the tree, inject
docstring decoys, strip comments / asserts — whatever you want.
- ``code_transform(code_obj, module_name) -> types.CodeType``
Runs after ``compile(..., optimize=2)`` and before ``marshal.dumps``.
Useful for bytecode-level transforms (e.g. custom metadata scrubbing).
This example shows both: an AST pass that strips every docstring and a code
pass that blanks ``co_name`` on nested code objects. Run against the bundled
``testpackage`` fixture.
"""
from __future__ import annotations
import ast
import os
import sys
import types
import paker
class _DocstringStripper(ast.NodeTransformer):
"""Remove the leading string expression from every module / class / def."""
def visit_Module(self, node):
self.generic_visit(node)
_drop_leading_docstring(node.body)
return node
def visit_FunctionDef(self, node):
self.generic_visit(node)
_drop_leading_docstring(node.body)
return node
visit_AsyncFunctionDef = visit_FunctionDef
def visit_ClassDef(self, node):
self.generic_visit(node)
_drop_leading_docstring(node.body)
return node
def _drop_leading_docstring(body: list[ast.stmt]) -> None:
if (
body
and isinstance(body[0], ast.Expr)
and isinstance(body[0].value, ast.Constant)
and isinstance(body[0].value.value, str)
):
body.pop(0)
if not body:
body.append(ast.Pass())
def strip_docstrings(tree: ast.AST, module_name: str) -> ast.AST:
"""An ``ast_transform`` — wipes every docstring in the module."""
return _DocstringStripper().visit(tree)
def blank_nested_qualnames(
code: types.CodeType,
module_name: str,
) -> types.CodeType:
"""A ``code_transform`` — clear ``co_qualname`` on nested code objects.
Complements paker's default ``strip_metadata=True`` (which also clears
``co_filename`` and top-level qualnames). Shown here as an illustration;
paker ships equivalent scrubbing out of the box.
"""
def scrub(co: types.CodeType, top: bool) -> types.CodeType:
new_consts = tuple(
scrub(c, top=False) if isinstance(c, types.CodeType) else c
for c in co.co_consts
)
repl = {"co_consts": new_consts}
if not top and hasattr(co, "co_qualname"):
repl["co_qualname"] = ""
return co.replace(**repl)
return scrub(code, top=True)
if __name__ == "__main__":
sys.path.insert(0, os.path.join(
os.path.dirname(__file__), "..", "..", "tests", "tests_data",
))
try:
for name in list(sys.modules):
if name.startswith("testpackage"):
del sys.modules[name]
# Pack with the hooks applied.
bundle = paker.dumps(
"testpackage",
ast_transform=strip_docstrings,
code_transform=blank_nested_qualnames,
)
# Load and use — the stripped docstrings don't break execution.
for name in list(sys.modules):
if name.startswith("testpackage"):
del sys.modules[name]
with paker.loads(bundle):
import testpackage
assert testpackage.Fibonacci.fib(5) == [0, 1, 1, 2, 3]
print("loaded OK; docstrings:", testpackage.Fibonacci.__doc__)
print(
"fib().__doc__:",
testpackage.Fibonacci.fib.__doc__,
)
finally:
sys.path.pop(0)