Skip to content

Commit 7c88edf

Browse files
committed
Use free function to get a nodes parent.
Creates a new module for getting and setting the parent node
1 parent e1f614b commit 7c88edf

23 files changed

Lines changed: 221 additions & 60 deletions

.github/workflows/release_test.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ jobs:
188188

189189
- name: Test typing
190190
run: |
191-
pip3.13 install mypy!=1.12.0 types-setuptools
191+
pip3.13 install 'mypy<1.12.0' types-setuptools
192192
mypy --strict typing_test/test_typing.py
193193
194194
if mypy --strict typing_test/test_badtyping.py; then

src/python_minifier/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import re
88

99
import python_minifier.ast_compat as ast
10+
from python_minifier.ast_annotation import add_parent
1011

1112
from python_minifier.ast_compare import CompareError, compare_ast
1213
from python_minifier.module_printer import ModulePrinter
@@ -115,6 +116,7 @@ def minify(
115116
# This will raise if the source file can't be parsed
116117
module = ast.parse(source, filename)
117118

119+
add_parent(module)
118120
add_namespace(module)
119121

120122
if remove_literal_statements:
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""
2+
This module provides utilities for annotating Abstract Syntax Tree (AST) nodes with parent references.
3+
"""
4+
5+
import ast
6+
7+
class _NoParent(ast.AST):
8+
"""A placeholder class used to indicate that a node has no parent."""
9+
10+
def __repr__(self):
11+
# type: () -> str
12+
return 'NoParent()'
13+
14+
def add_parent(node, parent=_NoParent()):
15+
# type: (ast.AST, ast.AST) -> None
16+
"""
17+
Recursively adds a parent reference to each node in the AST.
18+
19+
>>> tree = ast.parse('a = 1')
20+
>>> add_parent(tree)
21+
>>> get_parent(tree.body[0]) == tree
22+
True
23+
24+
:param node: The current AST node.
25+
:param parent: The parent :class:`ast.AST` node.
26+
"""
27+
28+
node._parent = parent # type: ignore[attr-defined]
29+
for child in ast.iter_child_nodes(node):
30+
add_parent(child, node)
31+
32+
def get_parent(node):
33+
# type: (ast.AST) -> ast.AST
34+
"""
35+
Retrieves the parent of the given AST node.
36+
37+
>>> tree = ast.parse('a = 1')
38+
>>> add_parent(tree)
39+
>>> get_parent(tree.body[0]) == tree
40+
True
41+
42+
:param node: The AST node whose parent is to be retrieved.
43+
:return: The parent AST node.
44+
:raises ValueError: If the node has no parent.
45+
"""
46+
47+
if not hasattr(node, '_parent') or isinstance(node._parent, _NoParent): # type: ignore[attr-defined]
48+
raise ValueError('Node has no parent')
49+
50+
return node._parent # type: ignore[attr-defined]
51+
52+
def set_parent(node, parent):
53+
# type: (ast.AST, ast.AST) -> None
54+
"""
55+
Replace the parent of the given AST node.
56+
57+
Create a simple AST:
58+
>>> tree = ast.parse('a = func()')
59+
>>> add_parent(tree)
60+
>>> isinstance(tree.body[0], ast.Assign) and isinstance(tree.body[0].value, ast.Call)
61+
True
62+
>>> assign = tree.body[0]
63+
>>> call = tree.body[0].value
64+
>>> get_parent(call) == assign
65+
True
66+
67+
Replace the parent of the call node:
68+
>>> tree.body[0] = call
69+
>>> set_parent(call, tree)
70+
>>> get_parent(call) == tree
71+
True
72+
>>> from python_minifier.ast_printer import print_ast
73+
>>> print(print_ast(tree))
74+
Module(body=[
75+
Call(Name('func'))
76+
])
77+
78+
:param node: The AST node whose parent is to be set.
79+
:param parent: The parent AST node.
80+
"""
81+
82+
node._parent = parent # type: ignore[attr-defined]

src/python_minifier/rename/mapper.py

Lines changed: 34 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,47 +3,47 @@
33
"""
44

55
import python_minifier.ast_compat as ast
6+
from python_minifier.ast_annotation import get_parent
67

78
from python_minifier.rename.util import is_namespace
89

910

1011
def add_parent_to_arguments(arguments, func):
11-
arguments.parent = func
1212
arguments.namespace = func
1313

1414
for arg in getattr(arguments, 'posonlyargs', []) + arguments.args:
15-
add_parent(arg, arguments, func)
15+
add_parent(arg, func)
1616
if hasattr(arg, 'annotation') and arg.annotation is not None:
17-
add_parent(arg.annotation, arguments, func.namespace)
17+
add_parent(arg.annotation, func.namespace)
1818

1919
if hasattr(arguments, 'kwonlyargs'):
2020
for arg in arguments.kwonlyargs:
21-
add_parent(arg, arguments, func)
21+
add_parent(arg, func)
2222
if arg.annotation is not None:
23-
add_parent(arg.annotation, arguments, func.namespace)
23+
add_parent(arg.annotation, func.namespace)
2424

2525
for node in arguments.kw_defaults:
2626
if node is not None:
27-
add_parent(node, arguments, func.namespace)
27+
add_parent(node, func.namespace)
2828

2929
for node in arguments.defaults:
30-
add_parent(node, arguments, func.namespace)
30+
add_parent(node, func.namespace)
3131

3232
if arguments.vararg:
3333
if hasattr(arguments, 'varargannotation') and arguments.varargannotation is not None:
34-
add_parent(arguments.varargannotation, arguments, func.namespace)
34+
add_parent(arguments.varargannotation, func.namespace)
3535
elif isinstance(arguments.vararg, str):
3636
pass
3737
else:
38-
add_parent(arguments.vararg, arguments, func)
38+
add_parent(arguments.vararg, func)
3939

4040
if arguments.kwarg:
4141
if hasattr(arguments, 'kwargannotation') and arguments.kwargannotation is not None:
42-
add_parent(arguments.kwargannotation, arguments, func.namespace)
42+
add_parent(arguments.kwargannotation, func.namespace)
4343
elif isinstance(arguments.kwarg, str):
4444
pass
4545
else:
46-
add_parent(arguments.kwarg, arguments, func)
46+
add_parent(arguments.kwarg, func)
4747

4848

4949
def add_parent_to_functiondef(functiondef):
@@ -55,17 +55,17 @@ def add_parent_to_functiondef(functiondef):
5555
add_parent_to_arguments(functiondef.args, func=functiondef)
5656

5757
for node in functiondef.body:
58-
add_parent(node, parent=functiondef, namespace=functiondef)
58+
add_parent(node, namespace=functiondef)
5959

6060
for node in functiondef.decorator_list:
61-
add_parent(node, parent=functiondef, namespace=functiondef.namespace)
61+
add_parent(node, namespace=functiondef.namespace)
6262

6363
if hasattr(functiondef, 'type_params') and functiondef.type_params is not None:
6464
for node in functiondef.type_params:
65-
add_parent(node, parent=functiondef, namespace=functiondef.namespace)
65+
add_parent(node, namespace=functiondef.namespace)
6666

6767
if hasattr(functiondef, 'returns') and functiondef.returns is not None:
68-
add_parent(functiondef.returns, parent=functiondef, namespace=functiondef.namespace)
68+
add_parent(functiondef.returns, namespace=functiondef.namespace)
6969

7070

7171
def add_parent_to_classdef(classdef):
@@ -74,65 +74,60 @@ def add_parent_to_classdef(classdef):
7474
"""
7575

7676
for node in classdef.bases:
77-
add_parent(node, parent=classdef, namespace=classdef.namespace)
77+
add_parent(node, namespace=classdef.namespace)
7878

7979
if hasattr(classdef, 'keywords'):
8080
for node in classdef.keywords:
81-
add_parent(node, parent=classdef, namespace=classdef.namespace)
81+
add_parent(node, namespace=classdef.namespace)
8282

8383
if hasattr(classdef, 'starargs') and classdef.starargs is not None:
84-
add_parent(classdef.starargs, parent=classdef, namespace=classdef.namespace)
84+
add_parent(classdef.starargs, namespace=classdef.namespace)
8585

8686
if hasattr(classdef, 'kwargs') and classdef.kwargs is not None:
87-
add_parent(classdef.kwargs, parent=classdef, namespace=classdef.namespace)
87+
add_parent(classdef.kwargs, namespace=classdef.namespace)
8888

8989
for node in classdef.body:
90-
add_parent(node, parent=classdef, namespace=classdef)
90+
add_parent(node, namespace=classdef)
9191

9292
for node in classdef.decorator_list:
93-
add_parent(node, parent=classdef, namespace=classdef.namespace)
93+
add_parent(node, namespace=classdef.namespace)
9494

9595
if hasattr(classdef, 'type_params') and classdef.type_params is not None:
9696
for node in classdef.type_params:
97-
add_parent(node, parent=classdef, namespace=classdef.namespace)
97+
add_parent(node, namespace=classdef.namespace)
9898

9999

100100
def add_parent_to_comprehension(node, namespace):
101101
assert isinstance(node, (ast.GeneratorExp, ast.SetComp, ast.DictComp, ast.ListComp))
102102

103103
if hasattr(node, 'elt'):
104-
add_parent(node.elt, parent=node, namespace=node)
104+
add_parent(node.elt, namespace=node)
105105
elif hasattr(node, 'key'):
106-
add_parent(node.key, parent=node, namespace=node)
107-
add_parent(node.value, parent=node, namespace=node)
106+
add_parent(node.key, namespace=node)
107+
add_parent(node.value, namespace=node)
108108

109109
iter_namespace = namespace
110110
for generator in node.generators:
111-
generator.parent = node
112111
generator.namespace = node
113112

114-
add_parent(generator.target, parent=generator, namespace=node)
115-
add_parent(generator.iter, parent=generator, namespace=iter_namespace)
113+
add_parent(generator.target, namespace=node)
114+
add_parent(generator.iter, namespace=iter_namespace)
116115
iter_namespace = node
117116
for if_ in generator.ifs:
118-
add_parent(if_, parent=generator, namespace=node)
117+
add_parent(if_, namespace=node)
119118

120119

121-
def add_parent(node, parent=None, namespace=None):
120+
def add_parent(node, namespace=None):
122121
"""
123-
Add a parent attribute to child nodes
124122
Add a namespace attribute to child nodes
125123
126-
:param node: The tree to add parent and namespace properties to
124+
:param node: The tree to add namespace properties to
127125
:type node: :class:`ast.AST`
128-
:param parent: The parent node of this node
129-
:type parent: :class:`ast.AST`
130126
:param namespace: The namespace Node that this node is in
131127
: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
132128
133129
"""
134130

135-
node.parent = parent if parent is not None else node
136131
node.namespace = namespace if namespace is not None else node
137132

138133
if is_namespace(node):
@@ -146,12 +141,12 @@ def add_parent(node, parent=None, namespace=None):
146141
add_parent_to_comprehension(node, namespace=namespace)
147142
elif isinstance(node, ast.Lambda):
148143
add_parent_to_arguments(node.args, func=node)
149-
add_parent(node.body, parent=node, namespace=node)
144+
add_parent(node.body, namespace=node)
150145
elif isinstance(node, ast.ClassDef):
151146
add_parent_to_classdef(node)
152147
else:
153148
for child in ast.iter_child_nodes(node):
154-
add_parent(child, parent=node, namespace=node)
149+
add_parent(child, namespace=node)
155150

156151
return
157152

@@ -163,11 +158,11 @@ def add_parent(node, parent=None, namespace=None):
163158
if isinstance(node, ast.Name) and isinstance(namespace, ast.ClassDef):
164159
if isinstance(node.ctx, ast.Load):
165160
namespace.nonlocal_names.add(node.id)
166-
elif isinstance(node.ctx, ast.Store) and isinstance(node.parent, ast.AugAssign):
161+
elif isinstance(node.ctx, ast.Store) and isinstance(get_parent(node), ast.AugAssign):
167162
namespace.nonlocal_names.add(node.id)
168163

169164
for child in ast.iter_child_nodes(node):
170-
add_parent(child, parent=node, namespace=namespace)
165+
add_parent(child, namespace=namespace)
171166

172167

173168
def add_namespace(module):

src/python_minifier/rename/rename_literals.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import python_minifier.ast_compat as ast
2+
from python_minifier.ast_annotation import get_parent, set_parent
23

34
from python_minifier.rename.binding import Binding
45
from python_minifier.rename.util import insert
@@ -7,8 +8,8 @@
78

89

910
def replace(old_node, new_node):
10-
parent = old_node.parent
11-
new_node.parent = parent
11+
parent = get_parent(old_node)
12+
set_parent(new_node, parent)
1213
new_node.namespace = old_node.namespace
1314

1415
for field, old_value in ast.iter_fields(parent):
@@ -202,7 +203,7 @@ def get_binding(self, value, node):
202203

203204
def visit_Str(self, node):
204205

205-
if isinstance(node.parent, ast.Expr):
206+
if isinstance(get_parent(node), ast.Expr):
206207
# This is literal statement
207208
# The RemoveLiteralStatements transformer must have left it here, so ignore it.
208209
return

src/python_minifier/transforms/constant_folding.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import sys
33

44
import python_minifier.ast_compat as ast
5+
from python_minifier.ast_annotation import get_parent
56

67
from python_minifier.ast_compare import compare_ast
78
from python_minifier.expression_printer import ExpressionPrinter
@@ -93,7 +94,7 @@ def visit_BinOp(self, node):
9394
return node
9495

9596
# New representation is shorter and has the same value, so use it
96-
return self.add_child(new_node, node.parent, node.namespace)
97+
return self.add_child(new_node, get_parent(node), node.namespace)
9798

9899

99100
def equal_value_and_type(a, b):

0 commit comments

Comments
 (0)