diff --git a/AUTHORS.md b/AUTHORS.md index e0511bb9b7c..644c38e81c3 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -79,6 +79,7 @@ Multiple contributions by: - [Hadi Alqattan](mailto:alqattanhadizaki@gmail.com) - [Hassan Abouelela](mailto:hassan@hassanamr.com) - [Heaford](mailto:dan@heaford.com) +- Henri Holopainen - [Hugo Barrera](mailto::hugo@barrera.io) - Hugo van Kemenade - [Hynek Schlawack](mailto:hs@ox.cx) diff --git a/CHANGES.md b/CHANGES.md index 9446927b8d1..a3cf622a30b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,7 +12,7 @@ ### Preview style - +- Fix crash when hugging parens with #type:ignore comments (#4037) ### Configuration diff --git a/src/black/comments.py b/src/black/comments.py index 862fc7607cc..c1bcd3f48ee 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -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 @@ -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: diff --git a/src/black/linegen.py b/src/black/linegen.py index e2c961d7a01..0198f12885e 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -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, @@ -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, @@ -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 diff --git a/src/black/lines.py b/src/black/lines.py index 3ade0a5f4a5..0560591f19a 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -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, @@ -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, diff --git a/src/black/nodes.py b/src/black/nodes.py index 9251b0defb0..fc0c5a16b5f 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -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. diff --git a/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py index 97b5b2e8dd1..c4556deb1ac 100644 --- a/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py +++ b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py @@ -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({ @@ -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