Skip to content

Commit a969e00

Browse files
Standardize type comments to form # type: (value) (psf#2097)
Co-authored-by: Pedro Mezacasa Muller <[email protected]>
1 parent 950ec38 commit a969e00

File tree

9 files changed

+127
-41
lines changed

9 files changed

+127
-41
lines changed

CHANGES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
### Preview style
2020

2121
<!-- Changes that affect Black's preview style -->
22-
22+
- Standardize type comments to form `# type: <value>` (#4645)
2323
- Fix a bug where one-liner functions/conditionals marked with `# fmt: skip`
2424
would still be formatted (#4552)
2525

docs/the_black_code_style/future_style.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ Currently, the following features are included in the preview style:
2929
- `fix_fmt_skip_in_one_liners`: Fix `# fmt: skip` behaviour on one-liner declarations,
3030
such as `def foo(): return "mock" # fmt: skip`, where previously the declaration
3131
would have been incorrectly collapsed.
32+
- `standardize_type_comments`: Format type comments which have zero or more spaces between `#` and `type:` or between `type:` and value
33+
to `# type: (value)`
3234

3335
(labels/unstable-features)=
3436

src/black/comments.py

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
first_leaf_of,
1414
make_simple_prefix,
1515
preceding_leaf,
16+
is_type_comment_string,
1617
syms,
1718
)
1819
from blib2to3.pgen2 import token
@@ -50,7 +51,7 @@ class ProtoComment:
5051
leading_whitespace: str # leading whitespace before the comment, if any
5152

5253

53-
def generate_comments(leaf: LN) -> Iterator[Leaf]:
54+
def generate_comments(leaf: LN, mode: Mode) -> Iterator[Leaf]:
5455
"""Clean the prefix of the `leaf` and generate comments from it, if any.
5556
5657
Comments in lib2to3 are shoved into the whitespace prefix. This happens
@@ -70,15 +71,17 @@ def generate_comments(leaf: LN) -> Iterator[Leaf]:
7071
are emitted with a fake STANDALONE_COMMENT token identifier.
7172
"""
7273
total_consumed = 0
73-
for pc in list_comments(leaf.prefix, is_endmarker=leaf.type == token.ENDMARKER):
74+
for pc in list_comments(
75+
leaf.prefix, is_endmarker=leaf.type == token.ENDMARKER, mode=mode
76+
):
7477
total_consumed = pc.consumed
7578
prefix = make_simple_prefix(pc.newlines, pc.form_feed)
7679
yield Leaf(pc.type, pc.value, prefix=prefix)
7780
normalize_trailing_prefix(leaf, total_consumed)
7881

7982

8083
@lru_cache(maxsize=4096)
81-
def list_comments(prefix: str, *, is_endmarker: bool) -> list[ProtoComment]:
84+
def list_comments(prefix: str, *, is_endmarker: bool, mode: Mode) -> list[ProtoComment]:
8285
"""Return a list of :class:`ProtoComment` objects parsed from the given `prefix`."""
8386
result: list[ProtoComment] = []
8487
if not prefix or "#" not in prefix:
@@ -109,7 +112,7 @@ def list_comments(prefix: str, *, is_endmarker: bool) -> list[ProtoComment]:
109112
comment_type = token.COMMENT # simple trailing comment
110113
else:
111114
comment_type = STANDALONE_COMMENT
112-
comment = make_comment(line)
115+
comment = make_comment(line, mode=mode)
113116
result.append(
114117
ProtoComment(
115118
type=comment_type,
@@ -140,7 +143,7 @@ def normalize_trailing_prefix(leaf: LN, total_consumed: int) -> None:
140143
leaf.prefix = ""
141144

142145

143-
def make_comment(content: str) -> str:
146+
def make_comment(content: str, mode: Mode) -> str:
144147
"""Return a consistently formatted comment from the given `content` string.
145148
146149
All comments (except for "##", "#!", "#:", '#'") should have a single
@@ -158,9 +161,18 @@ def make_comment(content: str) -> str:
158161
if (
159162
content
160163
and content[0] == NON_BREAKING_SPACE
161-
and not content.lstrip().startswith("type:")
164+
and not is_type_comment_string("# " + content.lstrip(), mode=mode)
162165
):
163166
content = " " + content[1:] # Replace NBSP by a simple space
167+
if (
168+
Preview.standardize_type_comments in mode
169+
and content
170+
and NON_BREAKING_SPACE not in content
171+
and is_type_comment_string("# " + content.lstrip(), mode=mode)
172+
):
173+
type_part, value_part = content.strip().split(":", 1)
174+
content = type_part.strip() + ": " + value_part.strip()
175+
164176
if content and content[0] not in COMMENT_EXCEPTIONS:
165177
content = " " + content
166178
return "#" + content
@@ -184,7 +196,7 @@ def convert_one_fmt_off_pair(
184196
"""
185197
for leaf in node.leaves():
186198
previous_consumed = 0
187-
for comment in list_comments(leaf.prefix, is_endmarker=False):
199+
for comment in list_comments(leaf.prefix, is_endmarker=False, mode=mode):
188200
is_fmt_off = comment.value in FMT_OFF
189201
is_fmt_skip = _contains_fmt_skip_comment(comment.value, mode)
190202
if (not is_fmt_off and not is_fmt_skip) or (
@@ -274,13 +286,13 @@ def generate_ignored_nodes(
274286
return
275287
container: Optional[LN] = container_of(leaf)
276288
while container is not None and container.type != token.ENDMARKER:
277-
if is_fmt_on(container):
289+
if is_fmt_on(container, mode=mode):
278290
return
279291

280292
# fix for fmt: on in children
281-
if children_contains_fmt_on(container):
293+
if children_contains_fmt_on(container, mode=mode):
282294
for index, child in enumerate(container.children):
283-
if isinstance(child, Leaf) and is_fmt_on(child):
295+
if isinstance(child, Leaf) and is_fmt_on(child, mode=mode):
284296
if child.type in CLOSING_BRACKETS:
285297
# This means `# fmt: on` is placed at a different bracket level
286298
# than `# fmt: off`. This is an invalid use, but as a courtesy,
@@ -291,12 +303,14 @@ def generate_ignored_nodes(
291303
if (
292304
child.type == token.INDENT
293305
and index < len(container.children) - 1
294-
and children_contains_fmt_on(container.children[index + 1])
306+
and children_contains_fmt_on(
307+
container.children[index + 1], mode=mode
308+
)
295309
):
296310
# This means `# fmt: on` is placed right after an indentation
297311
# level, and we shouldn't swallow the previous INDENT token.
298312
return
299-
if children_contains_fmt_on(child):
313+
if children_contains_fmt_on(child, mode=mode):
300314
return
301315
yield child
302316
else:
@@ -317,7 +331,7 @@ def _generate_ignored_nodes_from_fmt_skip(
317331
ignored_nodes: list[LN] = []
318332
# Need to properly format the leaf prefix to compare it to comment.value,
319333
# which is also formatted
320-
comments = list_comments(leaf.prefix, is_endmarker=False)
334+
comments = list_comments(leaf.prefix, is_endmarker=False, mode=mode)
321335
if not comments or comment.value != comments[0].value:
322336
return
323337
if prev_sibling is not None:
@@ -393,24 +407,24 @@ def _generate_ignored_nodes_from_fmt_skip(
393407
yield from iter(ignored_nodes)
394408

395409

396-
def is_fmt_on(container: LN) -> bool:
410+
def is_fmt_on(container: LN, mode: Mode) -> bool:
397411
"""Determine whether formatting is switched on within a container.
398412
Determined by whether the last `# fmt:` comment is `on` or `off`.
399413
"""
400414
fmt_on = False
401-
for comment in list_comments(container.prefix, is_endmarker=False):
415+
for comment in list_comments(container.prefix, is_endmarker=False, mode=mode):
402416
if comment.value in FMT_ON:
403417
fmt_on = True
404418
elif comment.value in FMT_OFF:
405419
fmt_on = False
406420
return fmt_on
407421

408422

409-
def children_contains_fmt_on(container: LN) -> bool:
423+
def children_contains_fmt_on(container: LN, mode: Mode) -> bool:
410424
"""Determine if children have formatting switched on."""
411425
for child in container.children:
412426
leaf = first_leaf_of(child)
413-
if leaf is not None and is_fmt_on(leaf):
427+
if leaf is not None and is_fmt_on(leaf, mode=mode):
414428
return True
415429

416430
return False

src/black/linegen.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def visit_default(self, node: LN) -> Iterator[Line]:
139139
"""Default `visit_*()` implementation. Recurses to children of `node`."""
140140
if isinstance(node, Leaf):
141141
any_open_brackets = self.current_line.bracket_tracker.any_open_brackets()
142-
for comment in generate_comments(node):
142+
for comment in generate_comments(node, mode=self.mode):
143143
if any_open_brackets:
144144
# any comment within brackets is subject to splitting
145145
self.current_line.append(comment)
@@ -249,6 +249,7 @@ def visit_dictsetmaker(self, node: Node) -> Iterator[Line]:
249249
maybe_make_parens_invisible_in_atom(
250250
child,
251251
parent=node,
252+
mode=self.mode,
252253
remove_brackets_around_comma=False,
253254
)
254255
else:
@@ -269,6 +270,7 @@ def visit_funcdef(self, node: Node) -> Iterator[Line]:
269270
if maybe_make_parens_invisible_in_atom(
270271
child,
271272
parent=node,
273+
mode=self.mode,
272274
remove_brackets_around_comma=False,
273275
):
274276
wrap_in_parentheses(node, child, visible=False)
@@ -362,7 +364,7 @@ def visit_power(self, node: Node) -> Iterator[Line]:
362364
):
363365
wrap_in_parentheses(node, leaf)
364366

365-
remove_await_parens(node)
367+
remove_await_parens(node, mode=self.mode)
366368

367369
yield from self.visit_default(node)
368370

@@ -409,7 +411,9 @@ def foo(a: int, b: float = 7): ...
409411
def foo(a: (int), b: (float) = 7): ...
410412
"""
411413
assert len(node.children) == 3
412-
if maybe_make_parens_invisible_in_atom(node.children[2], parent=node):
414+
if maybe_make_parens_invisible_in_atom(
415+
node.children[2], parent=node, mode=self.mode
416+
):
413417
wrap_in_parentheses(node, node.children[2], visible=False)
414418

415419
yield from self.visit_default(node)
@@ -515,7 +519,9 @@ def visit_atom(self, node: Node) -> Iterator[Line]:
515519
first.type == token.LBRACE and last.type == token.RBRACE
516520
):
517521
# Lists or sets of one item
518-
maybe_make_parens_invisible_in_atom(node.children[1], parent=node)
522+
maybe_make_parens_invisible_in_atom(
523+
node.children[1], parent=node, mode=self.mode
524+
)
519525

520526
yield from self.visit_default(node)
521527

@@ -1381,7 +1387,7 @@ def normalize_invisible_parens( # noqa: C901
13811387
Standardizes on visible parentheses for single-element tuples, and keeps
13821388
existing visible parentheses for other tuples and generator expressions.
13831389
"""
1384-
for pc in list_comments(node.prefix, is_endmarker=False):
1390+
for pc in list_comments(node.prefix, is_endmarker=False, mode=mode):
13851391
if pc.value in FMT_OFF:
13861392
# This `node` has a prefix with `# fmt: off`, don't mess with parens.
13871393
return
@@ -1433,15 +1439,17 @@ def normalize_invisible_parens( # noqa: C901
14331439
if maybe_make_parens_invisible_in_atom(
14341440
child,
14351441
parent=node,
1442+
mode=mode,
14361443
remove_brackets_around_comma=True,
14371444
):
14381445
wrap_in_parentheses(node, child, visible=False)
14391446
elif isinstance(child, Node) and node.type == syms.with_stmt:
1440-
remove_with_parens(child, node)
1447+
remove_with_parens(child, node, mode=mode)
14411448
elif child.type == syms.atom:
14421449
if maybe_make_parens_invisible_in_atom(
14431450
child,
14441451
parent=node,
1452+
mode=mode,
14451453
):
14461454
wrap_in_parentheses(node, child, visible=False)
14471455
elif is_one_tuple(child):
@@ -1493,7 +1501,7 @@ def _normalize_import_from(parent: Node, child: LN, index: int) -> None:
14931501
parent.append_child(Leaf(token.RPAR, ""))
14941502

14951503

1496-
def remove_await_parens(node: Node) -> None:
1504+
def remove_await_parens(node: Node, mode: Mode) -> None:
14971505
if node.children[0].type == token.AWAIT and len(node.children) > 1:
14981506
if (
14991507
node.children[1].type == syms.atom
@@ -1502,6 +1510,7 @@ def remove_await_parens(node: Node) -> None:
15021510
if maybe_make_parens_invisible_in_atom(
15031511
node.children[1],
15041512
parent=node,
1513+
mode=mode,
15051514
remove_brackets_around_comma=True,
15061515
):
15071516
wrap_in_parentheses(node, node.children[1], visible=False)
@@ -1570,7 +1579,7 @@ def _maybe_wrap_cms_in_parens(
15701579
node.insert_child(1, new_child)
15711580

15721581

1573-
def remove_with_parens(node: Node, parent: Node) -> None:
1582+
def remove_with_parens(node: Node, parent: Node, mode: Mode) -> None:
15741583
"""Recursively hide optional parens in `with` statements."""
15751584
# Removing all unnecessary parentheses in with statements in one pass is a tad
15761585
# complex as different variations of bracketed statements result in pretty
@@ -1592,21 +1601,23 @@ def remove_with_parens(node: Node, parent: Node) -> None:
15921601
if maybe_make_parens_invisible_in_atom(
15931602
node,
15941603
parent=parent,
1604+
mode=mode,
15951605
remove_brackets_around_comma=True,
15961606
):
15971607
wrap_in_parentheses(parent, node, visible=False)
15981608
if isinstance(node.children[1], Node):
1599-
remove_with_parens(node.children[1], node)
1609+
remove_with_parens(node.children[1], node, mode=mode)
16001610
elif node.type == syms.testlist_gexp:
16011611
for child in node.children:
16021612
if isinstance(child, Node):
1603-
remove_with_parens(child, node)
1613+
remove_with_parens(child, node, mode=mode)
16041614
elif node.type == syms.asexpr_test and not any(
16051615
leaf.type == token.COLONEQUAL for leaf in node.leaves()
16061616
):
16071617
if maybe_make_parens_invisible_in_atom(
16081618
node.children[0],
16091619
parent=node,
1620+
mode=mode,
16101621
remove_brackets_around_comma=True,
16111622
):
16121623
wrap_in_parentheses(node, node.children[0], visible=False)
@@ -1615,6 +1626,7 @@ def remove_with_parens(node: Node, parent: Node) -> None:
16151626
def maybe_make_parens_invisible_in_atom(
16161627
node: LN,
16171628
parent: LN,
1629+
mode: Mode,
16181630
remove_brackets_around_comma: bool = False,
16191631
) -> bool:
16201632
"""If it's safe, make the parens in the atom `node` invisible, recursively.
@@ -1668,13 +1680,14 @@ def maybe_make_parens_invisible_in_atom(
16681680
if (
16691681
# If the prefix of `middle` includes a type comment with
16701682
# ignore annotation, then we do not remove the parentheses
1671-
not is_type_ignore_comment_string(middle.prefix.strip())
1683+
not is_type_ignore_comment_string(middle.prefix.strip(), mode=mode)
16721684
):
16731685
first.value = ""
16741686
last.value = ""
16751687
maybe_make_parens_invisible_in_atom(
16761688
middle,
16771689
parent=parent,
1690+
mode=mode,
16781691
remove_brackets_around_comma=remove_brackets_around_comma,
16791692
)
16801693

src/black/lines.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -286,9 +286,9 @@ def contains_uncollapsable_type_comments(self) -> bool:
286286
comment_seen = False
287287
for leaf_id, comments in self.comments.items():
288288
for comment in comments:
289-
if is_type_comment(comment):
289+
if is_type_comment(comment, mode=self.mode):
290290
if comment_seen or (
291-
not is_type_ignore_comment(comment)
291+
not is_type_ignore_comment(comment, mode=self.mode)
292292
and leaf_id not in ignored_ids
293293
):
294294
return True
@@ -325,7 +325,7 @@ def contains_unsplittable_type_ignore(self) -> bool:
325325
# line.
326326
for node in self.leaves[-2:]:
327327
for comment in self.comments.get(id(node), []):
328-
if is_type_ignore_comment(comment):
328+
if is_type_ignore_comment(comment, mode=self.mode):
329329
return True
330330

331331
return False
@@ -400,7 +400,7 @@ def append_comment(self, comment: Leaf) -> bool:
400400
and not last_leaf.value
401401
and last_leaf.parent
402402
and len(list(last_leaf.parent.leaves())) <= 3
403-
and not is_type_comment(comment)
403+
and not is_type_comment(comment, mode=self.mode)
404404
):
405405
# Comments on an optional parens wrapping a single leaf should belong to
406406
# the wrapped node except if it's a type comment. Pinning the comment like

src/black/mode.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ class Preview(Enum):
204204
multiline_string_handling = auto()
205205
always_one_newline_after_import = auto()
206206
fix_fmt_skip_in_one_liners = auto()
207+
standardize_type_comments = auto()
207208

208209

209210
UNSTABLE_FEATURES: set[Preview] = {
@@ -285,3 +286,18 @@ def get_cache_key(self) -> str:
285286
features_and_magics,
286287
]
287288
return ".".join(parts)
289+
290+
def __hash__(self) -> int:
291+
return hash((
292+
frozenset(self.target_versions),
293+
self.line_length,
294+
self.string_normalization,
295+
self.is_pyi,
296+
self.is_ipynb,
297+
self.skip_source_first_line,
298+
self.magic_trailing_comma,
299+
frozenset(self.python_cell_magics),
300+
self.preview,
301+
self.unstable,
302+
frozenset(self.enabled_features),
303+
))

0 commit comments

Comments
 (0)