Skip to content

Commit 7095eed

Browse files
committed
Add transforms that mimic Pythons -O option.
remove_asserts and remove_debug
1 parent 8939b2d commit 7095eed

File tree

12 files changed

+468
-5
lines changed

12 files changed

+468
-5
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
word = 'hello'
2+
assert word is 'goodbye'
3+
print(word)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
Remove Asserts
2+
==============
3+
4+
This transform removes assert statements.
5+
6+
Assert statements are evaluated by Python when it is not started with the `-O` option.
7+
This transform is only safe to use if the minified output will by run with the `-O` option, or
8+
you are certain that the assert statements are not needed.
9+
10+
11+
If a statement is required, the assert statement will be replaced by a zero expression statement.
12+
13+
The transform is disabled by default. Enable it by passing the ``remove_asserts=True`` argument to the :func:`python_minifier.minify` function,
14+
or passing ``--remove-asserts`` to the pyminify command.
15+
16+
Example
17+
-------
18+
19+
Input
20+
~~~~~
21+
22+
.. literalinclude:: remove_asserts.py
23+
24+
Output
25+
~~~~~~
26+
27+
.. literalinclude:: remove_asserts.min.py
28+
:language: python
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
value = 10
2+
3+
# Truthy
4+
if __debug__:
5+
value += 1
6+
7+
if __debug__ is True:
8+
value += 1
9+
10+
if __debug__ is not False:
11+
value += 1
12+
13+
if __debug__ == True:
14+
value += 1
15+
16+
17+
# Falsy
18+
if not __debug__:
19+
value += 1
20+
21+
if __debug__ is False:
22+
value += 1
23+
24+
if __debug__ is not True:
25+
value += 1
26+
27+
if __debug__ == False:
28+
value += 1
29+
30+
print(value)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
Remove Debug
2+
============
3+
4+
This transform removes if statements that test ``__debug__`` is ``True``.
5+
6+
The builtin ``__debug__`` constant is True if Python is not stated with the ``-O`` option.
7+
This transform is only safe to use if the minified output will by run with the ``-O`` option, or
8+
you are certain that any If statement that tests ``__debug__`` can be removed
9+
10+
The condition is not evaluated. The statement is only removed if the condition exactly matches one of the forms:
11+
12+
```python
13+
if __debug__:
14+
pass
15+
if __debug__ is True:
16+
pass
17+
if __debug__ is not False:
18+
pass
19+
if __debug__ == True:
20+
pass
21+
```
22+
23+
If a statement is required, the If statement will be replaced by a zero expression statement.
24+
25+
The transform is disabled by default. Enable it by passing the ``remove_debug=True`` argument to the :func:`python_minifier.minify` function,
26+
or passing ``--remove-debug`` to the pyminify command.
27+
28+
Example
29+
-------
30+
31+
Input
32+
~~~~~
33+
34+
.. literalinclude:: remove_debug.py
35+
36+
Output
37+
~~~~~~
38+
39+
.. literalinclude:: remove_debug.min.py
40+
:language: python

