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

First draft for 2024 stable style #4064

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
36 changes: 36 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,42 @@ Together with this release, we'll put out an alpha release 24.1a1 showcasing the
This release (23.12.0) will still produce the 2023 style. Most but not all of the
changes in `--preview` mode will be in the 2024 stable style.

A draft for the 2024 stable style (#4064), stabilizing the following changes:

- Multiline strings passed as the sole function arguments are formatted more compactly
(#1879)
- Dummy class and function implementations consisting only of `...` are formatted more
compactly (#3796)
- If an assignment statement is too long, we now prefer splitting on the right-hand side
(#3368)
- Hex codes in Unicode escape sequences are now standardized to lowercase (#2916)
- Allow empty first lines at the beginning of most blocks (#3967, #4061)
- Add parentheses around long type annotations (#3899)
- Standardize on a single newline after module docstrings (#3932)
- Fix incorrect magic trailing comma handling in return types (#3916)
- Remove blank lines before class docstrings (#3692)
- Wrap multiple context managers in parentheses if combined in a single `with` statement
(#3489)
- Fix bug in line length calculations for power operations (#3942)
- Add trailing commas to collection literals even if there's a comment after the last
entry (#3393)
- When using `--skip-magic-trailing-comma` or `-C`, trailing commas are stripped from
subscript expressions with more than 1 element (#3209)
- Add extra blank lines in stubs in a few cases (#3564, #3862)
- Accept raw strings as docstrings (#3947)
- Split long lines in case blocks (#4024)
- Stop removing spaces from walrus operators within subscripts (#3823)
- Fix incorrect formatting of certain async statements (#3609)
- Allow combining `# fmt: skip` with other comments (#3959)

The following two changes may be included, but have outstanding issues that will need to
be resolved:

- Long values in dict literals are now wrapped in parentheses; correspondingly
unnecessary parentheses around short values in dict literals are now removed; long
string lambda values are now wrapped in parentheses (#3440)
- Add parentheses around `if`-`else` expressions (#2278)

### Stable style

- Fix bug where `# fmt: off` automatically dedents when used with the `--line-ranges`
Expand Down
32 changes: 14 additions & 18 deletions src/black/comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from functools import lru_cache
from typing import Collection, Final, Iterator, List, Optional, Tuple, Union

from black.mode import Mode, Preview
from black.mode import Mode
from black.nodes import (
CLOSING_BRACKETS,
STANDALONE_COMMENT,
Expand Down Expand Up @@ -391,22 +391,18 @@ def _contains_fmt_skip_comment(comment_line: str, mode: Mode) -> bool:
# noqa:XXX # fmt:skip # a nice line <-- multiple comments (Preview)
# pylint:XXX; fmt:skip <-- list of comments (; separated, Preview)
"""
semantic_comment_blocks = (
[
comment_line,
*[
_COMMENT_PREFIX + comment.strip()
for comment in comment_line.split(_COMMENT_PREFIX)[1:]
],
*[
_COMMENT_PREFIX + comment.strip()
for comment in comment_line.strip(_COMMENT_PREFIX).split(
_COMMENT_LIST_SEPARATOR
)
],
]
if Preview.single_line_format_skip_with_multiple_comments in mode
else [comment_line]
)
semantic_comment_blocks = [
comment_line,
*[
_COMMENT_PREFIX + comment.strip()
for comment in comment_line.split(_COMMENT_PREFIX)[1:]
],
*[
_COMMENT_PREFIX + comment.strip()
for comment in comment_line.strip(_COMMENT_PREFIX).split(
_COMMENT_LIST_SEPARATOR
)
],
]

return any(comment in FMT_SKIP for comment in semantic_comment_blocks)
112 changes: 34 additions & 78 deletions src/black/linegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,8 @@ def line(self, indent: int = 0) -> Iterator[Line]:
self.current_line.depth += indent
return # Line is empty, don't emit. Creating a new one unnecessary.

if (
Preview.improved_async_statements_handling in self.mode
and len(self.current_line.leaves) == 1
and is_async_stmt_or_funcdef(self.current_line.leaves[0])
if len(self.current_line.leaves) == 1 and is_async_stmt_or_funcdef(
self.current_line.leaves[0]
):
# Special case for async def/for/with statements. `visit_async_stmt`
# adds an `ASYNC` leaf then visits the child def/for/with statement
Expand Down Expand Up @@ -164,16 +162,15 @@ def visit_default(self, node: LN) -> Iterator[Line]:
def visit_test(self, node: Node) -> Iterator[Line]:
"""Visit an `x if y else z` test"""

if Preview.parenthesize_conditional_expressions in self.mode:
already_parenthesized = (
node.prev_sibling and node.prev_sibling.type == token.LPAR
)
already_parenthesized = (
node.prev_sibling and node.prev_sibling.type == token.LPAR
)

if not already_parenthesized:
lpar = Leaf(token.LPAR, "")
rpar = Leaf(token.RPAR, "")
node.insert_child(0, lpar)
node.append_child(rpar)
if not already_parenthesized:
lpar = Leaf(token.LPAR, "")
rpar = Leaf(token.RPAR, "")
node.insert_child(0, lpar)
node.append_child(rpar)

yield from self.visit_default(node)

Expand Down Expand Up @@ -233,20 +230,19 @@ def visit_paramspec(self, node: Node) -> Iterator[Line]:
node.children[1].prefix = ""

def visit_dictsetmaker(self, node: Node) -> Iterator[Line]:
if Preview.wrap_long_dict_values_in_parens in self.mode:
for i, child in enumerate(node.children):
if i == 0:
continue
if node.children[i - 1].type == token.COLON:
if child.type == syms.atom and child.children[0].type == token.LPAR:
if maybe_make_parens_invisible_in_atom(
child,
parent=node,
remove_brackets_around_comma=False,
):
wrap_in_parentheses(node, child, visible=False)
else:
for i, child in enumerate(node.children):
if i == 0:
continue
if node.children[i - 1].type == token.COLON:
if child.type == syms.atom and child.children[0].type == token.LPAR:
if maybe_make_parens_invisible_in_atom(
child,
parent=node,
remove_brackets_around_comma=False,
):
wrap_in_parentheses(node, child, visible=False)
else:
wrap_in_parentheses(node, child, visible=False)
yield from self.visit_default(node)

def visit_funcdef(self, node: Node) -> Iterator[Line]:
Expand Down Expand Up @@ -285,9 +281,7 @@ def visit_match_case(self, node: Node) -> Iterator[Line]:

def visit_suite(self, node: Node) -> Iterator[Line]:
"""Visit a suite."""
if (
self.mode.is_pyi or Preview.dummy_implementations in self.mode
) and is_stub_suite(node, self.mode):
if is_stub_suite(node):
yield from self.visit(node.children[2])
else:
yield from self.visit_default(node)
Expand All @@ -301,23 +295,15 @@ def visit_simple_stmt(self, node: Node) -> Iterator[Line]:
prev_type = child.type

if node.parent and node.parent.type in STATEMENT:
if Preview.dummy_implementations in self.mode:
condition = is_function_or_class(node.parent)
else:
condition = self.mode.is_pyi
if condition and is_stub_body(node):
if is_stub_body(node) and is_function_or_class(node.parent):
yield from self.visit_default(node)
else:
yield from self.line(+1)
yield from self.visit_default(node)
yield from self.line(-1)

else:
if (
not (self.mode.is_pyi or Preview.dummy_implementations in self.mode)
or not node.parent
or not is_stub_suite(node.parent, self.mode)
):
if not node.parent or not is_stub_suite(node.parent):
yield from self.line()
yield from self.visit_default(node)

Expand All @@ -335,11 +321,7 @@ def visit_async_stmt(self, node: Node) -> Iterator[Line]:
break

internal_stmt = next(children)
if Preview.improved_async_statements_handling in self.mode:
yield from self.visit(internal_stmt)
else:
for child in internal_stmt.children:
yield from self.visit(child)
yield from self.visit(internal_stmt)

def visit_decorators(self, node: Node) -> Iterator[Line]:
"""Visit decorators."""
Expand Down Expand Up @@ -413,10 +395,9 @@ def foo(a: int, b: float = 7): ...

def foo(a: (int), b: (float) = 7): ...
"""
if Preview.parenthesize_long_type_hints in self.mode:
assert len(node.children) == 3
if maybe_make_parens_invisible_in_atom(node.children[2], parent=node):
wrap_in_parentheses(node, node.children[2], visible=False)
assert len(node.children) == 3
if maybe_make_parens_invisible_in_atom(node.children[2], parent=node):
wrap_in_parentheses(node, node.children[2], visible=False)

yield from self.visit_default(node)

Expand Down Expand Up @@ -522,13 +503,7 @@ def __post_init__(self) -> None:
self.visit_with_stmt = partial(v, keywords={"with"}, parens={"with"})
self.visit_classdef = partial(v, keywords={"class"}, parens=Ø)

# When this is moved out of preview, add ":" directly to ASSIGNMENTS in nodes.py
if Preview.parenthesize_long_type_hints in self.mode:
assignments = ASSIGNMENTS | {":"}
else:
assignments = ASSIGNMENTS
self.visit_expr_stmt = partial(v, keywords=Ø, parens=assignments)

self.visit_expr_stmt = partial(v, keywords=Ø, parens=ASSIGNMENTS)
self.visit_return_stmt = partial(v, keywords={"return"}, parens={"return"})
self.visit_import_from = partial(v, keywords=Ø, parens={"import"})
self.visit_del_stmt = partial(v, keywords=Ø, parens={"del"})
Expand Down Expand Up @@ -569,9 +544,7 @@ def transform_line(
# We need the line string when power operators are hugging to determine if we should
# split the line. Default to line_str, if no power operator are present on the line.
line_str_hugging_power_ops = (
(_hugging_power_ops_line_to_string(line, features, mode) or line_str)
if Preview.fix_power_op_line_length in mode
else line_str
_hugging_power_ops_line_to_string(line, features, mode) or line_str
)

ll = mode.line_length
Expand Down Expand Up @@ -681,9 +654,6 @@ def should_split_funcdef_with_rhs(line: Line, mode: Mode) -> bool:
"""If a funcdef has a magic trailing comma in the return type, then we should first
split the line with rhs to respect the comma.
"""
if Preview.respect_magic_trailing_comma_in_return_type not in mode:
return False

return_type_leaves: List[Leaf] = []
in_return_type = False

Expand Down Expand Up @@ -912,9 +882,6 @@ def _maybe_split_omitting_optional_parens(
try:
# The RHSResult Omitting Optional Parens.
rhs_oop = _first_right_hand_split(line, omit=omit)
prefer_splitting_rhs_mode = (
Preview.prefer_splitting_right_hand_side_of_assignments in line.mode
)
is_split_right_after_equal = (
len(rhs.head.leaves) >= 2 and rhs.head.leaves[-2].type == token.EQUAL
)
Expand All @@ -930,8 +897,7 @@ def _maybe_split_omitting_optional_parens(
)
if (
not (
prefer_splitting_rhs_mode
and is_split_right_after_equal
is_split_right_after_equal
and rhs_head_contains_brackets
and rhs_head_short_enough
and rhs_head_explode_blocked_by_magic_trailing_comma
Expand Down Expand Up @@ -1217,11 +1183,7 @@ def append_to_line(leaf: Leaf) -> Iterator[Line]:
trailing_comma_safe and Feature.TRAILING_COMMA_IN_CALL in features
)

if (
Preview.add_trailing_comma_consistently in mode
and last_leaf.type == STANDALONE_COMMENT
and leaf_idx == last_non_comment_leaf
):
if last_leaf.type == STANDALONE_COMMENT and leaf_idx == last_non_comment_leaf:
current_line = _safe_add_trailing_comma(
trailing_comma_safe, delimiter_priority, current_line
)
Expand Down Expand Up @@ -1308,11 +1270,7 @@ def normalize_invisible_parens( # noqa: C901

# Fixes a bug where invisible parens are not properly wrapped around
# case blocks.
if (
isinstance(child, Node)
and child.type == syms.case_block
and Preview.long_case_block_line_splitting in mode
):
if isinstance(child, Node) and child.type == syms.case_block:
normalize_invisible_parens(
child, parens_after={"case"}, mode=mode, features=features
)
Expand Down Expand Up @@ -1367,7 +1325,6 @@ def normalize_invisible_parens( # noqa: C901
and child.next_sibling is not None
and child.next_sibling.type == token.COLON
and child.value == "case"
and Preview.long_case_block_line_splitting in mode
):
# A special patch for "case case:" scenario, the second occurrence
# of case will be not parsed as a Python keyword.
Expand Down Expand Up @@ -1441,7 +1398,6 @@ def _maybe_wrap_cms_in_parens(
"""
if (
Feature.PARENTHESIZED_CONTEXT_MANAGERS not in features
or Preview.wrap_multiple_context_managers_in_parens not in mode
or len(node.children) <= 2
# If it's an atom, it's already wrapped in parens.
or node.children[1].type == syms.atom
Expand Down