From 5c346f801afa01d4d7a48b7542222293f201c7fe Mon Sep 17 00:00:00 2001 From: jbjd Date: Sun, 14 Dec 2025 15:34:39 -0600 Subject: [PATCH 1/4] Refactor import squash --- .../parser/skipper.py | 29 ++++++++++--------- tests/parser/test_imports.py | 3 +- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/personal_python_ast_optimizer/parser/skipper.py b/personal_python_ast_optimizer/parser/skipper.py index e171532..bfef3bc 100644 --- a/personal_python_ast_optimizer/parser/skipper.py +++ b/personal_python_ast_optimizer/parser/skipper.py @@ -88,18 +88,10 @@ def generic_visit(self, node) -> ast.AST: for field, old_value in ast.iter_fields(node): if isinstance(old_value, list): new_values = [] - combined_import: ast.Import | None = None + self._combine_imports(old_value) for value in old_value: if isinstance(value, ast.AST): value = self.visit(value) - if isinstance(value, ast.Import): - if combined_import is None: - combined_import = value - else: - self._ast_import_combine(combined_import, value) - continue - else: - combined_import = None if value is None: continue elif not isinstance(value, ast.AST): @@ -124,6 +116,21 @@ def generic_visit(self, node) -> ast.AST: setattr(node, field, new_node) return node + @staticmethod + def _combine_imports(body: list) -> None: + if not body: + return + + new_body = [body[0]] + + for i in range(1, len(body)): + if isinstance(body[i], ast.Import) and isinstance(new_body[-1], ast.Import): + new_body[-1].names += body[i].names + else: + new_body.append(body[i]) + + body[:] = new_body + def visit_Module(self, node: ast.Module) -> ast.AST: if not self._has_code_to_skip(): return node @@ -632,7 +639,3 @@ def _ast_constants_operation( raise ValueError(f"Invalid operation: {operation.__class__.__name__}") return ast.Constant(result) - - @staticmethod - def _ast_import_combine(target: ast.Import, to_be_combined: ast.Import) -> None: - target.names += to_be_combined.names diff --git a/tests/parser/test_imports.py b/tests/parser/test_imports.py index 75a560c..298f11b 100644 --- a/tests/parser/test_imports.py +++ b/tests/parser/test_imports.py @@ -37,6 +37,7 @@ def test_import_same_line(): before_and_after = BeforeAndAfter( """ import test +import test2 def i(): import a import d @@ -44,7 +45,7 @@ def i(): print() import e """, - """import test + """import test,test2 def i():import a,d;from b import c;print();import e""", ) run_minifier_and_assert_correct(before_and_after) From 777bb5ca94346ddeca39ffc1410bc27491cfc71e Mon Sep 17 00:00:00 2001 From: jbjd Date: Sun, 14 Dec 2025 15:47:10 -0600 Subject: [PATCH 2/4] Combine from imports --- .../parser/skipper.py | 22 ++++++++++++++----- tests/parser/test_imports.py | 10 +++++++-- version.txt | 2 +- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/personal_python_ast_optimizer/parser/skipper.py b/personal_python_ast_optimizer/parser/skipper.py index bfef3bc..bc278ad 100644 --- a/personal_python_ast_optimizer/parser/skipper.py +++ b/personal_python_ast_optimizer/parser/skipper.py @@ -104,9 +104,9 @@ def generic_visit(self, node) -> ast.AST: and not new_values and not isinstance(node, ast.Module) ): - old_value[:] = [ast.Pass()] - else: - old_value[:] = new_values + new_values = [ast.Pass()] + + old_value[:] = new_values elif isinstance(old_value, ast.AST): new_node = self.visit(old_value) @@ -124,10 +124,20 @@ def _combine_imports(body: list) -> None: new_body = [body[0]] for i in range(1, len(body)): - if isinstance(body[i], ast.Import) and isinstance(new_body[-1], ast.Import): - new_body[-1].names += body[i].names + this_node = body[i] + last_node = new_body[-1] + + if isinstance(this_node, ast.Import) and isinstance(last_node, ast.Import): + last_node.names += this_node.names + elif ( + isinstance(this_node, ast.ImportFrom) + and isinstance(last_node, ast.ImportFrom) + and this_node.module == last_node.module + and this_node.level == last_node.level + ): + last_node.names += this_node.names else: - new_body.append(body[i]) + new_body.append(this_node) body[:] = new_body diff --git a/tests/parser/test_imports.py b/tests/parser/test_imports.py index 298f11b..68e5a04 100644 --- a/tests/parser/test_imports.py +++ b/tests/parser/test_imports.py @@ -10,11 +10,15 @@ from __future__ import with_statement """ +_futures_imports_inline: str = ( + "from __future__ import annotations,generator_stop,unicode_literals,with_statement" +) + @pytest.mark.parametrize( "version,skip_type_hints,after", [ - (None, False, _futures_imports.strip()), + (None, False, _futures_imports_inline), ((3, 7), False, "from __future__ import annotations"), ((3, 7), True, ""), ], @@ -42,11 +46,13 @@ def i(): import a import d from b import c + from b import d as e + from .b import f print() import e """, """import test,test2 -def i():import a,d;from b import c;print();import e""", +def i():import a,d;from b import c,d as e;from .b import f;print();import e""", ) run_minifier_and_assert_correct(before_and_after) diff --git a/version.txt b/version.txt index a1ef0ca..50e2274 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -5.0.2 +5.0.3 From 07542c156fb5130e9aeafe08c49e61a7ef8bf891 Mon Sep 17 00:00:00 2001 From: jbjd Date: Sun, 14 Dec 2025 18:23:04 -0600 Subject: [PATCH 3/4] Clean u --- personal_python_ast_optimizer/parser/skipper.py | 8 ++++---- personal_python_ast_optimizer/parser/utils.py | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/personal_python_ast_optimizer/parser/skipper.py b/personal_python_ast_optimizer/parser/skipper.py index bc278ad..da13b5f 100644 --- a/personal_python_ast_optimizer/parser/skipper.py +++ b/personal_python_ast_optimizer/parser/skipper.py @@ -127,9 +127,9 @@ def _combine_imports(body: list) -> None: this_node = body[i] last_node = new_body[-1] - if isinstance(this_node, ast.Import) and isinstance(last_node, ast.Import): - last_node.names += this_node.names - elif ( + if ( + isinstance(this_node, ast.Import) and isinstance(last_node, ast.Import) + ) or ( isinstance(this_node, ast.ImportFrom) and isinstance(last_node, ast.ImportFrom) and this_node.module == last_node.module @@ -246,7 +246,7 @@ def visit_Assign(self, node: ast.Assign) -> ast.AST | None: # TODO: Currently if a.b.c.d only "c" and "d" are checked var_name: str = get_node_name(node.targets[0]) - parent_var_name: str = get_node_name(getattr(node.targets[0], "value", object)) + parent_var_name: str = get_node_name(getattr(node.targets[0], "value", None)) if ( var_name in self.tokens_config.variables_to_skip diff --git a/personal_python_ast_optimizer/parser/utils.py b/personal_python_ast_optimizer/parser/utils.py index 83a96b9..9de267a 100644 --- a/personal_python_ast_optimizer/parser/utils.py +++ b/personal_python_ast_optimizer/parser/utils.py @@ -5,7 +5,7 @@ from personal_python_ast_optimizer.parser.config import TokensToSkip -def get_node_name(node: object) -> str: +def get_node_name(node: ast.AST | None) -> str: """Gets id or attr which both can represent var names""" if isinstance(node, ast.Call): node = node.func @@ -98,8 +98,9 @@ def remove_duplicate_slots( node.value.elts = unique_objects -def first_occurrence_of_type(data: list, target_type) -> int: +def first_occurrence_of_type(data: list, target_type: object) -> int: for index, element in enumerate(data): if isinstance(element, target_type): return index + return -1 From 8a7a6b502cc13286f4871af933d46e6d51a899ae Mon Sep 17 00:00:00 2001 From: jbjd Date: Sun, 14 Dec 2025 18:35:28 -0600 Subject: [PATCH 4/4] Remove some redundant code --- personal_python_ast_optimizer/parser/skipper.py | 2 +- personal_python_ast_optimizer/parser/utils.py | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/personal_python_ast_optimizer/parser/skipper.py b/personal_python_ast_optimizer/parser/skipper.py index da13b5f..55315d3 100644 --- a/personal_python_ast_optimizer/parser/skipper.py +++ b/personal_python_ast_optimizer/parser/skipper.py @@ -521,7 +521,7 @@ def visit_arguments(self, node: ast.arguments) -> ast.AST: def visit_BoolOp(self, node: ast.BoolOp) -> ast.AST: parsed_node: ast.BoolOp = self.generic_visit(node) # type: ignore - if isinstance(parsed_node.op, ast.Or) or isinstance(parsed_node.op, ast.And): + if isinstance(parsed_node.op, (ast.Or, ast.And)): # For And nodes left values that are Truthy and const can be removed # and vice versa remove_if: bool = isinstance(parsed_node.op, ast.And) diff --git a/personal_python_ast_optimizer/parser/utils.py b/personal_python_ast_optimizer/parser/utils.py index 9de267a..11ec7a7 100644 --- a/personal_python_ast_optimizer/parser/utils.py +++ b/personal_python_ast_optimizer/parser/utils.py @@ -74,11 +74,7 @@ def skip_decorators( def remove_duplicate_slots( node: ast.Assign | ast.AnnAssign, warn_duplicates: bool = True ) -> None: - if ( - isinstance(node.value, ast.Tuple) - or isinstance(node.value, ast.List) - or isinstance(node.value, ast.Set) - ): + if isinstance(node.value, (ast.Tuple, ast.List, ast.Set)): found_values: set[str] = set() unique_objects: list[ast.expr] = [] for const_value in node.value.elts: @@ -98,7 +94,7 @@ def remove_duplicate_slots( node.value.elts = unique_objects -def first_occurrence_of_type(data: list, target_type: object) -> int: +def first_occurrence_of_type(data: list, target_type: type) -> int: for index, element in enumerate(data): if isinstance(element, target_type): return index