Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove unnecessary parentheses from with statements #2926

Merged
merged 24 commits into from Apr 3, 2022
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1a499b2
Remove unnecessary parentheses from WITH statements
jpy-git Mar 15, 2022
1a2c13b
Add unit tests
jpy-git Mar 15, 2022
27a4bfd
Move functionality to preview mode
jpy-git Mar 15, 2022
ddc1a3f
Merge branch 'main' into with_parens
jpy-git Mar 16, 2022
240a6f8
Make logic more DRY
jpy-git Mar 16, 2022
cd0695d
Use or instead of and
jpy-git Mar 16, 2022
0e0d9f8
Make unit test py>=3.9
jpy-git Mar 17, 2022
3c14ab2
re-add preview style comment in CHANGES.md
jpy-git Mar 18, 2022
254fc91
Merge branch 'main' into with_parens
jpy-git Mar 21, 2022
d5db697
Update for latest master
jpy-git Mar 21, 2022
87aa74d
make redundant parens enum entry consistent
jpy-git Mar 21, 2022
aaa902a
add brackets for clarity
jpy-git Mar 23, 2022
9b62d0e
Merge branch 'main' into with_parens
JelleZijlstra Mar 24, 2022
68bb6d8
Merge branch 'main' into with_parens
JelleZijlstra Mar 24, 2022
3f8db3d
Fix condition after merge
jpy-git Mar 24, 2022
ac698f8
One-pass fix for removing brackets in for/with
jpy-git Mar 25, 2022
6532485
Merge branch 'main' into with_parens
JelleZijlstra Mar 26, 2022
e61783a
fix parent in as_expr logic
jpy-git Mar 26, 2022
639a2e4
Merge branch 'main' into with_parens
JelleZijlstra Mar 26, 2022
afcb8be
Merge branch 'main' into with_parens
JelleZijlstra Mar 30, 2022
1540bfa
Add deeply nested with bracket examples
jpy-git Apr 2, 2022
0f0a01a
Add deeply nested for bracket examples
jpy-git Apr 2, 2022
bd7373c
Merge branch 'main' into with_parens
jpy-git Apr 2, 2022
99167c2
Moar code comments 'cause ASTs are no joke
ichard26 Apr 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Expand Up @@ -15,6 +15,7 @@
<!-- Changes that affect Black's preview style -->

