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

Don't hug parens if it would mean putting two # type: ignore comments on the same line #4037

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ Multiple contributions by:
- [Hadi Alqattan](mailto:[email protected])
- [Hassan Abouelela](mailto:[email protected])
- [Heaford](mailto:[email protected])
- Henri Holopainen
- [Hugo Barrera](mailto::[email protected])
- Hugo van Kemenade
- [Hynek Schlawack](mailto:[email protected])
Expand Down
2 changes: 1 addition & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

### Preview style

<!-- Changes that affect Black's preview style -->
- Fix crash when hugging parens with #type:ignore comments (#4037)

### Configuration

Expand Down
40 changes: 37 additions & 3 deletions src/black/comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,18 @@
from blib2to3.pytree import Leaf, Node

# types
COMMENT_EXCEPTIONS = " !:#'"
_TYPE_PREFIX = "# type:"
_COMMENT_PREFIX = "# "
_COMMENT_LIST_SEPARATOR = ";"

LN = Union[Leaf, Node]

FMT_OFF: Final = {"# fmt: off", "# fmt:off", "# yapf: disable"}
FMT_SKIP: Final = {"# fmt: skip", "# fmt:skip"}
FMT_ON: Final = {"# fmt: on", "# fmt:on", "# yapf: enable"}

COMMENT_EXCEPTIONS = " !:#'"
_COMMENT_PREFIX = "# "
_COMMENT_LIST_SEPARATOR = ";"
_TYPE_IGNORE: Final = {_TYPE_PREFIX + "ignore", _TYPE_PREFIX + " ignore"}


@dataclass
Expand Down Expand Up @@ -325,6 +328,37 @@ def children_contains_fmt_on(container: LN) -> bool:
return False


def contains_type_ignore_comment(comment_list: List[Leaf]) -> bool:
"""Return True if the given list contains a type comment with ignore annotation."""
for comment in comment_list:
if is_type_ignore_comment(comment):
return True
return False


def is_type_comment(leaf: Leaf) -> bool:
"""Return True if the given leaf is a type comment. This function should only
be used for general type comments (excluding ignore annotations, which should
use `is_type_ignore_comment`). Note that general type comments are no longer
used in modern version of Python, this function may be deprecated in the future."""
t = leaf.type
v = leaf.value
return t in {token.COMMENT, STANDALONE_COMMENT} and v.startswith(_TYPE_PREFIX)


def is_type_ignore_comment(leaf: Leaf) -> bool:
"""Return True if the given leaf is a type comment with ignore annotation."""
t = leaf.type
v = leaf.value
return t in {token.COMMENT, STANDALONE_COMMENT} and is_type_ignore_comment_string(v)


def is_type_ignore_comment_string(value: str) -> bool:
"""Return True if the given string match with type comment with
ignore annotation."""
return any(value.startswith(type_ignore) for type_ignore in _TYPE_IGNORE)


