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,175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
For each node in an AST set the namespace to use for name binding and resolution
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import python_minifier.ast_compat as ast
|
|
6
|
+
|
|
7
|
+
from python_minifier.rename.util import is_namespace
|
|
8
|
+
from python_minifier.util import is_ast_node
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def add_parent_to_arguments(arguments, func):
|
|
12
|
+
arguments.parent = func
|
|
13
|
+
arguments.namespace = func
|
|
14
|
+
|
|
15
|
+
for arg in getattr(arguments, 'posonlyargs', []) + arguments.args:
|
|
16
|
+
add_parent(arg, arguments, func)
|
|
17
|
+
if hasattr(arg, 'annotation') and arg.annotation is not None:
|
|
18
|
+
add_parent(arg.annotation, arguments, func.namespace)
|
|
19
|
+
|
|
20
|
+
if hasattr(arguments, 'kwonlyargs'):
|
|
21
|
+
for arg in arguments.kwonlyargs:
|
|
22
|
+
add_parent(arg, arguments, func)
|
|
23
|
+
if arg.annotation is not None:
|
|
24
|
+
add_parent(arg.annotation, arguments, func.namespace)
|
|
25
|
+
|
|
26
|
+
for node in arguments.kw_defaults:
|
|
27
|
+
if node is not None:
|
|
28
|
+
add_parent(node, arguments, func.namespace)
|
|
29
|
+
|
|
30
|
+
for node in arguments.defaults:
|
|
31
|
+
add_parent(node, arguments, func.namespace)
|
|
32
|
+
|
|
33
|
+
if arguments.vararg:
|
|
34
|
+
if hasattr(arguments, 'varargannotation') and arguments.varargannotation is not None:
|
|
35
|
+
add_parent(arguments.varargannotation, arguments, func.namespace)
|
|
36
|
+
elif isinstance(arguments.vararg, str):
|
|
37
|
+
pass
|
|
38
|
+
else:
|
|
39
|
+
add_parent(arguments.vararg, arguments, func)
|
|
40
|
+
|
|
41
|
+
if arguments.kwarg:
|
|
42
|
+
if hasattr(arguments, 'kwargannotation') and arguments.kwargannotation is not None:
|
|
43
|
+
add_parent(arguments.kwargannotation, arguments, func.namespace)
|
|
44
|
+
elif isinstance(arguments.kwarg, str):
|
|
45
|
+
pass
|
|
46
|
+
else:
|
|
47
|
+
add_parent(arguments.kwarg, arguments, func)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def add_parent_to_functiondef(functiondef):
|
|
51
|
+
"""
|
|
52
|
+
Add correct parent and namespace attributes to functiondef nodes
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
if functiondef.args is not None:
|
|
56
|
+
add_parent_to_arguments(functiondef.args, func=functiondef)
|
|
57
|
+
|
|
58
|
+
for node in functiondef.body:
|
|
59
|
+
add_parent(node, parent=functiondef, namespace=functiondef)
|
|
60
|
+
|
|
61
|
+
for node in functiondef.decorator_list:
|
|
62
|
+
add_parent(node, parent=functiondef, namespace=functiondef.namespace)
|
|
63
|
+
|
|
64
|
+
if hasattr(functiondef, 'type_params') and functiondef.type_params is not None:
|
|
65
|
+
for node in functiondef.type_params:
|
|
66
|
+
add_parent(node, parent=functiondef, namespace=functiondef.namespace)
|
|
67
|
+
|
|
68
|
+
if hasattr(functiondef, 'returns') and functiondef.returns is not None:
|
|
69
|
+
add_parent(functiondef.returns, parent=functiondef, namespace=functiondef.namespace)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def add_parent_to_classdef(classdef):
|
|
73
|
+
"""
|
|
74
|
+
Add correct parent and namespace attributes to classdef nodes
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
for node in classdef.bases:
|
|
78
|
+
add_parent(node, parent=classdef, namespace=classdef.namespace)
|
|
79
|
+
|
|
80
|
+
if hasattr(classdef, 'keywords'):
|
|
81
|
+
for node in classdef.keywords:
|
|
82
|
+
add_parent(node, parent=classdef, namespace=classdef.namespace)
|
|
83
|
+
|
|
84
|
+
if hasattr(classdef, 'starargs') and classdef.starargs is not None:
|
|
85
|
+
add_parent(classdef.starargs, parent=classdef, namespace=classdef.namespace)
|
|
86
|
+
|
|
87
|
+
if hasattr(classdef, 'kwargs') and classdef.kwargs is not None:
|
|
88
|
+
add_parent(classdef.kwargs, parent=classdef, namespace=classdef.namespace)
|
|
89
|
+
|
|
90
|
+
for node in classdef.body:
|
|
91
|
+
add_parent(node, parent=classdef, namespace=classdef)
|
|
92
|
+
|
|
93
|
+
for node in classdef.decorator_list:
|
|
94
|
+
add_parent(node, parent=classdef, namespace=classdef.namespace)
|
|
95
|
+
|
|
96
|
+
if hasattr(classdef, 'type_params') and classdef.type_params is not None:
|
|
97
|
+
for node in classdef.type_params:
|
|
98
|
+
add_parent(node, parent=classdef, namespace=classdef.namespace)
|
|
99
|
+
|
|
100
|
+
def add_parent_to_comprehension(node, namespace):
|
|
101
|
+
assert is_ast_node(node, (ast.GeneratorExp, 'SetComp', 'DictComp', 'ListComp'))
|
|
102
|
+
|
|
103
|
+
if hasattr(node, 'elt'):
|
|
104
|
+
add_parent(node.elt, parent=node, namespace=node)
|
|
105
|
+
elif hasattr(node, 'key'):
|
|
106
|
+
add_parent(node.key, parent=node, namespace=node)
|
|
107
|
+
add_parent(node.value, parent=node, namespace=node)
|
|
108
|
+
|
|
109
|
+
iter_namespace = namespace
|
|
110
|
+
for generator in node.generators:
|
|
111
|
+
generator.parent = node
|
|
112
|
+
generator.namespace = node
|
|
113
|
+
|
|
114
|
+
add_parent(generator.target, parent=generator, namespace=node)
|
|
115
|
+
add_parent(generator.iter, parent=generator, namespace=iter_namespace)
|
|
116
|
+
iter_namespace = node
|
|
117
|
+
for if_ in generator.ifs:
|
|
118
|
+
add_parent(if_, parent=generator, namespace=node)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def add_parent(node, parent=None, namespace=None):
|
|
122
|
+
"""
|
|
123
|
+
Add a parent attribute to child nodes
|
|
124
|
+
Add a namespace attribute to child nodes
|
|
125
|
+
|
|
126
|
+
:param node: The tree to add parent and namespace properties to
|
|
127
|
+
:type node: :class:`ast.AST`
|
|
128
|
+
:param parent: The parent node of this node
|
|
129
|
+
:type parent: :class:`ast.AST`
|
|
130
|
+
:param namespace: The namespace Node that this node is in
|
|
131
|
+
:type namespace: ast.Lambda or ast.Module or ast.FunctionDef or ast.AsyncFunctionDef or ast.ClassDef or ast.DictComp or ast.SetComp or ast.ListComp or ast.Generator
|
|
132
|
+
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
node.parent = parent if parent is not None else node
|
|
136
|
+
node.namespace = namespace if namespace is not None else node
|
|
137
|
+
|
|
138
|
+
if is_namespace(node):
|
|
139
|
+
node.bindings = []
|
|
140
|
+
node.global_names = set()
|
|
141
|
+
node.nonlocal_names = set()
|
|
142
|
+
|
|
143
|
+
if is_ast_node(node, (ast.FunctionDef, 'AsyncFunctionDef')):
|
|
144
|
+
add_parent_to_functiondef(node)
|
|
145
|
+
elif is_ast_node(node, (ast.GeneratorExp, 'SetComp', 'DictComp', 'ListComp')):
|
|
146
|
+
add_parent_to_comprehension(node, namespace=namespace)
|
|
147
|
+
elif isinstance(node, ast.Lambda):
|
|
148
|
+
add_parent_to_arguments(node.args, func=node)
|
|
149
|
+
add_parent(node.body, parent=node, namespace=node)
|
|
150
|
+
elif isinstance(node, ast.ClassDef):
|
|
151
|
+
add_parent_to_classdef(node)
|
|
152
|
+
else:
|
|
153
|
+
for child in ast.iter_child_nodes(node):
|
|
154
|
+
add_parent(child, parent=node, namespace=node)
|
|
155
|
+
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
if isinstance(node, ast.Global):
|
|
159
|
+
namespace.global_names.update(node.names)
|
|
160
|
+
if is_ast_node(node, 'Nonlocal'):
|
|
161
|
+
namespace.nonlocal_names.update(node.names)
|
|
162
|
+
|
|
163
|
+
if isinstance(node, ast.Name):
|
|
164
|
+
if isinstance(namespace, ast.ClassDef):
|
|
165
|
+
if isinstance(node.ctx, ast.Load):
|
|
166
|
+
namespace.nonlocal_names.add(node.id)
|
|
167
|
+
elif isinstance(node.ctx, ast.Store) and isinstance(node.parent, ast.AugAssign):
|
|
168
|
+
namespace.nonlocal_names.add(node.id)
|
|
169
|
+
|
|
170
|
+
for child in ast.iter_child_nodes(node):
|
|
171
|
+
add_parent(child, parent=node, namespace=namespace)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def add_namespace(module):
|
|
175
|
+
add_parent(module)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
import keyword
|
|
3
|
+
import random
|
|
4
|
+
import string
|
|
5
|
+
|
|
6
|
+
from python_minifier.rename.util import builtins
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def random_generator(length=40):
|
|
10
|
+
valid_first = string.ascii_uppercase + string.ascii_lowercase
|
|
11
|
+
valid_rest = string.digits + valid_first + '_'
|
|
12
|
+
|
|
13
|
+
while True:
|
|
14
|
+
first = [random.choice(valid_first)]
|
|
15
|
+
rest = [random.choice(valid_rest) for i in range(length - 1)]
|
|
16
|
+
yield ''.join(first + rest)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def name_generator():
|
|
20
|
+
valid_first = string.ascii_uppercase + string.ascii_lowercase
|
|
21
|
+
valid_rest = string.digits + valid_first + '_'
|
|
22
|
+
|
|
23
|
+
for c in valid_first:
|
|
24
|
+
yield c
|
|
25
|
+
|
|
26
|
+
for length in itertools.count(1):
|
|
27
|
+
for first in valid_first:
|
|
28
|
+
for rest in itertools.product(valid_rest, repeat=length):
|
|
29
|
+
name = first
|
|
30
|
+
name += ''.join(rest)
|
|
31
|
+
yield name
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def name_filter():
|
|
35
|
+
"""
|
|
36
|
+
Yield all valid python identifiers
|
|
37
|
+
|
|
38
|
+
Name are returned sorted by length, then string sort order.
|
|
39
|
+
|
|
40
|
+
Names that already have meaning in python (keywords and builtins)
|
|
41
|
+
will not be included in the output.
|
|
42
|
+
|
|
43
|
+
:rtype: Iterable[str]
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
reserved = keyword.kwlist + dir(builtins)
|
|
48
|
+
|
|
49
|
+
for name in name_generator():
|
|
50
|
+
if name not in reserved:
|
|
51
|
+
yield name
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import python_minifier.ast_compat as ast
|
|
2
|
+
|
|
3
|
+
from python_minifier.rename.binding import Binding
|
|
4
|
+
from python_minifier.rename.util import insert
|
|
5
|
+
from python_minifier.transforms.suite_transformer import NodeVisitor
|
|
6
|
+
from python_minifier.util import is_ast_node
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def replace(old_node, new_node):
|
|
10
|
+
parent = old_node.parent
|
|
11
|
+
new_node.parent = parent
|
|
12
|
+
new_node.namespace = old_node.namespace
|
|
13
|
+
|
|
14
|
+
for field, old_value in ast.iter_fields(parent):
|
|
15
|
+
if old_value is old_node:
|
|
16
|
+
setattr(parent, field, new_node)
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
if isinstance(old_value, list):
|
|
20
|
+
for i, value in enumerate(old_value):
|
|
21
|
+
if value is old_node:
|
|
22
|
+
old_value[i] = new_node
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class HoistedBinding(Binding):
|
|
27
|
+
def __init__(self, value_node, *args, **kwargs):
|
|
28
|
+
super(HoistedBinding, self).__init__(*args, **kwargs)
|
|
29
|
+
self._value_node = value_node
|
|
30
|
+
self._local_namespace = None
|
|
31
|
+
|
|
32
|
+
def __eq__(self, other):
|
|
33
|
+
return type(self.value) is type(other.value) and self.value == other.value
|
|
34
|
+
|
|
35
|
+
def __ne__(self, other):
|
|
36
|
+
return not self == other
|
|
37
|
+
|
|
38
|
+
def __hash__(self):
|
|
39
|
+
return hash(repr(self.value))
|
|
40
|
+
|
|
41
|
+
def set_local_namespace(self, node):
|
|
42
|
+
self._local_namespace = node
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def value(self):
|
|
46
|
+
if is_ast_node(self._value_node, (ast.Str, 'Bytes')):
|
|
47
|
+
return self._value_node.s
|
|
48
|
+
else:
|
|
49
|
+
return self._value_node.value
|
|
50
|
+
|
|
51
|
+
def __repr__(self):
|
|
52
|
+
return self.__class__.__name__ + '(value=%r)' % self.value
|
|
53
|
+
|
|
54
|
+
def new_mention_count(self):
|
|
55
|
+
# All mentions must be literals, which would be replaced
|
|
56
|
+
# Plus an Assign with the new name
|
|
57
|
+
return len(self.references) + 1
|
|
58
|
+
|
|
59
|
+
def old_mention_count(self):
|
|
60
|
+
# For hoisted bindings, the old 'name' is the literal
|
|
61
|
+
# It would be mentioned once, in the Assign
|
|
62
|
+
return 1
|
|
63
|
+
|
|
64
|
+
def additional_byte_cost(self):
|
|
65
|
+
return 2 # '=' + '\n'
|
|
66
|
+
|
|
67
|
+
def rename(self, new_name):
|
|
68
|
+
|
|
69
|
+
for node in self.references:
|
|
70
|
+
replace(node, ast.Name(id=new_name, ctx=ast.Load()))
|
|
71
|
+
|
|
72
|
+
self._local_namespace.body = list(
|
|
73
|
+
insert(
|
|
74
|
+
self._local_namespace.body,
|
|
75
|
+
ast.Assign(targets=[ast.Name(id=new_name, ctx=ast.Store())], value=self._value_node),
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
self._name = new_name
|
|
80
|
+
|
|
81
|
+
def should_rename(self, new_name):
|
|
82
|
+
current_cost = len(self.references) * len(repr(self.value))
|
|
83
|
+
rename_cost = (self.old_mention_count() * len(repr(self.value))) + ((self.new_mention_count()) * len(new_name)) + self.additional_byte_cost()
|
|
84
|
+
|
|
85
|
+
return rename_cost <= current_cost
|
|
86
|
+
|
|
87
|
+
class HoistedValue(object):
|
|
88
|
+
"""
|
|
89
|
+
HoistedValue comparator object
|
|
90
|
+
|
|
91
|
+
This is for wrapping a value in a set or dict key, and
|
|
92
|
+
ensures different types hash differently, even if they compare equal.
|
|
93
|
+
|
|
94
|
+
The problematic values are str/bytes/unicode and int/float.
|
|
95
|
+
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(self, value):
|
|
99
|
+
self._value = value
|
|
100
|
+
|
|
101
|
+
def __hash__(self):
|
|
102
|
+
return hash(str(type(self._value)) + str(hash(self._value)))
|
|
103
|
+
|
|
104
|
+
def __eq__(self, other):
|
|
105
|
+
return type(self._value) == type(other._value) and self._value == other._value
|
|
106
|
+
|
|
107
|
+
def __ne__(self, other):
|
|
108
|
+
return not self == other
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class HoistLiterals(NodeVisitor):
|
|
112
|
+
"""
|
|
113
|
+
Hoist literal strings to module level variables
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __call__(self, module, ignore_slots=True):
|
|
117
|
+
self.module = module
|
|
118
|
+
self._ignore_slots = ignore_slots
|
|
119
|
+
self._hoisted = {}
|
|
120
|
+
self.visit(module)
|
|
121
|
+
self.place_bindings()
|
|
122
|
+
|
|
123
|
+
def nearest_function_namespace(self, node):
|
|
124
|
+
"""
|
|
125
|
+
Return the namespace node for the nearest function scope.
|
|
126
|
+
|
|
127
|
+
This could be itself.
|
|
128
|
+
|
|
129
|
+
:param node: The node to get the function namespace of
|
|
130
|
+
:type node: ast.Node
|
|
131
|
+
:rtype: ast.Node
|
|
132
|
+
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
if is_ast_node(node.namespace, (ast.FunctionDef, ast.Module, 'AsyncFunctionDef')):
|
|
136
|
+
return node.namespace
|
|
137
|
+
return self.nearest_function_namespace(node.namespace)
|
|
138
|
+
|
|
139
|
+
def namespace_path(self, node):
|
|
140
|
+
"""
|
|
141
|
+
Return the path of function namespace nodes from the module node down to the input node
|
|
142
|
+
|
|
143
|
+
With the source module:
|
|
144
|
+
>>> def a():
|
|
145
|
+
... def b():
|
|
146
|
+
... c
|
|
147
|
+
|
|
148
|
+
>>> namespace_path(c)
|
|
149
|
+
[a, b, c]
|
|
150
|
+
|
|
151
|
+
:param node:
|
|
152
|
+
:type node: ast.Node
|
|
153
|
+
:rtype: list[ast.AST]
|
|
154
|
+
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
l = []
|
|
158
|
+
|
|
159
|
+
while True:
|
|
160
|
+
namespace = self.nearest_function_namespace(node)
|
|
161
|
+
l.insert(0, namespace)
|
|
162
|
+
|
|
163
|
+
if isinstance(namespace, ast.Module):
|
|
164
|
+
break
|
|
165
|
+
|
|
166
|
+
node = namespace
|
|
167
|
+
|
|
168
|
+
return l
|
|
169
|
+
|
|
170
|
+
def common_path(self, n1_path, n2_path):
|
|
171
|
+
|
|
172
|
+
path = []
|
|
173
|
+
for n1_step, n2_step in zip(n1_path, n2_path):
|
|
174
|
+
if n1_step is not n2_step:
|
|
175
|
+
return path
|
|
176
|
+
path.append(n1_step)
|
|
177
|
+
return path
|
|
178
|
+
|
|
179
|
+
def place_bindings(self):
|
|
180
|
+
for binding in self._hoisted.values():
|
|
181
|
+
|
|
182
|
+
namespace_path = []
|
|
183
|
+
|
|
184
|
+
for node in binding.references:
|
|
185
|
+
if not namespace_path:
|
|
186
|
+
namespace_path = self.namespace_path(node)
|
|
187
|
+
else:
|
|
188
|
+
namespace_path = self.common_path(namespace_path, self.namespace_path(node))
|
|
189
|
+
|
|
190
|
+
namespace_path[-1].bindings.append(binding)
|
|
191
|
+
binding.set_local_namespace(namespace_path[-1])
|
|
192
|
+
|
|
193
|
+
def get_binding(self, value, node):
|
|
194
|
+
hoisted_value = HoistedValue(value)
|
|
195
|
+
if hoisted_value in self._hoisted:
|
|
196
|
+
return self._hoisted[hoisted_value]
|
|
197
|
+
|
|
198
|
+
binding = HoistedBinding(node)
|
|
199
|
+
self._hoisted[hoisted_value] = binding
|
|
200
|
+
return binding
|
|
201
|
+
|
|
202
|
+
def visit_Str(self, node):
|
|
203
|
+
|
|
204
|
+
if isinstance(node.parent, ast.Expr):
|
|
205
|
+
# This is literal statement
|
|
206
|
+
# The RemoveLiteralStatements transformer must have left it here, so ignore it.
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
self.get_binding(node.s, node).add_reference(node)
|
|
210
|
+
|
|
211
|
+
def visit_Bytes(self, node):
|
|
212
|
+
self.visit_Str(node)
|
|
213
|
+
|
|
214
|
+
def visit_JoinedStr(self, node):
|
|
215
|
+
for v in node.values:
|
|
216
|
+
if is_ast_node(v, ast.Str):
|
|
217
|
+
# Can't hoist this!
|
|
218
|
+
continue
|
|
219
|
+
else:
|
|
220
|
+
self.visit(v)
|
|
221
|
+
|
|
222
|
+
def visit_NameConstant(self, node):
|
|
223
|
+
self.get_binding(node.value, node).add_reference(node)
|
|
224
|
+
|
|
225
|
+
def visit_match_case(self, node):
|
|
226
|
+
# Can't hoist literals in a pattern
|
|
227
|
+
|
|
228
|
+
if node.guard is not None:
|
|
229
|
+
self.visit(node.guard)
|
|
230
|
+
|
|
231
|
+
for n in node.body:
|
|
232
|
+
self.visit(n)
|
|
233
|
+
|
|
234
|
+
def visit_Assign(self, node):
|
|
235
|
+
if not self._ignore_slots:
|
|
236
|
+
return self.generic_visit(node)
|
|
237
|
+
|
|
238
|
+
if not is_ast_node(node.namespace, ast.ClassDef):
|
|
239
|
+
return self.generic_visit(node)
|
|
240
|
+
|
|
241
|
+
for target in node.targets:
|
|
242
|
+
if is_ast_node(target, ast.Name) and target.id == '__slots__':
|
|
243
|
+
# This is a __slots__ assignment, don't hoist the literals
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
return self.generic_visit(node)
|
|
247
|
+
|
|
248
|
+
def rename_literals(module):
|
|
249
|
+
HoistLiterals()(module)
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import python_minifier.ast_compat as ast
|
|
2
|
+
|
|
3
|
+
from python_minifier.rename.binding import NameBinding
|
|
4
|
+
from python_minifier.rename.name_generator import name_filter
|
|
5
|
+
from python_minifier.rename.util import is_namespace
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def all_bindings(node):
|
|
9
|
+
"""
|
|
10
|
+
All bindings in a module
|
|
11
|
+
|
|
12
|
+
:param node: The module to get bindings in
|
|
13
|
+
:type node: :class:`ast.AST`
|
|
14
|
+
:rtype: Iterable[ast.AST, Binding]
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
if is_namespace(node):
|
|
19
|
+
for binding in node.bindings:
|
|
20
|
+
yield node, binding
|
|
21
|
+
|
|
22
|
+
for child in ast.iter_child_nodes(node):
|
|
23
|
+
for namespace, binding in all_bindings(child):
|
|
24
|
+
yield namespace, binding
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def sorted_bindings(module):
|
|
28
|
+
"""
|
|
29
|
+
All bindings in a modules sorted by descending number of references
|
|
30
|
+
|
|
31
|
+
:param module: The module to get bindings in
|
|
32
|
+
:type module: :class:`ast.AST`
|
|
33
|
+
:rtype: Iterable[ast.AST, Binding]
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def comp(tup):
|
|
38
|
+
namespace, binding = tup
|
|
39
|
+
return binding.new_mention_count()
|
|
40
|
+
|
|
41
|
+
return sorted(all_bindings(module), key=comp, reverse=True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def reservation_scope(namespace, binding):
|
|
45
|
+
"""
|
|
46
|
+
Get the namespaces that are in the bindings reservation scope
|
|
47
|
+
|
|
48
|
+
Returns the namespace nodes the binding name must be resolvable in
|
|
49
|
+
|
|
50
|
+
:param namespace: The local namespace of a binding
|
|
51
|
+
:type namespace: :class:`ast.AST`
|
|
52
|
+
:param binding: The binding to get the reservation scope for
|
|
53
|
+
:type binding: Binding
|
|
54
|
+
:rtype: set[ast.AST]
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
namespaces = set([namespace])
|
|
59
|
+
|
|
60
|
+
for node in binding.references:
|
|
61
|
+
while node is not namespace:
|
|
62
|
+
namespaces.add(node.namespace)
|
|
63
|
+
node = node.namespace
|
|
64
|
+
|
|
65
|
+
return namespaces
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def add_assigned(node):
|
|
69
|
+
"""
|
|
70
|
+
Add the assigned_names attribute to namespace nodes in a module
|
|
71
|
+
|
|
72
|
+
:param node: The module to add the assigned_names attribute to
|
|
73
|
+
:type node: :class:`ast.Module`
|
|
74
|
+
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
if is_namespace(node):
|
|
78
|
+
node.assigned_names = set()
|
|
79
|
+
|
|
80
|
+
for child in ast.iter_child_nodes(node):
|
|
81
|
+
add_assigned(child)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def reserve_name(name, reservation_scope):
|
|
85
|
+
"""
|
|
86
|
+
Reserve a name in a reservation scope
|
|
87
|
+
|
|
88
|
+
:param str name: The name to reserve
|
|
89
|
+
:param reservation_scope:
|
|
90
|
+
:type reservation_scope: Iterable[:class:`ast.AST`]
|
|
91
|
+
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
for namespace in reservation_scope:
|
|
95
|
+
namespace.assigned_names.add(name)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class UniqueNameAssigner(object):
|
|
99
|
+
"""
|
|
100
|
+
Assign new names to renamed bindings
|
|
101
|
+
|
|
102
|
+
Assigns a unique name to every binding
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def __init__(self):
|
|
106
|
+
self.name_generator = name_filter()
|
|
107
|
+
self.names = []
|
|
108
|
+
|
|
109
|
+
def available_name(self):
|
|
110
|
+
return next(self.name_generator)
|
|
111
|
+
|
|
112
|
+
def __call__(self, module):
|
|
113
|
+
assert isinstance(module, ast.Module)
|
|
114
|
+
|
|
115
|
+
for namespace, binding in sorted_bindings(module):
|
|
116
|
+
if binding.allow_rename:
|
|
117
|
+
binding.new_name = self.available_name()
|
|
118
|
+
|
|
119
|
+
return module
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class NameAssigner(object):
|
|
123
|
+
"""
|
|
124
|
+
Assign new names to renamed bindings
|
|
125
|
+
|
|
126
|
+
This assigner creates a name 'reservation scope' containing each namespace a binding is referenced in, including
|
|
127
|
+
transitive namespaces. Bindings are then assigned the first available name that has no references in their
|
|
128
|
+
reservation scope. This means names will be reused in sibling namespaces, and shadowed where possible in child
|
|
129
|
+
namespaces.
|
|
130
|
+
|
|
131
|
+
Bindings are assigned names in order of most references, with names assigned shortest first.
|
|
132
|
+
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
def __init__(self, name_generator=None):
|
|
136
|
+
self.name_generator = name_generator if name_generator is not None else name_filter()
|
|
137
|
+
self.names = []
|
|
138
|
+
|
|
139
|
+
def iter_names(self):
|
|
140
|
+
for name in self.names:
|
|
141
|
+
yield name
|
|
142
|
+
|
|
143
|
+
while True:
|
|
144
|
+
name = next(self.name_generator)
|
|
145
|
+
self.names.append(name)
|
|
146
|
+
yield name
|
|
147
|
+
|
|
148
|
+
def available_name(self, reservation_scope, prefix=''):
|
|
149
|
+
"""
|
|
150
|
+
Search for the first name that is not in reservation scope
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
for name in self.iter_names():
|
|
154
|
+
if self.is_available(prefix + name, reservation_scope):
|
|
155
|
+
return prefix + name
|
|
156
|
+
|
|
157
|
+
def is_available(self, name, reservation_scope):
|
|
158
|
+
"""
|
|
159
|
+
Is a name unreserved in a reservation scope
|
|
160
|
+
|
|
161
|
+
:param str name: the name to check availability of
|
|
162
|
+
:param reservation_scope: The scope to check
|
|
163
|
+
:type reservation_scope: Iterable[:class:`ast.AST`]
|
|
164
|
+
:rtype: bool
|
|
165
|
+
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
for namespace in reservation_scope:
|
|
169
|
+
if name in namespace.assigned_names:
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
return True
|
|
173
|
+
|
|
174
|
+
def __call__(self, module, prefix_globals, reserved_globals=None):
|
|
175
|
+
assert isinstance(module, ast.Module)
|
|
176
|
+
add_assigned(module)
|
|
177
|
+
|
|
178
|
+
for namespace, binding in all_bindings(module):
|
|
179
|
+
if binding.reserved is not None:
|
|
180
|
+
scope = reservation_scope(namespace, binding)
|
|
181
|
+
reserve_name(binding.reserved, scope)
|
|
182
|
+
|
|
183
|
+
if reserved_globals is not None:
|
|
184
|
+
for name in reserved_globals:
|
|
185
|
+
module.assigned_names.add(name)
|
|
186
|
+
|
|
187
|
+
for namespace, binding in sorted_bindings(module):
|
|
188
|
+
scope = reservation_scope(namespace, binding)
|
|
189
|
+
|
|
190
|
+
if binding.allow_rename:
|
|
191
|
+
|
|
192
|
+
if isinstance(namespace, ast.Module) and prefix_globals:
|
|
193
|
+
name = self.available_name(scope, prefix='_')
|
|
194
|
+
else:
|
|
195
|
+
name = self.available_name(scope)
|
|
196
|
+
|
|
197
|
+
def should_rename():
|
|
198
|
+
if binding.should_rename(name):
|
|
199
|
+
return True
|
|
200
|
+
|
|
201
|
+
# It's no longer efficient to do this rename
|
|
202
|
+
|
|
203
|
+
if isinstance(binding, NameBinding):
|
|
204
|
+
# Check that the original name is still available
|
|
205
|
+
|
|
206
|
+
if binding.reserved == binding.name:
|
|
207
|
+
# We already reserved it (this is probably an arg)
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
if not self.is_available(binding.name, scope):
|
|
211
|
+
# The original name has already been assigned to another binding,
|
|
212
|
+
# so we need to rename this anyway.
|
|
213
|
+
return True
|
|
214
|
+
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
if should_rename():
|
|
218
|
+
binding.rename(name)
|
|
219
|
+
else:
|
|
220
|
+
# Any existing name will become reserved
|
|
221
|
+
binding.disallow_rename()
|
|
222
|
+
|
|
223
|
+
if binding.name is not None:
|
|
224
|
+
reserve_name(binding.name, scope)
|
|
225
|
+
|
|
226
|
+
return module
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def rename(module, prefix_globals=False, preserved_globals=None):
|
|
230
|
+
NameAssigner()(module, prefix_globals, preserved_globals)
|