Skip to content

Commit

Permalink
fix indentation of line breaks in long type hints by adding parens (#…
Browse files Browse the repository at this point in the history
…3899)

* fix indentation of line breaks in long type hints by adding parentheses, and remove unnecessary parentheses

* add entry in CHANGES.md, make the style change only in preview mode
  • Loading branch information
jakkdl authored Sep 22, 2023
1 parent e974fc3 commit 8c5d96f
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 3 deletions.
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@

### Preview style

- Long type hints are now wrapped in parentheses and properly indented when split across
multiple lines (#3899)

<!-- Changes that affect Black's preview style -->

### Configuration
Expand Down
30 changes: 28 additions & 2 deletions src/black/linegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,24 @@ def visit_factor(self, node: Node) -> Iterator[Line]:
node.insert_child(index, Node(syms.atom, [lpar, operand, rpar]))
yield from self.visit_default(node)

def visit_tname(self, node: Node) -> Iterator[Line]:
"""
Add potential parentheses around types in function parameter lists to be made
into real parentheses in case the type hint is too long to fit on a line
Examples:
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)

yield from self.visit_default(node)

def visit_STRING(self, leaf: Leaf) -> Iterator[Line]:
if Preview.hex_codes_in_unicode_sequences in self.mode:
normalize_unicode_escape_sequences(leaf)
Expand Down Expand Up @@ -498,7 +516,14 @@ def __post_init__(self) -> None:
self.visit_except_clause = partial(v, keywords={"except"}, parens={"except"})
self.visit_with_stmt = partial(v, keywords={"with"}, parens={"with"})
self.visit_classdef = partial(v, keywords={"class"}, parens=Ø)
self.visit_expr_stmt = partial(v, keywords=Ø, parens=ASSIGNMENTS)

# 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_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 @@ -1368,7 +1393,7 @@ def maybe_make_parens_invisible_in_atom(
Returns whether the node should itself be wrapped in invisible parentheses.
"""
if (
node.type != syms.atom
node.type not in (syms.atom, syms.expr)
or is_empty_tuple(node)
or is_one_tuple(node)
or (is_yield(node) and parent.type != syms.expr_stmt)
Expand All @@ -1392,6 +1417,7 @@ def maybe_make_parens_invisible_in_atom(
syms.except_clause,
syms.funcdef,
syms.with_stmt,
syms.tname,
# these ones aren't useful to end users, but they do please fuzzers
syms.for_stmt,
syms.del_stmt,
Expand Down
1 change: 1 addition & 0 deletions src/black/mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ class Preview(Enum):
# for https://github.com/psf/black/issues/3117 to be fixed.
string_processing = auto()
parenthesize_conditional_expressions = auto()
parenthesize_long_type_hints = auto()
skip_magic_trailing_comma_in_subscript = auto()
wrap_long_dict_values_in_parens = auto()
wrap_multiple_context_managers_in_parens = auto()
Expand Down
2 changes: 1 addition & 1 deletion tests/data/preview/long_strings__type_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,6 @@ def func(


def func(
argument: ("int |" "str"),
argument: "int |" "str",
) -> Set["int |" " str"]:
pass
187 changes: 187 additions & 0 deletions tests/data/preview_py_310/pep604_union_types_line_breaks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
# This has always worked
z= Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong

# "AnnAssign"s now also work
z: Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong
z: (Short
| Short2
| Short3
| Short4)
z: (int)
z: ((int))


z: Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong = 7
z: (Short
| Short2
| Short3
| Short4) = 8
z: (int) = 2.3
z: ((int)) = foo()

# In case I go for not enforcing parantheses, this might get improved at the same time
x = (
z
== 9999999999999999999999999999999999999999
| 9999999999999999999999999999999999999999
| 9999999999999999999999999999999999999999
| 9999999999999999999999999999999999999999,
y
== 9999999999999999999999999999999999999999
+ 9999999999999999999999999999999999999999
+ 9999999999999999999999999999999999999999
+ 9999999999999999999999999999999999999999,
)

x = (
z == (9999999999999999999999999999999999999999
| 9999999999999999999999999999999999999999
| 9999999999999999999999999999999999999999
| 9999999999999999999999999999999999999999),
y == (9999999999999999999999999999999999999999
+ 9999999999999999999999999999999999999999
+ 9999999999999999999999999999999999999999
+ 9999999999999999999999999999999999999999),
)

# handle formatting of "tname"s in parameter list

# remove unnecessary paren
def foo(i: (int)) -> None: ...


# this is a syntax error in the type annotation according to mypy, but it's not invalid *python* code, so make sure we don't mess with it and make it so.
def foo(i: (int,)) -> None: ...

def foo(
i: int,
x: Loooooooooooooooooooooooong
| Looooooooooooooooong
| Looooooooooooooooooooong
| Looooooong,
*,
s: str,
) -> None:
pass


@app.get("/path/")
async def foo(
q: str
| None = Query(None, title="Some long title", description="Some long description")
):
pass


def f(
max_jobs: int
| None = Option(
None, help="Maximum number of jobs to launch. And some additional text."
),
another_option: bool = False
):
...


# output
# This has always worked
z = (
Loooooooooooooooooooooooong
| Loooooooooooooooooooooooong
| Loooooooooooooooooooooooong
| Loooooooooooooooooooooooong
)

# "AnnAssign"s now also work
z: (
Loooooooooooooooooooooooong
| Loooooooooooooooooooooooong
| Loooooooooooooooooooooooong
| Loooooooooooooooooooooooong
)
z: Short | Short2 | Short3 | Short4
z: int
z: int


z: (
Loooooooooooooooooooooooong
| Loooooooooooooooooooooooong
| Loooooooooooooooooooooooong
| Loooooooooooooooooooooooong
) = 7
z: Short | Short2 | Short3 | Short4 = 8
z: int = 2.3
z: int = foo()

# In case I go for not enforcing parantheses, this might get improved at the same time
x = (
z
== 9999999999999999999999999999999999999999
| 9999999999999999999999999999999999999999
| 9999999999999999999999999999999999999999
| 9999999999999999999999999999999999999999,
y
== 9999999999999999999999999999999999999999
+ 9999999999999999999999999999999999999999
+ 9999999999999999999999999999999999999999
+ 9999999999999999999999999999999999999999,
)

x = (
z
== (
9999999999999999999999999999999999999999
| 9999999999999999999999999999999999999999
| 9999999999999999999999999999999999999999
| 9999999999999999999999999999999999999999
),
y
== (
9999999999999999999999999999999999999999
+ 9999999999999999999999999999999999999999
+ 9999999999999999999999999999999999999999
+ 9999999999999999999999999999999999999999
),
)

# handle formatting of "tname"s in parameter list


# remove unnecessary paren
def foo(i: int) -> None: ...


# this is a syntax error in the type annotation according to mypy, but it's not invalid *python* code, so make sure we don't mess with it and make it so.
def foo(i: (int,)) -> None: ...


def foo(
i: int,
x: (
Loooooooooooooooooooooooong
| Looooooooooooooooong
| Looooooooooooooooooooong
| Looooooong
),
*,
s: str,
) -> None:
pass


@app.get("/path/")
async def foo(
q: str | None = Query(
None, title="Some long title", description="Some long description"
)
):
pass


def f(
max_jobs: int | None = Option(
None, help="Maximum number of jobs to launch. And some additional text."
),
another_option: bool = False,
): ...

0 comments on commit 8c5d96f

Please sign in to comment.