From 45e8d6bb5dceebef0e1c3d0103534849363692de Mon Sep 17 00:00:00 2001 From: anthonychen000 Date: Tue, 22 Apr 2025 02:08:46 -0400 Subject: [PATCH] Fix #2097: Recognize type comments with spaces after # Updated comment recognition functions (is_type_comment, is_type_ignore_comment, and helpers) to correctly identify type/ignore comments even when extra whitespace exists between '#' and 'type:'. This addresses the core recognition bug described in issue #2097 without implementing comment standardization. Includes test case and CHANGELOG entry. Also includes unrelated mypy fix for action/main.py. --- CHANGES.md | 2 + action/main.py | 2 +- src/black/linegen.py | 6 +++ src/black/nodes.py | 36 +++++++++++++- tests/data/cases/context_managers_39.py | 40 ++++++++++++++++ tests/data/cases/type_comments_recognition.py | 48 +++++++++++++++++++ 6 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 tests/data/cases/type_comments_recognition.py diff --git a/CHANGES.md b/CHANGES.md index b7520f3f93a..4f8cffee3fb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,8 @@ - Handle `# fmt: skip` followed by a comment at the end of file (#4635) - Fix crash when a tuple appears in the `as` clause of a `with` statement (#4634) +- Fix crash when tuple is used as a context manager inside a `with` statement (#4646) +- Recognize type comments and ignore comments with leading whitespace after `#` (#2097) ### Preview style diff --git a/action/main.py b/action/main.py index f7fdda7efb6..30162d6fc7c 100644 --- a/action/main.py +++ b/action/main.py @@ -53,7 +53,7 @@ def read_version_specifier_from_pyproject() -> str: ) sys.exit(1) - import tomllib # type: ignore[import-not-found,unreachable] + import tomllib # type: ignore[import-untyped, unreachable] try: with Path("pyproject.toml").open("rb") as fp: diff --git a/src/black/linegen.py b/src/black/linegen.py index 52ef2cf0131..fa574ca215e 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -40,6 +40,7 @@ ensure_visible, fstring_to_string, get_annotation_type, + has_sibling_with_type, is_arith_like, is_async_stmt_or_funcdef, is_atom_with_invisible_parens, @@ -1628,6 +1629,11 @@ def maybe_make_parens_invisible_in_atom( or is_empty_tuple(node) or is_one_tuple(node) or (is_tuple(node) and parent.type == syms.asexpr_test) + or ( + is_tuple(node) + and parent.type == syms.with_stmt + and has_sibling_with_type(node, token.COMMA) + ) or (is_yield(node) and parent.type != syms.expr_stmt) or ( # This condition tries to prevent removing non-optional brackets diff --git a/src/black/nodes.py b/src/black/nodes.py index 665cb15d910..5f25ea2de3c 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -938,7 +938,16 @@ def is_type_comment(leaf: Leaf) -> bool: 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:") + return t in {token.COMMENT, STANDALONE_COMMENT} and is_type_comment_string(v) + + +def is_type_comment_string(value: str) -> bool: + """Helper: Return True if the string value looks like a potential type comment + (general or ignore) using flexible whitespace checking after '#'. + Recognizes '# type:' and '# type:' but NOT '# type :'. + """ + content_after_hash = value[1:].lstrip() + return content_after_hash.startswith("type:") def is_type_ignore_comment(leaf: Leaf) -> bool: @@ -951,7 +960,12 @@ def is_type_ignore_comment(leaf: Leaf) -> bool: 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") + content_after_hash = value[1:].lstrip() + if not content_after_hash.startswith("type:"): + return False + part_after_type_colon = content_after_hash[5:] + cleaned_part_after_colon = part_after_type_colon.lstrip() + return cleaned_part_after_colon.startswith("ignore") def wrap_in_parentheses(parent: Node, child: LN, *, visible: bool = True) -> None: @@ -1058,3 +1072,21 @@ def furthest_ancestor_with_last_leaf(leaf: Leaf) -> LN: while node.parent and node.parent.children and node is node.parent.children[-1]: node = node.parent return node + + +def has_sibling_with_type(node: LN, type: int) -> bool: + # Check previous siblings + sibling = node.prev_sibling + while sibling is not None: + if sibling.type == type: + return True + sibling = sibling.prev_sibling + + # Check next siblings + sibling = node.next_sibling + while sibling is not None: + if sibling.type == type: + return True + sibling = sibling.next_sibling + + return False diff --git a/tests/data/cases/context_managers_39.py b/tests/data/cases/context_managers_39.py index ff4289d3a4b..f4934cb07e4 100644 --- a/tests/data/cases/context_managers_39.py +++ b/tests/data/cases/context_managers_39.py @@ -89,6 +89,26 @@ async def func(): with (x, y) as z: pass + +# don't remove the brackets here, it changes the meaning of the code. +# even though the code will always trigger a runtime error +with (name_5, name_4), name_5: + pass + + +def test_tuple_as_contextmanager(): + from contextlib import nullcontext + + try: + with (nullcontext(),nullcontext()),nullcontext(): + pass + except TypeError: + # test passed + pass + else: + # this should be a type error + assert False + # output @@ -182,3 +202,23 @@ async def func(): # don't remove the brackets here, it changes the meaning of the code. with (x, y) as z: pass + + +# don't remove the brackets here, it changes the meaning of the code. +# even though the code will always trigger a runtime error +with (name_5, name_4), name_5: + pass + + +def test_tuple_as_contextmanager(): + from contextlib import nullcontext + + try: + with (nullcontext(), nullcontext()), nullcontext(): + pass + except TypeError: + # test passed + pass + else: + # this should be a type error + assert False diff --git a/tests/data/cases/type_comments_recognition.py b/tests/data/cases/type_comments_recognition.py new file mode 100644 index 00000000000..cc1e9e3cd0a --- /dev/null +++ b/tests/data/cases/type_comments_recognition.py @@ -0,0 +1,48 @@ +# Issue #2097 + +# Variable assignment case (misformatted type comment) +perfectly_fine_variable = get_value() # type: MyValue + +# A standard one for comparison (misformatted spacing) +another_variable = get_another() # type: AnotherValue + +# Regular comment for comparison +regular_var = 10# A regular comment + + +# Function definition case +def process_data( + user_data, # type: UserDict + session_id # type: SessionID +): + # Function body starts here + ... + + +# Another misformatted type comment +short_var = 1 # type: int + +# output +# Issue #2097 + +# Variable assignment case (misformatted type comment) +perfectly_fine_variable = get_value() # type: MyValue + +# A standard one for comparison (misformatted spacing) +another_variable = get_another() # type: AnotherValue + +# Regular comment for comparison +regular_var = 10 # A regular comment + + +# Function definition case +def process_data( + user_data, # type: UserDict + session_id, # type: SessionID +): + # Function body starts here + ... + + +# Another misformatted type comment +short_var = 1 # type: int \ No newline at end of file