From e6977a2877df3aeda8c8f4fdc087b0c8df4dcddc Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 30 Nov 2023 09:58:50 +0800 Subject: [PATCH] Prefer splitting right hand side of assignments --- crates/ruff_formatter/src/builders.rs | 11 +- .../src/format_element/document.rs | 13 +- .../ruff_formatter/src/format_element/tag.rs | 10 ++ crates/ruff_formatter/src/printer/mod.rs | 48 ++++- crates/ruff_python_formatter/src/builders.rs | 12 +- crates/ruff_python_formatter/src/context.rs | 4 + .../src/expression/mod.rs | 16 ++ crates/ruff_python_formatter/src/lib.rs | 14 +- crates/ruff_python_formatter/src/options.rs | 4 + crates/ruff_python_formatter/src/preview.rs | 14 ++ .../src/statement/stmt_assign.rs | 74 ++++++-- ...es__pep604_union_types_line_breaks.py.snap | 16 +- ...bility@cases__preview_long_strings.py.snap | 57 +++--- ...s__preview_long_strings__edge_case.py.snap | 45 +++-- ...__preview_long_strings__regression.py.snap | 34 ++-- ...ty@cases__preview_prefer_rhs_split.py.snap | 155 +++------------- ...ion__optional_parentheses_comments.py.snap | 166 ++++++++++++++++++ .../format@expression__unsplittable.py.snap | 23 +++ ..._opening_parentheses_comment_value.py.snap | 17 ++ .../tests/snapshots/format@preview.py.snap | 30 ++-- .../format@statement__ann_assign.py.snap | 30 ++++ .../format@statement__assign.py.snap | 79 +++++++++ 22 files changed, 612 insertions(+), 260 deletions(-) create mode 100644 crates/ruff_python_formatter/src/preview.rs 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/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__pep604_union_types_line_breaks.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__pep604_union_types_line_breaks.py.snap index 278a20e4a66501..b6349e7946e3d9 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__pep604_union_types_line_breaks.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__pep604_union_types_line_breaks.py.snap @@ -95,7 +95,7 @@ def f( ```diff --- Black +++ Ruff -@@ -7,26 +7,16 @@ +@@ -7,26 +7,18 @@ ) # "AnnAssign"s now also work @@ -123,14 +123,16 @@ def f( -z: Short | Short2 | Short3 | Short4 = 8 -z: int = 2.3 -z: int = foo() -+z: Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong = 7 ++z: Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong = ( ++ 7 ++) +z: (Short | Short2 | Short3 | Short4) = 8 +z: (int) = 2.3 +z: (int) = foo() # In case I go for not enforcing parantheses, this might get improved at the same time x = ( -@@ -63,7 +53,7 @@ +@@ -63,7 +55,7 @@ # remove unnecessary paren @@ -139,7 +141,7 @@ def f( # this is a syntax error in the type annotation according to mypy, but it's not invalid *python* code, so make sure we don't mess with it and make it so. -@@ -72,12 +62,10 @@ +@@ -72,12 +64,10 @@ def foo( i: int, @@ -156,7 +158,7 @@ def f( *, s: str, ) -> None: -@@ -88,7 +76,7 @@ +@@ -88,7 +78,7 @@ async def foo( q: str | None = Query( None, title="Some long title", description="Some long description" @@ -185,7 +187,9 @@ z: (int) z: (int) -z: Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong = 7 +z: Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong = ( + 7 +) z: (Short | Short2 | Short3 | Short4) = 8 z: (int) = 2.3 z: (int) = foo() diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings.py.snap index b3a03820fa4cad..1e1c84f8b3ab8a 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings.py.snap @@ -341,7 +341,7 @@ log.info(f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share ```diff --- Black +++ Ruff -@@ -1,175 +1,108 @@ +@@ -1,175 +1,110 @@ -x = ( - "This is a really long string that can't possibly be expected to fit all together" - " on one line. In fact it may even take up three or more lines... like four or" @@ -349,12 +349,12 @@ log.info(f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share -) +x = "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." --x += ( + x += ( - "This is a really long string that can't possibly be expected to fit all together" - " on one line. In fact it may even take up three or more lines... like four or" - " five... but probably just three." --) -+x += "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." ++ "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + ) y = "Short string" @@ -560,7 +560,7 @@ log.info(f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share ) bad_split2 = ( -@@ -205,11 +138,9 @@ +@@ -205,11 +140,9 @@ xxx, yyy, zzz, @@ -575,7 +575,7 @@ log.info(f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share ) bad_split_func3( -@@ -228,29 +159,28 @@ +@@ -228,29 +161,28 @@ ) inline_comments_func1( @@ -615,7 +615,7 @@ log.info(f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share ) fmt_string2 = "But what about when the string is {} but {}".format( -@@ -259,8 +189,8 @@ +@@ -259,8 +191,8 @@ ) old_fmt_string1 = ( @@ -626,7 +626,7 @@ log.info(f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share ) old_fmt_string2 = "This is a %s %s %s %s" % ( -@@ -271,8 +201,7 @@ +@@ -271,8 +203,7 @@ ) old_fmt_string3 = ( @@ -636,7 +636,7 @@ log.info(f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share % ( "really really really really really", "old", -@@ -281,26 +210,14 @@ +@@ -281,26 +212,14 @@ ) ) @@ -667,7 +667,7 @@ log.info(f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share "Arg #2", "Arg #3", "Arg #4", -@@ -315,80 +232,72 @@ +@@ -315,80 +234,72 @@ triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched.""" @@ -772,7 +772,7 @@ log.info(f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share x, y, z, -@@ -397,7 +306,7 @@ +@@ -397,7 +308,7 @@ func_with_bad_parens( x, y, @@ -781,22 +781,19 @@ log.info(f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share z, ) -@@ -408,50 +317,27 @@ - + CONCATENATED +@@ -409,49 +320,28 @@ + "using the '+' operator." ) --annotated_variable: Final = ( + annotated_variable: Final = ( - "This is a large string that has a type annotation attached to it. A type" - " annotation should NOT stop a long string from being wrapped." --) --annotated_variable: Literal["fakse_literal"] = ( ++ "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." + ) + annotated_variable: Literal["fakse_literal"] = ( - "This is a large string that has a type annotation attached to it. A type" - " annotation should NOT stop a long string from being wrapped." --) -+annotated_variable: Final = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." -+annotated_variable: Literal[ -+ "fakse_literal" -+] = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." ++ "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." + ) -backslashes = ( - "This is a really long string with \"embedded\" double quotes and 'single' quotes" @@ -843,7 +840,7 @@ log.info(f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share long_unmergable_string_with_pragma = ( "This is a really long string that can't be merged because it has a likely pragma at the end" # type: ignore -@@ -468,51 +354,24 @@ +@@ -468,51 +358,24 @@ " of it." ) @@ -904,7 +901,7 @@ log.info(f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share ) dict_with_lambda_values = { -@@ -524,61 +383,54 @@ +@@ -524,61 +387,54 @@ # Complex string concatenations with a method call in the middle. code = ( @@ -990,7 +987,9 @@ log.info(f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share ```python x = "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." -x += "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." +x += ( + "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." +) y = "Short string" @@ -1307,10 +1306,12 @@ annotated_variable: Final = ( + CONCATENATED + "using the '+' operator." ) -annotated_variable: Final = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." -annotated_variable: Literal[ - "fakse_literal" -] = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +annotated_variable: Final = ( + "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +) +annotated_variable: Literal["fakse_literal"] = ( + "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +) backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\" backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\\\" 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..ac2902bad29203 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 @@ -56,35 +56,35 @@ msg += "This long string should not be split at any point ever since it is just -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" + ) -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 allll".format( ++ "ha" + ) -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( ++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 alllllllllll".format( + "ha" ) -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 allll" - .format("ha") -+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 allll".format( -+ "ha" - ) +-) -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 alllllllllll" - .format("ha") -+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 alllllllllll".format( -+ "ha" - ) +-) -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" @@ -114,7 +114,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 +46,31 @@ ternary_expression = ( "Short String" if some_condition @@ -125,8 +125,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 +152,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" ``` @@ -213,7 +216,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 +235,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_long_strings__regression.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings__regression.py.snap index 1cde924609d48b..762af6aa16c7ee 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings__regression.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings__regression.py.snap @@ -832,7 +832,7 @@ s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry: some_commented_string = ( # This comment stays at the top. "This string is long but not so long that it needs hahahah toooooo be so greatttt" -@@ -279,38 +280,27 @@ +@@ -279,36 +280,25 @@ ) lpar_and_rpar_have_comments = func_call( # LPAR Comment @@ -852,33 +852,31 @@ s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry: - f" {'' if ID is None else ID} | perl -nE 'print if /^{field}:/'" -) +cmd_fstring = f"sudo -E deluge-console info --detailed --sort-reverse=time_added {'' if ID is None else ID} | perl -nE 'print if /^{field}:/'" -+ -+cmd_fstring = f"sudo -E deluge-console info --detailed --sort-reverse=time_added {'{{}}' if ID is None else ID} | perl -nE 'print if /^{field}:/'" -cmd_fstring = ( - "sudo -E deluge-console info --detailed --sort-reverse=time_added" - f" {'{{}}' if ID is None else ID} | perl -nE 'print if /^{field}:/'" -) -+cmd_fstring = f"sudo -E deluge-console info --detailed --sort-reverse=time_added {{'' if ID is None else ID}} | perl -nE 'print if /^{field}:/'" ++cmd_fstring = f"sudo -E deluge-console info --detailed --sort-reverse=time_added {'{{}}' if ID is None else ID} | perl -nE 'print if /^{field}:/'" -cmd_fstring = ( - "sudo -E deluge-console info --detailed --sort-reverse=time_added {'' if ID is" - f" None else ID}} | perl -nE 'print if /^{field}:/'" -) -+fstring = f"This string really doesn't need to be an {{{{fstring}}}}, but this one most certainly, absolutely {does}." ++cmd_fstring = f"sudo -E deluge-console info --detailed --sort-reverse=time_added {{'' if ID is None else ID}} | perl -nE 'print if /^{field}:/'" ++fstring = f"This string really doesn't need to be an {{{{fstring}}}}, but this one most certainly, absolutely {does}." ++ fstring = ( - "This string really doesn't need to be an {{fstring}}, but this one most" - f" certainly, absolutely {does}." + f"We have to remember to escape {braces}." " Like {these}." f" But not {this}." ) - +- -fstring = f"We have to remember to escape {braces}. Like {{these}}. But not {this}." -- + class A: - class B: - def foo(): @@ -364,10 +354,7 @@ def foo(): if not hasattr(module, name): @@ -933,7 +931,7 @@ s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry: ) -@@ -432,14 +415,12 @@ +@@ -432,9 +415,7 @@ assert xxxxxxx_xxxx in [ x.xxxxx.xxxxxx.xxxxx.xxxxxx, x.xxxxx.xxxxxx.xxxxx.xxxx, @@ -943,15 +941,7 @@ s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry: + ], "xxxxxxxxxxx xxxxxxx xxxx (xxxxxx xxxx) %x xxx xxxxx" % xxxxxxx_xxxx --value.__dict__[key] = ( -- "test" # set some Thrift field to non-None in the struct aa bb cc dd ee --) -+value.__dict__[ -+ key -+] = "test" # set some Thrift field to non-None in the struct aa bb cc dd ee - - RE_ONE_BACKSLASH = { - "asdf_hjkl_jkl": re.compile( + value.__dict__[key] = ( @@ -449,8 +430,7 @@ RE_TWO_BACKSLASHES = { @@ -1627,9 +1617,9 @@ class xxxxxxxxxxxxxxxxxxxxx(xxxx.xxxxxxxxxxxxx): ], "xxxxxxxxxxx xxxxxxx xxxx (xxxxxx xxxx) %x xxx xxxxx" % xxxxxxx_xxxx -value.__dict__[ - key -] = "test" # set some Thrift field to non-None in the struct aa bb cc dd ee +value.__dict__[key] = ( + "test" # set some Thrift field to non-None in the struct aa bb cc dd ee +) RE_ONE_BACKSLASH = { "asdf_hjkl_jkl": re.compile( 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@expression__optional_parentheses_comments.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__optional_parentheses_comments.py.snap index 4279cd03f29dba..e87ba67ac6a905 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__optional_parentheses_comments.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__optional_parentheses_comments.py.snap @@ -421,4 +421,170 @@ def test6(): ``` +## Preview changes +```diff +--- Stable ++++ Preview +@@ -72,19 +72,21 @@ + ## Breaking left + + # Should break `[a]` first +-____[ +- a +-] = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvv # c ++____[a] = ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvv # c ++) + +-____[ +- a +-] = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvv # cc ++____[a] = ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvv # cc ++) + + ( + # some weird comments + ____[aaaaaaaaa] + # some weird comments 2 +-) = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvv # c ++) = ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvv # c ++) + + # Preserve trailing assignment comments when the expression has own line comments + ____aaa = ( +@@ -110,12 +112,16 @@ + if True: + if True: + # Black layout +- model.config.use_cache = False # FSTM still requires this hack -> FSTM should probably be refactored s ++ model.config.use_cache = ( ++ False # FSTM still requires this hack -> FSTM should probably be refactored s ++ ) + + ## Annotated Assign + + # 88 characters unparenthesized +-____a: a = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvv # c ++____a: a = ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvv # c ++) + + # 88 characters + ____a: a = ( +@@ -123,10 +129,14 @@ + ) + + # 89 characters parenthesized (collapse) +-____a: a = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvv # c ++____a: a = ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvv # c ++) + + # 88 characters unparenthesized +-____a: a = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvv # c ++____a: a = ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvv # c ++) + + # 88 characters + ____a: a = ( +@@ -134,16 +144,20 @@ + ) + + # 89 characters parenthesized (collapse) +-____a: a = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvv # c ++____a: a = ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvv # c ++) + +-_a: a[ +- b +-] = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvv # c ++_a: a[b] = ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvv # c ++) + + ## Augmented Assign + + # 88 characters unparenthesized +-____aa += aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvv # c ++____aa += ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvv # c ++) + + # 88 characters + ____aa += ( +@@ -151,10 +165,14 @@ + ) + + # 89 characters parenthesized (collapse) +-____aa += aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvv # c ++____aa += ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvv # c ++) + + # 88 characters unparenthesized +-____aa += aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvv # c ++____aa += ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvv # c ++) + + # 88 characters + ____aa += ( +@@ -162,14 +180,18 @@ + ) + + # 89 characters parenthesized (collapse) +-____aa += aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvv # c ++____aa += ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvv # c ++) + + ## Return + + + def test(): + # 88 characters unparenthesized +- return aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvv # c ++ return ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvv # c ++ ) + + + def test2(): +@@ -181,7 +203,9 @@ + + def test3(): + # 89 characters parenthesized (collapse) +- return aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvv # c ++ return ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvv # c ++ ) + + + ## Return Parenthesized +@@ -189,7 +213,9 @@ + + def test4(): + # 88 characters unparenthesized +- return aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvv # c ++ return ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvv # c ++ ) + + + def test5(): +@@ -201,4 +227,6 @@ + + def test6(): + # 89 characters parenthesized (collapse) +- return aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvv # c ++ return ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvv # c ++ ) +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__unsplittable.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__unsplittable.py.snap index a40713b2d1f4c7..3aceb816b486b5 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__unsplittable.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__unsplittable.py.snap @@ -219,4 +219,27 @@ def f(): ``` +## Preview changes +```diff +--- Stable ++++ Preview +@@ -94,9 +94,13 @@ + if True: + if True: + # Black layout +- model.config.use_cache = False # FSTM still requires this hack -> FSTM should probably be refactored s ++ model.config.use_cache = ( ++ False # FSTM still requires this hack -> FSTM should probably be refactored s ++ ) + # Ruff layout +- model.config.use_cache = False # FSTM still requires this hack -> FSTM should probably be refactored s ++ model.config.use_cache = ( ++ False # FSTM still requires this hack -> FSTM should probably be refactored s ++ ) + + + # Regression test for https://github.com/astral-sh/ruff/issues/7463 +``` + + 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..8fd73124467f4e 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap @@ -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..0ea87da1f5709c 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 @@ -67,4 +67,34 @@ class DefaultRunner: ``` +## Preview changes +```diff +--- Stable ++++ Preview +@@ -7,9 +7,9 @@ + Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb() + ) + +-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: ( +- Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +-) = Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb() ++bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: (Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb) = ( ++ Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb() ++) + + JSONSerializable: TypeAlias = ( + "str | int | float | bool | None | list | tuple | JSONMapping" +@@ -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: +``` + +