- Code cell separators `#%%` are now standardised to `# %%` (#2919)
- Remove unnecessary parentheses from `with` statements (#2926)
- Avoid magic-trailing-comma in single-element subscripts (#2942)

### _Blackd_
Expand Down
47 changes: 41 additions & 6 deletions src/black/linegen.py
Expand Up @@ -319,7 +319,10 @@ def __post_init__(self) -> None:
v, keywords={"try", "except", "else", "finally"}, parens=脴
)
self.visit_except_clause = partial(v, keywords={"except"}, parens=脴)
self.visit_with_stmt = partial(v, keywords={"with"}, parens=脴)
if self.mode.preview:
self.visit_with_stmt = partial(v, keywords={"with"}, parens={"with"})
else:
self.visit_with_stmt = partial(v, keywords={"with"}, parens=脴)
self.visit_funcdef = partial(v, keywords={"def"}, parens=脴)
self.visit_classdef = partial(v, keywords={"class"}, parens=脴)
self.visit_expr_stmt = partial(v, keywords=脴, parens=ASSIGNMENTS)
Expand Down Expand Up @@ -841,8 +844,26 @@ def normalize_invisible_parens(

if check_lpar:
if child.type == syms.atom:
if maybe_make_parens_invisible_in_atom(child, parent=node):
if maybe_make_parens_invisible_in_atom(
child,
parent=node,
preview=preview,
):
wrap_in_parentheses(node, child, visible=False)
elif (
preview
and isinstance(child, Node)
and child.type == syms.asexpr_test
and not any(leaf.type == token.COLONEQUAL for leaf in child.leaves())
):
# make parentheses invisible,
# unless the asexpr contains an assignment expression.
if maybe_make_parens_invisible_in_atom(
child.children[0],
parent=child,
preview=preview,
):
wrap_in_parentheses(child, child.children[0], visible=False)
elif is_one_tuple(child):
wrap_in_parentheses(node, child, visible=True)
elif node.type == syms.import_from:
Expand All @@ -862,24 +883,34 @@ def normalize_invisible_parens(
elif not (isinstance(child, Leaf) and is_multiline_string(child)):
wrap_in_parentheses(node, child, visible=False)

check_lpar = isinstance(child, Leaf) and child.value in parens_after
comma_check = child.type == token.COMMA if preview else False

check_lpar = isinstance(child, Leaf) and (
child.value in parens_after or comma_check
)


def maybe_make_parens_invisible_in_atom(node: LN, parent: LN) -> bool:
def maybe_make_parens_invisible_in_atom(
node: LN,
parent: LN,
preview: bool = False,
) -> bool:
"""If it's safe, make the parens in the atom `node` invisible, recursively.
Additionally, remove repeated, adjacent invisible parens from the atom `node`
as they are redundant.

Returns whether the node should itself be wrapped in invisible parentheses.

`preview` enables the preview feature for removing redundant parentheses.
"""
with_stmt_check = parent.type != syms.with_stmt if preview else True

if (
node.type != syms.atom
or is_empty_tuple(node)
or is_one_tuple(node)
or (is_yield(node) and parent.type != syms.expr_stmt)
or max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY
or (max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY and with_stmt_check)
):
return False

Expand All @@ -902,7 +933,11 @@ def maybe_make_parens_invisible_in_atom(node: LN, parent: LN) -> bool:
# make parentheses invisible
first.value = ""
last.value = ""
maybe_make_parens_invisible_in_atom(middle, parent=parent)
maybe_make_parens_invisible_in_atom(
middle,
parent=parent,
preview=preview,
)

if is_atom_with_invisible_parens(middle):
# Strip the invisible parens from `middle` by replacing
Expand Down
1 change: 1 addition & 0 deletions src/black/mode.py
Expand Up @@ -127,6 +127,7 @@ class Preview(Enum):
"""Individual preview style features."""

string_processing = auto()
remove_redundant_parens = auto()
one_element_subscript = auto()


Expand Down
79 changes: 79 additions & 0 deletions tests/data/remove_with_brackets.py
@@ -0,0 +1,79 @@
with (open("bla.txt")):
JelleZijlstra marked this conversation as resolved.
Show resolved Hide resolved
pass

with (open("bla.txt")), (open("bla.txt")):
pass

with (open("bla.txt") as f):
pass

# Remove brackets within alias expression
with (open("bla.txt")) as f:
pass

# Remove brackets around one-line context managers
with (open("bla.txt") as f, (open("x"))):
pass

with ((open("bla.txt")) as f, open("x")):
pass

with (CtxManager1() as example1, CtxManager2() as example2):
...

# Brackets remain when using magic comma
with (CtxManager1() as example1, CtxManager2() as example2,):
...

# Brackets remain for multi-line context managers
with (CtxManager1() as example1, CtxManager2() as example2, CtxManager2() as example2, CtxManager2() as example2, CtxManager2() as example2):
...

# Don't touch assignment expressions
with (y := open("./test.py")) as f:
pass

# output
with open("bla.txt"):
pass

with open("bla.txt"), open("bla.txt"):
pass

with open("bla.txt") as f:
pass

# Remove brackets within alias expression
with open("bla.txt") as f:
pass

# Remove brackets around one-line context managers
with open("bla.txt") as f, open("x"):
pass

with open("bla.txt") as f, open("x"):
pass

with CtxManager1() as example1, CtxManager2() as example2:
...

# Brackets remain when using magic comma
with (
CtxManager1() as example1,
CtxManager2() as example2,
):
...

# Brackets remain for multi-line context managers
with (
CtxManager1() as example1,
CtxManager2() as example2,
CtxManager2() as example2,
CtxManager2() as example2,
CtxManager2() as example2,
):
...

# Don't touch assignment expressions
with (y := open("./test.py")) as f:
pass
10 changes: 10 additions & 0 deletions tests/test_format.py
Expand Up @@ -190,6 +190,16 @@ def test_pep_570() -> None:
assert_format(source, expected, minimum_version=(3, 8))


def test_remove_with_brackets() -> None:
source, expected = read_data("remove_with_brackets")
assert_format(
source,
expected,
black.Mode(preview=True),
minimum_version=(3, 9),
)


@pytest.mark.parametrize("filename", PY310_CASES)
def test_python_310(filename: str) -> None:
source, expected = read_data(filename)
Expand Down