From d0ff3bd6cb82e35b1529155d29fca2c13442e68d Mon Sep 17 00:00:00 2001 From: Pedro Mezacasa Muller <114496585+Pedro-Muller29@users.noreply.github.com> Date: Wed, 9 Apr 2025 01:42:17 -0300 Subject: [PATCH] Fix crash when a tuple is used as a ContextManager (#4646) --- CHANGES.md | 1 + src/black/linegen.py | 6 ++++ src/black/nodes.py | 18 +++++++++++ tests/data/cases/context_managers_39.py | 40 +++++++++++++++++++++++++ 4 files changed, 65 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index b7520f3..65bd285 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,7 @@ - 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) ### Preview style diff --git a/src/black/linegen.py b/src/black/linegen.py index 52ef2cf..fa574ca 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 665cb15..ac346f2 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -1058,3 +1058,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 ff4289d..f4934cb 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