Skip to content

Commit 92424ce

Browse files
committed
Python: Fix corner case in printer
1 parent fcc44d0 commit 92424ce

2 files changed

Lines changed: 134 additions & 0 deletions

File tree

rewrite-python/rewrite/src/rewrite/python/printer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1199,6 +1199,7 @@ def visit_assignment(self, assignment: 'j.Assignment', p: PrintOutputCapture) ->
11991199
is_regular_assignment = (
12001200
isinstance(parent_value, j.Block) or
12011201
isinstance(parent_value, py.CompilationUnit) or
1202+
isinstance(parent_value, py.ExpressionStatement) or # J.Assignment is both Expression and Statement, so the wrapping ExpressionStatement is redundant here, but template-generated replacements can produce this nesting
12021203
(isinstance(parent_value, j.If) and parent_value.then_part == assignment) or
12031204
(isinstance(parent_value, j.If.Else) and parent_value.body == assignment) or
12041205
(isinstance(parent_value, Loop) and parent_value.body == assignment) # ty: ignore[unresolved-attribute] # Loop base class doesn't have body

rewrite-python/rewrite/tests/python/template/test_template.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,139 @@ def test_builder_chaining(self):
192192
assert builder.imports("import z") is builder
193193

194194

195+
class TestTemplateApplyRecipe:
196+
"""End-to-end tests for pattern match + template apply in recipes."""
197+
198+
def test_replace_method_call_with_assignment(self):
199+
"""Test that pattern({obj}.setX({val})) + template({obj}.x = {val}) produces '=' not ':='."""
200+
from rewrite import ExecutionContext, Recipe, TreeVisitor
201+
from rewrite.python.visitor import PythonVisitor
202+
from rewrite.python.template import pattern, capture
203+
from rewrite.java.tree import MethodInvocation
204+
from rewrite.test import RecipeSpec, python
205+
206+
obj = capture('obj')
207+
val = capture('val')
208+
pat = pattern("{obj}.setDaemon({val})", obj=obj, val=val)
209+
tmpl = template("{obj}.daemon = {val}", obj=obj, val=val)
210+
211+
class TestRecipe(Recipe):
212+
@property
213+
def name(self) -> str:
214+
return "test.ReplaceSetterWithAssignment"
215+
216+
@property
217+
def display_name(self) -> str:
218+
return "Test"
219+
220+
@property
221+
def description(self) -> str:
222+
return "Test"
223+
224+
def editor(self):
225+
class Visitor(PythonVisitor[ExecutionContext]):
226+
def visit_method_invocation(self, method, p):
227+
method = super().visit_method_invocation(method, p)
228+
match = pat.match(method, self.cursor)
229+
if match:
230+
return tmpl.apply(self.cursor, values=match)
231+
return method
232+
return Visitor()
233+
234+
spec = RecipeSpec(recipe=TestRecipe())
235+
spec.rewrite_run(
236+
python(
237+
"thread.setDaemon(True)",
238+
"thread.daemon = True",
239+
)
240+
)
241+
242+
def test_replace_method_call_with_method_call(self):
243+
"""Test that pattern(datetime.utcnow()) + template(datetime.now(datetime.UTC)) works."""
244+
from rewrite import ExecutionContext, Recipe, TreeVisitor
245+
from rewrite.python.visitor import PythonVisitor
246+
from rewrite.python.template import pattern, capture
247+
from rewrite.java.tree import MethodInvocation
248+
from rewrite.test import RecipeSpec, python
249+
250+
pat = pattern("datetime.utcnow()")
251+
tmpl = template("datetime.now(datetime.UTC)")
252+
253+
class TestRecipe(Recipe):
254+
@property
255+
def name(self) -> str:
256+
return "test.ReplaceUtcNow"
257+
258+
@property
259+
def display_name(self) -> str:
260+
return "Test"
261+
262+
@property
263+
def description(self) -> str:
264+
return "Test"
265+
266+
def editor(self):
267+
class Visitor(PythonVisitor[ExecutionContext]):
268+
def visit_method_invocation(self, method, p):
269+
method = super().visit_method_invocation(method, p)
270+
match = pat.match(method, self.cursor)
271+
if match:
272+
return tmpl.apply(self.cursor, values=match)
273+
return method
274+
return Visitor()
275+
276+
spec = RecipeSpec(recipe=TestRecipe())
277+
spec.rewrite_run(
278+
python(
279+
"now = datetime.utcnow()",
280+
"now = datetime.now(datetime.UTC)",
281+
)
282+
)
283+
284+
def test_replace_method_call_with_field_access(self):
285+
"""Test that pattern({obj}.getName()) + template({obj}.name) works."""
286+
from rewrite import ExecutionContext, Recipe, TreeVisitor
287+
from rewrite.python.visitor import PythonVisitor
288+
from rewrite.python.template import pattern, capture
289+
from rewrite.java.tree import MethodInvocation
290+
from rewrite.test import RecipeSpec, python
291+
292+
obj = capture('obj')
293+
pat = pattern("{obj}.getName()", obj=obj)
294+
tmpl = template("{obj}.name", obj=obj)
295+
296+
class TestRecipe(Recipe):
297+
@property
298+
def name(self) -> str:
299+
return "test.ReplaceGetterWithProperty"
300+
301+
@property
302+
def display_name(self) -> str:
303+
return "Test"
304+
305+
@property
306+
def description(self) -> str:
307+
return "Test"
308+
309+
def editor(self):
310+
class Visitor(PythonVisitor[ExecutionContext]):
311+
def visit_method_invocation(self, method, p):
312+
method = super().visit_method_invocation(method, p)
313+
match = pat.match(method, self.cursor)
314+
if match:
315+
return tmpl.apply(self.cursor, values=match)
316+
return method
317+
return Visitor()
318+
319+
spec = RecipeSpec(recipe=TestRecipe())
320+
spec.rewrite_run(
321+
python(
322+
"name = thread.getName()",
323+
"name = thread.name",
324+
)
325+
)
326+
327+
195328
class TestTemplateApply:
196329
"""Tests for Template.apply()."""
197330

0 commit comments

Comments
 (0)