Consistently wrap two context managers in parens (in --preview). (#3589)

Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
This commit is contained in:
Yilei "Dolee" Yang 2023-03-09 22:01:20 -08:00 committed by GitHub
parent 4a063a9f8d
commit d16a1dbd05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 127 additions and 22 deletions

View File

@ -16,6 +16,8 @@
- Add trailing commas to collection literals even if there's a comment after the last
entry (#3393)
- `with` statements that contain two context managers will be consistently wrapped in
parentheses (#3589)
### Configuration

View File

@ -2,7 +2,7 @@
Generating lines of code.
"""
import sys
from dataclasses import dataclass, replace
from dataclasses import replace
from enum import Enum, auto
from functools import partial, wraps
from typing import Collection, Iterator, List, Optional, Set, Union, cast
@ -16,6 +16,7 @@
from black.comments import FMT_OFF, generate_comments, list_comments
from black.lines import (
Line,
RHSResult,
append_leaves,
can_be_split,
can_omit_invisible_parens,
@ -647,17 +648,6 @@ def left_hand_split(
yield result
@dataclass
class _RHSResult:
"""Intermediate split result from a right hand split."""
head: Line
body: Line
tail: Line
opening_bracket: Leaf
closing_bracket: Leaf
def right_hand_split(
line: Line,
mode: Mode,
@ -681,7 +671,7 @@ def right_hand_split(
def _first_right_hand_split(
line: Line,
omit: Collection[LeafID] = (),
) -> _RHSResult:
) -> RHSResult:
"""Split the line into head, body, tail starting with the last bracket pair.
Note: this function should not have side effects. It's relied upon by
@ -723,11 +713,11 @@ def _first_right_hand_split(
tail_leaves, line, opening_bracket, component=_BracketSplitComponent.tail
)
bracket_split_succeeded_or_raise(head, body, tail)
return _RHSResult(head, body, tail, opening_bracket, closing_bracket)
return RHSResult(head, body, tail, opening_bracket, closing_bracket)
def _maybe_split_omitting_optional_parens(
rhs: _RHSResult,
rhs: RHSResult,
line: Line,
mode: Mode,
features: Collection[Feature] = (),
@ -747,11 +737,11 @@ def _maybe_split_omitting_optional_parens(
# there are no standalone comments in the body
and not rhs.body.contains_standalone_comments(0)
# and we can actually remove the parens
and can_omit_invisible_parens(rhs.body, mode.line_length)
and can_omit_invisible_parens(rhs, mode.line_length)
):
omit = {id(rhs.closing_bracket), *omit}
try:
# The _RHSResult Omitting Optional Parens.
# The RHSResult Omitting Optional Parens.
rhs_oop = _first_right_hand_split(line, omit=omit)
if not (
Preview.prefer_splitting_right_hand_side_of_assignments in line.mode
@ -803,7 +793,7 @@ def _maybe_split_omitting_optional_parens(
yield result
def _prefer_split_rhs_oop(rhs_oop: _RHSResult, mode: Mode) -> bool:
def _prefer_split_rhs_oop(rhs_oop: RHSResult, mode: Mode) -> bool:
"""
Returns whether we should prefer the result from a split omitting optional parens.
"""

View File

@ -15,7 +15,7 @@
cast,
)
from black.brackets import DOT_PRIORITY, BracketTracker
from black.brackets import COMMA_PRIORITY, DOT_PRIORITY, BracketTracker
from black.mode import Mode, Preview
from black.nodes import (
BRACKETS,
@ -28,6 +28,7 @@
is_multiline_string,
is_one_sequence_between,
is_type_comment,
is_with_stmt,
replace_child,
syms,
whitespace,
@ -122,6 +123,11 @@ def is_import(self) -> bool:
"""Is this an import line?"""
return bool(self) and is_import(self.leaves[0])
@property
def is_with_stmt(self) -> bool:
"""Is this a with_stmt line?"""
return bool(self) and is_with_stmt(self.leaves[0])
@property
def is_class(self) -> bool:
"""Is this line a class definition?"""
@ -449,6 +455,17 @@ def __bool__(self) -> bool:
return bool(self.leaves or self.comments)
@dataclass
class RHSResult:
"""Intermediate split result from a right hand split."""
head: Line
body: Line
tail: Line
opening_bracket: Leaf
closing_bracket: Leaf
@dataclass
class LinesBlock:
"""Class that holds information about a block of formatted lines.
@ -830,25 +847,42 @@ def can_be_split(line: Line) -> bool:
def can_omit_invisible_parens(
line: Line,
rhs: RHSResult,
line_length: int,
) -> bool:
"""Does `line` have a shape safe to reformat without optional parens around it?
"""Does `rhs.body` have a shape safe to reformat without optional parens around it?
Returns True for only a subset of potentially nice looking formattings but
the point is to not return false positives that end up producing lines that
are too long.
"""
line = rhs.body
bt = line.bracket_tracker
if not bt.delimiters:
# Without delimiters the optional parentheses are useless.
return True
max_priority = bt.max_delimiter_priority()
if bt.delimiter_count_with_priority(max_priority) > 1:
delimiter_count = bt.delimiter_count_with_priority(max_priority)
if delimiter_count > 1:
# With more than one delimiter of a kind the optional parentheses read better.
return False
if delimiter_count == 1:
if (
Preview.wrap_multiple_context_managers_in_parens in line.mode
and max_priority == COMMA_PRIORITY
and rhs.head.is_with_stmt
):
# For two context manager with statements, the optional parentheses read
# better. In this case, `rhs.body` is the context managers part of
# the with statement. `rhs.head` is the `with (` part on the previous
# line.
return False
# Otherwise it may also read better, but we don't do it today and requires
# careful considerations for all possible cases. See
# https://github.com/psf/black/issues/2156.
if max_priority == DOT_PRIORITY:
# A single stranded method call doesn't require optional parentheses.
return True

View File

@ -789,6 +789,16 @@ def is_import(leaf: Leaf) -> bool:
)
def is_with_stmt(leaf: Leaf) -> bool:
"""Return True if the given leaf starts a with statement."""
return bool(
leaf.type == token.NAME
and leaf.value == "with"
and leaf.parent
and leaf.parent.type == syms.with_stmt
)
def is_type_comment(leaf: Leaf, suffix: str = "") -> bool:
"""Return True if the given leaf is a special comment.
Only returns true for type comments for now."""

View File

@ -16,6 +16,14 @@
pass
with mock.patch.object(
self.my_runner, "first_method", autospec=True
) as mock_run_adb, mock.patch.object(
self.my_runner, "second_method", autospec=True, return_value="foo"
):
pass
# output
# This file doesn't use any Python 3.9+ only grammars.
@ -28,3 +36,11 @@
with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4:
pass
with mock.patch.object(
self.my_runner, "first_method", autospec=True
) as mock_run_adb, mock.patch.object(
self.my_runner, "second_method", autospec=True, return_value="foo"
):
pass

View File

@ -23,6 +23,14 @@
pass
with mock.patch.object(
self.my_runner, "first_method", autospec=True
) as mock_run_adb, mock.patch.object(
self.my_runner, "second_method", autospec=True, return_value="foo"
):
pass
# output
@ -36,3 +44,11 @@
with new_new_new1() as cm1, new_new_new2():
pass
with mock.patch.object(
self.my_runner, "first_method", autospec=True
) as mock_run_adb, mock.patch.object(
self.my_runner, "second_method", autospec=True, return_value="foo"
):
pass

View File

@ -49,6 +49,24 @@
pass
with mock.patch.object(
self.my_runner, "first_method", autospec=True
) as mock_run_adb, mock.patch.object(
self.my_runner, "second_method", autospec=True, return_value="foo"
):
pass
with xxxxxxxx.some_kind_of_method(
some_argument=[
"first",
"second",
"third",
]
).another_method() as cmd:
pass
# output
@ -102,3 +120,22 @@
) as cm2,
):
pass
with (
mock.patch.object(self.my_runner, "first_method", autospec=True) as mock_run_adb,
mock.patch.object(
self.my_runner, "second_method", autospec=True, return_value="foo"
),
):
pass
with xxxxxxxx.some_kind_of_method(
some_argument=[
"first",
"second",
"third",
]
).another_method() as cmd:
pass