diff --git a/CHANGES.md b/CHANGES.md index 7e6ce687155..d66a9e68212 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -124,6 +124,8 @@ directories. - Fix crash when multiple `# fmt: skip` comments are used in a multi-part if-clause, on string literals, or on dictionary entries with long lines (#4872) - Fix possible crash when `fmt: ` directives aren't on the top level (#4856) +- Preserve parentheses when `# type: ignore` comments would be merged with other + comments on the same line, preventing AST equivalence failures (#4888) ### Preview style diff --git a/src/black/lines.py b/src/black/lines.py index 9be4f0e7b66..3c7b6bb6c91 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -948,6 +948,44 @@ def can_omit_invisible_parens( """ line = rhs.body + # We can't omit parens if doing so would result in a type: ignore comment + # sharing a line with other comments, as that breaks type: ignore parsing. + # Check if the opening bracket (last leaf of head) has comments that would merge + # with comments from the first line of the body. + if rhs.head.leaves: + opening_bracket = rhs.head.leaves[-1] + head_comments = rhs.head.comments.get(id(opening_bracket), []) + + # If there are comments on the opening bracket line, check if any would + # conflict with type: ignore comments in the body + if head_comments: + has_type_ignore_in_head = any( + is_type_ignore_comment(comment, mode=rhs.head.mode) + for comment in head_comments + ) + has_other_comment_in_head = any( + not is_type_ignore_comment(comment, mode=rhs.head.mode) + for comment in head_comments + ) + + # Check for comments in the body that would potentially end up on the + # same line as the head comments when parens are removed + has_type_ignore_in_body = False + has_other_comment_in_body = False + for leaf in rhs.body.leaves: + for comment in rhs.body.comments.get(id(leaf), []): + if is_type_ignore_comment(comment, mode=rhs.body.mode): + has_type_ignore_in_body = True + else: + has_other_comment_in_body = True + + # Preserve parens if we have both type: ignore and other comments that + # could end up on the same line + if (has_type_ignore_in_head and has_other_comment_in_body) or ( + has_other_comment_in_head and has_type_ignore_in_body + ): + return False + # We need optional parens in order to split standalone comments to their own lines # if there are no nested parens around the standalone comments closing_bracket: Leaf | None = None diff --git a/tests/data/cases/type_ignore_with_other_comment.py b/tests/data/cases/type_ignore_with_other_comment.py new file mode 100644 index 00000000000..bfa330d4b02 --- /dev/null +++ b/tests/data/cases/type_ignore_with_other_comment.py @@ -0,0 +1,27 @@ +import pandas as pd + +interval_td = pd.Interval( + pd.Timedelta("1 days"), pd.Timedelta("2 days"), closed="neither" +) + +_td = ( # pyright: ignore[reportOperatorIssue,reportUnknownVariableType] + interval_td + - pd.Interval( # type: ignore[operator] + pd.Timedelta(1, "ns"), pd.Timedelta(2, "ns") + ) +) + +# output + +import pandas as pd + +interval_td = pd.Interval( + pd.Timedelta("1 days"), pd.Timedelta("2 days"), closed="neither" +) + +_td = ( # pyright: ignore[reportOperatorIssue,reportUnknownVariableType] + interval_td + - pd.Interval( # type: ignore[operator] + pd.Timedelta(1, "ns"), pd.Timedelta(2, "ns") + ) +)