Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create a new module for getting and setting the parent node #117

Merged
merged 1 commit into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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