def contains_pragma_comment(comment_list: List[Leaf]) -> bool:
"""
Returns:
Expand Down
36 changes: 31 additions & 5 deletions src/black/linegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@
get_leaves_inside_matching_brackets,
max_delimiter_priority_in_atom,
)
from black.comments import FMT_OFF, generate_comments, list_comments
from black.comments import (
FMT_OFF,
contains_type_ignore_comment,
generate_comments,
is_type_ignore_comment_string,
list_comments,
)
from black.lines import (
Line,
RHSResult,
Expand Down Expand Up @@ -51,7 +57,6 @@
is_stub_body,
is_stub_suite,
is_tuple_containing_walrus,
is_type_ignore_comment_string,
is_vararg,
is_walrus_assignment,
is_yield,
Expand Down Expand Up @@ -826,9 +831,30 @@ def _first_right_hand_split(
and body_leaves[-1].type in [token.RBRACE, token.RSQB]
and body_leaves[-1].opening_bracket is body_leaves[is_unpacking]
):
head_leaves = head_leaves + body_leaves[: 1 + is_unpacking]
tail_leaves = body_leaves[-1:] + tail_leaves
body_leaves = body_leaves[1 + is_unpacking : -1]
last_leaf_on_head_line = head_leaves[-1]
last_leaf_on_first_body_line = [
leaf for leaf in body_leaves if leaf.lineno == body_leaves[0].lineno
][-1]
last_leaf_on_last_body_line = [
leaf for leaf in body_leaves if leaf.lineno == body_leaves[-1].lineno
][-1]
last_leaf_on_tail_line = tail_leaves[-1]

start_blocked_by_type_ignore = contains_type_ignore_comment(
line.comments.get(id(last_leaf_on_head_line), [])
) and contains_type_ignore_comment(
line.comments.get(id(last_leaf_on_first_body_line), [])
)
end_blocked_by_type_ignore = contains_type_ignore_comment(
line.comments.get(id(last_leaf_on_last_body_line), [])
) and contains_type_ignore_comment(
line.comments.get(id(last_leaf_on_tail_line), [])
)

if not (start_blocked_by_type_ignore or end_blocked_by_type_ignore):
head_leaves = head_leaves + body_leaves[: 1 + is_unpacking]
tail_leaves = body_leaves[-1:] + tail_leaves
body_leaves = body_leaves[1 + is_unpacking : -1]

head = bracket_split_build_line(
head_leaves, line, opening_bracket, component=_BracketSplitComponent.head
Expand Down
3 changes: 1 addition & 2 deletions src/black/lines.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
)

from black.brackets import COMMA_PRIORITY, DOT_PRIORITY, BracketTracker
from black.comments import is_type_comment, is_type_ignore_comment
from black.mode import Mode, Preview
from black.nodes import (
BRACKETS,
Expand All @@ -28,8 +29,6 @@
is_import,
is_multiline_string,
is_one_sequence_between,
is_type_comment,
is_type_ignore_comment,
is_with_or_async_with_stmt,
replace_child,
syms,
Expand Down
23 changes: 0 additions & 23 deletions src/black/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -838,29 +838,6 @@ def is_async_stmt_or_funcdef(leaf: Leaf) -> bool:
)


def is_type_comment(leaf: Leaf) -> bool:
"""Return True if the given leaf is a type comment. This function should only
be used for general type comments (excluding ignore annotations, which should
use `is_type_ignore_comment`). Note that general type comments are no longer
used in modern version of Python, this function may be deprecated in the future."""
t = leaf.type
v = leaf.value
return t in {token.COMMENT, STANDALONE_COMMENT} and v.startswith("# type:")


def is_type_ignore_comment(leaf: Leaf) -> bool:
"""Return True if the given leaf is a type comment with ignore annotation."""
t = leaf.type
v = leaf.value
return t in {token.COMMENT, STANDALONE_COMMENT} and is_type_ignore_comment_string(v)


def is_type_ignore_comment_string(value: str) -> bool:
"""Return True if the given string match with type comment with
ignore annotation."""
return value.startswith("# type: ignore")


def wrap_in_parentheses(parent: Node, child: LN, *, visible: bool = True) -> None:
"""Wrap `child` in parentheses.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,35 @@ def foo_square_brackets(request):
for individual in container["nested"]
])

func( # type:ignore
[ # type: ignore
"a"
]
)

func(
[
"a"
] # type:ignore
) # type: ignore

func(
[ # type:ignore
"a" # type: ignore
] # type:ignore
)

func( # type:ignore
[
"a" # type: ignore
]
) # type:ignore

func( # type:ignore
[ # type: ignore
"a" # type:ignore
] # type: ignore
) # type:ignore
# output
def foo_brackets(request):
return JsonResponse({
Expand Down Expand Up @@ -343,3 +372,25 @@ def foo_square_brackets(request):
# Foobar
for individual in container["nested"]
])

func( # type:ignore
["a"] # type: ignore
)

func(
["a"] # type:ignore
) # type: ignore

func([ # type:ignore
"a" # type: ignore
]) # type:ignore

func([ # type:ignore
"a" # type: ignore
]) # type:ignore

func( # type:ignore
[ # type: ignore
"a" # type:ignore
] # type: ignore
) # type:ignore
Loading