python-minifier 2.11.2__py2-none-any.whl
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/__init__.py +269 -0
- python_minifier/__init__.pyi +37 -0
- python_minifier/__main__.py +332 -0
- python_minifier/ast_compare.py +104 -0
- python_minifier/ast_compat.py +40 -0
- python_minifier/ast_printer.py +130 -0
- python_minifier/expression_printer.py +753 -0
- python_minifier/f_string.py +446 -0
- python_minifier/ministring.py +179 -0
- python_minifier/module_printer.py +862 -0
- python_minifier/py.typed +0 -0
- python_minifier/rename/__init__.py +6 -0
- python_minifier/rename/bind_names.py +192 -0
- python_minifier/rename/binding.py +495 -0
- python_minifier/rename/mapper.py +175 -0
- python_minifier/rename/name_generator.py +51 -0
- python_minifier/rename/rename_literals.py +249 -0
- python_minifier/rename/renamer.py +230 -0
- python_minifier/rename/resolve_names.py +106 -0
- python_minifier/rename/util.py +203 -0
- python_minifier/token_printer.py +299 -0
- python_minifier/transforms/__init__.py +0 -0
- python_minifier/transforms/combine_imports.py +76 -0
- python_minifier/transforms/constant_folding.py +114 -0
- python_minifier/transforms/remove_annotations.py +130 -0
- python_minifier/transforms/remove_annotations_options.py +35 -0
- python_minifier/transforms/remove_annotations_options.pyi +17 -0
- python_minifier/transforms/remove_asserts.py +26 -0
- python_minifier/transforms/remove_debug.py +53 -0
- python_minifier/transforms/remove_exception_brackets.py +124 -0
- python_minifier/transforms/remove_explicit_return_none.py +38 -0
- python_minifier/transforms/remove_literal_statements.py +61 -0
- python_minifier/transforms/remove_object_base.py +24 -0
- python_minifier/transforms/remove_pass.py +26 -0
- python_minifier/transforms/remove_posargs.py +13 -0
- python_minifier/transforms/suite_transformer.py +196 -0
- python_minifier/util.py +48 -0
- python_minifier-2.11.2.dist-info/LICENSE +21 -0
- python_minifier-2.11.2.dist-info/METADATA +177 -0
- python_minifier-2.11.2.dist-info/RECORD +44 -0
- python_minifier-2.11.2.dist-info/WHEEL +5 -0
- python_minifier-2.11.2.dist-info/entry_points.txt +3 -0
- python_minifier-2.11.2.dist-info/top_level.txt +1 -0
- python_minifier-2.11.2.dist-info/zip-safe +1 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import python_minifier.ast_compat as ast
|
|
2
|
+
|
|
3
|
+
from python_minifier.rename.binding import BuiltinBinding, NameBinding
|
|
4
|
+
from python_minifier.rename.util import get_global_namespace, get_nonlocal_namespace, builtins
|
|
5
|
+
from python_minifier.util import is_ast_node
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_binding(name, namespace):
|
|
9
|
+
if name in namespace.global_names and not isinstance(namespace, ast.Module):
|
|
10
|
+
return get_binding(name, get_global_namespace(namespace))
|
|
11
|
+
elif name in namespace.nonlocal_names and not isinstance(namespace, ast.Module):
|
|
12
|
+
return get_binding(name, get_nonlocal_namespace(namespace))
|
|
13
|
+
|
|
14
|
+
for binding in namespace.bindings:
|
|
15
|
+
if binding.name == name:
|
|
16
|
+
return binding
|
|
17
|
+
|
|
18
|
+
if not isinstance(namespace, ast.Module):
|
|
19
|
+
return get_binding(name, get_nonlocal_namespace(namespace))
|
|
20
|
+
|
|
21
|
+
else:
|
|
22
|
+
# This is unresolved at global scope - is it a builtin?
|
|
23
|
+
if name in dir(builtins):
|
|
24
|
+
if name in ['exec', 'eval', 'locals', 'globals', 'vars']:
|
|
25
|
+
namespace.tainted = True
|
|
26
|
+
|
|
27
|
+
binding = BuiltinBinding(name, namespace)
|
|
28
|
+
namespace.bindings.append(binding)
|
|
29
|
+
return binding
|
|
30
|
+
|
|
31
|
+
else:
|
|
32
|
+
binding = NameBinding(name)
|
|
33
|
+
binding.disallow_rename()
|
|
34
|
+
namespace.bindings.append(binding)
|
|
35
|
+
return binding
|
|
36
|
+
|
|
37
|
+
def get_binding_disallow_class_namespace_rename(name, namespace):
|
|
38
|
+
binding = get_binding(name, namespace)
|
|
39
|
+
|
|
40
|
+
if isinstance(namespace, ast.ClassDef):
|
|
41
|
+
# This name will become an attribute of a class, so it can't be renamed
|
|
42
|
+
binding.disallow_rename()
|
|
43
|
+
|
|
44
|
+
return binding
|
|
45
|
+
|
|
46
|
+
def resolve_names(node):
|
|
47
|
+
"""
|
|
48
|
+
Resolve unbound names to a NameBinding
|
|
49
|
+
|
|
50
|
+
:param node: The module to resolve names in
|
|
51
|
+
:type node: :class:`ast.Module`
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load):
|
|
56
|
+
get_binding(node.id, node.namespace).add_reference(node)
|
|
57
|
+
elif isinstance(node, ast.Name) and node.id in node.namespace.nonlocal_names:
|
|
58
|
+
binding = get_binding(node.id, node.namespace)
|
|
59
|
+
binding.add_reference(node)
|
|
60
|
+
|
|
61
|
+
if isinstance(node.ctx, ast.Store) and isinstance(node.namespace, ast.ClassDef):
|
|
62
|
+
binding.disallow_rename()
|
|
63
|
+
|
|
64
|
+
elif isinstance(node, ast.ClassDef) and node.name in node.namespace.nonlocal_names:
|
|
65
|
+
binding = get_binding_disallow_class_namespace_rename(node.name, node.namespace)
|
|
66
|
+
binding.add_reference(node)
|
|
67
|
+
|
|
68
|
+
elif is_ast_node(node, (ast.FunctionDef, 'AsyncFunctionDef')) and node.name in node.namespace.nonlocal_names:
|
|
69
|
+
binding = get_binding_disallow_class_namespace_rename(node.name, node.namespace)
|
|
70
|
+
binding.add_reference(node)
|
|
71
|
+
|
|
72
|
+
elif isinstance(node, ast.alias):
|
|
73
|
+
|
|
74
|
+
if node.asname is not None:
|
|
75
|
+
if node.asname in node.namespace.nonlocal_names:
|
|
76
|
+
binding = get_binding_disallow_class_namespace_rename(node.asname, node.namespace)
|
|
77
|
+
binding.add_reference(node)
|
|
78
|
+
|
|
79
|
+
else:
|
|
80
|
+
# This binds the root module only for a dotted import
|
|
81
|
+
root_module = node.name.split('.')[0]
|
|
82
|
+
|
|
83
|
+
if root_module in node.namespace.nonlocal_names:
|
|
84
|
+
binding = get_binding_disallow_class_namespace_rename(root_module, node.namespace)
|
|
85
|
+
binding.add_reference(node)
|
|
86
|
+
|
|
87
|
+
if '.' in node.name:
|
|
88
|
+
binding.disallow_rename()
|
|
89
|
+
|
|
90
|
+
elif isinstance(node, ast.ExceptHandler) and node.name is not None:
|
|
91
|
+
if isinstance(node.name, str) and node.name in node.namespace.nonlocal_names:
|
|
92
|
+
get_binding_disallow_class_namespace_rename(node.name, node.namespace).add_reference(node)
|
|
93
|
+
|
|
94
|
+
elif is_ast_node(node, 'Nonlocal'):
|
|
95
|
+
for name in node.names:
|
|
96
|
+
get_binding_disallow_class_namespace_rename(name, node.namespace).add_reference(node)
|
|
97
|
+
elif is_ast_node(node, ('MatchAs', 'MatchStar')) and node.name in node.namespace.nonlocal_names:
|
|
98
|
+
get_binding_disallow_class_namespace_rename(node.name, node.namespace).add_reference(node)
|
|
99
|
+
elif is_ast_node(node, 'MatchMapping') and node.rest in node.namespace.nonlocal_names:
|
|
100
|
+
get_binding_disallow_class_namespace_rename(node.rest, node.namespace).add_reference(node)
|
|
101
|
+
|
|
102
|
+
elif is_ast_node(node, 'Exec'):
|
|
103
|
+
get_global_namespace(node).tainted = True
|
|
104
|
+
|
|
105
|
+
for child in ast.iter_child_nodes(node):
|
|
106
|
+
resolve_names(child)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import python_minifier.ast_compat as ast
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from python_minifier.util import is_ast_node
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def create_is_namespace():
|
|
8
|
+
|
|
9
|
+
namespace_nodes = (ast.FunctionDef, ast.Lambda, ast.ClassDef, ast.Module, ast.GeneratorExp)
|
|
10
|
+
|
|
11
|
+
if sys.version_info >= (2, 7):
|
|
12
|
+
namespace_nodes += (ast.SetComp, ast.DictComp)
|
|
13
|
+
|
|
14
|
+
if sys.version_info >= (3, 0):
|
|
15
|
+
namespace_nodes += (ast.ListComp,)
|
|
16
|
+
|
|
17
|
+
if sys.version_info >= (3, 5):
|
|
18
|
+
namespace_nodes += (ast.AsyncFunctionDef,)
|
|
19
|
+
|
|
20
|
+
return lambda node: isinstance(node, namespace_nodes)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
is_namespace = create_is_namespace()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def iter_child_namespaces(node):
|
|
27
|
+
|
|
28
|
+
for child in ast.iter_child_nodes(node):
|
|
29
|
+
if is_namespace(child):
|
|
30
|
+
yield child
|
|
31
|
+
else:
|
|
32
|
+
for c in iter_child_namespaces(child):
|
|
33
|
+
yield c
|
|
34
|
+
|
|
35
|
+
def get_global_namespace(node):
|
|
36
|
+
"""
|
|
37
|
+
Return the global namespace for a node
|
|
38
|
+
|
|
39
|
+
:rtype: :class:`ast.Module`
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
if node.namespace is node:
|
|
44
|
+
return node
|
|
45
|
+
|
|
46
|
+
return get_global_namespace(node.namespace)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_nonlocal_namespace(node):
|
|
50
|
+
"""
|
|
51
|
+
Return the nonlocal namespace for a node
|
|
52
|
+
|
|
53
|
+
The nonlocal namespace is the closest parent function scope's namespace.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
if isinstance(node.namespace, ast.ClassDef):
|
|
57
|
+
return get_nonlocal_namespace(node.namespace)
|
|
58
|
+
|
|
59
|
+
return node.namespace
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def arg_rename_in_place(node):
|
|
63
|
+
"""
|
|
64
|
+
Can this argument node by safely renamed
|
|
65
|
+
|
|
66
|
+
'self', 'cls', 'args', and 'kwargs' are not commonly referenced by the caller, so
|
|
67
|
+
can be safely renamed. Comprehension arguments are not accessible from outside, so
|
|
68
|
+
can be renamed.
|
|
69
|
+
|
|
70
|
+
If the argument is positional-only, it can be safely renamed
|
|
71
|
+
|
|
72
|
+
Other arguments may be referenced by the caller as keyword arguments, so should not be
|
|
73
|
+
renamed in place. The name assigner may still decide to bind the argument to a new name
|
|
74
|
+
inside the function namespace.
|
|
75
|
+
|
|
76
|
+
:param node: The argument node
|
|
77
|
+
:rtype node: :class:`ast.arg`
|
|
78
|
+
:rtype: bool
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
func = node.namespace
|
|
83
|
+
|
|
84
|
+
if isinstance(func, ast.comprehension):
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
if isinstance(func.namespace, ast.ClassDef) and not isinstance(func, ast.Lambda):
|
|
88
|
+
all_args = (func.args.posonlyargs if hasattr(func.args, 'posonlyargs') else []) + func.args.args
|
|
89
|
+
if len(all_args) > 0 and node is all_args[0]:
|
|
90
|
+
if len(func.decorator_list) == 0:
|
|
91
|
+
# rename 'self'
|
|
92
|
+
return True
|
|
93
|
+
elif (
|
|
94
|
+
len(func.decorator_list) == 1
|
|
95
|
+
and isinstance(func.decorator_list[0], ast.Name)
|
|
96
|
+
and func.decorator_list[0].id == 'classmethod'
|
|
97
|
+
):
|
|
98
|
+
# rename 'cls'
|
|
99
|
+
return True
|
|
100
|
+
|
|
101
|
+
if func.args.vararg is node or func.args.kwarg is node:
|
|
102
|
+
# starargs
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
if hasattr(func.args, 'posonlyargs') and node in func.args.posonlyargs:
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def insert(suite, new_node):
|
|
112
|
+
"""
|
|
113
|
+
Insert a node into a suite
|
|
114
|
+
|
|
115
|
+
Inserts new_node as early as possible in the suite, but after docstrings and `import __future__` statements.
|
|
116
|
+
|
|
117
|
+
:param suite: The existing suite to insert the node into
|
|
118
|
+
:param new_node: The node to insert
|
|
119
|
+
:return: :class:`collections.Iterable[Node]`
|
|
120
|
+
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
inserted = False
|
|
124
|
+
for node in suite:
|
|
125
|
+
|
|
126
|
+
if not inserted:
|
|
127
|
+
if (isinstance(node, ast.ImportFrom) and node.module == '__future__') or (
|
|
128
|
+
isinstance(node, ast.Expr) and is_ast_node(node.value, ast.Str)
|
|
129
|
+
):
|
|
130
|
+
pass
|
|
131
|
+
else:
|
|
132
|
+
yield new_node
|
|
133
|
+
inserted = True
|
|
134
|
+
|
|
135
|
+
yield node
|
|
136
|
+
|
|
137
|
+
if not inserted:
|
|
138
|
+
yield new_node
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def allow_rename_locals(node, rename_locals, preserve_locals=None):
|
|
142
|
+
|
|
143
|
+
if preserve_locals is None:
|
|
144
|
+
preserve_locals = []
|
|
145
|
+
|
|
146
|
+
if not isinstance(node, ast.Module) and is_namespace(node):
|
|
147
|
+
for binding in node.bindings:
|
|
148
|
+
if rename_locals is False:
|
|
149
|
+
binding.disallow_rename()
|
|
150
|
+
elif binding.name in preserve_locals:
|
|
151
|
+
binding.disallow_rename()
|
|
152
|
+
|
|
153
|
+
for child in ast.iter_child_nodes(node):
|
|
154
|
+
allow_rename_locals(child, rename_locals, preserve_locals)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def find__all__(module):
|
|
158
|
+
|
|
159
|
+
names = []
|
|
160
|
+
|
|
161
|
+
def is_assign_all_node(node):
|
|
162
|
+
if isinstance(node, ast.Assign):
|
|
163
|
+
for name in node.targets:
|
|
164
|
+
if isinstance(name, ast.Name) and name.id == '__all__':
|
|
165
|
+
return True
|
|
166
|
+
|
|
167
|
+
elif is_ast_node(node, (ast.AugAssign, 'AnnAssign')):
|
|
168
|
+
if isinstance(node.target, ast.Name) and node.target.id == '__all__':
|
|
169
|
+
return True
|
|
170
|
+
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
for node in ast.iter_child_nodes(module):
|
|
174
|
+
if not is_assign_all_node(node):
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
if not isinstance(node.value, ast.List):
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
for el in node.value.elts:
|
|
181
|
+
if is_ast_node(el, ast.Str):
|
|
182
|
+
names.append(el.s)
|
|
183
|
+
|
|
184
|
+
return names
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def allow_rename_globals(module, rename_globals=False, preserve_globals=None):
|
|
188
|
+
|
|
189
|
+
if preserve_globals is None:
|
|
190
|
+
preserve_globals = []
|
|
191
|
+
|
|
192
|
+
preserve_globals.extend(find__all__(module))
|
|
193
|
+
|
|
194
|
+
for binding in module.bindings:
|
|
195
|
+
if rename_globals is False or binding.name in preserve_globals:
|
|
196
|
+
binding.disallow_rename()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
import builtins
|
|
201
|
+
except ImportError:
|
|
202
|
+
# noinspection PyCompatibility
|
|
203
|
+
import __builtin__ as builtins # type: ignore
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""Tools for assembling python code from tokens."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
class TokenTypes(object):
|
|
7
|
+
NoToken = 0
|
|
8
|
+
Identifier = 1
|
|
9
|
+
Keyword = 2
|
|
10
|
+
SoftKeyword = 3
|
|
11
|
+
NumberLiteral = 4
|
|
12
|
+
NonNumberLiteral = 5
|
|
13
|
+
Delimiter = 6
|
|
14
|
+
Operator = 7
|
|
15
|
+
NewLine = 8
|
|
16
|
+
EndStatement = 9
|
|
17
|
+
|
|
18
|
+
class Delimiter(object):
|
|
19
|
+
def __init__(self, terminal_printer, delimiter=',', add_parens=False):
|
|
20
|
+
"""
|
|
21
|
+
Delimited group printer
|
|
22
|
+
|
|
23
|
+
A group of items that should be delimited by a delimiter character.
|
|
24
|
+
Each call to new_item() will insert the delimiter character if necessary.
|
|
25
|
+
|
|
26
|
+
When used as a context manager, the group will be enclosed by the start and end characters if the group has any items.
|
|
27
|
+
|
|
28
|
+
>>> d = Delimiter(terminal_printer)
|
|
29
|
+
... d.new_item()
|
|
30
|
+
... terminal_printer.identifier('a')
|
|
31
|
+
... print(terminal_printer.code)
|
|
32
|
+
a
|
|
33
|
+
|
|
34
|
+
>>> d.new_item()
|
|
35
|
+
... terminal_printer.identifier('b')
|
|
36
|
+
... print(terminal_printer.code)
|
|
37
|
+
a,b
|
|
38
|
+
|
|
39
|
+
>>> with Delimiter(terminal_printer, add_parens=True) as d:
|
|
40
|
+
... d.new_item()
|
|
41
|
+
... terminal_printer.identifier('a')
|
|
42
|
+
... print(terminal_printer.code)
|
|
43
|
+
(a)
|
|
44
|
+
|
|
45
|
+
:param terminal_printer: The terminal printer to use.
|
|
46
|
+
:param delimiter: The delimiter to use.
|
|
47
|
+
:param add_parens: If the group should be enclosed by parentheses. Only used when used as a context manager.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
self._terminal_printer = terminal_printer
|
|
51
|
+
self._delimiter = delimiter
|
|
52
|
+
self._add_parens = add_parens
|
|
53
|
+
|
|
54
|
+
self._first = True
|
|
55
|
+
|
|
56
|
+
self._context_manager = False
|
|
57
|
+
|
|
58
|
+
def __enter__(self):
|
|
59
|
+
"""Open a delimited group."""
|
|
60
|
+
self._context_manager = True
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
64
|
+
"""Close the delimited group."""
|
|
65
|
+
if not self._first and self._add_parens:
|
|
66
|
+
self._terminal_printer.delimiter(')')
|
|
67
|
+
|
|
68
|
+
def new_item(self):
|
|
69
|
+
"""Add a new item to the delimited group."""
|
|
70
|
+
if self._first:
|
|
71
|
+
self._first = False
|
|
72
|
+
if self._context_manager and self._add_parens:
|
|
73
|
+
self._terminal_printer.delimiter('(')
|
|
74
|
+
else:
|
|
75
|
+
self._terminal_printer.delimiter(self._delimiter)
|
|
76
|
+
|
|
77
|
+
class TokenPrinter(object):
|
|
78
|
+
"""
|
|
79
|
+
Concatenates terminal symbols of the python grammar
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(self, prefer_single_line=False, allow_invalid_num_warnings=False):
|
|
83
|
+
"""
|
|
84
|
+
:param prefer_single_line: If True, chooses to put as much code as possible on a single line.
|
|
85
|
+
:param allow_invalid_num_warnings: If True, allows invalid number literals to be printe that may cause warnings.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
self._prefer_single_line = prefer_single_line
|
|
89
|
+
self._allow_invalid_num_warnings = allow_invalid_num_warnings
|
|
90
|
+
|
|
91
|
+
self._code = ''
|
|
92
|
+
self.indent = 0
|
|
93
|
+
self.unicode_literals = False
|
|
94
|
+
self.previous_token = TokenTypes.NoToken
|
|
95
|
+
|
|
96
|
+
def __str__(self):
|
|
97
|
+
"""Return the output code."""
|
|
98
|
+
return self._code
|
|
99
|
+
|
|
100
|
+
def identifier(self, name):
|
|
101
|
+
"""Add an identifier to the output code."""
|
|
102
|
+
assert isinstance(name, str)
|
|
103
|
+
|
|
104
|
+
if self.previous_token in [TokenTypes.Identifier, TokenTypes.Keyword, TokenTypes.SoftKeyword, TokenTypes.NumberLiteral]:
|
|
105
|
+
self.delimiter(' ')
|
|
106
|
+
|
|
107
|
+
self._code += name
|
|
108
|
+
self.previous_token = TokenTypes.Identifier
|
|
109
|
+
|
|
110
|
+
def keyword(self, kw):
|
|
111
|
+
"""Add a keyword to the output code."""
|
|
112
|
+
assert kw in [
|
|
113
|
+
'False', 'None', 'True', 'and', 'as',
|
|
114
|
+
'assert', 'async', 'await', 'break',
|
|
115
|
+
'class', 'continue', 'def', 'del',
|
|
116
|
+
'elif', 'else', 'except', 'finally',
|
|
117
|
+
'for', 'from', 'global', 'if', 'import',
|
|
118
|
+
'in', 'is', 'lambda', 'nonlocal', 'not',
|
|
119
|
+
'or', 'pass', 'raise', 'return',
|
|
120
|
+
'try', 'while', 'with', 'yield', '_',
|
|
121
|
+
'case', 'match', 'print', 'exec',
|
|
122
|
+
'type'
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
if self.previous_token in [TokenTypes.Identifier, TokenTypes.Keyword, TokenTypes.SoftKeyword, TokenTypes.NumberLiteral]:
|
|
126
|
+
self.delimiter(' ')
|
|
127
|
+
|
|
128
|
+
self._code += kw
|
|
129
|
+
|
|
130
|
+
if kw in ['_', 'case', 'match', 'type']:
|
|
131
|
+
self.previous_token = TokenTypes.SoftKeyword
|
|
132
|
+
else:
|
|
133
|
+
self.previous_token = TokenTypes.Keyword
|
|
134
|
+
|
|
135
|
+
def stringliteral(self, value):
|
|
136
|
+
"""Add a string literal to the output code."""
|
|
137
|
+
s = repr(value)
|
|
138
|
+
|
|
139
|
+
if sys.version_info < (3, 0) and self.unicode_literals:
|
|
140
|
+
if s[0] == 'u':
|
|
141
|
+
# Remove the u prefix since literals are unicode by default
|
|
142
|
+
s = s[1:]
|
|
143
|
+
else:
|
|
144
|
+
# Add a b prefix to indicate it is NOT unicode
|
|
145
|
+
s = 'b' + s
|
|
146
|
+
|
|
147
|
+
if len(s) > 0 and s[0].isalpha() and self.previous_token in [TokenTypes.Identifier, TokenTypes.Keyword, TokenTypes.SoftKeyword]:
|
|
148
|
+
self.delimiter(' ')
|
|
149
|
+
|
|
150
|
+
self._code += s
|
|
151
|
+
self.previous_token = TokenTypes.NonNumberLiteral
|
|
152
|
+
|
|
153
|
+
def bytesliteral(self, value):
|
|
154
|
+
"""Add a bytes literal to the output code."""
|
|
155
|
+
s = repr(value)
|
|
156
|
+
|
|
157
|
+
if len(s) > 0 and s[0].isalpha() and self.previous_token in [TokenTypes.Identifier, TokenTypes.Keyword, TokenTypes.SoftKeyword]:
|
|
158
|
+
self.delimiter(' ')
|
|
159
|
+
|
|
160
|
+
self._code += s
|
|
161
|
+
self.previous_token = TokenTypes.NonNumberLiteral
|
|
162
|
+
|
|
163
|
+
def fstring(self, s):
|
|
164
|
+
"""Add an f-string to the output code."""
|
|
165
|
+
assert isinstance(s, str)
|
|
166
|
+
|
|
167
|
+
if self.previous_token in [TokenTypes.Identifier, TokenTypes.Keyword, TokenTypes.SoftKeyword]:
|
|
168
|
+
self.delimiter(' ')
|
|
169
|
+
|
|
170
|
+
self._code += s
|
|
171
|
+
self.previous_token = TokenTypes.NonNumberLiteral
|
|
172
|
+
|
|
173
|
+
def delimiter(self, d):
|
|
174
|
+
"""Add a delimiter to the output code."""
|
|
175
|
+
assert d in [
|
|
176
|
+
'(', ')', '[', ']', '{', '}', ' ',
|
|
177
|
+
',', ':', '.', ';', '@', '=', '->',
|
|
178
|
+
'+=', '-=', '*=', '/=', '//=', '%=', '@=',
|
|
179
|
+
'&=', '|=', '^=', '>>=', '<<=', '**=', '|',
|
|
180
|
+
'`'
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
self._code += d
|
|
184
|
+
self.previous_token = TokenTypes.Delimiter
|
|
185
|
+
|
|
186
|
+
def operator(self, o):
|
|
187
|
+
"""Add an operator to the output code."""
|
|
188
|
+
assert o in [
|
|
189
|
+
'+', '-', '*', '**', '/', '//', '%', '@',
|
|
190
|
+
'<<', '>>', '&', '|', '^', '~', ':=',
|
|
191
|
+
'<', '>', '<=', '>=', '==', '!='
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
self._code += o
|
|
195
|
+
self.previous_token = TokenTypes.Operator
|
|
196
|
+
|
|
197
|
+
def integer(self, v):
|
|
198
|
+
"""Add an integer to the output code."""
|
|
199
|
+
|
|
200
|
+
s = repr(v)
|
|
201
|
+
h = hex(v)
|
|
202
|
+
|
|
203
|
+
if self.previous_token == TokenTypes.SoftKeyword:
|
|
204
|
+
self.delimiter(' ')
|
|
205
|
+
elif self.previous_token in [TokenTypes.Identifier, TokenTypes.Keyword]:
|
|
206
|
+
self.delimiter(' ')
|
|
207
|
+
|
|
208
|
+
self._code += h if len(h) < len(s) else s
|
|
209
|
+
|
|
210
|
+
self.previous_token = TokenTypes.NumberLiteral
|
|
211
|
+
|
|
212
|
+
def imagnumber(self, value):
|
|
213
|
+
"""Add a complex number to the output code."""
|
|
214
|
+
assert isinstance(value, complex)
|
|
215
|
+
|
|
216
|
+
s = repr(value)
|
|
217
|
+
|
|
218
|
+
if s in ['infj', 'inf*j']:
|
|
219
|
+
s = '1e999j'
|
|
220
|
+
elif s in ['-infj', '-inf*j']:
|
|
221
|
+
s = '-1e999j'
|
|
222
|
+
|
|
223
|
+
if self.previous_token == TokenTypes.SoftKeyword:
|
|
224
|
+
self.delimiter(' ')
|
|
225
|
+
elif self.previous_token in [TokenTypes.Identifier, TokenTypes.Keyword]:
|
|
226
|
+
self.delimiter(' ')
|
|
227
|
+
|
|
228
|
+
self._code += s
|
|
229
|
+
|
|
230
|
+
self.previous_token = TokenTypes.NumberLiteral
|
|
231
|
+
|
|
232
|
+
def floatnumber(self, v):
|
|
233
|
+
"""Add a float to the output code."""
|
|
234
|
+
assert isinstance(v, float)
|
|
235
|
+
|
|
236
|
+
s = repr(v)
|
|
237
|
+
|
|
238
|
+
s = s.replace('e+', 'e')
|
|
239
|
+
|
|
240
|
+
add_e = re.match(r'^(\d+?)(0+).0$', s)
|
|
241
|
+
if add_e:
|
|
242
|
+
s = add_e.group(1) + 'e' + str(len(add_e.group(2)))
|
|
243
|
+
|
|
244
|
+
if s == 'inf':
|
|
245
|
+
s = '1e999'
|
|
246
|
+
elif s == '-inf':
|
|
247
|
+
s = '-1e999'
|
|
248
|
+
elif s.startswith('0.'):
|
|
249
|
+
s = s[1:]
|
|
250
|
+
elif s.startswith('-0.'):
|
|
251
|
+
s = '-' + s[2:]
|
|
252
|
+
elif s.endswith('.0'):
|
|
253
|
+
s = s[:-1]
|
|
254
|
+
|
|
255
|
+
if self.previous_token == TokenTypes.SoftKeyword:
|
|
256
|
+
self.delimiter(' ')
|
|
257
|
+
elif self.previous_token in [TokenTypes.Identifier, TokenTypes.Keyword]:
|
|
258
|
+
self.delimiter(' ')
|
|
259
|
+
|
|
260
|
+
self._code += s
|
|
261
|
+
|
|
262
|
+
self.previous_token = TokenTypes.NumberLiteral
|
|
263
|
+
|
|
264
|
+
def newline(self):
|
|
265
|
+
""" Add a newline to the code. """
|
|
266
|
+
if self._code == '':
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
self._code = self._code.rstrip('\n\t;')
|
|
270
|
+
self._code += '\n'
|
|
271
|
+
self._code += '\t' * self.indent
|
|
272
|
+
|
|
273
|
+
self.previous_token = TokenTypes.NewLine
|
|
274
|
+
|
|
275
|
+
def enter_block(self):
|
|
276
|
+
"""Enter a new block, indenting the code."""
|
|
277
|
+
self.indent += 1
|
|
278
|
+
self.newline()
|
|
279
|
+
|
|
280
|
+
def leave_block(self):
|
|
281
|
+
"""Leave a block, un-indenting the code."""
|
|
282
|
+
self.indent -= 1
|
|
283
|
+
self.newline()
|
|
284
|
+
|
|
285
|
+
def end_statement(self):
|
|
286
|
+
""" End a statement with a newline, or a semi-colon if it saves characters. """
|
|
287
|
+
|
|
288
|
+
if self.indent == 0:
|
|
289
|
+
self.newline()
|
|
290
|
+
else:
|
|
291
|
+
if self._code[-1] != ';':
|
|
292
|
+
self._code += ';'
|
|
293
|
+
|
|
294
|
+
self.previous_token = TokenTypes.EndStatement
|
|
295
|
+
|
|
296
|
+
def append(self, code, token_type):
|
|
297
|
+
""" Append arbitrary string to the output."""
|
|
298
|
+
self._code += code
|
|
299
|
+
self.previous_token = token_type
|
|
File without changes
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import python_minifier.ast_compat as ast
|
|
2
|
+
|
|
3
|
+
from python_minifier.transforms.suite_transformer import SuiteTransformer
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CombineImports(SuiteTransformer):
|
|
7
|
+
"""
|
|
8
|
+
Combine multiple import statements where possible
|
|
9
|
+
|
|
10
|
+
This doesn't change the order of imports
|
|
11
|
+
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def _combine_import(self, node_list, parent):
|
|
15
|
+
|
|
16
|
+
alias = []
|
|
17
|
+
namespace = None
|
|
18
|
+
|
|
19
|
+
for statement in node_list:
|
|
20
|
+
namespace = statement.namespace
|
|
21
|
+
if isinstance(statement, ast.Import):
|
|
22
|
+
alias += statement.names
|
|
23
|
+
else:
|
|
24
|
+
if alias:
|
|
25
|
+
yield self.add_child(ast.Import(names=alias), parent=parent, namespace=namespace)
|
|
26
|
+
alias = []
|
|
27
|
+
|
|
28
|
+
yield statement
|
|
29
|
+
|
|
30
|
+
if alias:
|
|
31
|
+
yield self.add_child(ast.Import(names=alias), parent=parent, namespace=namespace)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _combine_import_from(self, node_list, parent):
|
|
35
|
+
|
|
36
|
+
prev_import = None
|
|
37
|
+
alias = []
|
|
38
|
+
|
|
39
|
+
def combine(statement):
|
|
40
|
+
if not isinstance(statement, ast.ImportFrom):
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
if len(statement.names) == 1 and statement.names[0].name == '*':
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
if prev_import is None:
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
if statement.module == prev_import.module and statement.level == prev_import.level:
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
for statement in node_list:
|
|
55
|
+
if combine(statement):
|
|
56
|
+
prev_import = statement
|
|
57
|
+
alias += statement.names
|
|
58
|
+
else:
|
|
59
|
+
if alias:
|
|
60
|
+
yield self.add_child(
|
|
61
|
+
ast.ImportFrom(module=prev_import.module, names=alias, level=prev_import.level), parent=parent, namespace=prev_import.namespace
|
|
62
|
+
)
|
|
63
|
+
alias = []
|
|
64
|
+
|
|
65
|
+
yield statement
|
|
66
|
+
|
|
67
|
+
if alias:
|
|
68
|
+
yield self.add_child(
|
|
69
|
+
ast.ImportFrom(module=prev_import.module, names=alias, level=prev_import.level), parent=parent, namespace=prev_import.namespace
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def suite(self, node_list, parent):
|
|
73
|
+
a = list(self._combine_import(node_list, parent))
|
|
74
|
+
b = list(self._combine_import_from(a, parent))
|
|
75
|
+
|
|
76
|
+
return [self.visit(n) for n in b]
|