diff --git a/rewrite-python/rewrite/src/rewrite/python/_parser_visitor.py b/rewrite-python/rewrite/src/rewrite/python/_parser_visitor.py index 90dc7a19a0a..28ca9b69e15 100644 --- a/rewrite-python/rewrite/src/rewrite/python/_parser_visitor.py +++ b/rewrite-python/rewrite/src/rewrite/python/_parser_visitor.py @@ -2691,6 +2691,25 @@ def visit_Tuple(self, node): close_line == second_elt.lineno and close_col < second_elt_char_col ): maybe_parens = False + elif close_paren_idx is not None and len(node.elts) == 1: + # For single-element tuples like `("a"),`, check if the '(' + # wraps just the element (grouping parens) vs the tuple. + # If the token after ')' is ',' AND there is no ',' inside + # the parens, then '(' is a grouping paren and the trailing + # ',' outside creates the tuple. + # Compare with `("a",),` where ',' is inside the parens + # (making a proper tuple) and the outer ',' belongs to an + # enclosing context. + next_idx = close_paren_idx + 1 + while next_idx < len(self._tokens) and self._tokens[next_idx].type in _SKIP_TOKEN_TYPES: + next_idx += 1 + if next_idx < len(self._tokens) and self._tokens[next_idx].string == ',': + # Check if there's a comma inside the parens + prev_idx = close_paren_idx - 1 + while prev_idx > self._token_idx and self._tokens[prev_idx].type in _SKIP_TOKEN_TYPES: + prev_idx -= 1 + if prev_idx > self._token_idx and self._tokens[prev_idx].string != ',': + maybe_parens = False if maybe_parens: self._token_idx += 1 # consume '(' diff --git a/rewrite-python/rewrite/tests/python/all/tree/collection_literal_test.py b/rewrite-python/rewrite/tests/python/all/tree/collection_literal_test.py index 5ba084cc0c2..65fe5ab0eb5 100644 --- a/rewrite-python/rewrite/tests/python/all/tree/collection_literal_test.py +++ b/rewrite-python/rewrite/tests/python/all/tree/collection_literal_test.py @@ -44,6 +44,23 @@ def test_single_element_tuple_with_trailing_comma(): RecipeSpec().rewrite_run(python("t = (1 , )")) +def test_single_element_tuple_with_trailing_comma_outside_parens(): + # language=python - parenthesized expression with trailing comma outside + RecipeSpec().rewrite_run(python('("a"),\n')) + + +def test_single_element_tuple_with_trailing_comma_outside_parens_multiline(): + # language=python - multi-line parenthesized string with trailing comma outside + RecipeSpec().rewrite_run(python( + """\ +( + "Part 1" + " Part 2" +), +""" + )) + + def test_tuple_with_first_element_in_parens(): # language=python RecipeSpec().rewrite_run(python("x = (1) // 2, 0")) diff --git a/rewrite-python/rewrite/tests/python/all/tree/type_hint_test.py b/rewrite-python/rewrite/tests/python/all/tree/type_hint_test.py index 8fe18f37cfc..89e99a6d80e 100644 --- a/rewrite-python/rewrite/tests/python/all/tree/type_hint_test.py +++ b/rewrite-python/rewrite/tests/python/all/tree/type_hint_test.py @@ -88,6 +88,16 @@ def test_variable_with_parameterized_type_hint_in_quotes(): RecipeSpec().rewrite_run(python("""foo: Dict["Foo", str] = None""")) +def test_literal_string_type_hint_with_assignment(): + # language=python - parenthesized string with trailing comma (tuple) before Literal type hint + RecipeSpec().rewrite_run(python( + """\ +("a"), +y: Literal["test"] = "value" +""" + )) + + def test_variable_with_quoted_type_hint(): # language=python RecipeSpec().rewrite_run(python("""foo: 'Foo' = None"""))