Skip to content

Commit 374f003

Browse files
committed
Add remove exception brackets transform
1 parent 3bb1aaa commit 374f003

File tree

6 files changed

+162
-0
lines changed

6 files changed

+162
-0
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: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
Remove 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+
8+
This transform is enabled by default. Disable by passing the ``remove_exception_brackets=False`` argument to the :func:`python_minifier.minify` function,
9+
or passing ``--no-remove-exception-brackets`` to the pyminify command.
10+
11+
Example
12+
-------
13+
14+
Input
15+
~~~~~
16+
17+
.. literalinclude:: remove_exception_brackets.py
18+
19+
Output
20+
~~~~~~
21+
22+
.. literalinclude:: remove_exception_brackets.min.py
23+
: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_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_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_exception_brackets:
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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def minify(
2424
remove_asserts: bool = ...,
2525
remove_debug: bool = ...,
2626
remove_explicit_return_none: bool = ...
27+
remove_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-exception-brackets',
189+
action='store_false',
190+
help='Disable removing brackets when raising 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_exception_brackets=minification_args.remove_exception_brackets
305312
)
306313

307314

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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+
13+
from python_minifier.rename.binding import BuiltinBinding
14+
15+
# This list may vary between python versions
16+
builtin_exceptions = [
17+
'BaseException',
18+
'BaseExceptionGroup',
19+
'GeneratorExit',
20+
'KeyboardInterrupt',
21+
'SystemExit',
22+
'Exception',
23+
'ArithmeticError',
24+
'FloatingPointError',
25+
'OverflowError',
26+
'ZeroDivisionError',
27+
'AssertionError',
28+
'AttributeError',
29+
'BufferError',
30+
'EOFError',
31+
'ExceptionGroup',
32+
'BaseExceptionGroup',
33+
'ImportError',
34+
'ModuleNotFoundError',
35+
'LookupError',
36+
'IndexError',
37+
'KeyError',
38+
'MemoryError',
39+
'NameError',
40+
'UnboundLocalError',
41+
'OSError',
42+
'BlockingIOError',
43+
'ChildProcessError',
44+
'ConnectionError',
45+
'BrokenPipeError',
46+
'ConnectionAbortedError',
47+
'ConnectionRefusedError',
48+
'ConnectionResetError',
49+
'FileExistsError',
50+
'FileNotFoundError',
51+
'InterruptedError',
52+
'IsADirectoryError',
53+
'NotADirectoryError',
54+
'PermissionError',
55+
'ProcessLookupError',
56+
'TimeoutError',
57+
'ReferenceError',
58+
'RuntimeError',
59+
'NotImplementedError',
60+
'RecursionError',
61+
'StopAsyncIteration',
62+
'StopIteration',
63+
'SyntaxError',
64+
'IndentationError',
65+
'TabError',
66+
'SystemError',
67+
'TypeError',
68+
'ValueError',
69+
'UnicodeError',
70+
'UnicodeDecodeError',
71+
'UnicodeEncodeError',
72+
'UnicodeTranslateError',
73+
'Warning',
74+
'BytesWarning',
75+
'DeprecationWarning',
76+
'EncodingWarning',
77+
'FutureWarning',
78+
'ImportWarning',
79+
'PendingDeprecationWarning',
80+
'ResourceWarning',
81+
'RuntimeWarning',
82+
'SyntaxWarning',
83+
'UnicodeWarning',
84+
'UserWarning'
85+
]
86+
87+
def _remove_empty_call(binding):
88+
assert isinstance(binding, BuiltinBinding)
89+
90+
for name_node in binding.references:
91+
assert isinstance(name_node, ast.Name) # For this to be a builtin, all references must be name nodes as it is not defined anywhere
92+
93+
if not isinstance(name_node.parent, ast.Call):
94+
# This is not a call
95+
continue
96+
call_node = name_node.parent
97+
98+
if not isinstance(call_node.parent, ast.Raise):
99+
# This is not a raise statement
100+
continue
101+
raise_node = call_node.parent
102+
103+
if len(call_node.args) > 0 or len(call_node.keywords) > 0:
104+
# This is a call with arguments
105+
continue
106+
107+
# This is an instance of the exception being called with no arguments
108+
# let's replace it with just the name, cutting out the Call node
109+
110+
if raise_node.exc is call_node:
111+
raise_node.exc = name_node
112+
elif raise_node.cause is call_node:
113+
raise_node.cause = name_node
114+
name_node.parent = raise_node
115+
116+
def remove_no_arg_exception_call(module):
117+
assert isinstance(module, ast.Module)
118+
119+
for binding in module.bindings:
120+
if isinstance(binding, BuiltinBinding) and binding.name in builtin_exceptions:
121+
# We can remove any calls to builtin exceptions
122+
_remove_empty_call(binding)

0 commit comments

Comments
 (0)