From 8140dd852106a0f4b0a30cf80c83e13f08340be2 Mon Sep 17 00:00:00 2001 From: Pedro Mezacasa Muller Date: Tue, 8 Apr 2025 17:26:29 -0300 Subject: [PATCH 1/3] fix crash when tuple as context manager --- 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 b7520f3f93a..65bd285f452 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 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..ac346f2411b 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 ff4289d3a4b..35b28c58da3 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 tough 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 tough 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 From 222f22c32bc5881723d5e6ac1ba573b0a6eebd3f Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 8 Apr 2025 20:39:19 -0700 Subject: [PATCH 2/3] Update tests/data/cases/context_managers_39.py --- tests/data/cases/context_managers_39.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/data/cases/context_managers_39.py b/tests/data/cases/context_managers_39.py index 35b28c58da3..1c165c844d8 100644 --- a/tests/data/cases/context_managers_39.py +++ b/tests/data/cases/context_managers_39.py @@ -91,7 +91,7 @@ async def func(): # don't remove the brackets here, it changes the meaning of the code. -# even tough the code will always trigger a runtime error +# even though the code will always trigger a runtime error with (name_5, name_4), name_5: pass From 40644b09f4ea71d9e9a18cbaf12e518cb5ad1846 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 8 Apr 2025 20:55:14 -0700 Subject: [PATCH 3/3] Update tests/data/cases/context_managers_39.py --- tests/data/cases/context_managers_39.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/data/cases/context_managers_39.py b/tests/data/cases/context_managers_39.py index 1c165c844d8..f4934cb07e4 100644 --- a/tests/data/cases/context_managers_39.py +++ b/tests/data/cases/context_managers_39.py @@ -205,7 +205,7 @@ async def func(): # don't remove the brackets here, it changes the meaning of the code. -# even tough the code will always trigger a runtime error +# even though the code will always trigger a runtime error with (name_5, name_4), name_5: pass