Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def create_minify_unparser_with_exclusions(
"functions": [
("_function_helper", skip_func_def),
("visit_Assign", skip_func_assign),
("visit_Call", skip_func_call),
("visit_Expr", skip_func_call),
],
"variables": [
("visit_AnnAssign", skip_var_ann_assign),
Expand Down
1 change: 1 addition & 0 deletions personal_python_ast_optimizer/flake_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ def run_autoflake(source: str, remove_unused_imports: bool = False) -> str:
remove_duplicate_keys=True,
remove_unused_variables=True,
remove_rhs_for_unused_variables=True,
ignore_pass_statements=True,
)
75 changes: 44 additions & 31 deletions personal_python_ast_optimizer/parser/exclusion_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import Callable, Literal

from personal_python_ast_optimizer.parser.config import TokensToSkipConfig
from personal_python_ast_optimizer.parser.minifier import MinifyUnparser
from personal_python_ast_optimizer.parser.minifier import MinifyUnparser, SkipReason
from personal_python_ast_optimizer.parser.utils import (
TokensToSkip,
get_node_name,
Expand Down Expand Up @@ -47,19 +47,19 @@ def wrapper(self: MinifyUnparser, node: ast.Dict) -> None:
node.keys = list(new_dict.keys())
node.values = list(new_dict.values())

visit_Dict(node)
return visit_Dict(node)

return wrapper


def skip_if_name_main(visit_If: Callable):
def wrapper(self: MinifyUnparser, node: ast.If) -> None:
def wrapper(self: MinifyUnparser, node: ast.If) -> SkipReason | None:
if is_name_equals_main_node(node.test):
if node.orelse:
self.traverse(node.orelse)
return
return SkipReason.EXCLUDED

visit_If(node)
return visit_If(node)

return wrapper

Expand All @@ -71,44 +71,49 @@ def wrapper(self: MinifyUnparser, node: ast.ImportFrom) -> None:
alias for alias in node.names if alias.name not in from_imports_to_skip
]

visit_ImportFrom(node)
return visit_ImportFrom(node)

return wrapper


def skip_class(visit_ClassDef: Callable, classes_to_skip: TokensToSkip):
def wrapper(self, node: ast.ClassDef) -> None:
def wrapper(self, node: ast.ClassDef) -> SkipReason | None:
if node.name in classes_to_skip:
return
return SkipReason.EXCLUDED

ignore_base_classes(node, classes_to_skip)

visit_ClassDef(node)
return visit_ClassDef(node)

return wrapper


def skip_func_assign(visit_Assign: Callable, functions_to_skip: TokensToSkip):
def wrapper(self, node: ast.Assign) -> None:
def wrapper(self, node: ast.Assign) -> SkipReason | None:
if (
isinstance(node.value, ast.Call)
and get_node_name(node.value.func) in functions_to_skip
):
self.visit_Pass()
return
if _body_might_be_empty_without_pass(self):
self.visit_Pass()
return SkipReason.EXCLUDED

visit_Assign(node)
return visit_Assign(node)

return wrapper


def skip_func_call(visit_Call: Callable, functions_to_skip: TokensToSkip):
def wrapper(self: MinifyUnparser, node: ast.Call) -> None:
function_name: str = get_node_name(node.func)
if function_name in functions_to_skip:
self.visit_Pass()
else:
visit_Call(node)
def wrapper(self: MinifyUnparser, node: ast.Expr) -> SkipReason | None:
if (
isinstance(node.value, ast.Call)
and get_node_name(node.value.func) in functions_to_skip
):
if _body_might_be_empty_without_pass(self):
self.visit_Pass()
return SkipReason.EXCLUDED

return visit_Call(node)

return wrapper

Expand All @@ -118,39 +123,43 @@ def wrapper(
self: MinifyUnparser,
node: ast.FunctionDef,
fill_suffix: Literal["def", "async def"],
) -> None:
) -> SkipReason | None:
if node.name in functions_to_skip:
return
if _body_might_be_empty_without_pass(self):
self.visit_Pass()
return SkipReason.EXCLUDED

_function_helper(node, fill_suffix)
return _function_helper(node, fill_suffix)

return wrapper


def skip_var_ann_assign(visit_AnnAssign: Callable, vars_to_skip: TokensToSkip):
def wrapper(self: MinifyUnparser, node: ast.AnnAssign) -> None:
def wrapper(self: MinifyUnparser, node: ast.AnnAssign) -> SkipReason | None:
"""Only writes type annotations if necessary"""
var_name: str = get_node_name(node.target)
if var_name in vars_to_skip:
self.visit_Pass()
return
if _body_might_be_empty_without_pass(self):
self.visit_Pass()
return SkipReason.EXCLUDED

visit_AnnAssign(node)
return visit_AnnAssign(node)

return wrapper


