diff --git a/crates/ruff_formatter/src/builders.rs b/crates/ruff_formatter/src/builders.rs index fdf87e6327daf8..5f8c75059c5d86 100644 --- a/crates/ruff_formatter/src/builders.rs +++ b/crates/ruff_formatter/src/builders.rs @@ -7,7 +7,7 @@ use ruff_text_size::TextRange; use Tag::*; use crate::format_element::tag::{Condition, Tag}; -use crate::prelude::tag::{DedentMode, GroupMode, LabelId}; +use crate::prelude::tag::{BestFitParenthesizeMode, DedentMode, GroupMode, LabelId}; use crate::prelude::*; use crate::{write, Argument, Arguments, FormatContext, FormatOptions, GroupId, TextSize}; use crate::{Buffer, VecBuffer}; @@ -1553,6 +1553,7 @@ pub fn best_fit_parenthesize( BestFitParenthesize { content: Argument::new(content), group_id: None, + mode: BestFitParenthesizeMode::default(), } } @@ -1560,6 +1561,7 @@ pub fn best_fit_parenthesize( pub struct BestFitParenthesize<'a, Context> { content: Argument<'a, Context>, group_id: Option, + mode: BestFitParenthesizeMode, } impl BestFitParenthesize<'_, Context> { @@ -1570,12 +1572,19 @@ impl BestFitParenthesize<'_, Context> { self.group_id = group_id; self } + + #[must_use] + pub fn with_mode(mut self, mode: BestFitParenthesizeMode) -> Self { + self.mode = mode; + self + } } impl Format for BestFitParenthesize<'_, Context> { fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { f.write_element(FormatElement::Tag(StartBestFitParenthesize { id: self.group_id, + mode: self.mode, })); Arguments::from(&self.content).fmt(f)?; diff --git a/crates/ruff_formatter/src/format_element/document.rs b/crates/ruff_formatter/src/format_element/document.rs index d5e71fd495ce33..28de5ed35ac895 100644 --- a/crates/ruff_formatter/src/format_element/document.rs +++ b/crates/ruff_formatter/src/format_element/document.rs @@ -4,7 +4,7 @@ use std::ops::Deref; use rustc_hash::FxHashMap; use crate::format_element::tag::{Condition, DedentMode}; -use crate::prelude::tag::GroupMode; +use crate::prelude::tag::{BestFitParenthesizeMode, GroupMode}; use crate::prelude::*; use crate::source_code::SourceCode; use crate::{ @@ -518,7 +518,7 @@ impl Format> for &[FormatElement] { } } - StartBestFitParenthesize { id } => { + StartBestFitParenthesize { id, mode } => { write!(f, [token("best_fit_parenthesize(")])?; if let Some(group_id) = id { @@ -531,6 +531,15 @@ impl Format> for &[FormatElement] { ] )?; } + + match mode { + BestFitParenthesizeMode::BestFit => { + write!(f, [token("mode: BestFit,"), space()])?; + } + BestFitParenthesizeMode::Group => { + write!(f, [token("mode: Group,"), space()])?; + } + } } StartConditionalGroup(group) => { diff --git a/crates/ruff_formatter/src/format_element/tag.rs b/crates/ruff_formatter/src/format_element/tag.rs index e922425d20f2e3..9d24f4a8413b57 100644 --- a/crates/ruff_formatter/src/format_element/tag.rs +++ b/crates/ruff_formatter/src/format_element/tag.rs @@ -94,6 +94,7 @@ pub enum Tag { /// See [`crate::builders::best_fit_parenthesize`] for an in-depth explanation. StartBestFitParenthesize { id: Option, + mode: BestFitParenthesizeMode, }, EndBestFitParenthesize, } @@ -414,3 +415,12 @@ impl VerbatimKind { matches!(self, VerbatimKind::Bogus) } } + +#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)] +pub enum BestFitParenthesizeMode { + /// Behaves like `BestFit`, breaking from left to right. + #[default] + BestFit, + /// Behaves like a `Group` regular group, breaking from right to left. + Group, +} diff --git a/crates/ruff_formatter/src/printer/mod.rs b/crates/ruff_formatter/src/printer/mod.rs index e35c44ec28ef2c..a4170438fe374b 100644 --- a/crates/ruff_formatter/src/printer/mod.rs +++ b/crates/ruff_formatter/src/printer/mod.rs @@ -7,7 +7,7 @@ pub use printer_options::*; use ruff_text_size::{Ranged, TextLen, TextSize}; use crate::format_element::document::Document; -use crate::format_element::tag::{Condition, GroupMode}; +use crate::format_element::tag::{BestFitParenthesizeMode, Condition, GroupMode}; use crate::format_element::{BestFittingMode, BestFittingVariants, LineMode, PrintMode}; use crate::prelude::tag::{DedentMode, Tag, TagKind, VerbatimKind}; use crate::prelude::{tag, TextWidth}; @@ -174,7 +174,7 @@ impl<'a> Printer<'a> { stack.push(TagKind::Group, args.with_print_mode(print_mode)); } - FormatElement::Tag(StartBestFitParenthesize { id }) => { + FormatElement::Tag(StartBestFitParenthesize { id, mode: _ }) => { const OPEN_PAREN: FormatElement = FormatElement::Token { text: "(" }; const INDENT: FormatElement = FormatElement::Tag(Tag::StartIndent); const HARD_LINE_BREAK: FormatElement = FormatElement::Line(LineMode::Hard); @@ -1273,7 +1273,7 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { return Ok(self.fits_group(TagKind::Group, group.mode(), group.id(), args)); } - FormatElement::Tag(StartBestFitParenthesize { id }) => { + FormatElement::Tag(StartBestFitParenthesize { id, mode }) => { if let Some(id) = id { self.printer .state @@ -1281,28 +1281,58 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { .insert_print_mode(*id, args.mode()); } - // Don't use the parenthesized with indent layout even when measuring expanded mode similar to `BestFitting`. - // This is to expand the left and not right after the `(` parentheses (it is okay to expand after the content that it wraps). + match mode { + BestFitParenthesizeMode::BestFit => { + // Don't use the parenthesized with indent layout even when measuring expanded mode similar to `BestFitting`. + // This is to expand the left and not right after the `(` parentheses (it is okay to expand after the content that it wraps). + } + BestFitParenthesizeMode::Group => { + if args.mode().is_expanded() { + const OPEN_PAREN: FormatElement = FormatElement::Token { text: "(" }; + const INDENT: FormatElement = FormatElement::Tag(Tag::StartIndent); + const HARD_LINE_BREAK: FormatElement = + FormatElement::Line(LineMode::Hard); + + self.queue + .extend_back(&[OPEN_PAREN, INDENT, HARD_LINE_BREAK]); + } + } + } + self.stack.push(TagKind::BestFitParenthesize, args); } FormatElement::Tag(EndBestFitParenthesize) => { // If this is the end tag of the outer most parentheses for which we measure if it fits, // pop the indent. - if args.mode().is_expanded() && self.stack.top_kind() == Some(TagKind::Indent) { + if self.stack.top_kind() == Some(TagKind::Indent) { self.stack.pop(TagKind::Indent).unwrap(); let unindented = self.stack.pop(TagKind::BestFitParenthesize)?; // There's a hard line break after the indent but don't return `Fits::Yes` here // to ensure any trailing comments (that, unfortunately, are attached to the statement and not the expression) // fit too. - self.state.line_width = 0; + self.state.line_width = 1; self.state.pending_indent = unindented.indention(); + // Don't measure the `)` similar to the open parentheses but increment the line width. return Ok(self.fits_text(Text::Token(")"), unindented)); - } - self.stack.pop(TagKind::BestFitParenthesize)?; + // let indented = self.stack.pop(TagKind::Indent).unwrap(); + // self.stack.pop(TagKind::BestFitParenthesize)?; + // + // const END_INDENT: FormatElement = FormatElement::Tag(Tag::EndIndent); + // const HARD_LINE_BREAK: FormatElement = FormatElement::Line(LineMode::Hard); + // const CLOSE_PAREN: FormatElement = FormatElement::Token { text: ")" }; + // + // // I believe the new implementation is incorrect because it doesn't measure + // // all lines anymore? Because all lines is associated with the `BestFitParenthesize` element and we popped that one. + // self.queue + // .extend_back(&[END_INDENT, HARD_LINE_BREAK, CLOSE_PAREN]); + // self.stack.push(TagKind::Indent, indented); + } else { + self.stack.pop(TagKind::BestFitParenthesize)?; + } } FormatElement::Tag(StartConditionalGroup(group)) => { diff --git a/crates/ruff_python_formatter/src/builders.rs b/crates/ruff_python_formatter/src/builders.rs index e4e2909a4a6dde..c03a7e7f043586 100644 --- a/crates/ruff_python_formatter/src/builders.rs +++ b/crates/ruff_python_formatter/src/builders.rs @@ -1,4 +1,4 @@ -use ruff_formatter::{write, Argument, Arguments}; +use ruff_formatter::{write, Argument, Arguments, GroupId}; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::context::{NodeLevel, WithNodeLevel}; @@ -12,12 +12,14 @@ where { ParenthesizeIfExpands { inner: Argument::new(content), + group_id: None, indent: true, } } pub(crate) struct ParenthesizeIfExpands<'a, 'ast> { inner: Argument<'a, PyFormatContext<'ast>>, + group_id: Option, indent: bool, } @@ -26,6 +28,11 @@ impl ParenthesizeIfExpands<'_, '_> { self.indent = indent; self } + + pub(crate) fn with_group_id(mut self, id: Option) -> Self { + self.group_id = id; + self + } } impl<'ast> Format> for ParenthesizeIfExpands<'_, 'ast> { @@ -45,7 +52,8 @@ impl<'ast> Format> for ParenthesizeIfExpands<'_, 'ast> { }; if_group_breaks(&token(")")).fmt(f) - }))] + })) + .with_group_id(self.group_id)] ) } } diff --git a/crates/ruff_python_formatter/src/context.rs b/crates/ruff_python_formatter/src/context.rs index eb8fe7edf4155e..e12d8bb503926e 100644 --- a/crates/ruff_python_formatter/src/context.rs +++ b/crates/ruff_python_formatter/src/context.rs @@ -74,6 +74,10 @@ impl<'a> PyFormatContext<'a> { ..self } } + + pub(crate) const fn is_preview(&self) -> bool { + self.options.is_preview() + } } impl FormatContext for PyFormatContext<'_> { diff --git a/crates/ruff_python_formatter/src/expression/expr_call.rs b/crates/ruff_python_formatter/src/expression/expr_call.rs index 47cd6502558e08..4e048555069934 100644 --- a/crates/ruff_python_formatter/src/expression/expr_call.rs +++ b/crates/ruff_python_formatter/src/expression/expr_call.rs @@ -106,7 +106,15 @@ impl NeedsParentheses for ExprCall { ) { OptionalParentheses::Never } else { - self.func.needs_parentheses(self.into(), context) + match self.func.needs_parentheses(self.into(), context) { + // TODO: document, although I'm not entirely sure why the exception + // and fear that there are cases where this should not apply. Revisit later + // Helps with `function()` should be parenthesized but `function(args...)` should not. + OptionalParentheses::BestFit if self.arguments.is_empty() => { + OptionalParentheses::Multiline + } + parentheses => parentheses, + } } } } diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index 06cbe3bc2cebd7..ebfd3494ac4cf2 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -1,6 +1,7 @@ use std::cmp::Ordering; use std::slice; +use ruff_formatter::prelude::tag::BestFitParenthesizeMode; use ruff_formatter::{ write, FormatOwnedWithRule, FormatRefWithRule, FormatRule, FormatRuleWithOptions, }; @@ -21,6 +22,7 @@ use crate::expression::parentheses::{ OptionalParentheses, Parentheses, Parenthesize, }; use crate::prelude::*; +use crate::preview::is_prefer_splitting_right_hand_side_of_assignments_enabled; use crate::PyFormatOptions; mod binary_like; @@ -439,8 +441,22 @@ impl Format> for MaybeParenthesizeExpression<'_> { let group_id = f.group_id("optional_parentheses"); let f = &mut WithNodeLevel::new(NodeLevel::Expression(Some(group_id)), f); + let mode = if is_prefer_splitting_right_hand_side_of_assignments_enabled( + f.context(), + ) && (parent.is_stmt_assign() + || parent.is_stmt_aug_assign() + || parent.is_stmt_ann_assign() + || parent.is_stmt_return() + || parent.is_stmt_type_alias()) + { + BestFitParenthesizeMode::Group + } else { + BestFitParenthesizeMode::BestFit + }; + best_fit_parenthesize(&expression.format().with_options(Parentheses::Never)) .with_group_id(Some(group_id)) + .with_mode(mode) .fmt(f) } } diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index e9a3c34757da96..2981521941f897 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -32,6 +32,7 @@ mod options; pub(crate) mod other; pub(crate) mod pattern; mod prelude; +mod preview; mod shared_traits; pub(crate) mod statement; pub(crate) mod type_param; @@ -180,7 +181,7 @@ mod tests { use ruff_python_parser::{parse_ok_tokens, AsMode}; - use crate::{format_module_ast, format_module_source, PyFormatOptions}; + use crate::{format_module_ast, format_module_source, PreviewMode, PyFormatOptions}; /// Very basic test intentionally kept very similar to the CLI #[test] @@ -208,13 +209,7 @@ if True: #[test] fn quick_test() { let source = r#" -def main() -> None: - if True: - some_very_long_variable_name_abcdefghijk = Foo() - some_very_long_variable_name_abcdefghijk = some_very_long_variable_name_abcdefghijk[ - some_very_long_variable_name_abcdefghijk.some_very_long_attribute_name - == "This is a very long string abcdefghijk" - ] +this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = 0 "#; let source_type = PySourceType::Python; @@ -223,7 +218,8 @@ def main() -> None: // Parse the AST. let source_path = "code_inline.py"; let module = parse_ok_tokens(tokens, source, source_type.as_mode(), source_path).unwrap(); - let options = PyFormatOptions::from_extension(Path::new(source_path)); + let options = PyFormatOptions::from_extension(Path::new(source_path)) + .with_preview(PreviewMode::Enabled); let formatted = format_module_ast(&module, &comment_ranges, source, options).unwrap(); // Uncomment the `dbg` to print the IR. diff --git a/crates/ruff_python_formatter/src/options.rs b/crates/ruff_python_formatter/src/options.rs index 261dfeab190ffb..46b8d3eec0c3a4 100644 --- a/crates/ruff_python_formatter/src/options.rs +++ b/crates/ruff_python_formatter/src/options.rs @@ -123,6 +123,10 @@ impl PyFormatOptions { self.preview } + pub const fn is_preview(&self) -> bool { + self.preview.is_enabled() + } + #[must_use] pub fn with_indent_width(mut self, indent_width: IndentWidth) -> Self { self.indent_width = indent_width; diff --git a/crates/ruff_python_formatter/src/preview.rs b/crates/ruff_python_formatter/src/preview.rs new file mode 100644 index 00000000000000..9250f6840ac11e --- /dev/null +++ b/crates/ruff_python_formatter/src/preview.rs @@ -0,0 +1,14 @@ +///! Helpers to test if a specific preview style is enabled or not. +/// +/// The motivation for these functions isn't to avoid code duplication but to ease promoting preview styles +/// to stable. The challenge with directly using [`is_preview`](PyFormatContext::is_preview) is that it is unclear +/// for which specific feature this preview check is for. Having named functions simplifies the promotion: +/// Simply delete the function and let Rust tell you which checks you have to remove. +use crate::PyFormatContext; + +/// Returns `true` if the [`prefer_splitting_right_hand_side_of_assignments`](https://github.com/astral-sh/ruff/issues/6975) layout is enabled. +pub(crate) const fn is_prefer_splitting_right_hand_side_of_assignments_enabled( + context: &PyFormatContext, +) -> bool { + context.is_preview() +} diff --git a/crates/ruff_python_formatter/src/statement/stmt_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_assign.rs index 5044e450bbee05..f92b8bd79955ab 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_assign.rs @@ -1,3 +1,5 @@ +use crate::builders::parenthesize_if_expands; +use ruff_formatter::prelude::tag::BestFitParenthesizeMode; use ruff_formatter::{format_args, write, FormatError}; use ruff_python_ast::{AnyNodeRef, Expr, StmtAssign}; @@ -8,6 +10,7 @@ use crate::expression::parentheses::{ }; use crate::expression::{has_own_parentheses, maybe_parenthesize_expression}; use crate::prelude::*; +use crate::preview::is_prefer_splitting_right_hand_side_of_assignments_enabled; use crate::statement::trailing_semicolon; #[derive(Default)] @@ -25,24 +28,29 @@ impl FormatNodeRule for FormatStmtAssign { "Expected at least on assignment target", ))?; - write!( - f, - [ - first.format(), - space(), - token("="), - space(), - FormatTargets { targets: rest } - ] - )?; + write!(f, [first.format(), space(), token("="), space()])?; + + if is_prefer_splitting_right_hand_side_of_assignments_enabled(f.context()) { + for target in rest { + if has_own_parentheses(&target, f.context()).is_some() { + target.format().with_options(Parentheses::Never).fmt(f)?; + } else { + parenthesize_if_expands(&target.format().with_options(Parentheses::Never)) + .fmt(f)?; + } + write!(f, [space(), token("="), space()])?; + } + } else { + FormatTargets { targets: rest }.fmt(f)?; + } + // We could handle the trailing comments here similar to yield FormatStatementsLastExpression::new(value, item).fmt(f)?; if f.options().source_type().is_ipynb() && f.context().node_level().is_last_top_level_statement() - && rest.is_empty() - && first.is_name_expr() && trailing_semicolon(item.into(), f.context().source()).is_some() + && matches!(targets.as_slice(), [Expr::Name(_)]) { token(";").fmt(f)?; } @@ -237,9 +245,7 @@ impl Format> for FormatStatementsLastExpression<'_> { }; let group_id = f.group_id("optional_parentheses"); - let f = &mut WithNodeLevel::new(NodeLevel::Expression(Some(group_id)), f); - - best_fit_parenthesize(&format_with(|f| { + let format_content = format_with(|f| { inline_comments.mark_formatted(); self.expression @@ -255,9 +261,41 @@ impl Format> for FormatStatementsLastExpression<'_> { } Ok(()) - })) - .with_group_id(Some(group_id)) - .fmt(f)?; + }); + + let best_fit_layout = + if is_prefer_splitting_right_hand_side_of_assignments_enabled(f.context()) { + match self.parent { + AnyNodeRef::StmtAssign(StmtAssign { targets, .. }) => { + matches!(targets.as_slice(), [Expr::Name(_)]) + } + _ => false, + } + } else { + true + }; + + // Black always parenthesizes if the right side is splittable, either because it has multiple targets OR + // the expression itself can be split (not a name or attribute chain with names only). + if best_fit_layout { + let f = &mut WithNodeLevel::new(NodeLevel::Expression(Some(group_id)), f); + + let mode = + if is_prefer_splitting_right_hand_side_of_assignments_enabled(f.context()) { + BestFitParenthesizeMode::Group + } else { + BestFitParenthesizeMode::BestFit + }; + + best_fit_parenthesize(&format_content) + .with_group_id(Some(group_id)) + .with_mode(mode) + .fmt(f)?; + } else { + parenthesize_if_expands(&format_content) + .with_group_id(Some(group_id)) + .fmt(f)?; + } if !inline_comments.is_empty() { // If the line fits into the line width, format the comments after the parenthesized expression diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings__east_asian_width.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings__east_asian_width.py.snap index 7ba67786746f8e..66b75a75e18bfc 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings__east_asian_width.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings__east_asian_width.py.snap @@ -18,26 +18,25 @@ japanese = 'コードポイントの数は少ないが、実際の端末エミ ```diff --- Black +++ Ruff -@@ -1,16 +1,6 @@ - # The following strings do not have not-so-many chars, but are long enough +@@ -2,15 +2,11 @@ # when these are rendered in a monospace font (if the renderer respects # Unicode East Asian Width properties). --hangul = ( + hangul = ( - "코드포인트 수는 적으나 실제 터미널이나 에디터에서 렌더링될 땐 너무 길어서 줄바꿈이" - " 필요한 문자열" --) --hanzi = ( ++ "코드포인트 수는 적으나 실제 터미널이나 에디터에서 렌더링될 땐 너무 길어서 줄바꿈이 필요한 문자열" + ) + hanzi = ( - "中文測試:代碼點數量少,但在真正的終端模擬器或編輯器中呈現時太長," - "因此需要換行的字符串。" --) --japanese = ( ++ "中文測試:代碼點數量少,但在真正的終端模擬器或編輯器中呈現時太長,因此需要換行的字符串。" + ) + japanese = ( - "コードポイントの数は少ないが、" - "実際の端末エミュレータやエディタでレンダリングされる時は長すぎる為、" - "改行が要る文字列" --) -+hangul = "코드포인트 수는 적으나 실제 터미널이나 에디터에서 렌더링될 땐 너무 길어서 줄바꿈이 필요한 문자열" -+hanzi = "中文測試:代碼點數量少,但在真正的終端模擬器或編輯器中呈現時太長,因此需要換行的字符串。" -+japanese = "コードポイントの数は少ないが、実際の端末エミュレータやエディタでレンダリングされる時は長すぎる為、改行が要る文字列" ++ "コードポイントの数は少ないが、実際の端末エミュレータやエディタでレンダリングされる時は長すぎる為、改行が要る文字列" + ) ``` ## Ruff Output @@ -46,9 +45,15 @@ japanese = 'コードポイントの数は少ないが、実際の端末エミ # The following strings do not have not-so-many chars, but are long enough # when these are rendered in a monospace font (if the renderer respects # Unicode East Asian Width properties). -hangul = "코드포인트 수는 적으나 실제 터미널이나 에디터에서 렌더링될 땐 너무 길어서 줄바꿈이 필요한 문자열" -hanzi = "中文測試:代碼點數量少,但在真正的終端模擬器或編輯器中呈現時太長,因此需要換行的字符串。" -japanese = "コードポイントの数は少ないが、実際の端末エミュレータやエディタでレンダリングされる時は長すぎる為、改行が要る文字列" +hangul = ( + "코드포인트 수는 적으나 실제 터미널이나 에디터에서 렌더링될 땐 너무 길어서 줄바꿈이 필요한 문자열" +) +hanzi = ( + "中文測試:代碼點數量少,但在真正的終端模擬器或編輯器中呈現時太長,因此需要換行的字符串。" +) +japanese = ( + "コードポイントの数は少ないが、実際の端末エミュレータやエディタでレンダリングされる時は長すぎる為、改行が要る文字列" +) ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings__edge_case.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings__edge_case.py.snap index 4db3fa039c2a8a..46fbdc92295ad3 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings__edge_case.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings__edge_case.py.snap @@ -49,25 +49,24 @@ msg += "This long string should not be split at any point ever since it is just ```diff --- Black +++ Ruff -@@ -12,51 +12,33 @@ - some_variable = ( +@@ -13,50 +13,36 @@ "This string is long, just long enough that it needs to be split, u get? So we stay" ) --some_variable = ( + some_variable = ( - "This string is long, just long enough that it needs to be split, u get? So we" - " split" --) --some_variable = ( ++ "This string is long, just long enough that it needs to be split, u get? So we split" + ) + some_variable = ( - "This string is long, just long enough that it needs to be split, u get? So we" - " split" --) ++ "This string is long, just long enough that it needs to be split, u get? So we split" + ) -some_variable = ( - "This string is long but not so long that it needs hahahah toooooo be so greatttt" - " {} that I just can't think of any more good words to say about it at alll".format( - "ha" - ) -+some_variable = "This string is long, just long enough that it needs to be split, u get? So we split" -+some_variable = "This string is long, just long enough that it needs to be split, u get? So we split" +some_variable = "This string is long but not so long that it needs hahahah toooooo be so greatttt {} that I just can't think of any more good words to say about it at alll".format( + "ha" ) @@ -114,7 +113,7 @@ msg += "This long string should not be split at any point ever since it is just ) return ( "Hi there. This is areally really reallllly long string that needs to be split!!!" -@@ -64,9 +46,7 @@ +@@ -64,35 +50,31 @@ ternary_expression = ( "Short String" if some_condition @@ -125,8 +124,11 @@ msg += "This long string should not be split at any point ever since it is just ) return ( f"{x}/b/c/d/d/d/dadfjsadjsaidoaisjdsfjaofjdfijaidfjaodfjaoifjodjafojdoajaaaaaaaaaaa" -@@ -74,25 +54,19 @@ - return f"{x}/b/c/d/d/d/dadfjsadjsaidoaisjdsfjaofjdfijaidfjaodfjaoifjodjafojdoajaaaaaaaaaaaa" + ) +-return f"{x}/b/c/d/d/d/dadfjsadjsaidoaisjdsfjaofjdfijaidfjaodfjaoifjodjafojdoajaaaaaaaaaaaa" ++return ( ++ f"{x}/b/c/d/d/d/dadfjsadjsaidoaisjdsfjaofjdfijaidfjaodfjaoifjodjafojdoajaaaaaaaaaaaa" ++) assert ( str(result) - == "This long string should be split at some point right close to or around" @@ -149,11 +151,11 @@ msg += "This long string should not be split at any point ever since it is just msg += ( "This long string should be wrapped in parens at some point right around hereeeee" ) --msg += ( + msg += ( - "This long string should be split at some point right close to or around" - " hereeeeeeee" --) -+msg += "This long string should be split at some point right close to or around hereeeeeeee" ++ "This long string should be split at some point right close to or around hereeeeeeee" + ) msg += "This long string should not be split at any point ever since it is just righttt" ``` @@ -174,8 +176,12 @@ some_variable = ( some_variable = ( "This string is long, just long enough that it needs to be split, u get? So we stay" ) -some_variable = "This string is long, just long enough that it needs to be split, u get? So we split" -some_variable = "This string is long, just long enough that it needs to be split, u get? So we split" +some_variable = ( + "This string is long, just long enough that it needs to be split, u get? So we split" +) +some_variable = ( + "This string is long, just long enough that it needs to be split, u get? So we split" +) some_variable = "This string is long but not so long that it needs hahahah toooooo be so greatttt {} that I just can't think of any more good words to say about it at alll".format( "ha" ) @@ -213,7 +219,9 @@ ternary_expression = ( return ( f"{x}/b/c/d/d/d/dadfjsadjsaidoaisjdsfjaofjdfijaidfjaodfjaoifjodjafojdoajaaaaaaaaaaa" ) -return f"{x}/b/c/d/d/d/dadfjsadjsaidoaisjdsfjaofjdfijaidfjaodfjaoifjodjafojdoajaaaaaaaaaaaa" +return ( + f"{x}/b/c/d/d/d/dadfjsadjsaidoaisjdsfjaofjdfijaidfjaodfjaoifjodjafojdoajaaaaaaaaaaaa" +) assert ( str(result) == "This long string should be split at some point right close to or around hereeeeeee" @@ -230,7 +238,9 @@ assert ( msg += ( "This long string should be wrapped in parens at some point right around hereeeee" ) -msg += "This long string should be split at some point right close to or around hereeeeeeee" +msg += ( + "This long string should be split at some point right close to or around hereeeeeeee" +) msg += "This long string should not be split at any point ever since it is just righttt" ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_prefer_rhs_split.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_prefer_rhs_split.py.snap index 18b9fa9a062682..e799efc3145f69 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_prefer_rhs_split.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_prefer_rhs_split.py.snap @@ -118,57 +118,7 @@ a = ( ```diff --- Black +++ Ruff -@@ -1,29 +1,31 @@ --first_item, second_item = ( -- some_looooooooong_module.some_looooooooooooooong_function_name( -- first_argument, second_argument, third_argument -- ) -+( -+ first_item, -+ second_item, -+) = some_looooooooong_module.some_looooooooooooooong_function_name( -+ first_argument, second_argument, third_argument - ) - --some_dict["with_a_long_key"] = ( -- some_looooooooong_module.some_looooooooooooooong_function_name( -- first_argument, second_argument, third_argument -- ) -+some_dict[ -+ "with_a_long_key" -+] = some_looooooooong_module.some_looooooooooooooong_function_name( -+ first_argument, second_argument, third_argument - ) - - # Make sure it works when the RHS only has one pair of (optional) parens. --first_item, second_item = ( -- some_looooooooong_module.SomeClass.some_looooooooooooooong_variable_name --) -+( -+ first_item, -+ second_item, -+) = some_looooooooong_module.SomeClass.some_looooooooooooooong_variable_name - --some_dict["with_a_long_key"] = ( -- some_looooooooong_module.SomeClass.some_looooooooooooooong_variable_name --) -+some_dict[ -+ "with_a_long_key" -+] = some_looooooooong_module.SomeClass.some_looooooooooooooong_variable_name - - # Make sure chaining assignments work. --first_item, second_item, third_item, forth_item = m["everything"] = ( -- some_looooooooong_module.some_looooooooooooooong_function_name( -- first_argument, second_argument, third_argument -- ) -+first_item, second_item, third_item, forth_item = m[ -+ "everything" -+] = some_looooooooong_module.some_looooooooooooooong_function_name( -+ first_argument, second_argument, third_argument - ) - - # Make sure when the RHS's first split at the non-optional paren fits, -@@ -60,9 +62,7 @@ +@@ -60,9 +60,7 @@ some_arg ).intersection(pk_cols) @@ -179,76 +129,37 @@ a = ( some_kind_of_table[ some_key # type: ignore # noqa: E501 -@@ -85,15 +85,29 @@ - ) - - # Multiple targets --a = b = ( -- ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc --) -+a = ( -+ b -+) = ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc - --a = b = c = d = e = f = g = ( -+a = ( -+ b -+) = ( -+ c -+) = ( -+ d -+) = ( -+ e -+) = ( -+ f -+) = ( -+ g -+) = ( - hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh --) = i = j = ( -- kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk --) -+) = ( -+ i -+) = ( -+ j -+) = kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk - - a = ( - bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ``` ## Ruff Output ```python -( - first_item, - second_item, -) = some_looooooooong_module.some_looooooooooooooong_function_name( - first_argument, second_argument, third_argument +first_item, second_item = ( + some_looooooooong_module.some_looooooooooooooong_function_name( + first_argument, second_argument, third_argument + ) ) -some_dict[ - "with_a_long_key" -] = some_looooooooong_module.some_looooooooooooooong_function_name( - first_argument, second_argument, third_argument +some_dict["with_a_long_key"] = ( + some_looooooooong_module.some_looooooooooooooong_function_name( + first_argument, second_argument, third_argument + ) ) # Make sure it works when the RHS only has one pair of (optional) parens. -( - first_item, - second_item, -) = some_looooooooong_module.SomeClass.some_looooooooooooooong_variable_name +first_item, second_item = ( + some_looooooooong_module.SomeClass.some_looooooooooooooong_variable_name +) -some_dict[ - "with_a_long_key" -] = some_looooooooong_module.SomeClass.some_looooooooooooooong_variable_name +some_dict["with_a_long_key"] = ( + some_looooooooong_module.SomeClass.some_looooooooooooooong_variable_name +) # Make sure chaining assignments work. -first_item, second_item, third_item, forth_item = m[ - "everything" -] = some_looooooooong_module.some_looooooooooooooong_function_name( - first_argument, second_argument, third_argument +first_item, second_item, third_item, forth_item = m["everything"] = ( + some_looooooooong_module.some_looooooooooooooong_function_name( + first_argument, second_argument, third_argument + ) ) # Make sure when the RHS's first split at the non-optional paren fits, @@ -308,29 +219,15 @@ some_kind_of_instance.some_kind_of_map[a_key] = ( ) # Multiple targets -a = ( - b -) = ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc +a = b = ( + ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc +) -a = ( - b -) = ( - c -) = ( - d -) = ( - e -) = ( - f -) = ( - g -) = ( +a = b = c = d = e = f = g = ( hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh -) = ( - i -) = ( - j -) = kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +) = i = j = ( + kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +) a = ( bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb diff --git a/crates/ruff_python_formatter/tests/snapshots/format@parentheses__opening_parentheses_comment_value.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@parentheses__opening_parentheses_comment_value.py.snap index 2dc81368793b15..3d1392791bc68e 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@parentheses__opening_parentheses_comment_value.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@parentheses__opening_parentheses_comment_value.py.snap @@ -348,4 +348,21 @@ f5 = { # f5 ``` +## Preview changes +```diff +--- Stable ++++ Preview +@@ -9,7 +9,8 @@ + a3 = f( # a 3 + x + ) +-a4 = ( # a 4 ++a4 = ( ++ # a 4 + x + ) = a4 + a5: List( # a 5 +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap index c3504c47f856c5..3312c4ba87ba3e 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap @@ -125,9 +125,9 @@ def f(): bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ] = cccccccc.ccccccccccccc.cccccccc - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[ - bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb - ] = cccccccc.ccccccccccccc().cccccccc + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb] = ( + cccccccc.ccccccccccccc().cccccccc + ) aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb @@ -202,17 +202,17 @@ class RemoveNewlineBeforeClassDocstring: def f(): """Black's `Preview.prefer_splitting_right_hand_side_of_assignments`""" - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[ - bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb - ] = cccccccc.ccccccccccccc.cccccccc + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb] = ( + cccccccc.ccccccccccccc.cccccccc + ) - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[ - bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb - ] = cccccccc.ccccccccccccc().cccccccc + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb] = ( + cccccccc.ccccccccccccc().cccccccc + ) - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[ - bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb - ] = cccccccc.ccccccccccccc(d).cccccccc + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb] = ( + cccccccc.ccccccccccccc(d).cccccccc + ) aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb] = ( cccccccc.ccccccccccccc(d).cccccccc + e @@ -226,12 +226,12 @@ def f(): + eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee ) - self._cache: dict[ - DependencyCacheKey, list[list[DependencyPackage]] - ] = collections.defaultdict(list) - self._cached_dependencies_by_level: dict[ - int, list[DependencyCacheKey] - ] = collections.defaultdict(list) + self._cache: dict[DependencyCacheKey, list[list[DependencyPackage]]] = ( + collections.defaultdict(list) + ) + self._cached_dependencies_by_level: dict[int, list[DependencyCacheKey]] = ( + collections.defaultdict(list) + ) ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__ann_assign.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__ann_assign.py.snap index 69490f3caf53c9..f21d2ad10adb30 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__ann_assign.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__ann_assign.py.snap @@ -39,9 +39,9 @@ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = ( Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb() ) -bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: ( - Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb -) = Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb() +bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: (Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb) = ( + Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb() +) JSONSerializable: TypeAlias = ( "str | int | float | bool | None | list | tuple | JSONMapping" @@ -67,4 +67,21 @@ class DefaultRunner: ``` +## Preview changes +```diff +--- Stable ++++ Preview +@@ -29,6 +29,6 @@ + + # Regression test: Don't forget the parentheses in the annotation when breaking + class DefaultRunner: +- task_runner_cls: TaskRunnerProtocol | typing.Callable[ +- [], typing.Any +- ] = DefaultTaskRunner ++ task_runner_cls: TaskRunnerProtocol | typing.Callable[[], typing.Any] = ( ++ DefaultTaskRunner ++ ) +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__assign.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__assign.py.snap index 075824ab64f325..8e4337a298ade4 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__assign.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__assign.py.snap @@ -154,4 +154,83 @@ def main() -> None: ``` +## Preview changes +```diff +--- Stable ++++ Preview +@@ -1,7 +1,5 @@ + # break left hand side +-a1akjdshflkjahdslkfjlasfdahjlfds = ( +- bakjdshflkjahdslkfjlasfdahjlfds +-) = ( ++a1akjdshflkjahdslkfjlasfdahjlfds = bakjdshflkjahdslkfjlasfdahjlfds = ( + cakjdshflkjahdslkfjlasfdahjlfds + ) = kjaödkjaföjfahlfdalfhaöfaöfhaöfha = fkjaödkjaföjfahlfdalfhaöfaöfhaöfha = g = 3 + +@@ -9,30 +7,32 @@ + a2 = b2 = 2 + + # Break the last element +-a = ( +- asdf +-) = ( ++a = asdf = ( + fjhalsdljfalflaflapamsakjsdhflakjdslfjhalsdljfalflaflapamsakjsdhflakjdslfjhalsdljfal + ) = 1 + +-aa = [ +- bakjdshflkjahdslkfjlasfdahjlfds +-] = dddd = ddd = fkjaödkjaföjfahlfdalfhaöfaöfhaöfha = g = [3] ++aa = [bakjdshflkjahdslkfjlasfdahjlfds] = dddd = ddd = ( ++ fkjaödkjaföjfahlfdalfhaöfaöfhaöfha ++) = g = [3] + + aa = [] = dddd = ddd = fkjaödkjaföjfahlfdalfhaöfaöfhaöfha = g = [3] + +-aa = [ +- # foo +-] = dddd = ddd = fkjaödkjaföjfahlfdalfhaöfaöfhaöfha = g = [3] ++aa = ( ++ [ ++ # foo ++ ] ++) = dddd = ddd = fkjaödkjaföjfahlfdalfhaöfaöfhaöfha = g = [3] + + aa = [] = dddd = ddd = fkjaödkjaföjfahlfdalfhaöfaöfhaöfha = g = [3] + +-aaaa = ( # trailing ++aaaa = ( ++ # trailing + # comment + bbbbb + ) = cccccccccccccccc = 3 + +-x = ( # comment ++x = ( ++ # comment + [ # comment + a, + b, +@@ -51,11 +51,13 @@ + ) = 1 + + +-x = [ # comment +- a, +- b, +- c, +-] = 1 ++x = ( ++ [ # comment ++ a, ++ b, ++ c, ++ ] ++) = 1 + + + def main() -> None: +``` + +