src/python_minifier/__init__.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
)
2121
from python_minifier.transforms.combine_imports import CombineImports
2222
from python_minifier.transforms.remove_annotations import RemoveAnnotations
23+
from python_minifier.transforms.remove_asserts import RemoveAsserts
24+
from python_minifier.transforms.remove_debug import RemoveDebug
2325
from python_minifier.transforms.remove_literal_statements import RemoveLiteralStatements
2426
from python_minifier.transforms.remove_object_base import RemoveObject
2527
from python_minifier.transforms.remove_pass import RemovePass
@@ -59,7 +61,9 @@ def minify(
5961
preserve_globals=None,
6062
remove_object_base=True,
6163
convert_posargs_to_args=True,
62-
preserve_shebang=True
64+
preserve_shebang=True,
65+
remove_asserts=False,
66+
remove_debug=False
6367
):
6468
"""
6569
Minify a python module
@@ -87,6 +91,8 @@ def minify(
8791
:param bool remove_object_base: If object as a base class may be removed
8892
:param bool convert_posargs_to_args: If positional-only arguments will be converted to normal arguments
8993
:param bool preserve_shebang: Keep any shebang interpreter directive from the source in the minified output
94+
:param bool remove_asserts: If assert statements should be removed
95+
:param bool remove_debug: If conditional statements that test '__debug__ is True' should be removed
9096
9197
:rtype: str
9298
@@ -114,6 +120,12 @@ def minify(
114120
if remove_object_base:
115121
module = RemoveObject()(module)
116122

123+
if remove_asserts:
124+
module = RemoveAsserts()(module)
125+
126+
if remove_debug:
127+
module = RemoveDebug()(module)
128+
117129
bind_names(module)
118130
resolve_names(module)
119131

src/python_minifier/__init__.pyi

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ def minify(
1919
preserve_globals: Optional[List[Text]] = ...,
2020
remove_object_base: bool = ...,
2121
convert_posargs_to_args: bool = ...,
22-
preserve_shebang: bool = ...
22+
preserve_shebang: bool = ...,
23+
remove_asserts: bool = ...,
24+
remove_debug: bool = ...
2325
) -> Text: ...
2426

2527
def unparse(module: ast.Module) -> Text: ...

src/python_minifier/__main__.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,18 @@ def parse_args():
171171
help='Preserve any shebang line from the source',
172172
dest='preserve_shebang',
173173
)
174+
minification_options.add_argument(
175+
'--remove-asserts',
176+
action='store_true',
177+
help='Remove assert statements',
178+
dest='remove_asserts',
179+
)
180+
minification_options.add_argument(
181+
'--remove-debug',
182+
action='store_true',
183+
help='Remove conditional statements that test __debug__ is True',
184+
dest='remove_debug',
185+
)
174186

175187
parser.add_argument('--version', '-v', action='version', version=version)
176188

@@ -201,7 +213,7 @@ def error(os_error):
201213
if os.path.isdir(path_arg):
202214
for root, dirs, files in os.walk(path_arg, onerror=error, followlinks=True):
203215
for file in files:
204-
if file.endswith('.py'):
216+
if file.endswith('.py') or file.endswith('.pyw'):
205217
yield os.path.join(root, file)
206218
else:
207219
yield path_arg
@@ -234,7 +246,9 @@ def do_minify(source, filename, minification_args):
234246
preserve_globals=preserve_globals,
235247
remove_object_base=minification_args.remove_object_base,
236248
convert_posargs_to_args=minification_args.convert_posargs_to_args,
237-
preserve_shebang=minification_args.preserve_shebang
249+
preserve_shebang=minification_args.preserve_shebang,
250+
remove_asserts=minification_args.remove_asserts,
251+
remove_debug=minification_args.remove_debug
238252
)
239253

240254

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import ast
2+
3+
from python_minifier.transforms.suite_transformer import SuiteTransformer
4+
from python_minifier.util import is_ast_node
5+
6+
7+
class RemoveAsserts(SuiteTransformer):
8+
"""
9+
Remove assert statements
10+
11+
If a statement is syntactically necessary, use an empty expression instead
12+
"""
13+
14+
def __call__(self, node):
15+
return self.visit(node)
16+
17+
def suite(self, node_list, parent):
18+
without_assert = [self.visit(a) for a in filter(lambda n: not is_ast_node(n, ast.Assert), node_list)]
19+
20+
if len(without_assert) == 0:
21+
if isinstance(parent, ast.Module):
22+
return []
23+
else:
24+
return [self.add_child(ast.Expr(value=ast.Num(0)), parent=parent)]
25+
26+
return without_assert
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import ast
2+
3+
from python_minifier.transforms.suite_transformer import SuiteTransformer
4+
from python_minifier.util import is_ast_node
5+
6+
7+
class RemoveDebug(SuiteTransformer):
8+
"""
9+
Remove if statements where the condition tests __debug__ is True
10+
11+
If a statement is syntactically necessary, use an empty expression instead
12+
"""
13+
14+
def __call__(self, node):
15+
return self.visit(node)
16+
17+
def can_remove(self, node):
18+
if not isinstance(node, ast.If):
19+
return False
20+
21+
if is_ast_node(node.test, ast.Name) and node.test.id == '__debug__':
22+
return True
23+
24+
if isinstance(node.test, ast.Compare) and len(node.test.ops) == 1 and isinstance(node.test.ops[0], ast.Is) and is_ast_node(node.test.comparators[0], ast.NameConstant) and node.test.comparators[0].value is True:
25+
return True
26+
27+
if isinstance(node.test, ast.Compare) and len(node.test.ops) == 1 and isinstance(node.test.ops[0], ast.IsNot) and is_ast_node(node.test.comparators[0], ast.NameConstant) and node.test.comparators[0].value is False:
28+
return True
29+
30+
if isinstance(node.test, ast.Compare) and len(node.test.ops) == 1 and isinstance(node.test.ops[0], ast.Eq) and is_ast_node(node.test.comparators[0], ast.NameConstant) and node.test.comparators[0].value is True:
31+
return True
32+
33+
return False
34+
35+
def suite(self, node_list, parent):
36+
37+
without_debug = [self.visit(a) for a in filter(lambda n: not self.can_remove(n), node_list)]
38+
39+
if len(without_debug) == 0:
40+
if isinstance(parent, ast.Module):
41+
return []
42+
else:
43+
return [self.add_child(ast.Expr(value=ast.Num(0)), parent=parent)]
44+
45+
return without_debug

test/test_remove_assert.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import ast
2+
from python_minifier import add_namespace, bind_names, resolve_names
3+
from python_minifier.transforms.remove_asserts import RemoveAsserts
4+
from python_minifier.ast_compare import compare_ast
5+
6+
def remove_asserts(source):
7+
module = ast.parse(source, 'remove_asserts')
8+
9+
add_namespace(module)
10+
bind_names(module)
11+
resolve_names(module)
12+
return RemoveAsserts()(module)
13+
14+
def test_remove_assert_empty_module():
15+
source = 'assert False'
16+
expected = ''
17+
18+
expected_ast = ast.parse(expected)
19+
actual_ast = remove_asserts(source)
20+
compare_ast(expected_ast, actual_ast)
21+
22+
def test_remove_assert_module():
23+
source = '''import collections
24+
assert False
25+
a = 1
26+
assert False'''
27+
expected = '''import collections
28+
a=1'''
29+
30+
expected_ast = ast.parse(expected)
31+
actual_ast = remove_asserts(source)
32+
compare_ast(expected_ast, actual_ast)
33+
34+
def test_remove_if_empty():
35+
source = '''if True:
36+
assert False'''
37+
expected = '''if True:
38+
0'''
39+
40+
expected_ast = ast.parse(expected)
41+
actual_ast = remove_asserts(source)
42+
compare_ast(expected_ast, actual_ast)
43+
44+
def test_remove_if_line():
45+
source = '''if True: assert False'''
46+
expected = '''if True: 0'''
47+
48+
expected_ast = ast.parse(expected)
49+
actual_ast = remove_asserts(source)
50+
compare_ast(expected_ast, actual_ast)
51+
52+
def test_remove_suite():
53+
source = '''if True:
54+
assert False
55+
a=1
56+
assert False
57+
return None'''
58+
expected = '''if True:
59+
a=1
60+
return None'''
61+
62+
expected_ast = ast.parse(expected)
63+
actual_ast = remove_asserts(source)
64+
compare_ast(expected_ast, actual_ast)
65+
66+
def test_remove_from_class():
67+
source = '''class A:
68+
assert False
69+
a = 1
70+
assert False
71+
def b():
72+
assert False
73+
return 1
74+
assert False
75+
'''
76+
expected = '''class A:
77+
a=1
78+
def b():
79+
return 1
80+
'''
81+
82+
expected_ast = ast.parse(expected)
83+
actual_ast = remove_asserts(source)
84+
compare_ast(expected_ast, actual_ast)
85+
86+
def test_remove_from_class_empty():
87+
source = '''class A:
88+
assert False
89+
'''
90+
expected = 'class A:0'
91+
92+
expected_ast = ast.parse(expected)
93+
actual_ast = remove_asserts(source)
94+
compare_ast(expected_ast, actual_ast)
95+
96+
def test_remove_from_class_func_empty():
97+
source = '''class A:
98+
def b():
99+
assert False
100+
'''
101+
expected = '''class A:
102+
def b(): 0'''
103+
104+
expected_ast = ast.parse(expected)
105+
actual_ast = remove_asserts(source)
106+
compare_ast(expected_ast, actual_ast)

0 commit comments

Comments
 (0)