def skip_var_assign(visit_Assign: Callable, vars_to_skip: TokensToSkip):
def wrapper(self: MinifyUnparser, node: ast.Assign) -> None:
def wrapper(self: MinifyUnparser, node: ast.Assign) -> SkipReason | 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))
if var_name in vars_to_skip or parent_var_name in vars_to_skip:
self.visit_Pass()
return
if _body_might_be_empty_without_pass(self):
self.visit_Pass()
return SkipReason.EXCLUDED

visit_Assign(node)
return visit_Assign(node)

return wrapper

Expand All @@ -161,6 +170,10 @@ def wrapper(self: MinifyUnparser, node: ast.ClassDef | ast.FunctionDef) -> None:
n for n in node.decorator_list if get_node_name(n) not in decorators_to_skip
]

_write_decorators(node)
return _write_decorators(node)

return wrapper


def _body_might_be_empty_without_pass(parser: MinifyUnparser) -> bool:
return parser._indent > 0 and parser.previous_node_in_body is None
20 changes: 12 additions & 8 deletions personal_python_ast_optimizer/parser/minifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,16 @@ class SkipReason(Enum):
ALL_SUBNODES_REMOVED = 0
ANNOTATION = 1
FOLDED_CONSTANT = 2
EXCLUDED = 3


class MinifyUnparser(_Unparser):

__slots__ = (
"can_use_semicolon",
"constant_vars_to_fold",
"is_last_node_in_body",
"module_name",
"previous_node_in_body",
"target_python_version",
"within_class",
"within_function",
Expand All @@ -43,20 +44,21 @@ def __init__(
constant_vars_to_fold: dict[str, int | str] | None = None,
) -> None:
self._source: list[str] # type: ignore
self._indent: int # type: ignore
super().__init__()
self.module_name: str = module_name
self.target_python_version: tuple[int, int] | None = target_python_version
self.constant_vars_to_fold: dict[str, int | str] = (
constant_vars_to_fold if constant_vars_to_fold is not None else {}
)

self.can_use_semicolon: bool = False
self.previous_node_in_body: ast.stmt | None = None
self.is_last_node_in_body: bool = False
self.within_class: bool = False
self.within_function: bool = False

def fill(self, text: str = "", splitter: Literal["", "\n", ";"] = "\n") -> None:
"""Overrides super fill to use tabs over spaces"""
"""Overrides super fill to use tabs over spaces and different line splitters"""
match splitter:
case "\n":
self.maybe_newline()
Expand Down Expand Up @@ -85,13 +87,13 @@ def visit_node(
self,
node: ast.AST,
is_last_node_in_body: bool = False,
can_use_semicolon: bool = False,
last_visited_node: ast.stmt | None = None,
) -> SkipReason | None:
method = "visit_" + node.__class__.__name__
visitor = getattr(self, method, self.generic_visit)

self.is_last_node_in_body = is_last_node_in_body
self.can_use_semicolon = can_use_semicolon
self.previous_node_in_body = last_visited_node

return visitor(node) # type: ignore

Expand All @@ -100,10 +102,12 @@ def traverse(self, node: list[ast.stmt] | ast.AST) -> None:
last_visited_node: ast.stmt | None = None
last_index = len(node) - 1
for index, item in enumerate(node):
# TODO: store last visited node in context and make function for
# can use semicolon. Use new var to check if writing pass is needed in
# exclusion
is_last_node_in_body: bool = index == last_index
can_use_semicolon: bool = isinstance(last_visited_node, ast.Assign)
result: SkipReason | None = self.visit_node(
item, is_last_node_in_body, can_use_semicolon
item, is_last_node_in_body, last_visited_node
)
if result is None:
last_visited_node = item
Expand Down Expand Up @@ -380,7 +384,7 @@ def _get_line_splitter(self) -> Literal["", "\n", ";"]:
):
return ""

if self.can_use_semicolon:
if isinstance(self.previous_node_in_body, ast.Assign):
return ";"

return "\n"
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ authors = [
{name = "James Demetris"},
]
name = "personal-python-ast-optimizer"
version = "1.1.4"
version = "1.1.5"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
Expand Down
32 changes: 12 additions & 20 deletions tests/parser/test_assign.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,37 @@
from tests.utils import BeforeAndAfter, run_minifiyer_and_assert_correct

_assign_cases = [
(
BeforeAndAfter(
"""
BeforeAndAfter(
"""
if a > 6:
b = 3
c = 4
""",
"if a>6:b=3\nc=4",
)
"if a>6:b=3\nc=4",
),
(
BeforeAndAfter(
"""
BeforeAndAfter(
"""
if a > 6:
b = 3
c = 4
""",
"if a>6:\n\tb=3;c=4",
)
"if a>6:\n\tb=3;c=4",
),
(
BeforeAndAfter(
"""
BeforeAndAfter(
"""
if a > 6:
b = 3
""",
"if a>6:b=3",
)
"if a>6:b=3",
),
(
BeforeAndAfter(
"""
BeforeAndAfter(
"""
if a > 6:
b = 3
else:
c = 6
""",
"if a>6:b=3\nelse:c=6",
)
"if a>6:b=3\nelse:c=6",
),
]

Expand Down
Loading