python-minifier 3.1.1__tar.gz → 3.3.0__tar.gz
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.
- {python_minifier-3.1.1/src/python_minifier.egg-info → python_minifier-3.3.0}/PKG-INFO +1 -1
- {python_minifier-3.1.1 → python_minifier-3.3.0}/setup.py +1 -1
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/__init__.py +16 -7
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/__init__.pyi +7 -2
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/__main__.py +19 -2
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/expression_printer.py +3 -5
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/module_printer.py +3 -3
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/rename/rename_literals.py +3 -1
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/token_printer.py +1 -1
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/constant_folding.py +91 -23
- python_minifier-3.3.0/src/python_minifier/transforms/remove_dead_branches.py +273 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_debug.py +14 -12
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/suite_transformer.py +45 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0/src/python_minifier.egg-info}/PKG-INFO +1 -1
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier.egg-info/SOURCES.txt +3 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/subprocess_compat.py +1 -1
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_combine_imports.py +19 -0
- python_minifier-3.3.0/test/test_folding.py +690 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_is_constant_node.py +2 -2
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_match.py +28 -0
- python_minifier-3.3.0/test/test_prefer_single_line.py +217 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_remove_assert.py +56 -0
- python_minifier-3.3.0/test/test_remove_dead_branches.py +695 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_remove_debug.py +157 -5
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_remove_pass.py +79 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_unicode_cli.py +6 -9
- python_minifier-3.1.1/test/test_folding.py +0 -151
- {python_minifier-3.1.1 → python_minifier-3.3.0}/LICENSE +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/MANIFEST.in +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/README.md +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/setup.cfg +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/ast_annotation/__init__.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/ast_compare.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/ast_compat.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/ast_printer.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/f_string.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/ministring.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/py.typed +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/rename/README.md +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/rename/__init__.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/rename/bind_names.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/rename/binding.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/rename/mapper.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/rename/name_generator.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/rename/renamer.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/rename/resolve_names.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/rename/util.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/t_string.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/__init__.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/combine_imports.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_annotations.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_annotations_options.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_annotations_options.pyi +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_asserts.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_exception_brackets.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_explicit_return_none.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_literal_statements.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_object_base.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_pass.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_posargs.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/util.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier.egg-info/dependency_links.txt +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier.egg-info/entry_points.txt +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier.egg-info/top_level.txt +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier.egg-info/zip-safe +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/ast_annotation/test_add_parent.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/conftest.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/helpers.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/requirements.txt +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_assignment_expressions.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_await_fstring.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_best_effort_cli.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_bind_names.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_bind_names_namedexpr.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_bind_names_python2.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_bind_names_python312.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_bind_names_python313.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_bind_names_python33.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_comprehension_rename.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_decorator_expressions.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_dict_expansion.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_fstring.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_generic_types.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_hoist_literals.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_import.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_iterable_unpacking.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_match_rename.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_name_generator.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_nonlocal.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_posargs.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_preserve_shebang.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_raw_fstring_backslash.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_raw_tstring_backslash.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_remove_annotations.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_remove_exception_brackets.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_remove_explicit_return_none.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_remove_literal_statements.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_remove_object.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_rename_builtins.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_rename_locals.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_slice.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_template_strings.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_tuple_with_bug.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_type_param_defaults.py +0 -0
- {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_utf8_encoding.py +0 -0
|
@@ -25,6 +25,7 @@ from python_minifier.transforms.constant_folding import FoldConstants
|
|
|
25
25
|
from python_minifier.transforms.remove_annotations import RemoveAnnotations
|
|
26
26
|
from python_minifier.transforms.remove_annotations_options import RemoveAnnotationsOptions
|
|
27
27
|
from python_minifier.transforms.remove_asserts import RemoveAsserts
|
|
28
|
+
from python_minifier.transforms.remove_dead_branches import RemoveDeadBranches
|
|
28
29
|
from python_minifier.transforms.remove_debug import RemoveDebug
|
|
29
30
|
from python_minifier.transforms.remove_exception_brackets import remove_no_arg_exception_call
|
|
30
31
|
from python_minifier.transforms.remove_explicit_return_none import RemoveExplicitReturnNone
|
|
@@ -72,7 +73,9 @@ def minify(
|
|
|
72
73
|
remove_debug=False,
|
|
73
74
|
remove_explicit_return_none=True,
|
|
74
75
|
remove_builtin_exception_brackets=True,
|
|
75
|
-
constant_folding=True
|
|
76
|
+
constant_folding=True,
|
|
77
|
+
prefer_single_line=False,
|
|
78
|
+
remove_dead_branches=True
|
|
76
79
|
):
|
|
77
80
|
"""
|
|
78
81
|
Minify a python module
|
|
@@ -107,6 +110,8 @@ def minify(
|
|
|
107
110
|
:param bool remove_explicit_return_none: If explicit return None statements should be replaced with a bare return
|
|
108
111
|
:param bool remove_builtin_exception_brackets: If brackets should be removed when raising exceptions with no arguments
|
|
109
112
|
:param bool constant_folding: If literal expressions should be evaluated
|
|
113
|
+
:param bool prefer_single_line: If semi-colons should be preferred over newlines where there is no difference in output size
|
|
114
|
+
:param bool remove_dead_branches: If if-statements with a constant False test should be removed
|
|
110
115
|
|
|
111
116
|
:rtype: str
|
|
112
117
|
|
|
@@ -153,12 +158,15 @@ def minify(
|
|
|
153
158
|
if remove_debug:
|
|
154
159
|
module = RemoveDebug()(module)
|
|
155
160
|
|
|
156
|
-
if remove_explicit_return_none:
|
|
157
|
-
module = RemoveExplicitReturnNone()(module)
|
|
158
|
-
|
|
159
161
|
if constant_folding:
|
|
160
162
|
module = FoldConstants()(module)
|
|
161
163
|
|
|
164
|
+
if remove_dead_branches:
|
|
165
|
+
module = RemoveDeadBranches()(module)
|
|
166
|
+
|
|
167
|
+
if remove_explicit_return_none:
|
|
168
|
+
module = RemoveExplicitReturnNone()(module)
|
|
169
|
+
|
|
162
170
|
bind_names(module)
|
|
163
171
|
resolve_names(module)
|
|
164
172
|
|
|
@@ -192,7 +200,7 @@ def minify(
|
|
|
192
200
|
if convert_posargs_to_args:
|
|
193
201
|
module = remove_posargs(module)
|
|
194
202
|
|
|
195
|
-
minified = unparse(module)
|
|
203
|
+
minified = unparse(module, prefer_single_line=prefer_single_line)
|
|
196
204
|
|
|
197
205
|
if preserve_shebang is True:
|
|
198
206
|
shebang_line = _find_shebang(source)
|
|
@@ -219,7 +227,7 @@ def _find_shebang(source):
|
|
|
219
227
|
return None
|
|
220
228
|
|
|
221
229
|
|
|
222
|
-
def unparse(module):
|
|
230
|
+
def unparse(module, prefer_single_line=False):
|
|
223
231
|
"""
|
|
224
232
|
Turn a module AST into python code
|
|
225
233
|
|
|
@@ -228,13 +236,14 @@ def unparse(module):
|
|
|
228
236
|
|
|
229
237
|
:param module: The module to turn into python code
|
|
230
238
|
:type: module: :class:`ast.Module`
|
|
239
|
+
:param bool prefer_single_line: If semi-colons should be preferred over newlines where there is no difference in output size
|
|
231
240
|
:rtype: str
|
|
232
241
|
|
|
233
242
|
"""
|
|
234
243
|
|
|
235
244
|
assert isinstance(module, ast.Module)
|
|
236
245
|
|
|
237
|
-
printer = ModulePrinter()
|
|
246
|
+
printer = ModulePrinter(prefer_single_line=prefer_single_line)
|
|
238
247
|
printer(module)
|
|
239
248
|
|
|
240
249
|
try:
|
|
@@ -28,11 +28,16 @@ def minify(
|
|
|
28
28
|
remove_debug: bool = ...,
|
|
29
29
|
remove_explicit_return_none: bool = ...,
|
|
30
30
|
remove_builtin_exception_brackets: bool = ...,
|
|
31
|
-
constant_folding: bool =
|
|
31
|
+
constant_folding: bool = ...,
|
|
32
|
+
prefer_single_line: bool = ...,
|
|
33
|
+
remove_dead_branches: bool = ...
|
|
32
34
|
) -> Text: ...
|
|
33
35
|
|
|
34
36
|
|
|
35
|
-
def unparse(
|
|
37
|
+
def unparse(
|
|
38
|
+
module: ast.Module,
|
|
39
|
+
prefer_single_line: bool = ...
|
|
40
|
+
) -> Text: ...
|
|
36
41
|
|
|
37
42
|
|
|
38
43
|
def awslambda(
|
|
@@ -10,7 +10,6 @@ from python_minifier.transforms.remove_annotations_options import RemoveAnnotati
|
|
|
10
10
|
|
|
11
11
|
class MinificationNotBeneficialError(Exception):
|
|
12
12
|
"""Raised when minification results in larger output than the original."""
|
|
13
|
-
pass
|
|
14
13
|
|
|
15
14
|
def stdout_write_bytes(data):
|
|
16
15
|
"""Write bytes to stdout with proper Python 2.7/3.x compatibility."""
|
|
@@ -140,6 +139,13 @@ def parse_args():
|
|
|
140
139
|
dest='in_place'
|
|
141
140
|
)
|
|
142
141
|
|
|
142
|
+
parser.add_argument(
|
|
143
|
+
'--prefer-single-line',
|
|
144
|
+
action='store_true',
|
|
145
|
+
help='Prefer multiple statements on a single line separated by semicolons, instead of newlines, where there is no difference in output size',
|
|
146
|
+
dest='prefer_single_line',
|
|
147
|
+
)
|
|
148
|
+
|
|
143
149
|
# Minification arguments
|
|
144
150
|
minification_options = parser.add_argument_group('minification options', 'Options that affect how the source is minified')
|
|
145
151
|
minification_options.add_argument(
|
|
@@ -243,6 +249,12 @@ def parse_args():
|
|
|
243
249
|
help='Disable evaluating literal expressions',
|
|
244
250
|
dest='constant_folding',
|
|
245
251
|
)
|
|
252
|
+
minification_options.add_argument(
|
|
253
|
+
'--no-remove-dead-branches',
|
|
254
|
+
action='store_false',
|
|
255
|
+
help='Disable removing branches that are never executed',
|
|
256
|
+
dest='remove_dead_branches',
|
|
257
|
+
)
|
|
246
258
|
|
|
247
259
|
annotation_options = parser.add_argument_group('remove annotations options', 'Options that affect how annotations are removed')
|
|
248
260
|
annotation_options.add_argument(
|
|
@@ -298,6 +310,10 @@ def parse_args():
|
|
|
298
310
|
sys.stderr.write('error: --remove-class-attribute-annotations would do nothing when used with --no-remove-annotations\n')
|
|
299
311
|
sys.exit(1)
|
|
300
312
|
|
|
313
|
+
if args.remove_debug and not args.remove_dead_branches:
|
|
314
|
+
sys.stderr.write('error: --remove-debug would do nothing when used with --no-remove-dead-branches\n')
|
|
315
|
+
sys.exit(1)
|
|
316
|
+
|
|
301
317
|
return args
|
|
302
318
|
|
|
303
319
|
|
|
@@ -373,7 +389,8 @@ def do_minify(source, filename, minification_args):
|
|
|
373
389
|
remove_debug=minification_args.remove_debug,
|
|
374
390
|
remove_explicit_return_none=minification_args.remove_explicit_return_none,
|
|
375
391
|
remove_builtin_exception_brackets=minification_args.remove_exception_brackets,
|
|
376
|
-
constant_folding=minification_args.constant_folding
|
|
392
|
+
constant_folding=minification_args.constant_folding,
|
|
393
|
+
prefer_single_line=minification_args.prefer_single_line,
|
|
377
394
|
)
|
|
378
395
|
|
|
379
396
|
# Encode minified result to bytes for comparison and output
|
|
@@ -11,7 +11,7 @@ class ExpressionPrinter(object):
|
|
|
11
11
|
Builds the smallest possible exact representation of an ast
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
def __init__(self):
|
|
14
|
+
def __init__(self, prefer_single_line=False):
|
|
15
15
|
|
|
16
16
|
self.precedences = {
|
|
17
17
|
'Lambda': 2, # Lambda
|
|
@@ -34,7 +34,7 @@ class ExpressionPrinter(object):
|
|
|
34
34
|
'Tuple': 18, 'Set': 18, 'List': 18, 'Dict': 18, 'ListComp': 18, 'SetComp': 18, 'DictComp': 18, 'GeneratorExp': 18, # Container
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
self.printer = TokenPrinter()
|
|
37
|
+
self.printer = TokenPrinter(prefer_single_line=prefer_single_line)
|
|
38
38
|
|
|
39
39
|
def __call__(self, module):
|
|
40
40
|
"""
|
|
@@ -221,9 +221,7 @@ class ExpressionPrinter(object):
|
|
|
221
221
|
right_precedence = self.precedence(node.operand)
|
|
222
222
|
op_precedence = self.precedence(node)
|
|
223
223
|
|
|
224
|
-
if right_precedence != 0 and
|
|
225
|
-
(op_precedence > right_precedence)
|
|
226
|
-
):
|
|
224
|
+
if right_precedence != 0 and op_precedence > right_precedence:
|
|
227
225
|
self.printer.delimiter('(')
|
|
228
226
|
self._expression(node.operand)
|
|
229
227
|
self.printer.delimiter(')')
|
|
@@ -11,8 +11,8 @@ class ModulePrinter(ExpressionPrinter):
|
|
|
11
11
|
Builds the smallest possible exact representation of an ast
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
def __init__(self, indent_char='\t'):
|
|
15
|
-
super(ModulePrinter, self).__init__()
|
|
14
|
+
def __init__(self, indent_char='\t', prefer_single_line=False):
|
|
15
|
+
super(ModulePrinter, self).__init__(prefer_single_line=prefer_single_line)
|
|
16
16
|
self.indent_char = indent_char
|
|
17
17
|
|
|
18
18
|
def __call__(self, module):
|
|
@@ -602,7 +602,7 @@ class ModulePrinter(ExpressionPrinter):
|
|
|
602
602
|
|
|
603
603
|
if node.guard is not None:
|
|
604
604
|
self.printer.keyword('if')
|
|
605
|
-
self.
|
|
605
|
+
self._expression(node.guard)
|
|
606
606
|
|
|
607
607
|
self.printer.delimiter(':')
|
|
608
608
|
self._suite(node.body)
|
{python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/rename/rename_literals.py
RENAMED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from collections import OrderedDict
|
|
2
|
+
|
|
1
3
|
import python_minifier.ast_compat as ast
|
|
2
4
|
from python_minifier.ast_annotation import get_parent, set_parent
|
|
3
5
|
|
|
@@ -118,7 +120,7 @@ class HoistLiterals(NodeVisitor):
|
|
|
118
120
|
def __call__(self, module, ignore_slots=True):
|
|
119
121
|
self.module = module
|
|
120
122
|
self._ignore_slots = ignore_slots
|
|
121
|
-
self._hoisted =
|
|
123
|
+
self._hoisted = OrderedDict()
|
|
122
124
|
self.visit(module)
|
|
123
125
|
self.place_bindings()
|
|
124
126
|
|
|
@@ -306,7 +306,7 @@ class TokenPrinter(object):
|
|
|
306
306
|
def end_statement(self):
|
|
307
307
|
""" End a statement with a newline, or a semi-colon if it saves characters. """
|
|
308
308
|
|
|
309
|
-
if self.indent == 0:
|
|
309
|
+
if self.indent == 0 and not self._prefer_single_line:
|
|
310
310
|
self.newline()
|
|
311
311
|
else:
|
|
312
312
|
if self._code[-1] != ';':
|
{python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/constant_folding.py
RENAMED
|
@@ -10,40 +10,46 @@ from python_minifier.transforms.suite_transformer import SuiteTransformer
|
|
|
10
10
|
from python_minifier.util import is_constant_node
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
def is_foldable_constant(node):
|
|
14
14
|
"""
|
|
15
|
-
|
|
15
|
+
Check if a node is a constant expression that can participate in folding.
|
|
16
|
+
|
|
17
|
+
We can assume that children have already been folded, so foldable constants are either:
|
|
18
|
+
- Simple literals (Num, NameConstant)
|
|
19
|
+
- UnaryOp(USub/Invert) on a Num - these don't fold to shorter forms,
|
|
20
|
+
so they remain after child visiting. UAdd and Not would have been
|
|
21
|
+
folded away since they always produce shorter results.
|
|
16
22
|
"""
|
|
23
|
+
if is_constant_node(node, (ast.Num, ast.NameConstant)):
|
|
24
|
+
return True
|
|
17
25
|
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
if isinstance(node, ast.UnaryOp):
|
|
27
|
+
if isinstance(node.op, (ast.USub, ast.Invert)):
|
|
28
|
+
return is_constant_node(node.operand, ast.Num)
|
|
20
29
|
|
|
21
|
-
|
|
30
|
+
return False
|
|
22
31
|
|
|
23
|
-
node.left = self.visit(node.left)
|
|
24
|
-
node.right = self.visit(node.right)
|
|
25
32
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if not is_constant_node(node.right, (ast.Num, ast.NameConstant)):
|
|
31
|
-
return node
|
|
32
|
-
|
|
33
|
-
if isinstance(node.op, ast.Div):
|
|
34
|
-
# Folding div is subtle, since it can have different results in Python 2 and Python 3
|
|
35
|
-
# Do this once target version options have been implemented
|
|
36
|
-
return node
|
|
33
|
+
class FoldConstants(SuiteTransformer):
|
|
34
|
+
"""
|
|
35
|
+
Fold Constants if it would reduce the size of the source
|
|
36
|
+
"""
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
# It can also be slow to evaluate
|
|
41
|
-
return node
|
|
38
|
+
def __init__(self):
|
|
39
|
+
super(FoldConstants, self).__init__()
|
|
42
40
|
|
|
41
|
+
def fold(self, node):
|
|
43
42
|
# Evaluate the expression
|
|
44
43
|
try:
|
|
45
44
|
original_expression = unparse_expression(node)
|
|
46
|
-
|
|
45
|
+
|
|
46
|
+
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Invert) and is_constant_node(node.operand, ast.NameConstant) and isinstance(node.operand.value, bool):
|
|
47
|
+
# ~ on a bool is deprecated from python 3.12, to be removed in 3.16.
|
|
48
|
+
if sys.version_info >= (3, 16):
|
|
49
|
+
return node
|
|
50
|
+
original_value = ~int(node.operand.value)
|
|
51
|
+
else:
|
|
52
|
+
original_value = safe_eval(original_expression)
|
|
47
53
|
except Exception:
|
|
48
54
|
return node
|
|
49
55
|
|
|
@@ -96,6 +102,68 @@ class FoldConstants(SuiteTransformer):
|
|
|
96
102
|
# New representation is shorter and has the same value, so use it
|
|
97
103
|
return self.add_child(new_node, get_parent(node), node.namespace)
|
|
98
104
|
|
|
105
|
+
def visit_BinOp(self, node):
|
|
106
|
+
|
|
107
|
+
node.left = self.visit(node.left)
|
|
108
|
+
node.right = self.visit(node.right)
|
|
109
|
+
|
|
110
|
+
# Check this is a constant expression that could be folded
|
|
111
|
+
# We don't try to fold strings or bytes, since they have probably been arranged this way to make the source shorter and we are unlikely to beat that
|
|
112
|
+
if not is_foldable_constant(node.left):
|
|
113
|
+
return node
|
|
114
|
+
if not is_foldable_constant(node.right):
|
|
115
|
+
return node
|
|
116
|
+
|
|
117
|
+
if isinstance(node.op, ast.Div):
|
|
118
|
+
# Folding div is subtle, since it can have different results in Python 2 and Python 3
|
|
119
|
+
# Do this once target version options have been implemented
|
|
120
|
+
return node
|
|
121
|
+
|
|
122
|
+
if isinstance(node.op, ast.Pow):
|
|
123
|
+
# This can be folded, but it is unlikely to reduce the size of the source
|
|
124
|
+
# It can also be slow to evaluate
|
|
125
|
+
return node
|
|
126
|
+
|
|
127
|
+
return self.fold(node)
|
|
128
|
+
|
|
129
|
+
def visit_BoolOp(self, node):
|
|
130
|
+
node.values = [self.visit(v) for v in node.values]
|
|
131
|
+
|
|
132
|
+
# Check this is a constant expression that could be folded
|
|
133
|
+
if not all(is_foldable_constant(v) for v in node.values):
|
|
134
|
+
return node
|
|
135
|
+
|
|
136
|
+
return self.fold(node)
|
|
137
|
+
|
|
138
|
+
def visit_Compare(self, node):
|
|
139
|
+
node.left = self.visit(node.left)
|
|
140
|
+
node.comparators = [self.visit(c) for c in node.comparators]
|
|
141
|
+
|
|
142
|
+
operands = [node.left] + node.comparators
|
|
143
|
+
if not all(is_foldable_constant(n) for n in operands):
|
|
144
|
+
return node
|
|
145
|
+
|
|
146
|
+
if any(isinstance(op, (ast.Is, ast.IsNot)) for op in node.ops):
|
|
147
|
+
# Identity is only language-guaranteed for the singletons
|
|
148
|
+
if not all(is_constant_node(n, ast.NameConstant) for n in operands):
|
|
149
|
+
return node
|
|
150
|
+
|
|
151
|
+
return self.fold(node)
|
|
152
|
+
|
|
153
|
+
def visit_UnaryOp(self, node):
|
|
154
|
+
|
|
155
|
+
node.operand = self.visit(node.operand)
|
|
156
|
+
|
|
157
|
+
# Only fold if the operand is a foldable constant
|
|
158
|
+
if not is_foldable_constant(node.operand):
|
|
159
|
+
return node
|
|
160
|
+
|
|
161
|
+
# Only fold these unary operators
|
|
162
|
+
if not isinstance(node.op, (ast.USub, ast.UAdd, ast.Invert, ast.Not)):
|
|
163
|
+
return node
|
|
164
|
+
|
|
165
|
+
return self.fold(node)
|
|
166
|
+
|
|
99
167
|
|
|
100
168
|
def equal_value_and_type(a, b):
|
|
101
169
|
if type(a) != type(b):
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import python_minifier.ast_compat as ast
|
|
2
|
+
|
|
3
|
+
from python_minifier.transforms.suite_transformer import SuiteTransformer
|
|
4
|
+
from python_minifier.util import is_constant_node
|
|
5
|
+
|
|
6
|
+
class NamespaceProperties(object):
|
|
7
|
+
"""
|
|
8
|
+
The analysed properties of a namespace, including the names defined in it and whether it is a generator
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
self.local_names = set()
|
|
13
|
+
self.nonlocal_names = set()
|
|
14
|
+
self.global_names = set()
|
|
15
|
+
self.is_generator = False
|
|
16
|
+
|
|
17
|
+
def __eq__(self, other):
|
|
18
|
+
assert isinstance(other, NamespaceProperties)
|
|
19
|
+
return (
|
|
20
|
+
self.local_names == other.local_names and
|
|
21
|
+
self.nonlocal_names == other.nonlocal_names and
|
|
22
|
+
self.global_names == other.global_names and
|
|
23
|
+
self.is_generator is other.is_generator
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def __ne__(self, other):
|
|
27
|
+
return not self == other
|
|
28
|
+
|
|
29
|
+
def __repr__(self):
|
|
30
|
+
return 'NamespaceProperties(local_names=%r, nonlocal_names=%r, global_names=%r, is_generator=%r)' % (
|
|
31
|
+
self.local_names, self.nonlocal_names, self.global_names, self.is_generator
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def in_function_scope(namespace):
|
|
36
|
+
"""
|
|
37
|
+
Is this namespace nested in a function scope?
|
|
38
|
+
|
|
39
|
+
A name bound anywhere in a class body - even in unreachable code - makes
|
|
40
|
+
loads of that name in the class body fall through to the global scope,
|
|
41
|
+
instead of an enclosing function scope cell. Removing such a binding is
|
|
42
|
+
only safe when there is no enclosing function scope to fall through to.
|
|
43
|
+
"""
|
|
44
|
+
while not isinstance(namespace, ast.Module):
|
|
45
|
+
if isinstance(namespace, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
46
|
+
return True
|
|
47
|
+
namespace = namespace.namespace
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class RemoveDeadBranches(SuiteTransformer):
|
|
52
|
+
"""
|
|
53
|
+
Remove if statements where the condition tests False
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __call__(self, node):
|
|
57
|
+
# Suites that have been condemned during this run, but may not have been
|
|
58
|
+
# detached from the tree yet. Shared across all suites so that decisions
|
|
59
|
+
# in nested or sibling suites don't count already-removed bindings.
|
|
60
|
+
self._removed_suites = []
|
|
61
|
+
return self.visit(node)
|
|
62
|
+
|
|
63
|
+
def get_namespace_properties(self, namespace, removed_suites=None):
|
|
64
|
+
"""
|
|
65
|
+
Gather the binding properties of a namespace
|
|
66
|
+
|
|
67
|
+
Walks the namespace and collects the names it binds (as local, nonlocal
|
|
68
|
+
and global name sets) and whether it is a generator.
|
|
69
|
+
|
|
70
|
+
:param namespace: The namespace node to analyse
|
|
71
|
+
:param removed_suites: Suites to skip while walking, for
|
|
72
|
+
branches that are being removed.
|
|
73
|
+
:type removed_suites: list or None
|
|
74
|
+
:rtype: NamespaceProperties
|
|
75
|
+
"""
|
|
76
|
+
if removed_suites is None:
|
|
77
|
+
removed_suites = []
|
|
78
|
+
|
|
79
|
+
properties = NamespaceProperties()
|
|
80
|
+
|
|
81
|
+
def explore_namespace(node):
|
|
82
|
+
binds_here = getattr(node, 'namespace', namespace) is namespace
|
|
83
|
+
|
|
84
|
+
if binds_here:
|
|
85
|
+
if isinstance(node, ast.Name):
|
|
86
|
+
if isinstance(node.ctx, (ast.Store, ast.Del, ast.Param)):
|
|
87
|
+
properties.local_names.add(node.id)
|
|
88
|
+
elif isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
89
|
+
properties.local_names.add(node.name)
|
|
90
|
+
elif isinstance(node, ast.alias):
|
|
91
|
+
# What about import *
|
|
92
|
+
|
|
93
|
+
if node.asname is not None:
|
|
94
|
+
properties.local_names.add(node.asname)
|
|
95
|
+
else:
|
|
96
|
+
properties.local_names.add(node.name.split('.')[0])
|
|
97
|
+
elif isinstance(node, ast.arguments):
|
|
98
|
+
if isinstance(node.vararg, str):
|
|
99
|
+
properties.local_names.add(node.vararg)
|
|
100
|
+
if isinstance(node.kwarg, str):
|
|
101
|
+
properties.local_names.add(node.kwarg)
|
|
102
|
+
elif isinstance(node, ast.arg):
|
|
103
|
+
properties.local_names.add(node.arg)
|
|
104
|
+
elif isinstance(node, ast.ExceptHandler):
|
|
105
|
+
if isinstance(node.name, str):
|
|
106
|
+
properties.local_names.add(node.name)
|
|
107
|
+
|
|
108
|
+
elif isinstance(node, ast.Global):
|
|
109
|
+
properties.global_names.update(node.names)
|
|
110
|
+
elif isinstance(node, ast.Nonlocal):
|
|
111
|
+
properties.nonlocal_names.update(node.names)
|
|
112
|
+
|
|
113
|
+
elif isinstance(node, ast.MatchAs):
|
|
114
|
+
if isinstance(node.name, str):
|
|
115
|
+
properties.local_names.add(node.name)
|
|
116
|
+
elif isinstance(node, ast.MatchStar):
|
|
117
|
+
if isinstance(node.name, str):
|
|
118
|
+
properties.local_names.add(node.name)
|
|
119
|
+
elif isinstance(node, ast.MatchMapping):
|
|
120
|
+
if isinstance(node.rest, str):
|
|
121
|
+
properties.local_names.add(node.rest)
|
|
122
|
+
|
|
123
|
+
elif isinstance(node, ast.TypeVar):
|
|
124
|
+
properties.local_names.add(node.name)
|
|
125
|
+
elif isinstance(node, ast.TypeVarTuple):
|
|
126
|
+
properties.local_names.add(node.name)
|
|
127
|
+
elif isinstance(node, ast.ParamSpec):
|
|
128
|
+
properties.local_names.add(node.name)
|
|
129
|
+
|
|
130
|
+
elif isinstance(node, (ast.Yield, ast.YieldFrom)):
|
|
131
|
+
properties.is_generator = True
|
|
132
|
+
|
|
133
|
+
# Skip recursing into the body field of child namespaces, as they can only affect the child namespace
|
|
134
|
+
# Other fields (decorators, annotations, arguments...) can affect this namespace,
|
|
135
|
+
# so we do want to recurse into them
|
|
136
|
+
skip_body = node is not namespace and isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda, ast.ClassDef))
|
|
137
|
+
|
|
138
|
+
for name, field in ast.iter_fields(node):
|
|
139
|
+
if field in removed_suites:
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
if skip_body and name == 'body':
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
if isinstance(field, ast.AST):
|
|
146
|
+
explore_namespace(field)
|
|
147
|
+
elif isinstance(field, list):
|
|
148
|
+
for item in field:
|
|
149
|
+
if isinstance(item, ast.AST):
|
|
150
|
+
explore_namespace(item)
|
|
151
|
+
|
|
152
|
+
explore_namespace(namespace)
|
|
153
|
+
|
|
154
|
+
return properties
|
|
155
|
+
|
|
156
|
+
def changes_semantics(self, namespace, candidate_suite):
|
|
157
|
+
"""
|
|
158
|
+
Does removing this branch change the semantics of the program?
|
|
159
|
+
|
|
160
|
+
:param namespace: The namespace of the If statement
|
|
161
|
+
:param candidate_suite: The branch that is being removed
|
|
162
|
+
:return: True if removing the branch changes the semantics of the program, False otherwise
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
# Gather properties in the namespace, excluding suites already condemned
|
|
166
|
+
properties = self.get_namespace_properties(namespace, removed_suites=self._removed_suites)
|
|
167
|
+
|
|
168
|
+
# Gather properties in the namespace, additionally excluding the candidate branch
|
|
169
|
+
candidate_properties = self.get_namespace_properties(namespace, removed_suites=[candidate_suite] + self._removed_suites)
|
|
170
|
+
|
|
171
|
+
declarations_changed = (
|
|
172
|
+
properties.is_generator != candidate_properties.is_generator
|
|
173
|
+
or properties.nonlocal_names != candidate_properties.nonlocal_names
|
|
174
|
+
or properties.global_names != candidate_properties.global_names
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if isinstance(namespace, ast.Module):
|
|
178
|
+
# The module namespace is dynamic, removing unreachable bindings doesn't
|
|
179
|
+
# change their resolution behaviour, so we can safely remove them.
|
|
180
|
+
return declarations_changed
|
|
181
|
+
elif isinstance(namespace, ast.ClassDef) and not in_function_scope(namespace):
|
|
182
|
+
# Class namespaces are also dynamic, but a binding in the class body stops
|
|
183
|
+
# loads deferring to an enclosing function scope cell, so unreachable local
|
|
184
|
+
# bindings can only be ignored when no function scope encloses the class.
|
|
185
|
+
return declarations_changed
|
|
186
|
+
else:
|
|
187
|
+
return properties != candidate_properties
|
|
188
|
+
|
|
189
|
+
def constant_test(self, node):
|
|
190
|
+
"""
|
|
191
|
+
The constant truth value of an if statement's test, or None
|
|
192
|
+
|
|
193
|
+
RemoveDebug marks if statements whose __debug__ test it has resolved
|
|
194
|
+
with a resolved_test attribute. Otherwise only the True and False
|
|
195
|
+
constants are recognised - before python 3.4 they are reassignable
|
|
196
|
+
names, so nothing is recognised.
|
|
197
|
+
|
|
198
|
+
:param node: The statement to examine
|
|
199
|
+
:type node: ast.AST
|
|
200
|
+
:rtype: bool or None
|
|
201
|
+
"""
|
|
202
|
+
if not isinstance(node, ast.If):
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
if hasattr(node, 'resolved_test'):
|
|
206
|
+
if node.resolved_test is True or node.resolved_test is False:
|
|
207
|
+
return node.resolved_test
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
if is_constant_node(node.test, ast.NameConstant) and isinstance(node.test.value, bool):
|
|
211
|
+
return node.test.value
|
|
212
|
+
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
def remove_false_branches(self, node_list):
|
|
216
|
+
suite = []
|
|
217
|
+
|
|
218
|
+
for node in node_list:
|
|
219
|
+
condition = self.constant_test(node)
|
|
220
|
+
|
|
221
|
+
# A branch of a constant test can be removed, if that doesn't change
|
|
222
|
+
# the semantics of the program
|
|
223
|
+
|
|
224
|
+
if condition is True and not self.changes_semantics(node.namespace, node.orelse):
|
|
225
|
+
# The else branch is dead, keep only the body
|
|
226
|
+
self._removed_suites.append(node.orelse)
|
|
227
|
+
suite.extend(self.remove_false_branches(node.body))
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
if condition is False and not self.changes_semantics(node.namespace, node.body):
|
|
231
|
+
# The body is dead, keep only the else branch
|
|
232
|
+
self._removed_suites.append(node.body)
|
|
233
|
+
suite.extend(self.remove_false_branches(node.orelse))
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
node = self.visit(node)
|
|
237
|
+
|
|
238
|
+
# An else suite that dead branch removal (or another transform) emptied
|
|
239
|
+
# or reduced to the 0 padding can be omitted.
|
|
240
|
+
orelse = getattr(node, 'orelse', None)
|
|
241
|
+
if orelse and self._is_empty_suite(orelse):
|
|
242
|
+
node.orelse = []
|
|
243
|
+
|
|
244
|
+
suite.append(node)
|
|
245
|
+
|
|
246
|
+
return suite
|
|
247
|
+
|
|
248
|
+
def suite(self, node_list, parent):
|
|
249
|
+
|
|
250
|
+
without_dead_branches = self.remove_false_branches(node_list)
|
|
251
|
+
|
|
252
|
+
if len(without_dead_branches) == 0:
|
|
253
|
+
if isinstance(parent, ast.Module):
|
|
254
|
+
return []
|
|
255
|
+
else:
|
|
256
|
+
return [self.add_child(ast.Expr(value=ast.Num(0)), parent=parent)]
|
|
257
|
+
|
|
258
|
+
return without_dead_branches
|
|
259
|
+
|
|
260
|
+
def _is_empty_suite(self, suite):
|
|
261
|
+
"""
|
|
262
|
+
Is the suite empty?
|
|
263
|
+
|
|
264
|
+
An empty suite is either a zero length list, or a list containing a single expression statement that is the constant 0.
|
|
265
|
+
The constant 0 is used by various transforms as a placeholder for an empty suite, because not all suites can be empty (e.g. the body of a function or class).
|
|
266
|
+
|
|
267
|
+
:param suite:
|
|
268
|
+
:type suite: list
|
|
269
|
+
:rtype: bool
|
|
270
|
+
"""
|
|
271
|
+
if len(suite) == 0:
|
|
272
|
+
return True
|
|
273
|
+
return len(suite) == 1 and isinstance(suite[0], ast.Expr) and is_constant_node(suite[0].value, ast.Num) and suite[0].value.n == 0
|