Skip to content

Commit 9dbb185

Browse files
Batch formatting of expressions in template (#27)
* Batch formatting of expressions in template
1 parent ac5bb70 commit 9dbb185

2 files changed

Lines changed: 163 additions & 16 deletions

File tree

.github/workflows/benchmark.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
path: .benchmarks
3434
key: benchmark-baseline-3.14-${{ runner.os }}-${{ github.run_number }}
3535
restore-keys: |
36-
benchmark-baseline-3.14-${{ runner.os }}
36+
benchmark-baseline-3.14-${{ runner.os }}-
3737
3838
# On master: save baseline results
3939
- name: Run benchmarks and save new baseline

ruff_cgx/template_formatter.py

Lines changed: 162 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,146 @@
2929
UNIQUE_ATTR = SORTING.index({"id", "ref", "key"})
3030

3131

32-
def format_python_expression(expr: str) -> str:
32+
def collect_expressions(node) -> list[str]:
33+
"""
34+
Collect all Python expressions from a template node tree.
35+
36+
Returns a list of expressions that need formatting.
37+
"""
38+
expressions = []
39+
40+
def _collect_from_node(n):
41+
# Skip text and comments
42+
if isinstance(n, (Comment, TextElement)):
43+
return
44+
45+
# Collect expressions from attributes
46+
if attributes := getattr(n, "attrs", None):
47+
for key, value in attributes.items():
48+
if (
49+
key.startswith((":", "@", "v-"))
50+
and isinstance(value, str)
51+
and value.strip()
52+
):
53+
if key.startswith("v-for"):
54+
# Split v-for into target and iterable
55+
expr = value.strip()
56+
parts = expr.split(" in ", 1)
57+
if len(parts) == 2:
58+
expressions.append(parts[0].strip())
59+
expressions.append(parts[1].strip())
60+
else:
61+
expressions.append(value.strip())
62+
63+
# Recurse into children
64+
if children := getattr(n, "children", None):
65+
for child in children:
66+
_collect_from_node(child)
67+
68+
_collect_from_node(node)
69+
return expressions
70+
71+
72+
def batch_format_expressions(expressions: list[str]) -> dict[str, str]:
73+
"""
74+
Format multiple Python expressions in a single ruff call.
75+
76+
Args:
77+
expressions: List of Python expressions to format
78+
79+
Returns:
80+
Dictionary mapping original expression to formatted expression
81+
"""
82+
if not expressions:
83+
return {}
84+
85+
# Filter out empty expressions
86+
non_empty = [e for e in expressions if e and e.strip()]
87+
if not non_empty:
88+
return {}
89+
90+
try:
91+
# Create a batch of assignments
92+
lines = [f"__e{i}__ = {expr}" for i, expr in enumerate(non_empty)]
93+
batch_source = "\n".join(lines)
94+
95+
# Format all at once
96+
formatted_batch = run_ruff_format(
97+
batch_source, use_single_quotes=True, skip_import_sort=True
98+
)
99+
100+
# Parse the results - need to handle multiline expressions
101+
result_map = {}
102+
formatted_lines = formatted_batch.strip().split("\n")
103+
formatted_lines_count = len(formatted_lines)
104+
105+
for i, expr in enumerate(non_empty):
106+
# Find the formatted expression for this variable
107+
prefix = f"__e{i}__ = "
108+
109+
# Find the start line
110+
start_idx = None
111+
for idx, line in enumerate(formatted_lines):
112+
if line.startswith(prefix):
113+
start_idx = idx
114+
break
115+
116+
if start_idx is None:
117+
continue
118+
119+
# Find the end line: next line that starts with __e (next assignment)
120+
end_idx = formatted_lines_count
121+
for idx in range(start_idx + 1, formatted_lines_count):
122+
line = formatted_lines[idx]
123+
if line.startswith("__e") and " = " in line:
124+
end_idx = idx
125+
break
126+
127+
# Extract the expression (everything after "variable = ")
128+
first_line = formatted_lines[start_idx][len(prefix) :]
129+
if start_idx + 1 < end_idx:
130+
# Multiline expression
131+
remaining_lines = formatted_lines[start_idx + 1 : end_idx]
132+
formatted_expr = first_line + "\n" + "\n".join(remaining_lines)
133+
else:
134+
# Single line expression
135+
formatted_expr = first_line
136+
137+
result_map[expr] = formatted_expr
138+
139+
return result_map
140+
141+
except Exception as e:
142+
logger.debug(f"Batch formatting failed: {e}")
143+
# Return empty dict - will fall back to individual formatting
144+
return {}
145+
146+
147+
def format_python_expression(
148+
expr: str, expression_cache: dict[str, str] | None = None
149+
) -> str:
33150
"""
34151
Format a Python expression using ruff with single quotes.
35152
36153
Args:
37154
expr: The Python expression as a string
155+
expression_cache: Optional cache of pre-formatted expressions
38156
39157
Returns:
40158
Formatted expression as a string (using single quotes)
41159
"""
42160
if not expr or not expr.strip():
43161
return expr
44162

163+
# Check cache first
164+
if expression_cache is not None and expr in expression_cache:
165+
return expression_cache[expr]
166+
45167
try:
168+
prefix = "__dummy__ = "
46169
# Wrap the expression in a dummy assignment to make it valid Python
47170
# This allows ruff to format it properly
48-
wrapped = f"__dummy__ = {expr}"
171+
wrapped = f"{prefix}{expr}"
49172

50173
# Format with ruff using single quotes
51174
# Skip import sorting since template expressions don't have imports
@@ -55,7 +178,6 @@ def format_python_expression(expr: str) -> str:
55178

56179
# Extract the expression back out
57180
# (remove "__dummy__ = " and trailing newline)
58-
prefix = "__dummy__ = "
59181
if formatted.startswith(prefix):
60182
formatted_expr = formatted[len(prefix) :].rstrip("\n")
61183
return formatted_expr
@@ -68,7 +190,9 @@ def format_python_expression(expr: str) -> str:
68190
return expr
69191

70192

71-
def format_list_expression(expr: str) -> str:
193+
def format_list_expression(
194+
expr: str, expression_cache: dict[str, str] | None = None
195+
) -> str:
72196
"""
73197
Format a Python list expression (v-for syntax) using ruff with single quotes.
74198
@@ -77,6 +201,7 @@ def format_list_expression(expr: str) -> str:
77201
78202
Args:
79203
expr: The list expression as a string (e.g., "item in items")
204+
expression_cache: Optional cache of pre-formatted expressions
80205
81206
Returns:
82207
Formatted expression as a string (using single quotes)
@@ -99,8 +224,10 @@ def format_list_expression(expr: str) -> str:
99224
target, iterable = parts
100225

101226
# Format each part using format_python_expression
102-
formatted_target = format_python_expression(target.strip())
103-
formatted_iterable = format_python_expression(iterable.strip())
227+
formatted_target = format_python_expression(target.strip(), expression_cache)
228+
formatted_iterable = format_python_expression(
229+
iterable.strip(), expression_cache
230+
)
104231

105232
return f"{formatted_target} in {formatted_iterable}"
106233

@@ -111,12 +238,20 @@ def format_list_expression(expr: str) -> str:
111238

112239
def format_template(template_node) -> tuple[list[str], tuple[int, int]]:
113240
"""
114-
Returns formatted node and the original location of the template node
241+
Returns formatted node and the original location of the template node.
242+
243+
Uses batch formatting for all Python expressions to improve performance.
115244
"""
116245
# Find beginning and end of script block
117246
start, end = template_node.location[0] - 1, template_node.end[0]
118247

119-
result = format_node(template_node, depth=0)
248+
# Collect all expressions and batch format them
249+
expressions = collect_expressions(template_node)
250+
expression_cache = batch_format_expressions(expressions) if expressions else None
251+
252+
# Format the node tree using the cached expressions
253+
result = format_node(template_node, depth=0, expression_cache=expression_cache)
254+
120255
# Replace all tabs with the default indent and add line breaks
121256
tab = "\t"
122257
result = [f"{line.replace(tab, INDENT)}\n" for line in result]
@@ -160,7 +295,9 @@ def sort_attr(attr):
160295
return f"{len(SORTING)}{attr}"
161296

162297

163-
def format_attribute(key, value, indent="", single_attribute=True):
298+
def format_attribute(
299+
key, value, indent="", single_attribute=True, expression_cache=None
300+
):
164301
"""
165302
Format an attribute key-value pair.
166303
@@ -176,10 +313,10 @@ def format_attribute(key, value, indent="", single_attribute=True):
176313
if key.startswith((":", "@", "v-")) and isinstance(value, str) and value.strip():
177314
value = value.strip()
178315
if key.startswith("v-for"):
179-
formatted_value = format_list_expression(value)
316+
formatted_value = format_list_expression(value, expression_cache)
180317
return f'{key}="{formatted_value}"'
181318
else:
182-
formatted_value = format_python_expression(value)
319+
formatted_value = format_python_expression(value, expression_cache)
183320
formatted_value = textwrap.indent(
184321
formatted_value, indent + ("" if single_attribute else INDENT)
185322
).lstrip()
@@ -188,7 +325,7 @@ def format_attribute(key, value, indent="", single_attribute=True):
188325
return f'{key}="{value}"'
189326

190327

191-
def format_node(node, depth: int) -> list[str]:
328+
def format_node(node, depth: int, expression_cache=None) -> list[str]:
192329
result = []
193330
indent = depth * INDENT
194331

@@ -201,13 +338,23 @@ def format_node(node, depth: int) -> list[str]:
201338
if node.attrs:
202339
if len(node.attrs) == 1:
203340
key, val = next(iter(node.attrs.items()))
204-
attr = format_attribute(key, val, indent, single_attribute=True)
341+
attr = format_attribute(
342+
key,
343+
val,
344+
indent,
345+
single_attribute=True,
346+
expression_cache=expression_cache,
347+
)
205348
start = f"{start} {attr}"
206349
else:
207350
attrs = []
208351
for key in sorted(node.attrs, key=sort_attr):
209352
attr = format_attribute(
210-
key, node.attrs[key], indent, single_attribute=False
353+
key,
354+
node.attrs[key],
355+
indent,
356+
single_attribute=False,
357+
expression_cache=expression_cache,
211358
)
212359
attrs.append(f"{indent}{INDENT}{attr}")
213360

@@ -228,7 +375,7 @@ def format_node(node, depth: int) -> list[str]:
228375

229376
if hasattr(node, "children"):
230377
for child in node.children:
231-
result.extend(format_node(child, depth + 1))
378+
result.extend(format_node(child, depth + 1, expression_cache))
232379

233380
if node.children:
234381
result.append(f"{indent}</{node.tag}>")

0 commit comments

Comments
 (0)