Skip to content

Commit 398b8c4

Browse files
Python: Integrate auto-formatting into template engine (#6728)
Add auto_format() and maybe_auto_format() helper functions to the Python format module, mirroring the JavaScript template system's pattern. After template application, _apply_coordinates now calls maybe_auto_format to normalize spacing, indentation, and other formatting on the templated subtree using the enclosing CompilationUnit's style context. The formatter resolves styles from the CompilationUnit in the cursor ancestry and formats only the templated subtree, not the whole file. When no CompilationUnit context is available (e.g. synthetic cursors in unit tests), formatting is gracefully skipped. Includes rewrite_run integration tests verifying operator spacing, method call formatting, and correct behavior in deeply nested scopes.
1 parent 5a57a9d commit 398b8c4

12 files changed

Lines changed: 1355 additions & 3 deletions

File tree

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,40 @@
11
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
22

3-
from .auto_format import AutoFormat
3+
from typing import Optional
4+
5+
from .auto_format import AutoFormat, AutoFormatVisitor
46
from .blank_lines import BlankLinesVisitor
57
from .minimum_viable_spacing import MinimumViableSpacingVisitor
68
from .normalize_format import NormalizeFormatVisitor
79
from .normalize_tabs_or_spaces import NormalizeTabsOrSpacesVisitor
810
from .spaces_visitor import SpacesVisitor
911
from .tabs_and_indents_visitor import TabsAndIndentsVisitor
1012

13+
from ...visitor import Cursor
14+
15+
16+
def auto_format(tree, p, stop_after=None, cursor: Optional[Cursor] = None):
17+
try:
18+
return AutoFormatVisitor(stop_after).visit(tree, p, cursor)
19+
except (ValueError, AttributeError):
20+
return tree
21+
22+
23+
def maybe_auto_format(before, after, p, stop_after=None, cursor: Optional[Cursor] = None):
24+
if before is not after:
25+
return auto_format(after, p, stop_after, cursor)
26+
return after
27+
28+
1129
__all__ = [
1230
'AutoFormat',
31+
'AutoFormatVisitor',
1332
'BlankLinesVisitor',
1433
'MinimumViableSpacingVisitor',
1534
'NormalizeFormatVisitor',
1635
'NormalizeTabsOrSpacesVisitor',
1736
'SpacesVisitor',
1837
'TabsAndIndentsVisitor',
38+
'auto_format',
39+
'maybe_auto_format',
1940
]

rewrite-python/rewrite/src/rewrite/python/template/comparator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ def _compare_arguments(
251251

252252
# Check for variadic capture
253253
if len(pattern_elements) == 1:
254-
pattern_arg = pattern_elements[0].element
254+
pattern_arg = pattern_elements[0]
255255
if isinstance(pattern_arg, j.Identifier):
256256
cap_name = from_placeholder(pattern_arg.simple_name)
257257
if cap_name and self._captures.get(cap_name, Capture(name=cap_name)).variadic:
@@ -267,7 +267,7 @@ def _compare_arguments(
267267

268268
# Compare each argument
269269
for p_elem, t_elem in zip(pattern_elements, target_elements):
270-
if not self._compare(p_elem.element, t_elem.element, cursor):
270+
if not self._compare(p_elem, t_elem, cursor):
271271
return False
272272

273273
return True

rewrite-python/rewrite/src/rewrite/python/template/engine.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,18 @@ def _apply_coordinates(
325325
result,
326326
)
327327

328+
# Auto-format the result
329+
try:
330+
from ..format import maybe_auto_format
331+
original = coordinates.tree
332+
result = maybe_auto_format(
333+
original, result, None, None,
334+
cursor.parent if cursor.parent is not None else None
335+
)
336+
except (ValueError, AttributeError):
337+
# No CompilationUnit in cursor ancestry — skip formatting
338+
pass
339+
328340
return result
329341

330342
@classmethod

rewrite-python/rewrite/tests/python/format/__init__.py

Whitespace-only changes.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Copyright 2025 the original author or authors.
2+
# <p>
3+
# Licensed under the Moderne Source Available License (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
# <p>
7+
# https://docs.moderne.io/licensing/moderne-source-available-license
8+
# <p>
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Unit tests for auto_format and maybe_auto_format helper functions."""
16+
17+
from rewrite.python.format import auto_format, maybe_auto_format
18+
from rewrite.python.template.engine import TemplateEngine
19+
20+
21+
class TestMaybeAutoFormat:
22+
"""Tests for maybe_auto_format identity guard."""
23+
24+
def test_same_object_returns_unchanged(self):
25+
"""When before is after (same identity), return after without formatting."""
26+
tree = TemplateEngine.get_template_tree("x+1", {})
27+
result = maybe_auto_format(tree, tree, None)
28+
assert result is tree
29+
30+
def test_different_object_no_cursor_skips_gracefully(self):
31+
"""When before is not after but no cursor, skip formatting without crash."""
32+
before = TemplateEngine.get_template_tree("x + 1", {})
33+
TemplateEngine.clear_cache()
34+
after = TemplateEngine.get_template_tree("y + 2", {})
35+
36+
result = maybe_auto_format(before, after, None)
37+
assert result is not None
38+
39+
40+
class TestAutoFormat:
41+
"""Tests for auto_format fallback behavior."""
42+
43+
def test_no_cursor_returns_tree_unchanged(self):
44+
"""auto_format without cursor skips formatting and returns tree."""
45+
tree = TemplateEngine.get_template_tree("x+1", {})
46+
result = auto_format(tree, None)
47+
assert result is tree

0 commit comments

Comments
 (0)