Skip to content

Commit 333acfe

Browse files
authored
Merge pull request #101 from dflook/python312-class-names
Fix ClassDef Name binding
2 parents f236db3 + 61ee2d7 commit 333acfe

16 files changed

+2843
-24
lines changed

.github/workflows/xtest.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ jobs:
1717
container:
1818
image: danielflook/python-minifier-build:${{ matrix.python }}-2024-01-12
1919
steps:
20+
- name: Cleanup
21+
run: |
22+
echo rm -vrf "$HOME/.*" "$HOME/*" "$GITHUB_WORKSPACE/.*" "$GITHUB_WORKSPACE/*"
23+
rm -vrf "$HOME/.*" "$HOME/*" "$GITHUB_WORKSPACE/.*" "$GITHUB_WORKSPACE/*"
24+
2025
- name: Checkout
2126
uses: actions/checkout@v3
2227
with:

corpus_test/generate_results.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import argparse
2-
import datetime
32
import gzip
43
import os
54
import sys
@@ -63,6 +62,9 @@ def minify_corpus_entry(corpus_path, corpus_entry):
6362
result.time = end_time - start_time
6463
result.outcome = 'UnstableMinification'
6564

65+
except AssertionError as assertion_error:
66+
result.outcome = 'Exception: AssertionError'
67+
6668
except Exception as exception:
6769
result.outcome = 'Exception: ' + str(exception)
6870

src/python_minifier/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,6 @@ def unparse(module):
228228
try:
229229
compare_ast(module, minified_module)
230230
except CompareError as compare_error:
231-
print(printer.code)
232231
raise UnstableMinification(compare_error, '', printer.code)
233232

234233
return printer.code

src/python_minifier/rename/bind_names.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,6 @@ def get_binding(self, name, namespace):
2424
# nonlocal names should not create a binding in any context
2525
assert name not in namespace.nonlocal_names
2626

27-
if isinstance(namespace, ast.ClassDef):
28-
binding = self.get_binding(name, get_nonlocal_namespace(namespace))
29-
binding.disallow_rename()
30-
return binding
31-
3227
for binding in namespace.bindings:
3328
if binding.name == name:
3429
break
@@ -43,6 +38,10 @@ def get_binding(self, name, namespace):
4338
# This is actually a syntax error - but we want the same syntax error after minifying!
4439
binding.disallow_rename()
4540

41+
if isinstance(namespace, ast.ClassDef):
42+
# This name will become an attribute of the class, so it can't be renamed
43+
binding.disallow_rename()
44+
4645
return binding
4746

4847
def visit_Name(self, node):

src/python_minifier/rename/binding.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ class Binding(object):
1111
:param name: A name for this binding
1212
:type name: str or None
1313
:param bool allow_rename: If this binding may be renamed
14-
:param int rename_cost: The cost of renaming this binding in bytes, NOT including the difference in name lengths
1514
1615
"""
1716

@@ -289,7 +288,7 @@ def __init__(self, name, *args, **kwargs):
289288
self.disallow_rename()
290289

291290
def __repr__(self):
292-
return self.__class__.__name__ + '(name=%r) <references=%r>' % (self._name, len(self._references))
291+
return self.__class__.__name__ + '(name=%r, allow_rename=%r) <references=%r>' % (self._name, self._allow_rename, len(self._references))
293292

294293
def should_rename(self, new_name):
295294
"""
@@ -448,3 +447,25 @@ def rename(self, new_name):
448447
),
449448
)
450449
)
450+
451+
def is_redefined(self):
452+
"""
453+
Do one of the references to this builtin name redefine it?
454+
455+
Could some references actually not be references to the builtin?
456+
457+
This can happen with code like:
458+
459+
class MyClass:
460+
IndexError = IndexError
461+
462+
"""
463+
464+
for node in self.references:
465+
if not isinstance(node, ast.Name):
466+
return True
467+
468+
if not isinstance(node.ctx, ast.Load):
469+
return True
470+
471+
return False

src/python_minifier/rename/mapper.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,13 @@ def add_parent(node, parent=None, namespace=None):
160160
if is_ast_node(node, 'Nonlocal'):
161161
namespace.nonlocal_names.update(node.names)
162162

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+
163170
for child in ast.iter_child_nodes(node):
164171
add_parent(child, parent=node, namespace=namespace)
165172

src/python_minifier/rename/resolve_names.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ def get_binding(name, namespace):
3434
namespace.bindings.append(binding)
3535
return binding
3636

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
3745

