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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve call chain #4153

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
- Address a missing case in the change to allow empty lines at the beginning of all
blocks, except immediately before a docstring (#4130)
- For stubs, fix logic to enforce empty line after nested classes with bodies (#4141)
- When a line contains more than one method call, it is treated as a call chain, and the
fluent style is prioritized for formatting this line (#4153)

### Configuration

Expand Down
5 changes: 3 additions & 2 deletions src/black/brackets.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,9 @@ def is_split_before_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Pr
if (
leaf.type == token.DOT
and leaf.parent
and leaf.parent.type not in {syms.import_from, syms.dotted_name}
and (previous is None or previous.type in CLOSING_BRACKETS)
and leaf.parent.type == syms.trailer
and previous
and previous.type in CLOSING_BRACKETS
):
return DOT_PRIORITY

Expand Down
5 changes: 2 additions & 3 deletions src/black/comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,9 +399,8 @@ def _contains_fmt_skip_comment(comment_line: str, mode: Mode) -> bool:
],
*[
_COMMENT_PREFIX + comment.strip()
for comment in comment_line.strip(_COMMENT_PREFIX).split(
_COMMENT_LIST_SEPARATOR
)
for comment in comment_line.strip(_COMMENT_PREFIX)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels worse than the previous formatting.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also don't like the new one, I personaly like is:

                for comment in (
                    comment_line.strip(_COMMENT_PREFIX).split(_COMMENT_LIST_SEPARATOR)
                )

anyway, I will revert this change.

.split(_COMMENT_LIST_SEPARATOR)
],
]
if Preview.single_line_format_skip_with_multiple_comments in mode
Expand Down
10 changes: 7 additions & 3 deletions src/black/linegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,7 @@ def _maybe_split_omitting_optional_parens(
and not line.is_import
# and we can actually remove the parens
and can_omit_invisible_parens(rhs, mode.line_length)
and not (Preview.improve_call_chain in mode and rhs.body.is_call_chain)
):
omit = {id(rhs.closing_bracket), *omit}
try:
Expand Down Expand Up @@ -1180,9 +1181,12 @@ def delimiter_split(
except ValueError:
raise CannotSplit("No delimiters found") from None

if delimiter_priority == DOT_PRIORITY:
if bt.delimiter_count_with_priority(delimiter_priority) == 1:
raise CannotSplit("Splitting a single attribute from its owner looks wrong")
if (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to keep this one as is, see the comment on the PR.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I'm not quite sure which COMMENT you're referring to?

not (Preview.improve_call_chain in mode and line.is_call_chain)
and delimiter_priority == DOT_PRIORITY
and bt.delimiter_count_with_priority(delimiter_priority) == 1
):
raise CannotSplit("Splitting a single attribute from its owner looks wrong")

current_line = Line(
mode=line.mode, depth=line.depth, inside_brackets=line.inside_brackets
Expand Down
28 changes: 28 additions & 0 deletions src/black/lines.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,34 @@ def is_chained_assignment(self) -> bool:
"""Is the line a chained assignment"""
return [leaf.type for leaf in self.leaves].count(token.EQUAL) > 1

@property
def is_call_chain(self) -> bool:
"""Is the line a call chain"""
if self.comments:
return False
line_node = self.leaves[0].parent
if not line_node:
return False
depth = 2 if line_node.type == syms.old_comp_for else 1

def get_call_count(node: Node, depth: int) -> int:
count = 0
for child in node.children:
if isinstance(child, Node):
if (
child.type == syms.trailer
and child.children[0].type == token.DOT
and child.next_sibling
and child.next_sibling.type == syms.trailer
and child.next_sibling.children[0].type in OPENING_BRACKETS
):
count += 1
elif depth - 1 > 0:
count += get_call_count(child, depth - 1)
return count

return get_call_count(line_node, depth) > 1

@property
def opens_block(self) -> bool:
"""Does this line open a new level of indentation."""
Expand Down
1 change: 1 addition & 0 deletions src/black/mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ class Preview(Enum):
allow_form_feeds = auto()
unify_docstring_detection = auto()
respect_east_asian_width = auto()
improve_call_chain = auto()


class Deprecated(UserWarning):
Expand Down
27 changes: 27 additions & 0 deletions tests/data/cases/preview_call_chain.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# flags: --preview
a = str(my_very_very_very_long_url_name.with_user(and_very_long_username).with_password(and_very_long_password))
a = my_very_very_very_long_url_name.with_user(and_very_long_username).with_password(and_very_long_password)


def foo():
completion_time = (a.read_namespaced_job(job.metadata.name, namespace="default").status().completion_time())


# output

a = str(
my_very_very_very_long_url_name.with_user(and_very_long_username)
.with_password(and_very_long_password)
)
a = (
my_very_very_very_long_url_name.with_user(and_very_long_username)
.with_password(and_very_long_password)
)


def foo():
completion_time = (
a.read_namespaced_job(job.metadata.name, namespace="default")
.status()
.completion_time()
)
17 changes: 10 additions & 7 deletions tests/data/cases/preview_context_managers_39.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,16 @@ async def func():
pass


with xxxxxxxx.some_kind_of_method(
some_argument=[
"first",
"second",
"third",
]
).another_method() as cmd:
with (
xxxxxxxx.some_kind_of_method(
some_argument=[
"first",
"second",
"third",
]
)
.another_method() as cmd
):
pass


Expand Down