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.
Files changed (44) hide show
  1. python_minifier/__init__.py +269 -0
  2. python_minifier/__init__.pyi +37 -0
  3. python_minifier/__main__.py +332 -0
  4. python_minifier/ast_compare.py +104 -0
  5. python_minifier/ast_compat.py +40 -0
  6. python_minifier/ast_printer.py +130 -0
  7. python_minifier/expression_printer.py +753 -0
  8. python_minifier/f_string.py +446 -0
  9. python_minifier/ministring.py +179 -0
  10. python_minifier/module_printer.py +862 -0
  11. python_minifier/py.typed +0 -0
  12. python_minifier/rename/__init__.py +6 -0
  13. python_minifier/rename/bind_names.py +192 -0
  14. python_minifier/rename/binding.py +495 -0
  15. python_minifier/rename/mapper.py +175 -0
  16. python_minifier/rename/name_generator.py +51 -0
  17. python_minifier/rename/rename_literals.py +249 -0
  18. python_minifier/rename/renamer.py +230 -0
  19. python_minifier/rename/resolve_names.py +106 -0
  20. python_minifier/rename/util.py +203 -0
  21. python_minifier/token_printer.py +299 -0
  22. python_minifier/transforms/__init__.py +0 -0
  23. python_minifier/transforms/combine_imports.py +76 -0
  24. python_minifier/transforms/constant_folding.py +114 -0
  25. python_minifier/transforms/remove_annotations.py +130 -0
  26. python_minifier/transforms/remove_annotations_options.py +35 -0
  27. python_minifier/transforms/remove_annotations_options.pyi +17 -0
  28. python_minifier/transforms/remove_asserts.py +26 -0
  29. python_minifier/transforms/remove_debug.py +53 -0
  30. python_minifier/transforms/remove_exception_brackets.py +124 -0
  31. python_minifier/transforms/remove_explicit_return_none.py +38 -0
  32. python_minifier/transforms/remove_literal_statements.py +61 -0
  33. python_minifier/transforms/remove_object_base.py +24 -0
  34. python_minifier/transforms/remove_pass.py +26 -0
  35. python_minifier/transforms/remove_posargs.py +13 -0
  36. python_minifier/transforms/suite_transformer.py +196 -0
  37. python_minifier/util.py +48 -0
  38. python_minifier-2.11.2.dist-info/LICENSE +21 -0
  39. python_minifier-2.11.2.dist-info/METADATA +177 -0
  40. python_minifier-2.11.2.dist-info/RECORD +44 -0
  41. python_minifier-2.11.2.dist-info/WHEEL +5 -0
  42. python_minifier-2.11.2.dist-info/entry_points.txt +3 -0
  43. python_minifier-2.11.2.dist-info/top_level.txt +1 -0
  44. 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]