Skip to content

Commit

Permalink
Use free function to get a nodes parent.
Browse files Browse the repository at this point in the history
Creates a new module for getting and setting the parent node
  • Loading branch information
dflook committed Oct 20, 2024
1 parent e1f614b commit 7c88edf
Show file tree
Hide file tree
Showing 23 changed files with 221 additions and 60 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ jobs:

- name: Test typing
run: |
pip3.13 install mypy!=1.12.0 types-setuptools
pip3.13 install 'mypy<1.12.0' types-setuptools
mypy --strict typing_test/test_typing.py
if mypy --strict typing_test/test_badtyping.py; then
Expand Down
2 changes: 2 additions & 0 deletions src/python_minifier/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import re

import python_minifier.ast_compat as ast
from python_minifier.ast_annotation import add_parent

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

add_parent(module)
add_namespace(module)

if remove_literal_statements:
Expand Down
82 changes: 82 additions & 0 deletions src/python_minifier/ast_annotation/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""
This module provides utilities for annotating Abstract Syntax Tree (AST) nodes with parent references.
"""

import ast

class _NoParent(ast.AST):
"""A placeholder class used to indicate that a node has no parent."""

def __repr__(self):
# type: () -> str
return 'NoParent()'

def add_parent(node, parent=_NoParent()):
# type: (ast.AST, ast.AST) -> None
"""
Recursively adds a parent reference to each node in the AST.
>>> tree = ast.parse('a = 1')
>>> add_parent(tree)
>>> get_parent(tree.body[0]) == tree
True
:param node: The current AST node.
:param parent: The parent :class:`ast.AST` node.
"""

node._parent = parent # type: ignore[attr-defined]
for child in ast.iter_child_nodes(node):
add_parent(child, node)

def get_parent(node):
# type: (ast.AST) -> ast.AST
"""
Retrieves the parent of the given AST node.
>>> tree = ast.parse('a = 1')
>>> add_parent(tree)
>>> get_parent(tree.body[0]) == tree
True
:param node: The AST node whose parent is to be retrieved.
:return: The parent AST node.
:raises ValueError: If the node has no parent.
"""

if not hasattr(node, '_parent') or isinstance(node._parent, _NoParent): # type: ignore[attr-defined]
raise ValueError('Node has no parent')

return node._parent # type: ignore[attr-defined]

def set_parent(node, parent):
# type: (ast.AST, ast.AST) -> None
"""
Replace the parent of the given AST node.
Create a simple AST:
>>> tree = ast.parse('a = func()')
>>> add_parent(tree)
>>> isinstance(tree.body[0], ast.Assign) and isinstance(tree.body[0].value, ast.Call)
True
>>> assign = tree.body[0]
>>> call = tree.body[0].value
>>> get_parent(call) == assign
True
Replace the parent of the call node:
>>> tree.body[0] = call
>>> set_parent(call, tree)
>>> get_parent(call) == tree
True
>>> from python_minifier.ast_printer import print_ast
>>> print(print_ast(tree))
Module(body=[
Call(Name('func'))
])
:param node: The AST node whose parent is to be set.
:param parent: The parent AST node.
"""

node._parent = parent # type: ignore[attr-defined]
73 changes: 34 additions & 39 deletions src/python_minifier/rename/mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,47 @@
"""

import python_minifier.ast_compat as ast
from python_minifier.ast_annotation import get_parent

from python_minifier.rename.util import is_namespace


def add_parent_to_arguments(arguments, func):
arguments.parent = func
arguments.namespace = func

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

if hasattr(arguments, 'kwonlyargs'):
for arg in arguments.kwonlyargs:
add_parent(arg, arguments, func)
add_parent(arg, func)
if arg.annotation is not None:
add_parent(arg.annotation, arguments, func.namespace)
add_parent(arg.annotation, func.namespace)

for node in arguments.kw_defaults:
if node is not None:
add_parent(node, arguments, func.namespace)
add_parent(node, func.namespace)

for node in arguments.defaults:
add_parent(node, arguments, func.namespace)
add_parent(node, func.namespace)

if arguments.vararg:
if hasattr(arguments, 'varargannotation') and arguments.varargannotation is not None:
add_parent(arguments.varargannotation, arguments, func.namespace)
add_parent(arguments.varargannotation, func.namespace)
elif isinstance(arguments.vararg, str):
pass
else:
add_parent(arguments.vararg, arguments, func)
add_parent(arguments.vararg, func)

if arguments.kwarg:
if hasattr(arguments, 'kwargannotation') and arguments.kwargannotation is not None:
add_parent(arguments.kwargannotation, arguments, func.namespace)
add_parent(arguments.kwargannotation, func.namespace)
elif isinstance(arguments.kwarg, str):
pass
else:
add_parent(arguments.kwarg, arguments, func)
add_parent(arguments.kwarg, func)


def add_parent_to_functiondef(functiondef):
Expand All @@ -55,17 +55,17 @@ def add_parent_to_functiondef(functiondef):
add_parent_to_arguments(functiondef.args, func=functiondef)

for node in functiondef.body:
add_parent(node, parent=functiondef, namespace=functiondef)
add_parent(node, namespace=functiondef)

for node in functiondef.decorator_list:
add_parent(node, parent=functiondef, namespace=functiondef.namespace)
add_parent(node, namespace=functiondef.namespace)

if hasattr(functiondef, 'type_params') and functiondef.type_params is not None:
for node in functiondef.type_params:
add_parent(node, parent=functiondef, namespace=functiondef.namespace)
add_parent(node, namespace=functiondef.namespace)

