forked from dflook/python-minifier
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path__init__.py
More file actions
258 lines (198 loc) · 8.87 KB
/
__init__.py
File metadata and controls
258 lines (198 loc) · 8.87 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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
"""
This package transforms python source code strings or ast.Module Nodes into
a 'minified' representation of the same source code.
"""
import ast
import re
from python_minifier.ast_compare import CompareError, compare_ast
from python_minifier.ast_printer import print_ast
from python_minifier.module_printer import ModulePrinter
from python_minifier.rename import (
rename_literals,
bind_names,
resolve_names,
rename,
allow_rename_globals,
allow_rename_locals,
add_namespace,
)
from python_minifier.transforms.combine_imports import CombineImports
from python_minifier.transforms.constant_folding import FoldConstants
from python_minifier.transforms.remove_annotations import RemoveAnnotations
from python_minifier.transforms.remove_annotations_options import RemoveAnnotationsOptions
from python_minifier.transforms.remove_asserts import RemoveAsserts
from python_minifier.transforms.remove_debug import RemoveDebug
from python_minifier.transforms.remove_explicit_return_none import RemoveExplicitReturnNone
from python_minifier.transforms.remove_exception_brackets import remove_no_arg_exception_call
from python_minifier.transforms.remove_literal_statements import RemoveLiteralStatements
from python_minifier.transforms.remove_object_base import RemoveObject
from python_minifier.transforms.remove_pass import RemovePass
from python_minifier.transforms.remove_posargs import remove_posargs
class UnstableMinification(RuntimeError):
"""
Raised when a minified module differs from the original module in an unexpected way.
This is raised when the minifier generates source code that doesn't parse back into the
original module (after known transformations).
This should never occur and is a bug.
"""
def __init__(self, exception, source, minified):
self.exception = exception
self.source = source
self.minified = minified
def __str__(self):
return 'Minification was unstable! Please create an issue at https://github.com/dflook/python-minifier/issues'
def minify(
source,
filename=None,
remove_annotations=RemoveAnnotationsOptions(),
remove_pass=True,
remove_literal_statements=False,
combine_imports=True,
hoist_literals=True,
rename_locals=True,
preserve_locals=None,
rename_globals=False,
preserve_globals=None,
remove_object_base=True,
convert_posargs_to_args=True,
preserve_shebang=True,
remove_asserts=False,
remove_debug=False,
remove_explicit_return_none=True,
remove_builtin_exception_brackets=True,
constant_folding=True
):
"""
Minify a python module
The module is transformed according the the arguments.
If all transformation arguments are False, no transformations are made to the AST, the returned string will
parse into exactly the same module.
Using the default arguments only transformations that are always or almost always safe are enabled.
:param str source: The python module source code
:param str filename: The original source filename if known
:param remove_annotations: Configures the removal of type annotations. True removes all annotations, False removes none. RemoveAnnotationsOptions can be used to configure the removal of specific annotations.
:type remove_annotations: bool or RemoveAnnotationsOptions
:param bool remove_pass: If Pass statements should be removed where possible
:param bool remove_literal_statements: If statements consisting of a single literal should be removed, including docstrings
:param bool combine_imports: Combine adjacent import statements where possible
:param bool hoist_literals: If str and byte literals may be hoisted to the module level where possible.
:param bool rename_locals: If local names may be shortened
:param preserve_locals: Locals names to leave unchanged when rename_locals is True
:type preserve_locals: list[str]
:param bool rename_globals: If global names may be shortened
:param preserve_globals: Global names to leave unchanged when rename_globals is True
:type preserve_globals: list[str]
:param bool remove_object_base: If object as a base class may be removed
:param bool convert_posargs_to_args: If positional-only arguments will be converted to normal arguments
:param bool preserve_shebang: Keep any shebang interpreter directive from the source in the minified output
:param bool remove_asserts: If assert statements should be removed
:param bool remove_debug: If conditional statements that test '__debug__ is True' should be removed
:param bool remove_explicit_return_none: If explicit return None statements should be replaced with a bare return
:param bool remove_builtin_exception_brackets: If brackets should be removed when raising exceptions with no arguments
:param bool constant_folding: If literal expressions should be evaluated
:rtype: str
"""
filename = filename or 'python_minifier.minify source'
# This will raise if the source file can't be parsed
module = ast.parse(source, filename)
add_namespace(module)
if remove_literal_statements:
module = RemoveLiteralStatements()(module)
if combine_imports:
module = CombineImports()(module)
if isinstance(remove_annotations, bool):
remove_annotations_options = RemoveAnnotationsOptions(
remove_variable_annotations=remove_annotations,
remove_return_annotations=remove_annotations,
remove_argument_annotations=remove_annotations,
remove_class_attribute_annotations=remove_annotations,
)
elif isinstance(remove_annotations, RemoveAnnotationsOptions):
remove_annotations_options = remove_annotations
else:
raise TypeError('remove_annotations must be a bool or RemoveAnnotationsOptions')
if remove_annotations_options:
module = RemoveAnnotations(remove_annotations_options)(module)
if remove_pass:
module = RemovePass()(module)
if remove_object_base:
module = RemoveObject()(module)
if remove_asserts:
module = RemoveAsserts()(module)
if remove_debug:
module = RemoveDebug()(module)
if remove_explicit_return_none:
module = RemoveExplicitReturnNone()(module)
if constant_folding:
module = FoldConstants()(module)
bind_names(module)
resolve_names(module)
if remove_builtin_exception_brackets and not module.tainted:
remove_no_arg_exception_call(module)
if module.tainted:
rename_globals = False
rename_locals = False
allow_rename_locals(module, rename_locals, preserve_locals)
allow_rename_globals(module, rename_globals, preserve_globals)
if hoist_literals:
rename_literals(module)
rename(module, prefix_globals=not rename_globals, preserved_globals=preserve_globals)
if convert_posargs_to_args:
module = remove_posargs(module)
minified = unparse(module)
if preserve_shebang is True:
shebang_line = _find_shebang(source)
if shebang_line is not None:
return shebang_line + '\n' + minified
return minified
def _find_shebang(source):
"""
Find a shebang line in source
"""
if isinstance(source, bytes):
shebang = re.match(br'^#!.*', source)
if shebang:
return shebang.group().decode()
else:
shebang = re.match(r'^#!.*', source)
if shebang:
return shebang.group()
return None
def unparse(module):
"""
Turn a module AST into python code
This returns an exact representation of the given module,
such that it can be parsed back into the same AST.
:param module: The module to turn into python code
:type: module: :class:`ast.Module`
:rtype: str
"""
assert isinstance(module, ast.Module)
printer = ModulePrinter()
printer(module)
try:
minified_module = ast.parse(printer.code, 'python_minifier.unparse output')
except SyntaxError as syntax_error:
raise UnstableMinification(syntax_error, '', printer.code)
try:
compare_ast(module, minified_module)
except CompareError as compare_error:
raise UnstableMinification(compare_error, '', printer.code)
return printer.code
def awslambda(source, filename=None, entrypoint=None):
"""
Minify a python module for use as an AWS Lambda function
This returns a string suitable for embedding in a cloudformation template.
When minifying, all transformations are enabled.
:param str source: The python module source code
:param str filename: The original source filename if known
:param entrypoint: The lambda entrypoint function
:type entrypoint: str or NoneType
:rtype: str
"""
rename_globals = True
if entrypoint is None:
rename_globals = False
return minify(
source, filename, remove_literal_statements=True, rename_globals=rename_globals, preserve_globals=[entrypoint],
)