3846
def resolve_names(node):
3947
"""
@@ -47,39 +55,49 @@ def resolve_names(node):
4755
if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load):
4856
get_binding(node.id, node.namespace).add_reference(node)
4957
elif isinstance(node, ast.Name) and node.id in node.namespace.nonlocal_names:
50-
get_binding(node.id, node.namespace).add_reference(node)
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()
5163

5264
elif isinstance(node, ast.ClassDef) and node.name in node.namespace.nonlocal_names:
53-
get_binding(node.name, node.namespace).add_reference(node)
65+
binding = get_binding_disallow_class_namespace_rename(node.name, node.namespace)
66+
binding.add_reference(node)
67+
5468
elif is_ast_node(node, (ast.FunctionDef, 'AsyncFunctionDef')) and node.name in node.namespace.nonlocal_names:
55-
get_binding(node.name, node.namespace).add_reference(node)
69+
binding = get_binding_disallow_class_namespace_rename(node.name, node.namespace)
70+
binding.add_reference(node)
71+
5672
elif isinstance(node, ast.alias):
5773

5874
if node.asname is not None:
5975
if node.asname in node.namespace.nonlocal_names:
60-
get_binding(node.asname, node.namespace).add_reference(node)
76+
binding = get_binding_disallow_class_namespace_rename(node.asname, node.namespace)
77+
binding.add_reference(node)
78+
6179
else:
6280
# This binds the root module only for a dotted import
6381
root_module = node.name.split('.')[0]
6482

6583
if root_module in node.namespace.nonlocal_names:
66-
binding = get_binding(root_module, node.namespace)
84+
binding = get_binding_disallow_class_namespace_rename(root_module, node.namespace)
6785
binding.add_reference(node)
6886

6987
if '.' in node.name:
7088
binding.disallow_rename()
7189

7290
elif isinstance(node, ast.ExceptHandler) and node.name is not None:
7391
if isinstance(node.name, str) and node.name in node.namespace.nonlocal_names:
74-
get_binding(node.name, node.namespace).add_reference(node)
92+
get_binding_disallow_class_namespace_rename(node.name, node.namespace).add_reference(node)
7593

7694
elif is_ast_node(node, 'Nonlocal'):
7795
for name in node.names:
78-
get_binding(name, node.namespace).add_reference(node)
96+
get_binding_disallow_class_namespace_rename(name, node.namespace).add_reference(node)
7997
elif is_ast_node(node, ('MatchAs', 'MatchStar')) and node.name in node.namespace.nonlocal_names:
80-
get_binding(node.name, node.namespace).add_reference(node)
98+
get_binding_disallow_class_namespace_rename(node.name, node.namespace).add_reference(node)
8199
elif is_ast_node(node, 'MatchMapping') and node.rest in node.namespace.nonlocal_names:
82-
get_binding(node.rest, node.namespace).add_reference(node)
100+
get_binding_disallow_class_namespace_rename(node.rest, node.namespace).add_reference(node)
83101

84102
elif is_ast_node(node, 'Exec'):
85103
get_global_namespace(node).tainted = True

src/python_minifier/rename/util.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ def create_is_namespace():
2323
is_namespace = create_is_namespace()
2424

2525

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+
2635
def get_global_namespace(node):
2736
"""
2837
Return the global namespace for a node

src/python_minifier/transforms/combine_imports.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,22 @@ class CombineImports(SuiteTransformer):
1414
def _combine_import(self, node_list, parent):
1515

1616
alias = []
17+
namespace = None
1718

1819
for statement in node_list:
20+
namespace = statement.namespace
1921
if isinstance(statement, ast.Import):
2022
alias += statement.names
2123
else:
2224
if alias:
23-
yield self.add_child(ast.Import(names=alias), parent=parent)
25+
yield self.add_child(ast.Import(names=alias), parent=parent, namespace=namespace)
2426
alias = []
2527

2628
yield statement
2729

2830
if alias:
29-
yield self.add_child(ast.Import(names=alias), parent=parent)
31+
yield self.add_child(ast.Import(names=alias), parent=parent, namespace=namespace)
32+
3033

3134
def _combine_import_from(self, node_list, parent):
3235

@@ -55,15 +58,15 @@ def combine(statement):
5558
else:
5659
if alias:
5760
yield self.add_child(
58-
ast.ImportFrom(module=prev_import.module, names=alias, level=prev_import.level), parent=parent
61+
ast.ImportFrom(module=prev_import.module, names=alias, level=prev_import.level), parent=parent, namespace=prev_import.namespace
5962
)
6063
alias = []
6164

6265
yield statement
6366

6467
if alias:
6568
yield self.add_child(
66-
ast.ImportFrom(module=prev_import.module, names=alias, level=prev_import.level), parent=parent
69+
ast.ImportFrom(module=prev_import.module, names=alias, level=prev_import.level), parent=parent, namespace=prev_import.namespace
6770
)
6871

6972
def suite(self, node_list, parent):

src/python_minifier/transforms/remove_exception_brackets.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ def _remove_empty_call(binding):
7777
assert isinstance(binding, BuiltinBinding)
7878

7979
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
80+
# For this to be a builtin, all references must be name nodes as it is not defined anywhere
81+
assert isinstance(name_node, ast.Name) and isinstance(name_node.ctx, ast.Load)
8182

8283
if not isinstance(name_node.parent, ast.Call):
8384
# This is not a call
@@ -110,7 +111,13 @@ def remove_no_arg_exception_call(module):
110111
return module
111112

112113
for binding in module.bindings:
113-
if isinstance(binding, BuiltinBinding) and binding.name in builtin_exceptions:
114+
if not isinstance(binding, BuiltinBinding):
115+
continue
116+
117+
if binding.is_redefined():
118+
continue
119+
120+
if binding.name in builtin_exceptions:
114121
# We can remove any calls to builtin exceptions
115122
_remove_empty_call(binding)
116123

0 commit comments

Comments
 (0)