Skip to content

Commit 35ff3a9

Browse files
authored
Merge pull request #89 from dflook/no-arg-exceptions
Replace exception calls with names
2 parents 3bb1aaa + bcef793 commit 35ff3a9

File tree

7 files changed

+286
-1
lines changed

7 files changed

+286
-1
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class MyBaseClass:
2+
def override_me(self):
3+
raise NotImplementedError()
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
Remove Builtin Exception Brackets
2+
=================================
3+
4+
This transform removes parentheses when raising builtin exceptions with no arguments.
5+
6+
The raise statement automatically instantiates exceptions with no arguments, so the parentheses are unnecessary.
7+
This transform does nothing on Python 2.
8+
9+
If the exception is not a builtin exception, or has arguments, the parentheses are not removed.
10+
11+
This transform is enabled by default. Disable by passing the ``remove_builtin_exception_brackets=False`` argument to the :func:`python_minifier.minify` function,
12+
or passing ``--no-remove-builtin-exception-brackets`` to the pyminify command.
13+
14+
Example
15+
-------
16+
17+
Input
18+
~~~~~
19+
20+
.. literalinclude:: remove_exception_brackets.py
21+
22+
Output
23+
~~~~~~
24+
25+
.. literalinclude:: remove_exception_brackets.min.py
26+
:language: python

