Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 8c5d96f

Browse files
authored
fix indentation of line breaks in long type hints by adding parens (psf#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
1 parent e974fc3 commit 8c5d96f

5 files changed

Lines changed: 220 additions & 3 deletions

File tree

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212

1313
### Preview style
1414

15+
- Long type hints are now wrapped in parentheses and properly indented when split across
16+
multiple lines (#3899)
17+
1518
<!-- Changes that affect Black's preview style -->
1619

1720
### Configuration

src/black/linegen.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,24 @@ def visit_factor(self, node: Node) -> Iterator[Line]:
397397
node.insert_child(index, Node(syms.atom, [lpar, operand, rpar]))
398398
yield from self.visit_default(node)
399399

400+
def visit_tname(self, node: Node) -> Iterator[Line]:
401+
"""
402+
Add potential parentheses around types in function parameter lists to be made
403+
into real parentheses in case the type hint is too long to fit on a line
404+
Examples:
405+
def foo(a: int, b: float = 7): ...
406+
407+
->
408+
409+
def foo(a: (int), b: (float) = 7): ...
410+
"""
411+
if Preview.parenthesize_long_type_hints in self.mode:
412+
assert len(node.children) == 3
413+
if maybe_make_parens_invisible_in_atom(node.children[2], parent=node):
414+
wrap_in_parentheses(node, node.children[2], visible=False)
415+
416+
yield from self.visit_default(node)
417+
400418
def visit_STRING(self, leaf: Leaf) -> Iterator[Line]:
401419
if Preview.hex_codes_in_unicode_sequences in self.mode:
402420
normalize_unicode_escape_sequences(leaf)
@@ -498,7 +516,14 @@ def __post_init__(self) -> None:
498516
self.visit_except_clause = partial(v, keywords={"except"}, parens={"except"})
499517
self.visit_with_stmt = partial(v, keywords={"with"}, parens={"with"})
500518
self.visit_classdef = partial(v, keywords={"class"}, parens=Ø)
501-
self.visit_expr_stmt = partial(v, keywords=Ø, parens=ASSIGNMENTS)
519+
520+
# When this is moved out of preview, add ":" directly to ASSIGNMENTS in nodes.py
521+
if Preview.parenthesize_long_type_hints in self.mode:
522+
assignments = ASSIGNMENTS | {":"}
523+
else:
524+
assignments = ASSIGNMENTS
525+
self.visit_expr_stmt = partial(v, keywords=Ø, parens=assignments)
526+
502527
self.visit_return_stmt = partial(v, keywords={"return"}, parens={"return"})
503528
self.visit_import_from = partial(v, keywords=Ø, parens={"import"})
504529
self.visit_del_stmt = partial(v, keywords=Ø, parens={"del"})
@@ -1368,7 +1393,7 @@ def maybe_make_parens_invisible_in_atom(
13681393
Returns whether the node should itself be wrapped in invisible parentheses.
13691394
"""
13701395
if (
1371-
node.type != syms.atom
1396+
node.type not in (syms.atom, syms.expr)
13721397
or is_empty_tuple(node)
13731398
or is_one_tuple(node)
13741399
or (is_yield(node) and parent.type != syms.expr_stmt)
@@ -1392,6 +1417,7 @@ def maybe_make_parens_invisible_in_atom(
13921417
syms.except_clause,
13931418
syms.funcdef,
13941419
syms.with_stmt,
1420+
syms.tname,
13951421
# these ones aren't useful to end users, but they do please fuzzers
13961422
syms.for_stmt,
13971423
syms.del_stmt,

src/black/mode.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ class Preview(Enum):
180180
# for https://github.com/psf/black/issues/3117 to be fixed.
181181
string_processing = auto()
182182
parenthesize_conditional_expressions = auto()
183+
parenthesize_long_type_hints = auto()
183184
skip_magic_trailing_comma_in_subscript = auto()
184185
wrap_long_dict_values_in_parens = auto()
185186
wrap_multiple_context_managers_in_parens = auto()

tests/data/preview/long_strings__type_annotations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,6 @@ def func(
5454

5555

5656
def func(
57-
argument: ("int |" "str"),
57+
argument: "int |" "str",
5858
) -> Set["int |" " str"]:
5959
pass
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# This has always worked
2+
z= Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong
3+
4+
# "AnnAssign"s now also work
5+
z: Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong
6+
z: (Short
7+
| Short2
8+
| Short3
9+
| Short4)
10+
z: (int)
11+
z: ((int))
12+
13+
14+
z: Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong = 7
15+
z: (Short
16+
| Short2
17+
| Short3
18+
| Short4) = 8
19+
z: (int) = 2.3
20+
z: ((int)) = foo()
21+
22+
# In case I go for not enforcing parantheses, this might get improved at the same time
23+
x = (
24+
z
25+
== 9999999999999999999999999999999999999999
26+
| 9999999999999999999999999999999999999999
27+
| 9999999999999999999999999999999999999999
28+
| 9999999999999999999999999999999999999999,
29+
y
30+
== 9999999999999999999999999999999999999999
31+
+ 9999999999999999999999999999999999999999
32+
+ 9999999999999999999999999999999999999999
33+
+ 9999999999999999999999999999999999999999,
34+
)
35+
36+
x = (
37+
z == (9999999999999999999999999999999999999999
38+
| 9999999999999999999999999999999999999999
39+
| 9999999999999999999999999999999999999999
40+
| 9999999999999999999999999999999999999999),
41+
y == (9999999999999999999999999999999999999999
42+
+ 9999999999999999999999999999999999999999
43+
+ 9999999999999999999999999999999999999999
44+
+ 9999999999999999999999999999999999999999),
45+
)
46+
47+
# handle formatting of "tname"s in parameter list
48+
49+
# remove unnecessary paren
50+
def foo(i: (int)) -> None: ...
51+
52+
53+
# 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.
54+
def foo(i: (int,)) -> None: ...
55+
56+
def foo(
57+
i: int,
58+
x: Loooooooooooooooooooooooong
59+
| Looooooooooooooooong
60+
| Looooooooooooooooooooong
61+
| Looooooong,
62+
*,
63+
s: str,
64+
) -> None:
65+
pass
66+
67+
68+
@app.get("/path/")
69+
async def foo(
70+
q: str
71+
| None = Query(None, title="Some long title", description="Some long description")
72+
):
73+
pass
74+
75+
76+
def f(
77+
max_jobs: int
78+
| None = Option(
79+
None, help="Maximum number of jobs to launch. And some additional text."
80+
),
81+
another_option: bool = False
82+
):
83+
...
84+
85+
86+
# output
87+
# This has always worked
88+
z = (
89+
Loooooooooooooooooooooooong
90+
| Loooooooooooooooooooooooong
91+
| Loooooooooooooooooooooooong
92+
| Loooooooooooooooooooooooong
93+
)
94+
95+
# "AnnAssign"s now also work
96+
z: (
97+
Loooooooooooooooooooooooong
98+
| Loooooooooooooooooooooooong
99+
| Loooooooooooooooooooooooong
100+
| Loooooooooooooooooooooooong
101+
)
102+
z: Short | Short2 | Short3 | Short4
103+
z: int
104+
z: int
105+
106+
107+
z: (
108+
Loooooooooooooooooooooooong
109+
| Loooooooooooooooooooooooong
110+
| Loooooooooooooooooooooooong
111+
| Loooooooooooooooooooooooong
112+
) = 7
113+
z: Short | Short2 | Short3 | Short4 = 8
114+
z: int = 2.3
115+
z: int = foo()
116+
117+
# In case I go for not enforcing parantheses, this might get improved at the same time
118+
x = (
119+
z
120+
== 9999999999999999999999999999999999999999
121+
| 9999999999999999999999999999999999999999
122+
| 9999999999999999999999999999999999999999
123+
| 9999999999999999999999999999999999999999,
124+
y
125+
== 9999999999999999999999999999999999999999
126+
+ 9999999999999999999999999999999999999999
127+
+ 9999999999999999999999999999999999999999
128+
+ 9999999999999999999999999999999999999999,
129+
)
130+
131+
x = (
132+
z
133+
== (
134+
9999999999999999999999999999999999999999
135+
| 9999999999999999999999999999999999999999
136+
| 9999999999999999999999999999999999999999
137+
| 9999999999999999999999999999999999999999
138+
),
139+
y
140+
== (
141+
9999999999999999999999999999999999999999
142+
+ 9999999999999999999999999999999999999999
143+
+ 9999999999999999999999999999999999999999
144+
+ 9999999999999999999999999999999999999999
145+
),
146+
)
147+
148+
# handle formatting of "tname"s in parameter list
149+
150+
151+
# remove unnecessary paren
152+
def foo(i: int) -> None: ...
153+
154+
155+
# 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.
156+
def foo(i: (int,)) -> None: ...
157+
158+
159+
def foo(
160+
i: int,
161+
x: (
162+
Loooooooooooooooooooooooong
163+
| Looooooooooooooooong
164+
| Looooooooooooooooooooong
165+
| Looooooong
166+
),
167+
*,
168+
s: str,
169+
) -> None:
170+
pass
171+
172+
173+
@app.get("/path/")
174+
async def foo(
175+
q: str | None = Query(
176+
None, title="Some long title", description="Some long description"
177+
)
178+
):
179+
pass
180+
181+
182+
def f(
183+
max_jobs: int | None = Option(
184+
None, help="Maximum number of jobs to launch. And some additional text."
185+
),
186+
another_option: bool = False,
187+
): ...

0 commit comments

Comments
 (0)