if hasattr(functiondef, 'returns') and functiondef.returns is not None:
add_parent(functiondef.returns, parent=functiondef, namespace=functiondef.namespace)
add_parent(functiondef.returns, namespace=functiondef.namespace)


def add_parent_to_classdef(classdef):
Expand All @@ -74,65 +74,60 @@ def add_parent_to_classdef(classdef):
"""

for node in classdef.bases:
add_parent(node, parent=classdef, namespace=classdef.namespace)
add_parent(node, namespace=classdef.namespace)

if hasattr(classdef, 'keywords'):
for node in classdef.keywords:
add_parent(node, parent=classdef, namespace=classdef.namespace)
add_parent(node, namespace=classdef.namespace)

if hasattr(classdef, 'starargs') and classdef.starargs is not None:
add_parent(classdef.starargs, parent=classdef, namespace=classdef.namespace)
add_parent(classdef.starargs, namespace=classdef.namespace)

if hasattr(classdef, 'kwargs') and classdef.kwargs is not None:
add_parent(classdef.kwargs, parent=classdef, namespace=classdef.namespace)
add_parent(classdef.kwargs, namespace=classdef.namespace)

for node in classdef.body:
add_parent(node, parent=classdef, namespace=classdef)
add_parent(node, namespace=classdef)

for node in classdef.decorator_list:
add_parent(node, parent=classdef, namespace=classdef.namespace)
add_parent(node, namespace=classdef.namespace)

if hasattr(classdef, 'type_params') and classdef.type_params is not None:
for node in classdef.type_params:
add_parent(node, parent=classdef, namespace=classdef.namespace)
add_parent(node, namespace=classdef.namespace)


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

if hasattr(node, 'elt'):
add_parent(node.elt, parent=node, namespace=node)
add_parent(node.elt, namespace=node)
elif hasattr(node, 'key'):
add_parent(node.key, parent=node, namespace=node)
add_parent(node.value, parent=node, namespace=node)
add_parent(node.key, namespace=node)
add_parent(node.value, namespace=node)

iter_namespace = namespace
for generator in node.generators:
generator.parent = node
generator.namespace = node

add_parent(generator.target, parent=generator, namespace=node)
add_parent(generator.iter, parent=generator, namespace=iter_namespace)
add_parent(generator.target, namespace=node)
add_parent(generator.iter, namespace=iter_namespace)
iter_namespace = node
for if_ in generator.ifs:
add_parent(if_, parent=generator, namespace=node)
add_parent(if_, namespace=node)


def add_parent(node, parent=None, namespace=None):
def add_parent(node, namespace=None):
"""
Add a parent attribute to child nodes
Add a namespace attribute to child nodes
:param node: The tree to add parent and namespace properties to
:param node: The tree to add namespace properties to
:type node: :class:`ast.AST`
:param parent: The parent node of this node
:type parent: :class:`ast.AST`
:param namespace: The namespace Node that this node is in
: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
"""

node.parent = parent if parent is not None else node
node.namespace = namespace if namespace is not None else node

if is_namespace(node):
Expand All @@ -146,12 +141,12 @@ def add_parent(node, parent=None, namespace=None):
add_parent_to_comprehension(node, namespace=namespace)
elif isinstance(node, ast.Lambda):
add_parent_to_arguments(node.args, func=node)
add_parent(node.body, parent=node, namespace=node)
add_parent(node.body, namespace=node)
elif isinstance(node, ast.ClassDef):
add_parent_to_classdef(node)
else:
for child in ast.iter_child_nodes(node):
add_parent(child, parent=node, namespace=node)
add_parent(child, namespace=node)

return

Expand All @@ -163,11 +158,11 @@ def add_parent(node, parent=None, namespace=None):
if isinstance(node, ast.Name) and isinstance(namespace, ast.ClassDef):
if isinstance(node.ctx, ast.Load):
namespace.nonlocal_names.add(node.id)
elif isinstance(node.ctx, ast.Store) and isinstance(node.parent, ast.AugAssign):
elif isinstance(node.ctx, ast.Store) and isinstance(get_parent(node), ast.AugAssign):
namespace.nonlocal_names.add(node.id)

for child in ast.iter_child_nodes(node):
add_parent(child, parent=node, namespace=namespace)
add_parent(child, namespace=namespace)


def add_namespace(module):
Expand Down
7 changes: 4 additions & 3 deletions src/python_minifier/rename/rename_literals.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import python_minifier.ast_compat as ast
from python_minifier.ast_annotation import get_parent, set_parent

from python_minifier.rename.binding import Binding
from python_minifier.rename.util import insert
Expand All @@ -7,8 +8,8 @@


def replace(old_node, new_node):
parent = old_node.parent
new_node.parent = parent
parent = get_parent(old_node)
set_parent(new_node, parent)
new_node.namespace = old_node.namespace

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

def visit_Str(self, node):

if isinstance(node.parent, ast.Expr):
if isinstance(get_parent(node), ast.Expr):
# This is literal statement
# The RemoveLiteralStatements transformer must have left it here, so ignore it.
return
Expand Down
3 changes: 2 additions & 1 deletion src/python_minifier/transforms/constant_folding.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import sys

import python_minifier.ast_compat as ast
from python_minifier.ast_annotation import get_parent

from python_minifier.ast_compare import compare_ast
from python_minifier.expression_printer import ExpressionPrinter
Expand Down Expand Up @@ -93,7 +94,7 @@ def visit_BinOp(self, node):
return node

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


def equal_value_and_type(a, b):
Expand Down
Loading

0 comments on commit 7c88edf

Please sign in to comment.