Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
- Improve `multiline_string_handling` with ternaries and dictionaries (#4657)
- Fix a bug where `string_processing` would not split f-strings directly after
expressions (#4680)
- Remove parentheses around multiple exception types in `except` and `except*` without
`as`. (#4720)

### Configuration

Expand Down
2 changes: 2 additions & 0 deletions docs/the_black_code_style/future_style.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ Currently, the following features are included in the preview style:
- `fix_fmt_skip_in_one_liners`: Fix `# fmt: skip` behaviour on one-liner declarations,
such as `def foo(): return "mock" # fmt: skip`, where previously the declaration
would have been incorrectly collapsed.
- `remove_parens_around_except_types`: Remove parentheses around multiple exception
types in `except` and `except*` without `as`. See PEP 758 for details.

(labels/unstable-features)=

Expand Down
42 changes: 32 additions & 10 deletions src/black/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1225,17 +1225,20 @@ def _format_str_once(
future_imports = get_future_imports(src_node)
versions = detect_target_versions(src_node, future_imports=future_imports)

context_manager_features = {
line_generation_features = {
feature
for feature in {Feature.PARENTHESIZED_CONTEXT_MANAGERS}
for feature in {
Feature.PARENTHESIZED_CONTEXT_MANAGERS,
Feature.UNPARENTHESIZED_EXCEPT_TYPES,
}
if supports_feature(versions, feature)
}
normalize_fmt_off(src_node, mode, lines)
if lines:
# This should be called after normalize_fmt_off.
convert_unchanged_lines(src_node, lines)

line_generator = LineGenerator(mode=mode, features=context_manager_features)
line_generator = LineGenerator(mode=mode, features=line_generation_features)
elt = EmptyLineTracker(mode=mode)
split_line_features = {
feature
Expand Down Expand Up @@ -1395,13 +1398,6 @@ def get_features_used( # noqa: C901
elif n.type == syms.match_stmt:
features.add(Feature.PATTERN_MATCHING)

elif (
n.type == syms.except_clause
and len(n.children) >= 2
and n.children[1].type == token.STAR
):
features.add(Feature.EXCEPT_STAR)

elif n.type in {syms.subscriptlist, syms.trailer} and any(
child.type == syms.star_expr for child in n.children
):
Expand All @@ -1423,6 +1419,32 @@ def get_features_used( # noqa: C901
):
features.add(Feature.TYPE_PARAM_DEFAULTS)

elif (
n.type == syms.except_clause
and len(n.children) >= 2
and (
n.children[1].type == token.STAR or n.children[1].type == syms.testlist
)
):
is_star_except = n.children[1].type == token.STAR

if is_star_except:
features.add(Feature.EXCEPT_STAR)

# Presence of except* pushes as clause 1 index back
has_as_clause = (
len(n.children) >= is_star_except + 3
and n.children[is_star_except + 2].type == token.NAME
and n.children[is_star_except + 2].value == "as" # type: ignore
)

# If there's no 'as' clause and the except expression is a testlist.
if not has_as_clause and (
(is_star_except and n.children[2].type == syms.testlist)
or (not is_star_except and n.children[1].type == syms.testlist)
):
features.add(Feature.UNPARENTHESIZED_EXCEPT_TYPES)

return features


Expand Down
63 changes: 53 additions & 10 deletions src/black/linegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,8 @@ def visit_dictsetmaker(self, node: Node) -> Iterator[Line]:
maybe_make_parens_invisible_in_atom(
child,
parent=node,
mode=self.mode,
features=self.features,
remove_brackets_around_comma=False,
)
else:
Expand All @@ -270,6 +272,8 @@ def visit_funcdef(self, node: Node) -> Iterator[Line]:
if maybe_make_parens_invisible_in_atom(
child,
parent=node,
mode=self.mode,
features=self.features,
remove_brackets_around_comma=False,
):
wrap_in_parentheses(node, child, visible=False)
Expand Down Expand Up @@ -363,7 +367,7 @@ def visit_power(self, node: Node) -> Iterator[Line]:
):
wrap_in_parentheses(node, leaf)

remove_await_parens(node)
remove_await_parens(node, mode=self.mode, features=self.features)

yield from self.visit_default(node)

Expand Down Expand Up @@ -410,7 +414,9 @@ def foo(a: int, b: float = 7): ...
def foo(a: (int), b: (float) = 7): ...
"""
assert len(node.children) == 3
if maybe_make_parens_invisible_in_atom(node.children[2], parent=node):
if maybe_make_parens_invisible_in_atom(
node.children[2], parent=node, mode=self.mode, features=self.features
):
wrap_in_parentheses(node, node.children[2], visible=False)

yield from self.visit_default(node)
Expand Down Expand Up @@ -516,7 +522,12 @@ def visit_atom(self, node: Node) -> Iterator[Line]:
first.type == token.LBRACE and last.type == token.RBRACE
):
# Lists or sets of one item
maybe_make_parens_invisible_in_atom(node.children[1], parent=node)
maybe_make_parens_invisible_in_atom(
node.children[1],
parent=node,
mode=self.mode,
features=self.features,
)

yield from self.visit_default(node)

Expand Down Expand Up @@ -1448,15 +1459,16 @@ def normalize_invisible_parens( # noqa: C901
if maybe_make_parens_invisible_in_atom(
child,
parent=node,
mode=mode,
features=features,
remove_brackets_around_comma=True,
):
wrap_in_parentheses(node, child, visible=False)
elif isinstance(child, Node) and node.type == syms.with_stmt:
remove_with_parens(child, node)
remove_with_parens(child, node, mode=mode, features=features)
elif child.type == syms.atom:
if maybe_make_parens_invisible_in_atom(
child,
parent=node,
child, parent=node, mode=mode, features=features
):
wrap_in_parentheses(node, child, visible=False)
elif is_one_tuple(child):
Expand Down Expand Up @@ -1508,7 +1520,7 @@ def _normalize_import_from(parent: Node, child: LN, index: int) -> None:
parent.append_child(Leaf(token.RPAR, ""))


def remove_await_parens(node: Node) -> None:
def remove_await_parens(node: Node, mode: Mode, features: Collection[Feature]) -> None:
if node.children[0].type == token.AWAIT and len(node.children) > 1:
if (
node.children[1].type == syms.atom
Expand All @@ -1517,6 +1529,8 @@ def remove_await_parens(node: Node) -> None:
if maybe_make_parens_invisible_in_atom(
node.children[1],
parent=node,
mode=mode,
features=features,
remove_brackets_around_comma=True,
):
wrap_in_parentheses(node, node.children[1], visible=False)
Expand Down Expand Up @@ -1585,7 +1599,9 @@ def _maybe_wrap_cms_in_parens(
node.insert_child(1, new_child)


def remove_with_parens(node: Node, parent: Node) -> None:
def remove_with_parens(
node: Node, parent: Node, mode: Mode, features: Collection[Feature]
) -> None:
"""Recursively hide optional parens in `with` statements."""
# Removing all unnecessary parentheses in with statements in one pass is a tad
# complex as different variations of bracketed statements result in pretty
Expand All @@ -1607,21 +1623,25 @@ def remove_with_parens(node: Node, parent: Node) -> None:
if maybe_make_parens_invisible_in_atom(
node,
parent=parent,
mode=mode,
features=features,
remove_brackets_around_comma=True,
):
wrap_in_parentheses(parent, node, visible=False)
if isinstance(node.children[1], Node):
remove_with_parens(node.children[1], node)
remove_with_parens(node.children[1], node, mode=mode, features=features)
elif node.type == syms.testlist_gexp:
for child in node.children:
if isinstance(child, Node):
remove_with_parens(child, node)
remove_with_parens(child, node, mode=mode, features=features)
elif node.type == syms.asexpr_test and not any(
leaf.type == token.COLONEQUAL for leaf in node.leaves()
):
if maybe_make_parens_invisible_in_atom(
node.children[0],
parent=node,
mode=mode,
features=features,
remove_brackets_around_comma=True,
):
wrap_in_parentheses(node, node.children[0], visible=False)
Expand All @@ -1630,6 +1650,8 @@ def remove_with_parens(node: Node, parent: Node) -> None:
def maybe_make_parens_invisible_in_atom(
node: LN,
parent: LN,
mode: Mode,
features: Collection[Feature],
remove_brackets_around_comma: bool = False,
) -> bool:
"""If it's safe, make the parens in the atom `node` invisible, recursively.
Expand All @@ -1655,6 +1677,25 @@ def maybe_make_parens_invisible_in_atom(
# and option to skip this check for `for` and `with` statements.
not remove_brackets_around_comma
and max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY
# Skip this check in Preview mode in order to
# Remove parentheses around multiple exception types in except and
# except* without as. See PEP 758 for details.
and not (
Preview.remove_parens_around_except_types in mode
and Feature.UNPARENTHESIZED_EXCEPT_TYPES in features
# is a tuple
and is_tuple(node)
# has a parent node
and node.parent is not None
# parent is an except clause
and node.parent.type == syms.except_clause
# is not immediately followed by as clause
and not (
node.next_sibling is not None
and is_name_token(node.next_sibling)
and node.next_sibling.value == "as"
)
)
)
or is_tuple_containing_walrus(node)
or is_tuple_containing_star(node)
Expand Down Expand Up @@ -1695,6 +1736,8 @@ def maybe_make_parens_invisible_in_atom(
maybe_make_parens_invisible_in_atom(
middle,
parent=parent,
mode=mode,
features=features,
remove_brackets_around_comma=remove_brackets_around_comma,
)

Expand Down
30 changes: 30 additions & 0 deletions src/black/mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class TargetVersion(Enum):
PY311 = 11
PY312 = 12
PY313 = 13
PY314 = 14

def pretty(self) -> str:
assert self.name[:2] == "PY"
Expand Down Expand Up @@ -53,6 +54,7 @@ class Feature(Enum):
TYPE_PARAMS = 18
FSTRING_PARSING = 19
TYPE_PARAM_DEFAULTS = 20
UNPARENTHESIZED_EXCEPT_TYPES = 21
FORCE_OPTIONAL_PARENTHESES = 50

# __future__ flags
Expand Down Expand Up @@ -186,10 +188,35 @@ class Feature(Enum):
Feature.FSTRING_PARSING,
Feature.TYPE_PARAM_DEFAULTS,
},
TargetVersion.PY314: {
Feature.F_STRINGS,
Feature.DEBUG_F_STRINGS,
Feature.NUMERIC_UNDERSCORES,
Feature.TRAILING_COMMA_IN_CALL,
Feature.TRAILING_COMMA_IN_DEF,
Feature.ASYNC_KEYWORDS,
Feature.FUTURE_ANNOTATIONS,
Feature.ASSIGNMENT_EXPRESSIONS,
Feature.RELAXED_DECORATORS,
Feature.POS_ONLY_ARGUMENTS,
Feature.UNPACKING_ON_FLOW,
Feature.ANN_ASSIGN_EXTENDED_RHS,
Feature.PARENTHESIZED_CONTEXT_MANAGERS,
Feature.PATTERN_MATCHING,
Feature.EXCEPT_STAR,
Feature.VARIADIC_GENERICS,
Feature.TYPE_PARAMS,
Feature.FSTRING_PARSING,
Feature.TYPE_PARAM_DEFAULTS,
Feature.UNPARENTHESIZED_EXCEPT_TYPES,
},
}


def supports_feature(target_versions: set[TargetVersion], feature: Feature) -> bool:
if not target_versions:
raise ValueError("target_versions must not be empty")

return all(feature in VERSION_TO_FEATURES[version] for version in target_versions)


Expand All @@ -204,6 +231,9 @@ class Preview(Enum):
multiline_string_handling = auto()
always_one_newline_after_import = auto()
fix_fmt_skip_in_one_liners = auto()
# Remove parentheses around multiple exception types in except and
# except* without as. See PEP 758 for details.
remove_parens_around_except_types = auto()


UNSTABLE_FEATURES: set[Preview] = {
Expand Down
6 changes: 4 additions & 2 deletions src/black/resources/black.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"py310",
"py311",
"py312",
"py313"
"py313",
"py314"
]
},
"description": "Python versions that should be supported by Black's output. You should include all versions that your code supports. By default, Black will infer target versions from the project metadata in pyproject.toml. If this does not yield conclusive results, Black will use per-file auto-detection."
Expand Down Expand Up @@ -84,7 +85,8 @@
"wrap_long_dict_values_in_parens",
"multiline_string_handling",
"always_one_newline_after_import",
"fix_fmt_skip_in_one_liners"
"fix_fmt_skip_in_one_liners",
"remove_parens_around_except_types"
]
},
"description": "Enable specific features included in the `--unstable` style. Requires `--preview`. No compatibility guarantees are provided on the behavior or existence of any unstable features."
Expand Down
2 changes: 1 addition & 1 deletion src/blib2to3/Grammar.txt
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ try_stmt: ('try' ':' suite
with_stmt: 'with' asexpr_test (',' asexpr_test)* ':' suite

# NB compile.c makes sure that the default except clause is last
except_clause: 'except' ['*'] [test [(',' | 'as') test]]
except_clause: 'except' ['*'] [testlist ['as' test]]
suite: simple_stmt | NEWLINE INDENT stmt+ DEDENT

# Backward compatibility cruft to support:
Expand Down
Loading
Loading