From d4a178079c196e5a7b8044e7eb724d865a78cb08 Mon Sep 17 00:00:00 2001 From: jbjd Date: Thu, 18 Dec 2025 19:30:00 -0600 Subject: [PATCH 01/14] Minor optimiztaions --- personal_python_ast_optimizer/parser/skipper.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/personal_python_ast_optimizer/parser/skipper.py b/personal_python_ast_optimizer/parser/skipper.py index 5e86e08..261243c 100644 --- a/personal_python_ast_optimizer/parser/skipper.py +++ b/personal_python_ast_optimizer/parser/skipper.py @@ -503,6 +503,8 @@ def visit_BinOp(self, node: ast.BinOp) -> ast.AST: def visit_arg(self, node: ast.arg) -> ast.AST: if self.token_types_config.skip_type_hints: node.annotation = None + return node + return self.generic_visit(node) def visit_arguments(self, node: ast.arguments) -> ast.AST: @@ -565,7 +567,6 @@ def _use_version_optimization(self, min_version: tuple[int, int]) -> bool: def _has_code_to_skip(self) -> bool: return ( self.target_python_version is not None - or len(self.optimizations_config.vars_to_fold) > 0 or self.optimizations_config.has_code_to_skip() or self.tokens_config.has_code_to_skip() or self.token_types_config.has_code_to_skip() From ea22a650465909e6eaa61f3ea5babbe3c0966dd7 Mon Sep 17 00:00:00 2001 From: jbjd Date: Thu, 18 Dec 2025 20:45:11 -0600 Subject: [PATCH 02/14] Pop instead of [:-1] and remove dead code --- personal_python_ast_optimizer/parser/skipper.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/personal_python_ast_optimizer/parser/skipper.py b/personal_python_ast_optimizer/parser/skipper.py index 261243c..1953f56 100644 --- a/personal_python_ast_optimizer/parser/skipper.py +++ b/personal_python_ast_optimizer/parser/skipper.py @@ -218,7 +218,7 @@ def _handle_function_node( if isinstance(last_body_node, ast.Return) and ( is_return_none(last_body_node) or last_body_node.value is None ): - node.body = node.body[:-1] + node.body.pop() return self.generic_visit(node) @@ -666,9 +666,6 @@ def generic_visit(self, node: ast.AST) -> ast.AST: if value is None: ast_removed = True continue - elif not isinstance(value, ast.AST): - new_values.extend(value) - continue new_values.append(value) From ad247623635b9304c444ec1f1c5dffb645c16843 Mon Sep 17 00:00:00 2001 From: jbjd Date: Thu, 18 Dec 2025 20:52:50 -0600 Subject: [PATCH 03/14] Simplify --- .../parser/skipper.py | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/personal_python_ast_optimizer/parser/skipper.py b/personal_python_ast_optimizer/parser/skipper.py index 1953f56..28eec83 100644 --- a/personal_python_ast_optimizer/parser/skipper.py +++ b/personal_python_ast_optimizer/parser/skipper.py @@ -181,23 +181,13 @@ def visit_ClassDef(self, node: ast.ClassDef) -> ast.AST | None: return self.generic_visit(node) - @_within_function_node def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.AST | None: return self._handle_function_node(node) - @_within_function_node def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> ast.AST | None: return self._handle_function_node(node) - def _should_skip_function( - self, node: ast.FunctionDef | ast.AsyncFunctionDef - ) -> bool: - """If a function node should be skipped""" - return node.name in self.tokens_config.functions_to_skip or ( - self.token_types_config.skip_overload_functions - and is_overload_function(node) - ) - + @_within_function_node def _handle_function_node( self, node: ast.FunctionDef | ast.AsyncFunctionDef ) -> ast.AST | None: @@ -222,6 +212,15 @@ def _handle_function_node( return self.generic_visit(node) + def _should_skip_function( + self, node: ast.FunctionDef | ast.AsyncFunctionDef + ) -> bool: + """Determines if a function node should be skipped.""" + return node.name in self.tokens_config.functions_to_skip or ( + self.token_types_config.skip_overload_functions + and is_overload_function(node) + ) + def visit_Attribute(self, node: ast.Attribute) -> ast.AST | None: if isinstance(node.value, ast.Name): if node.attr in self.optimizations_config.enums_to_fold.get( From 233ec8577f8f51652139f9ecc07bdfda1c76bf57 Mon Sep 17 00:00:00 2001 From: jbjd Date: Thu, 18 Dec 2025 20:54:21 -0600 Subject: [PATCH 04/14] Simplify --- personal_python_ast_optimizer/parser/skipper.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/personal_python_ast_optimizer/parser/skipper.py b/personal_python_ast_optimizer/parser/skipper.py index 28eec83..f0896e1 100644 --- a/personal_python_ast_optimizer/parser/skipper.py +++ b/personal_python_ast_optimizer/parser/skipper.py @@ -153,14 +153,14 @@ def visit_Module(self, node: ast.Module) -> ast.AST: if self.token_types_config.skip_dangling_expressions: skip_dangling_expressions(node) - module: ast.Module = self.generic_visit(node) # type:ignore + self.generic_visit(node) if self.optimizations_config.remove_unused_imports and self._has_imports: import_filter = UnusedImportSkipper() - import_filter.visit(module) + import_filter.visit(node) self._warn_unused_skips() - return module + return node @_within_class_node def visit_ClassDef(self, node: ast.ClassDef) -> ast.AST | None: From fdeee8e863f2b0c45baa1592afbbb5a8f2db813d Mon Sep 17 00:00:00 2001 From: jbjd Date: Thu, 18 Dec 2025 20:57:05 -0600 Subject: [PATCH 05/14] Simplify --- personal_python_ast_optimizer/parser/skipper.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/personal_python_ast_optimizer/parser/skipper.py b/personal_python_ast_optimizer/parser/skipper.py index f0896e1..4950f77 100644 --- a/personal_python_ast_optimizer/parser/skipper.py +++ b/personal_python_ast_optimizer/parser/skipper.py @@ -66,10 +66,10 @@ def __init__(self, config: SkipConfig) -> None: @staticmethod def _within_class_node(function): - def wrapper(self: "AstNodeSkipper", *args, **kwargs) -> ast.AST | None: + def wrapper(self: "AstNodeSkipper", *args) -> ast.AST | None: self._within_class = True try: - return function(self, *args, **kwargs) + return function(self, *args) finally: self._within_class = False @@ -77,10 +77,10 @@ def wrapper(self: "AstNodeSkipper", *args, **kwargs) -> ast.AST | None: @staticmethod def _within_function_node(function): - def wrapper(self: "AstNodeSkipper", *args, **kwargs) -> ast.AST | None: + def wrapper(self: "AstNodeSkipper", *args) -> ast.AST | None: self._within_function = True try: - return function(self, *args, **kwargs) + return function(self, *args) finally: self._within_function = False From dab9c1bc1f10749108313c132418d11d23de2b7e Mon Sep 17 00:00:00 2001 From: jbjd Date: Thu, 18 Dec 2025 21:11:37 -0600 Subject: [PATCH 06/14] Simplify --- personal_python_ast_optimizer/parser/minifier.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/personal_python_ast_optimizer/parser/minifier.py b/personal_python_ast_optimizer/parser/minifier.py index 119a55f..8bea305 100644 --- a/personal_python_ast_optimizer/parser/minifier.py +++ b/personal_python_ast_optimizer/parser/minifier.py @@ -1,5 +1,4 @@ import ast -from ast import _Unparser # type: ignore from typing import Iterable, Iterator, Literal from personal_python_ast_optimizer.parser.utils import node_inlineable @@ -10,7 +9,7 @@ ) -class MinifyUnparser(_Unparser): +class MinifyUnparser(ast._Unparser): __slots__ = ("can_write_body_in_one_line", "previous_node_in_body") @@ -37,7 +36,7 @@ def write(self, *text: str) -> None: """Write text, with some mapping replacements""" text = tuple(self._yield_updated_text(text)) - if len(text) == 0: + if not text: return first_letter_to_write: str = text[0][:1] @@ -104,7 +103,7 @@ def visit_Assert(self, node: ast.Assert) -> None: self.fill("assert ", splitter=self._get_line_splitter()) self.traverse(node.test) if node.msg: - self.write(", ") + self.write(",") self.traverse(node.msg) def visit_Delete(self, node: ast.Delete) -> None: From bbbedcd2775c8a15f05eb8e80bdf5de232e41ff5 Mon Sep 17 00:00:00 2001 From: jbjd Date: Thu, 18 Dec 2025 21:18:42 -0600 Subject: [PATCH 07/14] Copy over a bunch of code to skip one bool check --- .../parser/minifier.py | 52 +++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/personal_python_ast_optimizer/parser/minifier.py b/personal_python_ast_optimizer/parser/minifier.py index 8bea305..8d5a0a4 100644 --- a/personal_python_ast_optimizer/parser/minifier.py +++ b/personal_python_ast_optimizer/parser/minifier.py @@ -59,10 +59,6 @@ def _yield_updated_text(self, text_iter: Iterable[str]) -> Iterator[str]: elif text: yield text - def maybe_newline(self) -> None: - if self._source and self._source[-1] != "\n": - self.write("\n") - def visit_node( self, node: ast.AST, @@ -174,6 +170,54 @@ def visit_AugAssign(self, node: ast.AugAssign) -> None: self.write(self.binop[node.op.__class__.__name__] + "=") self.traverse(node.value) + def visit_ClassDef(self, node: ast.ClassDef) -> None: + self._write_decorators(node) + self.fill("class " + node.name) + if hasattr(node, "type_params"): + self._type_params_helper(node.type_params) + with self.delimit_if("(", ")", condition=node.bases or node.keywords): + comma = False + for e in node.bases: + if comma: + self.write(",") + else: + comma = True + self.traverse(e) + for e in node.keywords: + if comma: + self.write(",") + else: + comma = True + self.traverse(e) + + with self.block(): + self._write_docstring_and_traverse_body(node) + + def _function_helper( + self, + node: ast.FunctionDef | ast.AsyncFunctionDef, + fill_suffix: Literal["def", "async def"], + ) -> None: + self._write_decorators(node) + def_str = fill_suffix + " " + node.name + self.fill(def_str) + if hasattr(node, "type_params"): + self._type_params_helper(node.type_params) + with self.delimit("(", ")"): + self.traverse(node.args) + if node.returns: + self.write("->") + self.traverse(node.returns) + with self.block(extra=self.get_type_comment(node)): + self._write_docstring_and_traverse_body(node) + + def _write_decorators( + self, node: ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef + ) -> None: + for deco in node.decorator_list: + self.fill("@") + self.traverse(deco) + def _last_char_is(self, char_to_check: str) -> bool: return len(self._source) > 0 and self._source[-1][-1:] == char_to_check From 9f4a254808ad1c3de7038894d2803bc601160d03 Mon Sep 17 00:00:00 2001 From: jbjd Date: Thu, 18 Dec 2025 21:21:27 -0600 Subject: [PATCH 08/14] Simplify --- personal_python_ast_optimizer/parser/minifier.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/personal_python_ast_optimizer/parser/minifier.py b/personal_python_ast_optimizer/parser/minifier.py index 8d5a0a4..089b8c2 100644 --- a/personal_python_ast_optimizer/parser/minifier.py +++ b/personal_python_ast_optimizer/parser/minifier.py @@ -219,7 +219,7 @@ def _write_decorators( self.traverse(deco) def _last_char_is(self, char_to_check: str) -> bool: - return len(self._source) > 0 and self._source[-1][-1:] == char_to_check + return self._source and self._source[-1][-1:] == char_to_check def _get_space_before_write(self) -> str: if not self._source: From 65f14286acbba75a82824e6ff62897dd83599414 Mon Sep 17 00:00:00 2001 From: jbjd Date: Sat, 20 Dec 2025 11:13:58 -0600 Subject: [PATCH 09/14] Inline global and nonlocal --- .../parser/minifier.py | 34 ++++++++++++++++-- .../parser/skipper.py | 1 + personal_python_ast_optimizer/parser/utils.py | 18 ---------- tests/parser/test_global.py | 35 +++++++++++++++++++ 4 files changed, 67 insertions(+), 21 deletions(-) create mode 100644 tests/parser/test_global.py diff --git a/personal_python_ast_optimizer/parser/minifier.py b/personal_python_ast_optimizer/parser/minifier.py index 089b8c2..9314a55 100644 --- a/personal_python_ast_optimizer/parser/minifier.py +++ b/personal_python_ast_optimizer/parser/minifier.py @@ -1,7 +1,6 @@ import ast from typing import Iterable, Iterator, Literal -from personal_python_ast_optimizer.parser.utils import node_inlineable from personal_python_ast_optimizer.python_info import ( chars_that_dont_need_whitespace, comparison_and_conjunctions, @@ -77,7 +76,8 @@ def traverse(self, node: list[ast.stmt] | ast.AST) -> None: if isinstance(node, list): last_visited_node: ast.stmt | None = None can_write_body_in_one_line = ( - all(node_inlineable(sub_node) for sub_node in node) or len(node) == 1 + all(self._node_inlineable(sub_node) for sub_node in node) + or len(node) == 1 ) for sub_node in node: @@ -102,6 +102,14 @@ def visit_Assert(self, node: ast.Assert) -> None: self.write(",") self.traverse(node.msg) + def visit_Global(self, node: ast.Global) -> None: + self.fill("global ", splitter=self._get_line_splitter()) + self.interleave(lambda: self.write(","), self.write, node.names) + + def visit_Nonlocal(self, node: ast.Nonlocal) -> None: + self.fill("nonlocal ", splitter=self._get_line_splitter()) + self.interleave(lambda: self.write(","), self.write, node.names) + def visit_Delete(self, node: ast.Delete) -> None: self.fill("del ", splitter=self._get_line_splitter()) self._write_comma_delimitated_body(node.targets) @@ -240,7 +248,7 @@ def _get_line_splitter(self) -> Literal["", "\n", ";"]: if ( self._indent > 0 and self.previous_node_in_body is not None - and node_inlineable(self.previous_node_in_body) + and self._node_inlineable(self.previous_node_in_body) ): return ";" @@ -251,3 +259,23 @@ def _write_comma_delimitated_body( ) -> None: """Writes ast expr objects with comma delimitation""" self.interleave(lambda: self.write(","), self.traverse, body) + + @staticmethod + def _node_inlineable(node: ast.AST) -> bool: + return node.__class__.__name__ in [ + "Assert", + "AnnAssign", + "Assign", + "AugAssign", + "Break", + "Continue", + "Delete", + "Expr", + "Global", + "Import", + "ImportFrom", + "Nonlocal", + "Pass", + "Raise", + "Return", + ] diff --git a/personal_python_ast_optimizer/parser/skipper.py b/personal_python_ast_optimizer/parser/skipper.py index 4950f77..da972f3 100644 --- a/personal_python_ast_optimizer/parser/skipper.py +++ b/personal_python_ast_optimizer/parser/skipper.py @@ -422,6 +422,7 @@ def visit_IfExp(self, node: ast.IfExp) -> ast.AST | None: def visit_Return(self, node: ast.Return) -> ast.AST: if is_return_none(node): node.value = None + return node return self.generic_visit(node) diff --git a/personal_python_ast_optimizer/parser/utils.py b/personal_python_ast_optimizer/parser/utils.py index 728b708..9fa3e88 100644 --- a/personal_python_ast_optimizer/parser/utils.py +++ b/personal_python_ast_optimizer/parser/utils.py @@ -35,24 +35,6 @@ def is_return_none(node: ast.Return) -> bool: return isinstance(node.value, ast.Constant) and node.value.value is None -def node_inlineable(node: ast.AST) -> bool: - return node.__class__.__name__ in [ - "Assert", - "AnnAssign", - "Assign", - "AugAssign", - "Break", - "Continue", - "Delete", - "Expr", - "Import", - "ImportFrom", - "Pass", - "Raise", - "Return", - ] - - def skip_dangling_expressions( node: ast.Module | ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef, ) -> None: diff --git a/tests/parser/test_global.py b/tests/parser/test_global.py new file mode 100644 index 0000000..c1fce8f --- /dev/null +++ b/tests/parser/test_global.py @@ -0,0 +1,35 @@ +from tests.utils import BeforeAndAfter, run_minifier_and_assert_correct + + +def test_global_same_line(): + before_and_after = BeforeAndAfter( + """ +a = 1 +def test(): + global a + print(a) +""", + "a=1\ndef test():global a;print(a)", + ) + + run_minifier_and_assert_correct(before_and_after) + + +def test_nonlocal_same_line(): + before_and_after = BeforeAndAfter( + """ +def test(): + x = 1 + def i(): + nonlocal x + print(x) + i() +""", + """ +def test(): + x = 1 + def i():nonlocal x;print(x) + i()""".strip(), + ) + + run_minifier_and_assert_correct(before_and_after) From 34978f220c7bc5c05185b5211df0a5bd8274a5f6 Mon Sep 17 00:00:00 2001 From: jbjd Date: Sat, 20 Dec 2025 11:15:42 -0600 Subject: [PATCH 10/14] Fix test --- tests/parser/test_global.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/parser/test_global.py b/tests/parser/test_global.py index c1fce8f..8c19e77 100644 --- a/tests/parser/test_global.py +++ b/tests/parser/test_global.py @@ -27,9 +27,9 @@ def i(): """, """ def test(): - x = 1 - def i():nonlocal x;print(x) - i()""".strip(), +\tx=1 +\tdef i():nonlocal x;print(x) +\ti()""".strip(), ) run_minifier_and_assert_correct(before_and_after) From dda072880b8448adf2fe38bc9dce6c6b4e241569 Mon Sep 17 00:00:00 2001 From: jbjd Date: Sat, 20 Dec 2025 11:38:58 -0600 Subject: [PATCH 11/14] Empty visits --- personal_python_ast_optimizer/parser/minifier.py | 14 ++++++++------ personal_python_ast_optimizer/parser/skipper.py | 9 +++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/personal_python_ast_optimizer/parser/minifier.py b/personal_python_ast_optimizer/parser/minifier.py index 9314a55..d4367da 100644 --- a/personal_python_ast_optimizer/parser/minifier.py +++ b/personal_python_ast_optimizer/parser/minifier.py @@ -158,10 +158,10 @@ def visit_AnnAssign(self, node: ast.AnnAssign) -> None: "(", ")", not node.simple and isinstance(node.target, ast.Name) ): self.traverse(node.target) - self.write(": ") + self.write(":") self.traverse(node.annotation) if node.value: - self.write(" = ") + self.write("=") self.traverse(node.value) def visit_Assign(self, node: ast.Assign) -> None: @@ -230,10 +230,12 @@ def _last_char_is(self, char_to_check: str) -> bool: return self._source and self._source[-1][-1:] == char_to_check def _get_space_before_write(self) -> str: - if not self._source: - return "" - most_recent_token: str = self._source[-1] - return "" if most_recent_token[-1:] in chars_that_dont_need_whitespace else " " + return ( + "" + if not self._source + or self._source[-1][-1:] in chars_that_dont_need_whitespace + else " " + ) def _get_line_splitter(self) -> Literal["", "\n", ";"]: """Get character that starts the next line of code with the shortest diff --git a/personal_python_ast_optimizer/parser/skipper.py b/personal_python_ast_optimizer/parser/skipper.py index da972f3..f3c69fd 100644 --- a/personal_python_ast_optimizer/parser/skipper.py +++ b/personal_python_ast_optimizer/parser/skipper.py @@ -375,6 +375,9 @@ def visit_ImportFrom(self, node: ast.ImportFrom) -> ast.ImportFrom | None: self._has_imports = True return node + def visit_alias(self, node: ast.alias) -> ast.alias: + return node + def visit_Name(self, node: ast.Name) -> ast.AST: """Extends super's implementation by adding constant folding""" if node.id in self.optimizations_config.vars_to_fold: @@ -431,6 +434,12 @@ def visit_Pass(self, node: ast.Pass) -> None: are populated with a Pass node.""" return None # This could be toggleable + def visit_Break(self, node: ast.Break) -> ast.Break: + return node + + def visit_Continue(self, node: ast.Continue) -> ast.Continue: + return node + def visit_Call(self, node: ast.Call) -> ast.AST | None: if ( self.optimizations_config.assume_this_machine From dbb1a7c2b0fcf129ceebf971a95a770237e04d3e Mon Sep 17 00:00:00 2001 From: jbjd Date: Sat, 20 Dec 2025 11:57:04 -0600 Subject: [PATCH 12/14] Minor optimization --- personal_python_ast_optimizer/parser/skipper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/personal_python_ast_optimizer/parser/skipper.py b/personal_python_ast_optimizer/parser/skipper.py index f3c69fd..0bb20e7 100644 --- a/personal_python_ast_optimizer/parser/skipper.py +++ b/personal_python_ast_optimizer/parser/skipper.py @@ -108,7 +108,7 @@ def generic_visit(self, node: ast.AST) -> ast.AST: and not new_values and field == "body" ): - new_values = [ast.Pass()] + new_values.append(ast.Pass()) old_value[:] = new_values @@ -679,7 +679,7 @@ def generic_visit(self, node: ast.AST) -> ast.AST: new_values.append(value) if not isinstance(node, ast.Module) and not new_values and ast_removed: - new_values = [ast.Pass()] + new_values.append(ast.Pass()) old_value[:] = reversed(new_values) From a7ba7262e5557e13cb7e61d0a16e9b88ac1826b4 Mon Sep 17 00:00:00 2001 From: jbjd Date: Sat, 20 Dec 2025 11:57:40 -0600 Subject: [PATCH 13/14] Version bump --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 91ff572..26d99a2 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -5.2.0 +5.2.1 From 931df492f071440550cb2ef0cf18b37ab78e0d7b Mon Sep 17 00:00:00 2001 From: jbjd Date: Sat, 20 Dec 2025 12:04:45 -0600 Subject: [PATCH 14/14] Mypy --- personal_python_ast_optimizer/parser/minifier.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/personal_python_ast_optimizer/parser/minifier.py b/personal_python_ast_optimizer/parser/minifier.py index d4367da..ee430b0 100644 --- a/personal_python_ast_optimizer/parser/minifier.py +++ b/personal_python_ast_optimizer/parser/minifier.py @@ -8,7 +8,7 @@ ) -class MinifyUnparser(ast._Unparser): +class MinifyUnparser(ast._Unparser): # type: ignore __slots__ = ("can_write_body_in_one_line", "previous_node_in_body") @@ -185,18 +185,18 @@ def visit_ClassDef(self, node: ast.ClassDef) -> None: self._type_params_helper(node.type_params) with self.delimit_if("(", ")", condition=node.bases or node.keywords): comma = False - for e in node.bases: + for base in node.bases: if comma: self.write(",") else: comma = True - self.traverse(e) - for e in node.keywords: + self.traverse(base) + for kw in node.keywords: if comma: self.write(",") else: comma = True - self.traverse(e) + self.traverse(kw) with self.block(): self._write_docstring_and_traverse_body(node) @@ -227,7 +227,7 @@ def _write_decorators( self.traverse(deco) def _last_char_is(self, char_to_check: str) -> bool: - return self._source and self._source[-1][-1:] == char_to_check + return bool(self._source) and self._source[-1][-1:] == char_to_check def _get_space_before_write(self) -> str: return (