python-minifier 3.2.0__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.2.0/src/python_minifier.egg-info → python_minifier-3.3.0}/PKG-INFO +1 -1
- {python_minifier-3.2.0 → python_minifier-3.3.0}/setup.py +1 -1
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/__init__.py +9 -3
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/__init__.pyi +2 -1
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/__main__.py +10 -1
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/expression_printer.py +1 -3
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/module_printer.py +1 -1
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/rename/rename_literals.py +3 -1
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/constant_folding.py +33 -2
- python_minifier-3.3.0/src/python_minifier/transforms/remove_dead_branches.py +273 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_debug.py +14 -12
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/suite_transformer.py +45 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0/src/python_minifier.egg-info}/PKG-INFO +1 -1
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier.egg-info/SOURCES.txt +2 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/subprocess_compat.py +1 -1
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_combine_imports.py +19 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_folding.py +132 -2
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_is_constant_node.py +2 -2
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_match.py +28 -0
- {python_minifier-3.2.0 → 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.2.0 → python_minifier-3.3.0}/test/test_remove_debug.py +157 -5
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_remove_pass.py +79 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_unicode_cli.py +6 -9
- {python_minifier-3.2.0 → python_minifier-3.3.0}/LICENSE +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/MANIFEST.in +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/README.md +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/setup.cfg +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/ast_annotation/__init__.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/ast_compare.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/ast_compat.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/ast_printer.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/f_string.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/ministring.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/py.typed +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/rename/README.md +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/rename/__init__.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/rename/bind_names.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/rename/binding.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/rename/mapper.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/rename/name_generator.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/rename/renamer.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/rename/resolve_names.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/rename/util.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/t_string.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/token_printer.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/__init__.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/combine_imports.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_annotations.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_annotations_options.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_annotations_options.pyi +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_asserts.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_exception_brackets.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_explicit_return_none.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_literal_statements.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_object_base.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_pass.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_posargs.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/util.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier.egg-info/dependency_links.txt +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier.egg-info/entry_points.txt +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier.egg-info/top_level.txt +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier.egg-info/zip-safe +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/ast_annotation/test_add_parent.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/conftest.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/helpers.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/requirements.txt +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_assignment_expressions.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_await_fstring.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_best_effort_cli.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_bind_names.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_bind_names_namedexpr.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_bind_names_python2.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_bind_names_python312.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_bind_names_python313.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_bind_names_python33.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_comprehension_rename.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_decorator_expressions.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_dict_expansion.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_fstring.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_generic_types.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_hoist_literals.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_import.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_iterable_unpacking.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_match_rename.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_name_generator.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_nonlocal.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_posargs.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_prefer_single_line.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_preserve_shebang.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_raw_fstring_backslash.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_raw_tstring_backslash.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_remove_annotations.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_remove_exception_brackets.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_remove_explicit_return_none.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_remove_literal_statements.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_remove_object.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_rename_builtins.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_rename_locals.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_slice.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_template_strings.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_tuple_with_bug.py +0 -0
- {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_type_param_defaults.py +0 -0
- {python_minifier-3.2.0 → 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
|
|
@@ -74,6 +75,7 @@ def minify(
|
|
|
74
75
|
remove_builtin_exception_brackets=True,
|
|
75
76
|
constant_folding=True,
|
|
76
77
|
prefer_single_line=False,
|
|
78
|
+
remove_dead_branches=True
|
|
77
79
|
):
|
|
78
80
|
"""
|
|
79
81
|
Minify a python module
|
|
@@ -109,6 +111,7 @@ def minify(
|
|
|
109
111
|
:param bool remove_builtin_exception_brackets: If brackets should be removed when raising exceptions with no arguments
|
|
110
112
|
:param bool constant_folding: If literal expressions should be evaluated
|
|
111
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
|
|
112
115
|
|
|
113
116
|
:rtype: str
|
|
114
117
|
|
|
@@ -155,12 +158,15 @@ def minify(
|
|
|
155
158
|
if remove_debug:
|
|
156
159
|
module = RemoveDebug()(module)
|
|
157
160
|
|
|
158
|
-
if remove_explicit_return_none:
|
|
159
|
-
module = RemoveExplicitReturnNone()(module)
|
|
160
|
-
|
|
161
161
|
if constant_folding:
|
|
162
162
|
module = FoldConstants()(module)
|
|
163
163
|
|
|
164
|
+
if remove_dead_branches:
|
|
165
|
+
module = RemoveDeadBranches()(module)
|
|
166
|
+
|
|
167
|
+
if remove_explicit_return_none:
|
|
168
|
+
module = RemoveExplicitReturnNone()(module)
|
|
169
|
+
|
|
164
170
|
bind_names(module)
|
|
165
171
|
resolve_names(module)
|
|
166
172
|
|
|
@@ -29,7 +29,8 @@ def minify(
|
|
|
29
29
|
remove_explicit_return_none: bool = ...,
|
|
30
30
|
remove_builtin_exception_brackets: bool = ...,
|
|
31
31
|
constant_folding: bool = ...,
|
|
32
|
-
prefer_single_line: bool =
|
|
32
|
+
prefer_single_line: bool = ...,
|
|
33
|
+
remove_dead_branches: bool = ...
|
|
33
34
|
) -> Text: ...
|
|
34
35
|
|
|
35
36
|
|
|
@@ -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."""
|
|
@@ -250,6 +249,12 @@ def parse_args():
|
|
|
250
249
|
help='Disable evaluating literal expressions',
|
|
251
250
|
dest='constant_folding',
|
|
252
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
|
+
)
|
|
253
258
|
|
|
254
259
|
annotation_options = parser.add_argument_group('remove annotations options', 'Options that affect how annotations are removed')
|
|
255
260
|
annotation_options.add_argument(
|
|
@@ -305,6 +310,10 @@ def parse_args():
|
|
|
305
310
|
sys.stderr.write('error: --remove-class-attribute-annotations would do nothing when used with --no-remove-annotations\n')
|
|
306
311
|
sys.exit(1)
|
|
307
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
|
+
|
|
308
317
|
return args
|
|
309
318
|
|
|
310
319
|
|
|
@@ -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(')')
|
{python_minifier-3.2.0 → 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
|
|
{python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/constant_folding.py
RENAMED
|
@@ -14,7 +14,7 @@ def is_foldable_constant(node):
|
|
|
14
14
|
"""
|
|
15
15
|
Check if a node is a constant expression that can participate in folding.
|
|
16
16
|
|
|
17
|
-
We can
|
|
17
|
+
We can assume that children have already been folded, so foldable constants are either:
|
|
18
18
|
- Simple literals (Num, NameConstant)
|
|
19
19
|
- UnaryOp(USub/Invert) on a Num - these don't fold to shorter forms,
|
|
20
20
|
so they remain after child visiting. UAdd and Not would have been
|
|
@@ -42,7 +42,14 @@ class FoldConstants(SuiteTransformer):
|
|
|
42
42
|
# Evaluate the expression
|
|
43
43
|
try:
|
|
44
44
|
original_expression = unparse_expression(node)
|
|
45
|
-
|
|
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)
|
|
46
53
|
except Exception:
|
|
47
54
|
return node
|
|
48
55
|
|
|
@@ -119,6 +126,30 @@ class FoldConstants(SuiteTransformer):
|
|
|
119
126
|
|
|
120
127
|
return self.fold(node)
|
|
121
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
|
+
|
|
122
153
|
def visit_UnaryOp(self, node):
|
|
123
154
|
|
|
124
155
|
node.operand = self.visit(node.operand)
|
|
@@ -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
|
{python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_debug.py
RENAMED
|
@@ -8,9 +8,11 @@ from python_minifier.util import is_constant_node
|
|
|
8
8
|
|
|
9
9
|
class RemoveDebug(SuiteTransformer):
|
|
10
10
|
"""
|
|
11
|
-
|
|
11
|
+
Mark if statements whose condition tests __debug__ is True as dead
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
The marked statements are removed by the RemoveDeadBranches transform, which
|
|
14
|
+
keeps any else branch and any branch that can't be removed without changing
|
|
15
|
+
the meaning of the program.
|
|
14
16
|
"""
|
|
15
17
|
|
|
16
18
|
def __call__(self, node):
|
|
@@ -18,7 +20,11 @@ class RemoveDebug(SuiteTransformer):
|
|
|
18
20
|
|
|
19
21
|
def constant_value(self, node):
|
|
20
22
|
if sys.version_info < (3, 4):
|
|
21
|
-
|
|
23
|
+
# True and False are Name nodes before python 3.4.
|
|
24
|
+
# The comparator may be any expression, so check it is a Name.
|
|
25
|
+
if isinstance(node, ast.Name) and node.id in ('True', 'False'):
|
|
26
|
+
return node.id == 'True'
|
|
27
|
+
return None
|
|
22
28
|
elif is_constant_node(node, ast.NameConstant):
|
|
23
29
|
return node.value
|
|
24
30
|
return None
|
|
@@ -61,14 +67,10 @@ class RemoveDebug(SuiteTransformer):
|
|
|
61
67
|
return True
|
|
62
68
|
return False
|
|
63
69
|
|
|
64
|
-
def
|
|
70
|
+
def visit_If(self, node):
|
|
71
|
+
assert isinstance(node, ast.If)
|
|
65
72
|
|
|
66
|
-
|
|
73
|
+
if self.can_remove(node):
|
|
74
|
+
node.resolved_test = False
|
|
67
75
|
|
|
68
|
-
|
|
69
|
-
if isinstance(parent, ast.Module):
|
|
70
|
-
return []
|
|
71
|
-
else:
|
|
72
|
-
return [self.add_child(ast.Expr(value=ast.Num(0)), parent=parent)]
|
|
73
|
-
|
|
74
|
-
return without_debug
|
|
76
|
+
return self.generic_visit(node)
|
{python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/suite_transformer.py
RENAMED
|
@@ -117,6 +117,51 @@ class SuiteTransformer(NodeVisitor):
|
|
|
117
117
|
|
|
118
118
|
return node
|
|
119
119
|
|
|
120
|
+
def visit_TryStar(self, node):
|
|
121
|
+
return self.visit_Try(node)
|
|
122
|
+
|
|
123
|
+
def visit_TryFinally(self, node):
|
|
124
|
+
# Python 2
|
|
125
|
+
node.body = self.suite(node.body, parent=node)
|
|
126
|
+
node.finalbody = self.suite(node.finalbody, parent=node)
|
|
127
|
+
return node
|
|
128
|
+
|
|
129
|
+
def visit_TryExcept(self, node):
|
|
130
|
+
# Python 2
|
|
131
|
+
node.body = self.suite(node.body, parent=node)
|
|
132
|
+
|
|
133
|
+
node.handlers = [self.visit(h) for h in node.handlers]
|
|
134
|
+
|
|
135
|
+
if node.orelse:
|
|
136
|
+
node.orelse = self.suite(node.orelse, parent=node)
|
|
137
|
+
|
|
138
|
+
return node
|
|
139
|
+
|
|
140
|
+
def visit_ExceptHandler(self, node):
|
|
141
|
+
if node.type is not None:
|
|
142
|
+
node.type = self.visit(node.type)
|
|
143
|
+
|
|
144
|
+
if node.name is not None and isinstance(node.name, ast.AST):
|
|
145
|
+
# Python 2 uses a Name node
|
|
146
|
+
node.name = self.visit(node.name)
|
|
147
|
+
|
|
148
|
+
node.body = self.suite(node.body, parent=node)
|
|
149
|
+
return node
|
|
150
|
+
|
|
151
|
+
def visit_Match(self, node):
|
|
152
|
+
node.subject = self.visit(node.subject)
|
|
153
|
+
node.cases = [self.visit(c) for c in node.cases]
|
|
154
|
+
return node
|
|
155
|
+
|
|
156
|
+
def visit_match_case(self, node):
|
|
157
|
+
node.pattern = self.visit(node.pattern)
|
|
158
|
+
|
|
159
|
+
if node.guard is not None:
|
|
160
|
+
node.guard = self.visit(node.guard)
|
|
161
|
+
|
|
162
|
+
node.body = self.suite(node.body, parent=node)
|
|
163
|
+
return node
|
|
164
|
+
|
|
120
165
|
def visit_While(self, node):
|
|
121
166
|
node.test = self.visit(node.test)
|
|
122
167
|
|
|
@@ -40,6 +40,7 @@ src/python_minifier/transforms/remove_annotations.py
|
|
|
40
40
|
src/python_minifier/transforms/remove_annotations_options.py
|
|
41
41
|
src/python_minifier/transforms/remove_annotations_options.pyi
|
|
42
42
|
src/python_minifier/transforms/remove_asserts.py
|
|
43
|
+
src/python_minifier/transforms/remove_dead_branches.py
|
|
43
44
|
src/python_minifier/transforms/remove_debug.py
|
|
44
45
|
src/python_minifier/transforms/remove_exception_brackets.py
|
|
45
46
|
src/python_minifier/transforms/remove_explicit_return_none.py
|
|
@@ -83,6 +84,7 @@ test/test_raw_fstring_backslash.py
|
|
|
83
84
|
test/test_raw_tstring_backslash.py
|
|
84
85
|
test/test_remove_annotations.py
|
|
85
86
|
test/test_remove_assert.py
|
|
87
|
+
test/test_remove_dead_branches.py
|
|
86
88
|
test/test_remove_debug.py
|
|
87
89
|
test/test_remove_exception_brackets.py
|
|
88
90
|
test/test_remove_explicit_return_none.py
|
|
@@ -9,7 +9,7 @@ def run_subprocess(cmd, timeout=None, input_data=None, env=None):
|
|
|
9
9
|
# Python 3.5+ - encode string input to bytes for subprocess
|
|
10
10
|
input_bytes = input_data.encode('utf-8') if isinstance(input_data, str) else input_data
|
|
11
11
|
return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
12
|
-
input=input_bytes, timeout=timeout, env=env)
|
|
12
|
+
input=input_bytes, timeout=timeout, env=env, check=False)
|
|
13
13
|
# Python 2.7, 3.3, 3.4 - no subprocess.run, no timeout support
|
|
14
14
|
popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
15
15
|
stdin=subprocess.PIPE if input_data else None, env=env)
|
|
@@ -86,6 +86,25 @@ def test_import_in_function():
|
|
|
86
86
|
compare_ast(expected_ast, actual_ast)
|
|
87
87
|
|
|
88
88
|
|
|
89
|
+
def test_import_in_except_handler():
|
|
90
|
+
# SuiteTransformer now visits except handler bodies
|
|
91
|
+
source = '''try:
|
|
92
|
+
pass
|
|
93
|
+
except Exception:
|
|
94
|
+
import collection as c
|
|
95
|
+
import builtins
|
|
96
|
+
'''
|
|
97
|
+
expected = '''try:
|
|
98
|
+
pass
|
|
99
|
+
except Exception:
|
|
100
|
+
import collection as c, builtins
|
|
101
|
+
'''
|
|
102
|
+
|
|
103
|
+
expected_ast = ast.parse(expected)
|
|
104
|
+
actual_ast = combine_imports(ast.parse(source))
|
|
105
|
+
compare_ast(expected_ast, actual_ast)
|
|
106
|
+
|
|
107
|
+
|
|
89
108
|
def test_import_star():
|
|
90
109
|
source = '''
|
|
91
110
|
from breakfast import hashbrown
|