src/python_minifier/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from python_minifier.transforms.remove_asserts import RemoveAsserts
2626
from python_minifier.transforms.remove_debug import RemoveDebug
2727
from python_minifier.transforms.remove_explicit_return_none import RemoveExplicitReturnNone
28+
from python_minifier.transforms.remove_exception_brackets import remove_no_arg_exception_call
2829
from python_minifier.transforms.remove_literal_statements import RemoveLiteralStatements
2930
from python_minifier.transforms.remove_object_base import RemoveObject
3031
from python_minifier.transforms.remove_pass import RemovePass
@@ -68,6 +69,7 @@ def minify(
6869
remove_asserts=False,
6970
remove_debug=False,
7071
remove_explicit_return_none=True,
72+
remove_builtin_exception_brackets=True
7173
):
7274
"""
7375
Minify a python module
@@ -99,6 +101,7 @@ def minify(
99101
:param bool remove_asserts: If assert statements should be removed
100102
:param bool remove_debug: If conditional statements that test '__debug__ is True' should be removed
101103
:param bool remove_explicit_return_none: If explicit return None statements should be replaced with a bare return
104+
:param bool remove_builtin_exception_brackets: If brackets should be removed when raising exceptions with no arguments
102105
103106
:rtype: str
104107
@@ -150,6 +153,9 @@ def minify(
150153
bind_names(module)
151154
resolve_names(module)
152155

156+
if remove_builtin_exception_brackets and not module.tainted:
157+
remove_no_arg_exception_call(module)
158+
153159
if module.tainted:
154160
rename_globals = False
155161
rename_locals = False

src/python_minifier/__init__.pyi

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ def minify(
2323
preserve_shebang: bool = ...,
2424
remove_asserts: bool = ...,
2525
remove_debug: bool = ...,
26-
remove_explicit_return_none: bool = ...
26+
remove_explicit_return_none: bool = ...,
27+
remove_builtin_exception_brackets: bool = ...
2728
) -> Text: ...
2829

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

src/python_minifier/__main__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,12 @@ def parse_args():
184184
help='Replace explicit return None with a bare return',
185185
dest='remove_explicit_return_none',
186186
)
187+
minification_options.add_argument(
188+
'--no-remove-builtin-exception-brackets',
189+
action='store_false',
190+
help='Disable removing brackets when raising builtin exceptions with no arguments',
191+
dest='remove_exception_brackets',
192+
)
187193

188194
annotation_options = parser.add_argument_group('remove annotations options', 'Options that affect how annotations are removed')
189195
annotation_options.add_argument(
@@ -302,6 +308,7 @@ def do_minify(source, filename, minification_args):
302308
remove_asserts=minification_args.remove_asserts,
303309
remove_debug=minification_args.remove_debug,
304310
remove_explicit_return_none=minification_args.remove_explicit_return_none,
311+
remove_builtin_exception_brackets=minification_args.remove_exception_brackets
305312
)
306313

307314

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
"""
2+
Remove Call nodes that are only used to raise exceptions with no arguments
3+
4+
If a Raise statement is used on a Name and the name refers to an exception, it is automatically instantiated with no arguments
5+
We can remove any Call nodes that are only used to raise exceptions with no arguments and let the Raise statement do the instantiation.
6+
When printed, this essentially removes the brackets from the exception name.
7+
8+
We can't generally know if a name refers to an exception, so we only do this for builtin exceptions
9+
"""
10+
11+
import ast
12+
import sys
13+
14+
from python_minifier.rename.binding import BuiltinBinding
15+
16+
# These are always exceptions, in every version of python
17+
builtin_exceptions = [
18+
'SyntaxError', 'Exception', 'ValueError', 'BaseException', 'MemoryError', 'RuntimeError', 'DeprecationWarning', 'UnicodeEncodeError', 'KeyError', 'LookupError', 'TypeError', 'BufferError',
19+
'ImportError', 'OSError', 'StopIteration', 'ArithmeticError', 'UserWarning', 'PendingDeprecationWarning', 'RuntimeWarning', 'IndentationError', 'UnicodeTranslateError', 'UnboundLocalError',
20+
'AttributeError', 'EOFError', 'UnicodeWarning', 'BytesWarning', 'NameError', 'IndexError', 'TabError', 'SystemError', 'OverflowError', 'FutureWarning', 'SystemExit', 'Warning',
21+
'FloatingPointError', 'ReferenceError', 'UnicodeError', 'AssertionError', 'SyntaxWarning', 'UnicodeDecodeError', 'GeneratorExit', 'ImportWarning', 'KeyboardInterrupt', 'ZeroDivisionError',
22+
'NotImplementedError'
23+
]
24+
25+
# These are exceptions only in python 2.7
26+
builtin_exceptions_2_7 = [
27+
'IOError',
28+
'StandardError',
29+
'EnvironmentError',
30+
'VMSError',
31+
'WindowsError'
32+
]
33+
34+
# These are exceptions in 3.3+
35+
builtin_exceptions_3_3 = [
36+
'ChildProcessError',
37+
'ConnectionError',
38+
'BrokenPipeError',
39+
'ConnectionAbortedError',
40+
'ConnectionRefusedError',
41+
'ConnectionResetError',
42+
'FileExistsError',
43+
'FileNotFoundError',
44+
'InterruptedError',
45+
'IsADirectoryError',
46+
'NotADirectoryError',
47+
'PermissionError',
48+
'ProcessLookupError',
49+
'TimeoutError',
50+
'ResourceWarning',
51+
]
52+
53+
# These are exceptions in 3.5+
54+
builtin_exceptions_3_5 = [
55+
'StopAsyncIteration',
56+
'RecursionError',
57+
]
58+
59+
# These are exceptions in 3.6+
60+
builtin_exceptions_3_6 = [
61+
'ModuleNotFoundError'
62+
]
63+
64+
# These are exceptions in 3.10+
65+
builtin_exceptions_3_10 = [
66+
'EncodingWarning'
67+
]
68+
69+
# These are exceptions in 3.11+
70+
builtin_exceptions_3_11 = [
71+
'BaseExceptionGroup',
72+
'ExceptionGroup',
73+
'BaseExceptionGroup',
74+
]
75+
76+
def _remove_empty_call(binding):
77+
assert isinstance(binding, BuiltinBinding)
78+
79+
for name_node in binding.references:
80+
assert isinstance(name_node, ast.Name) # For this to be a builtin, all references must be name nodes as it is not defined anywhere
81+
82+
if not isinstance(name_node.parent, ast.Call):
83+
# This is not a call
84+
continue
85+
call_node = name_node.parent
86+
87+
if not isinstance(call_node.parent, ast.Raise):
88+
# This is not a raise statement
89+
continue
90+
raise_node = call_node.parent
91+
92+
if len(call_node.args) > 0 or len(call_node.keywords) > 0:
93+
# This is a call with arguments
94+
continue
95+
96+
# This is an instance of the exception being called with no arguments
97+
# let's replace it with just the name, cutting out the Call node
98+
99+
if raise_node.exc is call_node:
100+
raise_node.exc = name_node
101+
elif raise_node.cause is call_node:
102+
raise_node.cause = name_node
103+
name_node.parent = raise_node
104+
105+
106+
def remove_no_arg_exception_call(module):
107+
assert isinstance(module, ast.Module)
108+
109+
if sys.version_info < (3, 0):
110+
return module
111+
112+
for binding in module.bindings:
113+
if isinstance(binding, BuiltinBinding) and binding.name in builtin_exceptions:
114+
# We can remove any calls to builtin exceptions
115+
_remove_empty_call(binding)
116+
117+
return module
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import ast
2+
import sys
3+
4+
import pytest
5+
6+
from python_minifier.rename import add_namespace, bind_names, resolve_names
7+
from python_minifier.ast_compare import compare_ast
8+
from python_minifier.transforms.remove_exception_brackets import remove_no_arg_exception_call
9+
10+
11+
def remove_brackets(source):
12+
module = ast.parse(source, 'remove_brackets')
13+
14+
add_namespace(module)
15+
bind_names(module)
16+
resolve_names(module)
17+
return remove_no_arg_exception_call(module)
18+
19+
20+
def test_exception_brackets():
21+
"""This is a buitin so remove the brackets"""
22+
if sys.version_info < (3, 0):
23+
pytest.skip('transform does not work in this version of python')
24+
25+
source = 'def a(): raise Exception()'
26+
expected = 'def a(): raise Exception'
27+
28+
expected_ast = ast.parse(expected)
29+
actual_ast = remove_brackets(source)
30+
compare_ast(expected_ast, actual_ast)
31+
32+
def test_zero_division_error_brackets():
33+
"""This is a buitin so remove the brackets"""
34+
if sys.version_info < (3, 0):
35+
pytest.skip('transform does not work in this version of python')
36+
37+
source = 'def a(): raise ZeroDivisionError()'
38+
expected = 'def a(): raise ZeroDivisionError'
39+
40+
expected_ast = ast.parse(expected)
41+
actual_ast = remove_brackets(source)
42+
compare_ast(expected_ast, actual_ast)
43+
44+
def test_builtin_with_arg():
45+
"""This has an arg so dont' remove the brackets"""
46+
if sys.version_info < (3, 0):
47+
pytest.skip('transform does not work in this version of python')
48+
49+
source = 'def a(): raise Exception(1)'
50+
expected = 'def a(): raise Exception(1)'
51+
52+
expected_ast = ast.parse(expected)
53+
actual_ast = remove_brackets(source)
54+
compare_ast(expected_ast, actual_ast)
55+
56+
def test_one_division_error_brackets():
57+
"""This is not a builtin so don't remove the brackets even though it's not defined in the module"""
58+
if sys.version_info < (3, 0):
59+
pytest.skip('transform does not work in this version of python')
60+
61+
source = 'def a(): raise OneDivisionError()'
62+
expected = source
63+
64+
expected_ast = ast.parse(expected)
65+
actual_ast = remove_brackets(source)
66+
compare_ast(expected_ast, actual_ast)
67+
68+
def test_redefined():
69+
"""This is usually a builtin, but don't remove brackets if it's been redefined"""
70+
if sys.version_info < (3, 0):
71+
pytest.skip('transform does not work in this version of python')
72+
73+
source = '''
74+
def a():
75+
raise ZeroDivisionError()
76+
def b():
77+
ZeroDivisionError = blag
78+
raise ZeroDivisionError()
79+
'''
80+
expected = '''
81+
def a():
82+
raise ZeroDivisionError
83+
def b():
84+
ZeroDivisionError = blag
85+
raise ZeroDivisionError()
86+
'''
87+
expected_ast = ast.parse(expected)
88+
actual_ast = remove_brackets(source)
89+
compare_ast(expected_ast, actual_ast)
90+
91+
def test_raise_from():
92+
"""This is a builtin so remove the brackets"""
93+
if sys.version_info < (3, 0):
94+
pytest.skip('raise from not supported in this version of python')
95+
96+
source = 'def a(): raise Exception() from Exception()'
97+
expected = 'def a(): raise Exception from Exception'
98+
99+
expected_ast = ast.parse(expected)
100+
actual_ast = remove_brackets(source)
101+
compare_ast(expected_ast, actual_ast)
102+
103+
def test_raise_from_only():
104+
"""This is a builtin so remove the brackets"""
105+
if sys.version_info < (3, 0):
106+
pytest.skip('raise from not supported in this version of python')
107+
108+
source = 'def a(): raise Hello() from Exception()'
109+
expected = 'def a(): raise Hello() from Exception'
110+
111+
expected_ast = ast.parse(expected)
112+
actual_ast = remove_brackets(source)
113+
compare_ast(expected_ast, actual_ast)
114+
115+
def test_raise_from_arg():
116+
"""This is a builtin so remove the brackets"""
117+
if sys.version_info < (3, 0):
118+
pytest.skip('raise from not supported in this version of python')
119+
120+
source = 'def a(): raise Hello() from Exception(1)'
121+
expected = 'def a(): raise Hello() from Exception(1)'
122+
123+
expected_ast = ast.parse(expected)
124+
actual_ast = remove_brackets(source)
125+
compare_ast(expected_ast, actual_ast)

0 commit comments

Comments
 (0)