Skip to content

Commit

Permalink
Merge pull request #34 from robsdedude/allow-parens-string-concatenat…
Browse files Browse the repository at this point in the history
…ion-list-tuple

Allow parentheses around str concatenation in lists/tuples
  • Loading branch information
robsdedude authored Sep 18, 2023
2 parents bc60e84 + 95149a3 commit 393f93e
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 24 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ Changelog

## NEXT
***
**⭐ New**
* Exempt parentheses around multi-line strings in tuples and lists ([#34](https://github.com/robsdedude/flake8-picky-parentheses/pull/34)).


## 0.5.0
Expand All @@ -14,7 +16,7 @@ Changelog
## 0.4.0
***
**⭐ New**
* Add support for Python 3.11 ([#28](https://github.com/robsdedude/flake8-picky-parentheses/pull/28))
* Add support for Python 3.11 ([#28](https://github.com/robsdedude/flake8-picky-parentheses/pull/28)).


## 0.3.2
Expand Down
36 changes: 34 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,14 +184,15 @@ try to remove each pair of parentheses and see if the code still compiles and
yields the same AST (i.e., is semantically equivalent).
If it does, a flake (lint error) is reported. However, there are two notable
exceptions to this rule:
1. Parentheses for tuple literals
1. Parentheses for tuple literals.
2. A single pair or parentheses in expressions to highlight operator
precedence.
Even if these parentheses are redundant, they help to divide parts of
expressions and show sequence of actions.
3. Parts of slices
3. Parts of slices.
4. Multi-line<sup>[1)](#footnotes)</sup> `if` and `for` parts in comprehensions.
5. Multi-line<sup>[1)](#footnotes)</sup> keyword arguments or argument defaults.
6. String concatenation over several lines in lists and tuples .


Exception type 1:
Expand Down Expand Up @@ -289,6 +290,37 @@ def foo(bar=(a is b)):
...
```

Exception type 6:

```python
# GOOD
[
"a",
(
"b"
"c"
),
"d",
]

# This helps to avoid forgetting a comma at the end of a string spanning
# multiple lines. Compare with:
[
"a",
"b"
"c"
"d",
]
# Was the comma after "b" forgotten or was the string supposed to be "bc"?

# BAD
[
(
"a" "b"
),
]
```

### Footnotes:
1. Multi-line means that either
* the expression spans multiple lines, e.g.,
Expand Down
73 changes: 52 additions & 21 deletions src/flake8_picky_parentheses/_redundant_parentheses.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,9 @@ def _get_exceptions_from_ast(cls, sorted_parens_coords, tree, tokens):
rewrite_buffer = None
for _, parens_coord in enumerate(sorted_parens_coords):
node, pos, end, parents = nodes[nodes_idx]
while not cls._node_in_parens(parens_coord, pos, end):
while not cls._node_in_parens(
parens_coord, node, pos, end, tokens
):
nodes_idx += 1
if nodes_idx >= len(nodes):
return
Expand All @@ -302,68 +304,92 @@ def _get_exceptions_from_ast(cls, sorted_parens_coords, tree, tokens):
):
rewrite_buffer = ProblemRewrite(parens_coord.open_, None)
last_exception_node = node
elif (
continue
if (
parents
and isinstance(parents[0], special_ops_pair_exceptions)
and isinstance(node, special_ops_pair_exceptions)
):
rewrite_buffer = ProblemRewrite(parens_coord.open_, None)
last_exception_node = node
elif (
continue
if (
parents
and isinstance(parents[0], ast.Starred)
and isinstance(node, special_ops_pair_exceptions)
):
rewrite_buffer = ProblemRewrite(parens_coord.open_, None)
last_exception_node = node
elif (
continue
if (
parents
and isinstance(parents[0], ast.keyword)
and parents[0].arg is None
and isinstance(node, special_ops_pair_exceptions)
):
rewrite_buffer = ProblemRewrite(parens_coord.open_, None)
last_exception_node = node
elif isinstance(node, ast.Tuple):
if (
parents
and isinstance(parents[0], ast.Assign)
and node in parents[0].targets
):
rewrite_buffer = ProblemRewrite(
parens_coord.open_,
"PAR002: Dont use parentheses for unpacking"
)
last_exception_node = node
elif (
continue
if (
isinstance(node, ast.Tuple)
and parents
and isinstance(parents[0], ast.Assign)
and node in parents[0].targets
):
rewrite_buffer = ProblemRewrite(
parens_coord.open_,
"PAR002: Dont use parentheses for unpacking"
)
last_exception_node = node
continue
if (
parents
and isinstance(parents[0], ast.comprehension)
and (node in parents[0].ifs or node == parents[0].iter)
and (parens_coord.open_[0] != pos[0] or pos[0] != end[0])
):
rewrite_buffer = ProblemRewrite(parens_coord.open_, None)
last_exception_node = node
elif (
continue
if (
parents
and isinstance(parents[0], ast.keyword)
and (parens_coord.open_[0] != pos[0] or pos[0] != end[0])
):
rewrite_buffer = ProblemRewrite(parens_coord.open_, None)
last_exception_node = node
elif (
continue
if (
parents
and isinstance(parents[0], ast.arguments)
and node in parents[0].defaults
and (parens_coord.open_[0] != pos[0] or pos[0] != end[0])
):
rewrite_buffer = ProblemRewrite(parens_coord.open_, None)
last_exception_node = node
elif (
continue
if (
sys.version_info >= (3, 10)
and isinstance(node, ast.MatchSequence)
):
rewrite_buffer = ProblemRewrite(parens_coord.open_, None)
last_exception_node = node
continue
if (
parents
and isinstance(parents[0], (ast.Tuple, ast.List))
and isinstance(node, ast.Str)
):
tokens_slice = slice(parens_coord.token_indexes[0] + 1,
parens_coord.token_indexes[1])
string_tokens = [
token for token in tokens[tokens_slice]
if token.type == tokenize.STRING
]
if string_tokens[0].start[0] != string_tokens[-1].start[0]:
rewrite_buffer = ProblemRewrite(parens_coord.open_, None)
last_exception_node = node
continue

if rewrite_buffer is not None:
yield rewrite_buffer
Expand Down Expand Up @@ -469,8 +495,13 @@ def _get_exceptions_for_neighboring_parens(sorted_optional_parens_coords,
yield ProblemRewrite(coords2.open_, None)

@staticmethod
def _node_in_parens(parens_coord, pos, end):
def _node_in_parens(parens_coord, node, pos, end, tokens):
open_, _, _, close, _ = parens_coord
close = close[0], close[1] + 1
# close[1] + 1 to allow the closing parenthesis to be part of the node
if (
isinstance(node, ast.Tuple)
and sys.version_info < (3, 8)
):
# Python 3.7 does not include the redundant parentheses of tuples
return open_ < pos <= end < close
return open_ <= pos <= end <= close
89 changes: 89 additions & 0 deletions tests/test_redundant_parentheses.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ def test_unpacking(plugin, ws1, ws2, ws3, ws4):
assert lint_codes(plugin(s), ["PAR002"])


# BAD (don't use parentheses for unpacking)
def test_simple_unpacking(plugin):
s = """(a,) = ["a"]"""
assert lint_codes(plugin(s), ["PAR002"])
Expand Down Expand Up @@ -412,6 +413,94 @@ def test_one_line_expression_2(plugin):
assert lint_codes(plugin(s), ["PAR001"])


# BAD (single line strings in list/tuple)
# https://github.com/robsdedude/flake8-picky-parentheses/issues/32
@pytest.mark.parametrize("value", (
'[("a")]',
'[("a"),]',
'[("a" "b")]',
'[("a" "b"),]',
'[("a"), "b"]',
'["a", ("b")]',
'[("a"), "b",]',
'["a", ("b"),]',
'[("a"), "b"\n"c"]',
'["a"\n"c", ("b")]',
'[("a"), "b"\n"c",]',
'["a"\n"c", ("b"),]',
'[("a"\n), "b"]',
'["a", ("b"\n)]',
'[(\n"a"), "b",]',
'["a", (\n"b"),]',
'[("a"\n# comment\n), "b"]',
'["a", ("b"\n# comment\n)]',
'[(\n# comment\n"a"), "b",]',
'["a", (\n# comment\n"b"),]',
'(("a"),)',
'(("a" "b"),)',
'(("a"), "b")',
'("a", ("b"))',
'(("a"), "b",)',
'("a", ("b"),)',
'(("a"), "b"\n"c")',
'("a"\n"c", ("b"))',
'(("a"), "b"\n"c",)',
'("a"\n"c", ("b"),)',
'(("a"\n), "b")',
'("a", ("b"\n))',
'((\n"a"), "b",)',
'("a", (\n"b"),)',
'(("a"\n# comment\n), "b")',
'("a", ("b"\n# comment\n))',
'((\n# comment\n"a"), "b",)',
'("a", (\n# comment\n"b"),)',
))
@pytest.mark.parametrize("quote", ("'", '"'))
def test_single_line_strings(plugin, value, quote):
value = value.replace('"', quote)
s = f"a = {value}\n"
assert lint_codes(plugin(s), ["PAR001"])


# GOOD (multi-line strings in list/tuple)
# 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),)',
))
@pytest.mark.parametrize("quote", ("'", '"')[1:])
def test_grouped_single_line_strings(plugin, value, quote):
value = value.replace('"', quote)
s = f"a = {value}\n"
assert no_lint(plugin(s))


# GOOD (function call)
def test_function_call(plugin):
s = """foo("a")
Expand Down

0 comments on commit 393f93e

Please # to comment.