From 23de15249883bf7fd8a60ebd555d7ccb8a062150 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Thu, 16 Jan 2025 16:01:49 +0100 Subject: [PATCH] Allow parens around multi-line str as function argument --- CHANGELOG.md | 14 ++ README.md | 12 +- .../_redundant_parentheses.py | 24 ++- tests/test_redundant_parentheses.py | 185 ++++++++++++++---- 4 files changed, 197 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfd7e1f..f7b7959 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,20 @@ Changelog This is an almost empty patch as no breaking changes of Python 3.13 affect this plugin. Only documentation, project metadata as well as some tooling needed adjustments. +**🔧 Fixes** +* Make multi-line string exemptions more lenient ([#47](https://github.com/robsdedude/flake8-picky-parentheses/pull/47)). + Allow redundant parenthesis around multi-line string in function calls. + For example: + ```python + # GOOD + func( + ( + "a" + "b" + ), + "c", + ) + ``` ## 0.5.5 *** diff --git a/README.md b/README.md index 223b46c..fa115c5 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,7 @@ exceptions to this rule: 3. Parts of slices. 4. Multi-line[1)](#footnotes) expression, `if` and `for` parts in comprehensions. 5. Multi-line[1)](#footnotes) keyword arguments or argument defaults. - 6. String concatenation over several lines in lists and tuples . + 6. String concatenation over several lines in lists, tuples, and function arguments. Exception type 1: @@ -345,6 +345,16 @@ Exception type 6: "a" "b" ), ] + +# This also applies to function calls: +# GOOD +func( + ( + "a" + "b" + ), + "c", +) ``` ### Footnotes: diff --git a/src/flake8_picky_parentheses/_redundant_parentheses.py b/src/flake8_picky_parentheses/_redundant_parentheses.py index 05ac9d2..088b9e0 100644 --- a/src/flake8_picky_parentheses/_redundant_parentheses.py +++ b/src/flake8_picky_parentheses/_redundant_parentheses.py @@ -291,6 +291,7 @@ def _get_exceptions_from_ast(cls, sorted_parens_coords, tree, tokens): nodes_idx = 0 last_exception_node = None + skip_node = False rewrite_buffer = None for parens_coord in sorted_parens_coords: node, pos, end, parents = nodes[nodes_idx] @@ -301,6 +302,12 @@ def _get_exceptions_from_ast(cls, sorted_parens_coords, tree, tokens): if nodes_idx >= len(nodes): return node, pos, end, parents = nodes[nodes_idx] + if skip_node: + if last_exception_node is not node: + skip_node = False + rewrite_buffer = None + else: + continue if rewrite_buffer is not None and last_exception_node is not node: # moved to the next node => emmit the exception yield rewrite_buffer @@ -388,7 +395,10 @@ def _get_exceptions_from_ast(cls, sorted_parens_coords, tree, tokens): continue if ( parents - and isinstance(parents[0], (ast.Tuple, ast.List)) + and isinstance( + parents[0], + (ast.Tuple, ast.List, ast.Call, ast.keyword), + ) and isinstance(node, AstStr) ): tokens_slice = slice(parens_coord.token_indexes[0] + 1, @@ -402,6 +412,18 @@ def _get_exceptions_from_ast(cls, sorted_parens_coords, tree, tokens): if string_tokens[0].start[0] != string_tokens[-1].start[0]: rewrite_buffer = ProblemRewrite(parens_coord.open_, None) last_exception_node = node + + if isinstance(parents[0], ast.Call): + prev_token = tokens[parens_coord.token_indexes[0] - 1] + if prev_token.type == tokenize.NAME: + # For function calls, we want the multi-line string + # to provide an exception for the outermost + # parenthesis pair if that is the one enclosing the + # function call's arguments. + # The innermost parenthesis pair is already covered + # by _get_exceptions_for_neighboring_parens + yield ProblemRewrite(parens_coord.open_, None) + skip_node = True continue if rewrite_buffer is not None: diff --git a/tests/test_redundant_parentheses.py b/tests/test_redundant_parentheses.py index 22d7a31..f5d0709 100644 --- a/tests/test_redundant_parentheses.py +++ b/tests/test_redundant_parentheses.py @@ -462,45 +462,150 @@ def test_single_line_strings(plugin, value, quote): assert lint_codes(plugin(s), ["PAR001"]) -# GOOD (multi-line strings in list/tuple) +# GOOD (multi-line strings in list/tuple/func arg) # https://github.com/robsdedude/flake8-picky-parentheses/issues/32 @pytest.mark.parametrize("value", ( - '[("a"\n"b")]', - '[("a"\n"b"),]', - '[("a"\n"c"), "b"]', - '["a", ("b" \n"c")]', - '[("a"\n"c"), "b",]', - '["a", ("b"\n"c"),]', - '[(\n"a"\n"c"), "b"]', - '["a", (\n"b" \n"c")]', - '[(\n"a"\n"c"), "b",]', - '["a", (\n"b"\n"c"),]', - '[("a"\n"c"\n), "b"]', - '["a", ("b" \n"c"\n)]', - '[("a"\n"c"\n), "b",]', - '["a", ("b"\n"c"\n),]', - '(("a"\n"b"),)', - '(("a"\n"c"), "b")', - '("a", ("b" \n"c"))', - '(("a"\n"c"), "b",)', - '("a", ("b"\n"c"),)', - '((\n"a"\n"c"), "b")', - '("a", (\n"b" \n"c"))', - '((\n"a"\n"c"), "b",)', - '("a", (\n"b"\n"c"),)', - '(("a"\n"c"\n), "b")', - '("a", ("b" \n"c"\n))', - '(("a"\n"c"\n), "b",)', - '("a", ("b"\n"c"\n),)', - + # lists + 's = [("a"\n"b")]\n', + 's = [("a"\n"b"),]\n', + 's = [("a"\n"c"), "b"]\n', + 's = ["a", ("b" \n"c")]\n', + 's = [("a"\n"c"), "b",]\n', + 's = ["a", ("b"\n"c"),]\n', + 's = [(\n"a"\n"c"), "b"]\n', + 's = ["a", (\n"b" \n"c")]\n', + 's = [(\n"a"\n"c"), "b",]\n', + 's = ["a", (\n"b"\n"c"),]\n', + 's = [("a"\n"c"\n), "b"]\n', + 's = ["a", ("b" \n"c"\n)]\n', + 's = [("a"\n"c"\n), "b",]\n', + 's = ["a", ("b"\n"c"\n),]\n', + + # tuples + 's = (("a"\n"b"),)\n', + 's = (("a"\n"c"), "b")\n', + 's = ("a", ("b" \n"c"))\n', + 's = (("a"\n"c"), "b",)\n', + 's = ("a", ("b"\n"c"),)\n', + 's = ((\n"a"\n"c"), "b")\n', + 's = ("a", (\n"b" \n"c"))\n', + 's = ((\n"a"\n"c"), "b",)\n', + 's = ("a", (\n"b"\n"c"),)\n', + 's = (("a"\n"c"\n), "b")\n', + 's = ("a", ("b" \n"c"\n))\n', + 's = (("a"\n"c"\n), "b",)\n', + 's = ("a", ("b"\n"c"\n),)\n', + + # positional arguments + 'func(("a"\n"b"))\n', + 'func(("a"\n"b"),)\n', + 'func(("a"\n"c"), "b")\n', + 'func("a", ("b" \n"c"))\n', + 'func(("a"\n"c"), "b",)\n', + 'func("a", ("b"\n"c"),)\n', + 'func((\n"a"\n"c"), "b")\n', + 'func("a", (\n"b" \n"c"))\n', + 'func((\n"a"\n"c"), "b",)\n', + 'func("a", (\n"b"\n"c"),)\n', + 'func(("a"\n"c"\n), "b")\n', + 'func("a", ("b" \n"c"\n))\n', + 'func(("a"\n"c"\n), "b",)\n', + 'func("a", ("b"\n"c"\n),)\n', + + # keyword arguments + 'func(a=("a"\n"b"))\n', + 'func(a=("a"\n"b"),)\n', + 'func(a=("a"\n"c"), b="b")\n', + 'func(a="a", b=("b" \n"c"))\n', + 'func(a=("a"\n"c"), b="b",)\n', + 'func(a="a", b=("b"\n"c"),)\n', + 'func(a=(\n"a"\n"c"), b="b")\n', + 'func(a="a", b=(\n"b" \n"c"))\n', + 'func(a=(\n"a"\n"c"), b="b",)\n', + 'func(a="a", b=(\n"b"\n"c"),)\n', + 'func(a=("a"\n"c"\n), b="b")\n', + 'func(a="a", b=("b" \n"c"\n))\n', + 'func(a=("a"\n"c"\n), b="b",)\n', + 'func(a="a", b=("b"\n"c"\n),)\n', )) @pytest.mark.parametrize("quote", ("'", '"')[1:]) def test_grouped_single_line_strings(plugin, value, quote): - value = value.replace('"', quote) - s = f"a = {value}\n" + s = value.replace('"', quote) assert no_lint(plugin(s)) +# BAD (multi-line strings in list/tuple with double redundant parentheses) +# https://github.com/robsdedude/flake8-picky-parentheses/issues/32 +@pytest.mark.parametrize("value", ( + # lists + 's = [(("a"\n"b"))]\n', + 's = [(("a"\n"b")),]\n', + 's = [(("a"\n"c")), "b"]\n', + 's = ["a", (("b" \n"c"))]\n', + 's = [(("a"\n"c")), "b",]\n', + 's = ["a", (("b"\n"c")),]\n', + 's = [((\n"a"\n"c")), "b"]\n', + 's = ["a", ((\n"b" \n"c"))]\n', + 's = [((\n"a"\n"c")), "b",]\n', + 's = ["a", ((\n"b"\n"c")),]\n', + 's = [(("a"\n"c"\n)), "b"]\n', + 's = ["a", (("b" \n"c"\n))]\n', + 's = [(("a"\n"c"\n)), "b",]\n', + 's = ["a", (("b"\n"c"\n)),]\n', + + # tuples + 's = ((("a"\n"b")),)\n', + 's = ((("a"\n"c")), "b")\n', + 's = ("a", (("b" \n"c")))\n', + 's = ((("a"\n"c")), "b",)\n', + 's = ("a", (("b"\n"c")),)\n', + 's = (((\n"a"\n"c")), "b")\n', + 's = ("a", ((\n"b" \n"c")))\n', + 's = (((\n"a"\n"c")), "b",)\n', + 's = ("a", ((\n"b"\n"c")),)\n', + 's = ((("a"\n"c"\n)), "b")\n', + 's = ("a", (("b" \n"c"\n)))\n', + 's = ((("a"\n"c"\n)), "b",)\n', + 's = ("a", (("b"\n"c"\n)),)\n', + + # positional arguments + 'func((("a"\n"b")))\n', + 'func((("a"\n"b")),)\n', + 'func((("a"\n"c")), "b")\n', + 'func("a", (("b" \n"c")))\n', + 'func((("a"\n"c")), "b",)\n', + 'func("a", (("b"\n"c")),)\n', + 'func(((\n"a"\n"c")), "b")\n', + 'func("a", ((\n"b" \n"c")))\n', + 'func(((\n"a"\n"c")), "b",)\n', + 'func("a", ((\n"b"\n"c")),)\n', + 'func((("a"\n"c"\n)), "b")\n', + 'func("a", (("b" \n"c"\n)))\n', + 'func((("a"\n"c"\n)), "b",)\n', + 'func("a", (("b"\n"c"\n)),)\n', + + # keyword arguments + 'func(a=(("a"\n"b")))\n', + 'func(a=(("a"\n"b")),)\n', + 'func(a=(("a"\n"c")), b="b")\n', + 'func(a="a", b=(("b" \n"c")))\n', + 'func(a=(("a"\n"c")), b="b",)\n', + 'func(a="a", b=(("b"\n"c")),)\n', + 'func(a=((\n"a"\n"c")), b="b")\n', + 'func(a="a", b=((\n"b" \n"c")))\n', + 'func(a=((\n"a"\n"c")), b="b",)\n', + 'func(a="a", b=((\n"b"\n"c")),)\n', + 'func(a=(("a"\n"c"\n)), b="b")\n', + 'func(a="a", b=(("b" \n"c"\n)))\n', + 'func(a=(("a"\n"c"\n)), b="b",)\n', + 'func(a="a", b=(("b"\n"c"\n)),)\n', +)) +@pytest.mark.parametrize("quote", ("'", '"')[1:]) +def test_grouped_single_line_strings_double_parens(plugin, value, quote): + s = value.replace('"', quote) + assert lint_codes(plugin(s), ["PAR001"]) + + # GOOD (function call) def test_function_call(plugin): s = """foo("a") @@ -615,7 +720,7 @@ def test_unnecessary_parens(plugin): assert lint_codes(plugin(s), ["PAR001"]) -# BAD (one pair of parenthesis is enough) +# BAD (one pair of parentheses is enough) def test_bin_op_example_double_parens_1(plugin): s = """a = 1 * ((2 + 3)) """ @@ -624,28 +729,28 @@ def test_bin_op_example_double_parens_1(plugin): assert lints[0].startswith("1:9 ") or lints[0].startswith("1:10 ") -# BAD (one pair of parenthesis is enough) +# BAD (one pair of parentheses is enough) def test_bin_op_example_double_parens_2(plugin): s = """a = ((1 * 2)) + 3 """ assert lint_codes(plugin(s), ["PAR001"]) -# BAD (one pair of parenthesis is enough) +# BAD (one pair of parentheses is enough) def test_bin_op_example_double_parens_3(plugin): s = """a = 1 + ((2 * 3)) """ assert lint_codes(plugin(s), ["PAR001"]) -# BAD (one pair of parenthesis is enough) +# BAD (one pair of parentheses is enough) def test_bin_op_example_double_parens_4(plugin): s = """a = ((1 + 2)) * 3 """ assert lint_codes(plugin(s), ["PAR001"]) -# BAD (redundant parenthesis around 1) +# BAD (redundant parentheses around 1) def test_redundant_parens_around_tuple(plugin): s = """a = ((1),) """ @@ -1812,3 +1917,11 @@ def test_multi_line_ternary_op_in_dict_comprehension_value(plugin): } """ assert no_lint(plugin(s)) + + +def test_multiple_exceptions(plugin): + s = """\ +a = (1 + 2) + 3 +b = ((1 + 2) + 3) + 4 +""" + assert no_lint(plugin(s))