diff --git a/crates/ruff_python_formatter/src/preview.rs b/crates/ruff_python_formatter/src/preview.rs index 4de87cf05c9118..596f2b783b6916 100644 --- a/crates/ruff_python_formatter/src/preview.rs +++ b/crates/ruff_python_formatter/src/preview.rs @@ -33,3 +33,12 @@ pub(crate) const fn is_no_blank_line_before_class_docstring_enabled( ) -> bool { context.is_preview() } + +/// Returns `true` if the [`wrap_multiple_context_managers_in_parens`](https://github.com/astral-sh/ruff/issues/8889) preview style is enabled. +/// +/// Unlike Black, we re-use the same preview style feature flag for [`improved_async_statements_handling`](https://github.com/astral-sh/ruff/issues/8890) +pub(crate) const fn is_wrap_multiple_context_managers_in_parens_enabled( + context: &PyFormatContext, +) -> bool { + context.is_preview() +} diff --git a/crates/ruff_python_formatter/src/statement/stmt_with.rs b/crates/ruff_python_formatter/src/statement/stmt_with.rs index 06dc9a5f88f6b7..dd2fd04e95350d 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_with.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_with.rs @@ -9,8 +9,9 @@ use crate::comments::SourceComment; use crate::expression::parentheses::parenthesized; use crate::other::commas; use crate::prelude::*; +use crate::preview::is_wrap_multiple_context_managers_in_parens_enabled; use crate::statement::clause::{clause_body, clause_header, ClauseHeader}; -use crate::PyFormatOptions; +use crate::{PyFormatOptions, PythonVersion}; #[derive(Default)] pub struct FormatStmtWith; @@ -115,14 +116,22 @@ impl FormatNodeRule for FormatStmtWith { /// Returns `true` if the `with` items should be parenthesized, if at least one item expands. /// -/// Black parenthesizes `with` items if there's more than one item and they're already -/// parenthesized, _or_ there's a single item with a trailing comma. +/// Parenthesize `with` items if +/// * The last item has a trailing comma +/// * There's more than one item and they're already parenthesized +/// * There's more than one item and the target python version is >= 3.9 fn should_parenthesize( with: &StmtWith, options: &PyFormatOptions, context: &PyFormatContext, ) -> FormatResult { - if has_magic_trailing_comma(with, options, context) { + if with.items.len() <= 1 { + return Ok(has_magic_trailing_comma(with, options, context)); + } + + if is_wrap_multiple_context_managers_in_parens_enabled(context) + && options.target_version() >= PythonVersion::Py39 + { return Ok(true); } diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_context_managers_39.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_context_managers_39.py.snap deleted file mode 100644 index ed87cfaba128dd..00000000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_context_managers_39.py.snap +++ /dev/null @@ -1,342 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/preview_context_managers_39.py ---- -## Input - -```python -with \ - make_context_manager1() as cm1, \ - make_context_manager2() as cm2, \ - make_context_manager3() as cm3, \ - make_context_manager4() as cm4 \ -: - pass - - -# Leading comment -with \ - make_context_manager1() as cm1, \ - make_context_manager2(), \ - make_context_manager3() as cm3, \ - make_context_manager4() \ -: - pass - - -with \ - new_new_new1() as cm1, \ - new_new_new2() \ -: - pass - - -with ( - new_new_new1() as cm1, - new_new_new2() -): - pass - - -# Leading comment. -with ( - # First comment. - new_new_new1() as cm1, - # Second comment. - new_new_new2() - # Last comment. -): - pass - - -with \ - this_is_a_very_long_call(looong_arg1=looong_value1, looong_arg2=looong_value2) as cm1, \ - this_is_a_very_long_call(looong_arg1=looong_value1, looong_arg2=looong_value2, looong_arg3=looong_value3, looong_arg4=looong_value4) as cm2 \ -: - pass - - -with mock.patch.object( - self.my_runner, "first_method", autospec=True -) as mock_run_adb, mock.patch.object( - self.my_runner, "second_method", autospec=True, return_value="foo" -): - pass - - -with xxxxxxxx.some_kind_of_method( - some_argument=[ - "first", - "second", - "third", - ] -).another_method() as cmd: - pass - - -async def func(): - async with \ - make_context_manager1() as cm1, \ - make_context_manager2() as cm2, \ - make_context_manager3() as cm3, \ - make_context_manager4() as cm4 \ - : - pass - - async with some_function( - argument1, argument2, argument3="some_value" - ) as some_cm, some_other_function( - argument1, argument2, argument3="some_value" - ): - pass -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,19 +1,9 @@ --with ( -- make_context_manager1() as cm1, -- make_context_manager2() as cm2, -- make_context_manager3() as cm3, -- make_context_manager4() as cm4, --): -+with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4: - pass - - - # Leading comment --with ( -- make_context_manager1() as cm1, -- make_context_manager2(), -- make_context_manager3() as cm3, -- make_context_manager4(), --): -+with make_context_manager1() as cm1, make_context_manager2(), make_context_manager3() as cm3, make_context_manager4(): - pass - - -@@ -36,25 +26,21 @@ - pass - - --with ( -- this_is_a_very_long_call( -- looong_arg1=looong_value1, looong_arg2=looong_value2 -- ) as cm1, -- this_is_a_very_long_call( -- looong_arg1=looong_value1, -- looong_arg2=looong_value2, -- looong_arg3=looong_value3, -- looong_arg4=looong_value4, -- ) as cm2, --): -+with this_is_a_very_long_call( -+ looong_arg1=looong_value1, looong_arg2=looong_value2 -+) as cm1, this_is_a_very_long_call( -+ looong_arg1=looong_value1, -+ looong_arg2=looong_value2, -+ looong_arg3=looong_value3, -+ looong_arg4=looong_value4, -+) as cm2: - pass - - --with ( -- mock.patch.object(self.my_runner, "first_method", autospec=True) as mock_run_adb, -- mock.patch.object( -- self.my_runner, "second_method", autospec=True, return_value="foo" -- ), -+with mock.patch.object( -+ self.my_runner, "first_method", autospec=True -+) as mock_run_adb, mock.patch.object( -+ self.my_runner, "second_method", autospec=True, return_value="foo" - ): - pass - -@@ -70,16 +56,10 @@ - - - async def func(): -- async with ( -- make_context_manager1() as cm1, -- make_context_manager2() as cm2, -- make_context_manager3() as cm3, -- make_context_manager4() as cm4, -- ): -+ async with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4: - pass - -- async with ( -- some_function(argument1, argument2, argument3="some_value") as some_cm, -- some_other_function(argument1, argument2, argument3="some_value"), -- ): -+ async with some_function( -+ argument1, argument2, argument3="some_value" -+ ) as some_cm, some_other_function(argument1, argument2, argument3="some_value"): - pass -``` - -## Ruff Output - -```python -with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4: - pass - - -# Leading comment -with make_context_manager1() as cm1, make_context_manager2(), make_context_manager3() as cm3, make_context_manager4(): - pass - - -with new_new_new1() as cm1, new_new_new2(): - pass - - -with new_new_new1() as cm1, new_new_new2(): - pass - - -# Leading comment. -with ( - # First comment. - new_new_new1() as cm1, - # Second comment. - new_new_new2(), - # Last comment. -): - pass - - -with this_is_a_very_long_call( - looong_arg1=looong_value1, looong_arg2=looong_value2 -) as cm1, this_is_a_very_long_call( - looong_arg1=looong_value1, - looong_arg2=looong_value2, - looong_arg3=looong_value3, - looong_arg4=looong_value4, -) as cm2: - pass - - -with mock.patch.object( - self.my_runner, "first_method", autospec=True -) as mock_run_adb, mock.patch.object( - self.my_runner, "second_method", autospec=True, return_value="foo" -): - pass - - -with xxxxxxxx.some_kind_of_method( - some_argument=[ - "first", - "second", - "third", - ] -).another_method() as cmd: - pass - - -async def func(): - async with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4: - pass - - async with some_function( - argument1, argument2, argument3="some_value" - ) as some_cm, some_other_function(argument1, argument2, argument3="some_value"): - pass -``` - -## Black Output - -```python -with ( - make_context_manager1() as cm1, - make_context_manager2() as cm2, - make_context_manager3() as cm3, - make_context_manager4() as cm4, -): - pass - - -# Leading comment -with ( - make_context_manager1() as cm1, - make_context_manager2(), - make_context_manager3() as cm3, - make_context_manager4(), -): - pass - - -with new_new_new1() as cm1, new_new_new2(): - pass - - -with new_new_new1() as cm1, new_new_new2(): - pass - - -# Leading comment. -with ( - # First comment. - new_new_new1() as cm1, - # Second comment. - new_new_new2(), - # Last comment. -): - pass - - -with ( - this_is_a_very_long_call( - looong_arg1=looong_value1, looong_arg2=looong_value2 - ) as cm1, - this_is_a_very_long_call( - looong_arg1=looong_value1, - looong_arg2=looong_value2, - looong_arg3=looong_value3, - looong_arg4=looong_value4, - ) as cm2, -): - pass - - -with ( - mock.patch.object(self.my_runner, "first_method", autospec=True) as mock_run_adb, - mock.patch.object( - self.my_runner, "second_method", autospec=True, return_value="foo" - ), -): - pass - - -with xxxxxxxx.some_kind_of_method( - some_argument=[ - "first", - "second", - "third", - ] -).another_method() as cmd: - pass - - -async def func(): - async with ( - make_context_manager1() as cm1, - make_context_manager2() as cm2, - make_context_manager3() as cm3, - make_context_manager4() as cm4, - ): - pass - - async with ( - some_function(argument1, argument2, argument3="some_value") as some_cm, - some_other_function(argument1, argument2, argument3="some_value"), - ): - pass -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_context_managers_autodetect_310.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_context_managers_autodetect_310.py.snap deleted file mode 100644 index abee1610d434c5..00000000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_context_managers_autodetect_310.py.snap +++ /dev/null @@ -1,79 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/preview_context_managers_autodetect_310.py ---- -## Input - -```python -# This file uses pattern matching introduced in Python 3.10. - - -match http_code: - case 404: - print("Not found") - - -with \ - make_context_manager1() as cm1, \ - make_context_manager2() as cm2, \ - make_context_manager3() as cm3, \ - make_context_manager4() as cm4 \ -: - pass -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -6,10 +6,5 @@ - print("Not found") - - --with ( -- make_context_manager1() as cm1, -- make_context_manager2() as cm2, -- make_context_manager3() as cm3, -- make_context_manager4() as cm4, --): -+with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4: - pass -``` - -## Ruff Output - -```python -# This file uses pattern matching introduced in Python 3.10. - - -match http_code: - case 404: - print("Not found") - - -with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4: - pass -``` - -## Black Output - -```python -# This file uses pattern matching introduced in Python 3.10. - - -match http_code: - case 404: - print("Not found") - - -with ( - make_context_manager1() as cm1, - make_context_manager2() as cm2, - make_context_manager3() as cm3, - make_context_manager4() as cm4, -): - pass -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_context_managers_autodetect_311.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_context_managers_autodetect_311.py.snap deleted file mode 100644 index 71002c8d5d700f..00000000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_context_managers_autodetect_311.py.snap +++ /dev/null @@ -1,82 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/preview_context_managers_autodetect_311.py ---- -## Input - -```python -# This file uses except* clause in Python 3.11. - - -try: - some_call() -except* Error as e: - pass - - -with \ - make_context_manager1() as cm1, \ - make_context_manager2() as cm2, \ - make_context_manager3() as cm3, \ - make_context_manager4() as cm4 \ -: - pass -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -7,10 +7,5 @@ - pass - - --with ( -- make_context_manager1() as cm1, -- make_context_manager2() as cm2, -- make_context_manager3() as cm3, -- make_context_manager4() as cm4, --): -+with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4: - pass -``` - -## Ruff Output - -```python -# This file uses except* clause in Python 3.11. - - -try: - some_call() -except* Error as e: - pass - - -with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4: - pass -``` - -## Black Output - -```python -# This file uses except* clause in Python 3.11. - - -try: - some_call() -except* Error as e: - pass - - -with ( - make_context_manager1() as cm1, - make_context_manager2() as cm2, - make_context_manager3() as cm3, - make_context_manager4() as cm4, -): - pass -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_context_managers_autodetect_39.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_context_managers_autodetect_39.py.snap deleted file mode 100644 index e1aeaa9faf8b97..00000000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_context_managers_autodetect_39.py.snap +++ /dev/null @@ -1,81 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/preview_context_managers_autodetect_39.py ---- -## Input - -```python -# This file uses parenthesized context managers introduced in Python 3.9. - - -with \ - make_context_manager1() as cm1, \ - make_context_manager2() as cm2, \ - make_context_manager3() as cm3, \ - make_context_manager4() as cm4 \ -: - pass - - -with ( - new_new_new1() as cm1, - new_new_new2() -): - pass -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,12 +1,7 @@ - # This file uses parenthesized context managers introduced in Python 3.9. - - --with ( -- make_context_manager1() as cm1, -- make_context_manager2() as cm2, -- make_context_manager3() as cm3, -- make_context_manager4() as cm4, --): -+with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4: - pass - - -``` - -## Ruff Output - -```python -# This file uses parenthesized context managers introduced in Python 3.9. - - -with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4: - pass - - -with new_new_new1() as cm1, new_new_new2(): - pass -``` - -## Black Output - -```python -# This file uses parenthesized context managers introduced in Python 3.9. - - -with ( - make_context_manager1() as cm1, - make_context_manager2() as cm2, - make_context_manager3() as cm3, - make_context_manager4() as cm4, -): - pass - - -with new_new_new1() as cm1, new_new_new2(): - pass -``` - -