diff --git a/Cargo.lock b/Cargo.lock index 386f03e630..a206ca6caa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2340,6 +2340,7 @@ dependencies = [ "insta", "is-macro", "itertools", + "memchr", "once_cell", "ruff_formatter", "ruff_python_ast", diff --git a/crates/ruff_formatter/src/builders.rs b/crates/ruff_formatter/src/builders.rs index 1e66f0b621..881a80e557 100644 --- a/crates/ruff_formatter/src/builders.rs +++ b/crates/ruff_formatter/src/builders.rs @@ -2534,17 +2534,17 @@ impl<'a, Context> BestFitting<'a, Context> { impl Format for BestFitting<'_, Context> { fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { - let mut buffer = VecBuffer::new(f.state_mut()); let variants = self.variants.items(); let mut formatted_variants = Vec::with_capacity(variants.len()); for variant in variants { + let mut buffer = VecBuffer::with_capacity(8, f.state_mut()); buffer.write_element(FormatElement::Tag(StartEntry)); buffer.write_fmt(Arguments::from(variant))?; buffer.write_element(FormatElement::Tag(EndEntry)); - formatted_variants.push(buffer.take_vec().into_boxed_slice()); + formatted_variants.push(buffer.into_vec().into_boxed_slice()); } // SAFETY: The constructor guarantees that there are always at least two variants. It's, therefore, diff --git a/crates/ruff_formatter/src/format_element/document.rs b/crates/ruff_formatter/src/format_element/document.rs index 07670d63e1..42bc2b1f39 100644 --- a/crates/ruff_formatter/src/format_element/document.rs +++ b/crates/ruff_formatter/src/format_element/document.rs @@ -122,8 +122,12 @@ impl Document { expands } - let mut enclosing: Vec = Vec::new(); - let mut interned: FxHashMap<&Interned, bool> = FxHashMap::default(); + let mut enclosing = Vec::with_capacity(if self.is_empty() { + 0 + } else { + self.len().ilog2() as usize + }); + let mut interned = FxHashMap::default(); propagate_expands(self, &mut enclosing, &mut interned); } @@ -657,18 +661,17 @@ impl Format> for ContentArrayEnd { impl FormatElements for [FormatElement] { fn will_break(&self) -> bool { - use Tag::{EndLineSuffix, StartLineSuffix}; let mut ignore_depth = 0usize; for element in self { match element { // Line suffix // Ignore if any of its content breaks - FormatElement::Tag(StartLineSuffix) => { + FormatElement::Tag(Tag::StartLineSuffix | Tag::StartFitsExpanded(_)) => { ignore_depth += 1; } - FormatElement::Tag(EndLineSuffix) => { - ignore_depth -= 1; + FormatElement::Tag(Tag::EndLineSuffix | Tag::EndFitsExpanded) => { + ignore_depth = ignore_depth.saturating_sub(1); } FormatElement::Interned(interned) if ignore_depth == 0 => { if interned.will_break() { diff --git a/crates/ruff_python_formatter/Cargo.toml b/crates/ruff_python_formatter/Cargo.toml index 637276bab1..e63bbeb872 100644 --- a/crates/ruff_python_formatter/Cargo.toml +++ b/crates/ruff_python_formatter/Cargo.toml @@ -25,6 +25,7 @@ clap = { workspace = true } countme = "3.0.1" is-macro = { workspace = true } itertools = { workspace = true } +memchr = { workspace = true } once_cell = { workspace = true } rustc-hash = { workspace = true } serde = { workspace = true, optional = true } diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unary.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unary.py index 11106d7d23..9b08dc9baa 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unary.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unary.py @@ -140,3 +140,9 @@ if not \ # Regression: https://github.com/astral-sh/ruff/issues/5338 if a and not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: ... + +if ( + not + # comment + a): + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unsplittable.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unsplittable.py new file mode 100644 index 0000000000..83a1158ec3 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unsplittable.py @@ -0,0 +1,54 @@ +x = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + +x_aa = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +xxxxx = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + +while ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +): + pass + +while aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: + pass + +# Only applies in `Parenthesize::IfBreaks` positions +raise aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + +raise ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +) + +raise a from aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +raise a from aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + +# Can never apply on expression statement level +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + +# Is it only relevant for items that can't break + +aaaaaaa = 111111111111111111111111111111111111111111111111111111111111111111111111111111 +aaaaaaa = ( + 1111111111111111111111111111111111111111111111111111111111111111111111111111111 +) + +aaaaaaa = """111111111111111111111111111111111111111111111111111111111111111111111111111 +1111111111111111111111111111111111111111111111111111111111111111111111111111111111111""" + +# Never parenthesize multiline strings +aaaaaaa = ( + """1111111111111111111111111111111111111111111111111111111111111111111111111111 +1111111111111111111111111111111111111111111111111111111111111111111111111111111111111""" +) + + + +aaaaaaaa = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbb +aaaaaaaa = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + +aaaaaaaa = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + + +for converter in connection.ops.get_db_converters( + expression +) + expression.get_db_converters(connection): + ... diff --git a/crates/ruff_python_formatter/src/expression/expr_call.rs b/crates/ruff_python_formatter/src/expression/expr_call.rs index 5483aabdca..fe2813626f 100644 --- a/crates/ruff_python_formatter/src/expression/expr_call.rs +++ b/crates/ruff_python_formatter/src/expression/expr_call.rs @@ -69,7 +69,10 @@ impl NeedsParentheses for ExprCall { { OptionalParentheses::Multiline } else { - self.func.needs_parentheses(self.into(), context) + match self.func.needs_parentheses(self.into(), context) { + OptionalParentheses::BestFit => OptionalParentheses::Never, + parentheses => parentheses, + } } } } diff --git a/crates/ruff_python_formatter/src/expression/expr_constant.rs b/crates/ruff_python_formatter/src/expression/expr_constant.rs index 64cfb8cd4c..ed37dea4b3 100644 --- a/crates/ruff_python_formatter/src/expression/expr_constant.rs +++ b/crates/ruff_python_formatter/src/expression/expr_constant.rs @@ -5,7 +5,7 @@ use ruff_python_ast::{Constant, ExprConstant, Ranged}; use ruff_text_size::{TextLen, TextRange}; use crate::expression::number::{FormatComplex, FormatFloat, FormatInt}; -use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; +use crate::expression::parentheses::{should_use_best_fit, NeedsParentheses, OptionalParentheses}; use crate::expression::string::{ AnyString, FormatString, StringLayout, StringPrefix, StringQuotes, }; @@ -79,13 +79,16 @@ impl NeedsParentheses for ExprConstant { _parent: AnyNodeRef, context: &PyFormatContext, ) -> OptionalParentheses { - if self.value.is_implicit_concatenated() { - // Don't wrap triple quoted strings - if is_multiline_string(self, context.source()) { - OptionalParentheses::Never - } else { - OptionalParentheses::Multiline - } + if is_multiline_string(self, context.source()) + || self.value.is_none() + || self.value.is_bool() + || self.value.is_ellipsis() + { + OptionalParentheses::Never + } else if self.value.is_implicit_concatenated() { + OptionalParentheses::Multiline + } else if should_use_best_fit(self, context) { + OptionalParentheses::BestFit } else { OptionalParentheses::Never } @@ -99,7 +102,8 @@ pub(super) fn is_multiline_string(constant: &ExprConstant, source: &str) -> bool let quotes = StringQuotes::parse(&contents[TextRange::new(prefix.text_len(), contents.text_len())]); - quotes.is_some_and(StringQuotes::is_triple) && contents.contains(['\n', '\r']) + quotes.is_some_and(StringQuotes::is_triple) + && memchr::memchr2(b'\n', b'\r', contents.as_bytes()).is_some() } else { false } diff --git a/crates/ruff_python_formatter/src/expression/expr_f_string.rs b/crates/ruff_python_formatter/src/expression/expr_f_string.rs index 0c0165dd3f..2d272ce99c 100644 --- a/crates/ruff_python_formatter/src/expression/expr_f_string.rs +++ b/crates/ruff_python_formatter/src/expression/expr_f_string.rs @@ -1,6 +1,8 @@ use super::string::{AnyString, FormatString}; use crate::context::PyFormatContext; -use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; +use memchr::memchr2; + +use crate::expression::parentheses::{should_use_best_fit, NeedsParentheses, OptionalParentheses}; use crate::prelude::*; use crate::{FormatNodeRule, PyFormatter}; use ruff_formatter::FormatResult; @@ -20,8 +22,16 @@ impl NeedsParentheses for ExprFString { fn needs_parentheses( &self, _parent: AnyNodeRef, - _context: &PyFormatContext, + context: &PyFormatContext, ) -> OptionalParentheses { - OptionalParentheses::Multiline + if self.implicit_concatenated { + OptionalParentheses::Multiline + } else if memchr2(b'\n', b'\r', context.source()[self.range].as_bytes()).is_none() + && should_use_best_fit(self, context) + { + OptionalParentheses::BestFit + } else { + OptionalParentheses::Never + } } } diff --git a/crates/ruff_python_formatter/src/expression/expr_name.rs b/crates/ruff_python_formatter/src/expression/expr_name.rs index a2aef80593..acfd88aad2 100644 --- a/crates/ruff_python_formatter/src/expression/expr_name.rs +++ b/crates/ruff_python_formatter/src/expression/expr_name.rs @@ -1,5 +1,5 @@ use crate::comments::SourceComment; -use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; +use crate::expression::parentheses::{should_use_best_fit, NeedsParentheses, OptionalParentheses}; use crate::prelude::*; use crate::FormatNodeRule; use ruff_formatter::{write, FormatContext}; @@ -38,9 +38,13 @@ impl NeedsParentheses for ExprName { fn needs_parentheses( &self, _parent: AnyNodeRef, - _context: &PyFormatContext, + context: &PyFormatContext, ) -> OptionalParentheses { - OptionalParentheses::Never + if should_use_best_fit(self, context) { + OptionalParentheses::BestFit + } else { + OptionalParentheses::Never + } } } diff --git a/crates/ruff_python_formatter/src/expression/expr_subscript.rs b/crates/ruff_python_formatter/src/expression/expr_subscript.rs index 90c1d5c32d..169fbe9c4a 100644 --- a/crates/ruff_python_formatter/src/expression/expr_subscript.rs +++ b/crates/ruff_python_formatter/src/expression/expr_subscript.rs @@ -101,7 +101,10 @@ impl NeedsParentheses for ExprSubscript { { OptionalParentheses::Multiline } else { - self.value.needs_parentheses(self.into(), context) + match self.value.needs_parentheses(self.into(), context) { + OptionalParentheses::BestFit => OptionalParentheses::Never, + parentheses => parentheses, + } } } } diff --git a/crates/ruff_python_formatter/src/expression/expr_unary_op.rs b/crates/ruff_python_formatter/src/expression/expr_unary_op.rs index 7523edc777..971f7a2d09 100644 --- a/crates/ruff_python_formatter/src/expression/expr_unary_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_unary_op.rs @@ -3,7 +3,7 @@ use ruff_python_ast::{ExprUnaryOp, Ranged}; use ruff_text_size::{TextLen, TextRange}; use ruff_formatter::prelude::{hard_line_break, space, text}; -use ruff_formatter::{Format, FormatContext, FormatResult}; +use ruff_formatter::{Format, FormatResult}; use ruff_python_ast::node::AnyNodeRef; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; @@ -57,7 +57,7 @@ impl FormatNodeRule for FormatExprUnaryOp { // a) // ``` if !leading_operand_comments.is_empty() - && !is_operand_parenthesized(item, f.context().source_code().as_str()) + && !is_operand_parenthesized(item, f.context().source()) { hard_line_break().fmt(f)?; } else if op.is_not() { diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index 75dce1be22..6a2a9eb26d 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -1,7 +1,7 @@ use std::cmp::Ordering; use ruff_formatter::{ - write, FormatOwnedWithRule, FormatRefWithRule, FormatRule, FormatRuleWithOptions, + format_args, write, FormatOwnedWithRule, FormatRefWithRule, FormatRule, FormatRuleWithOptions, }; use ruff_python_ast as ast; use ruff_python_ast::node::AnyNodeRef; @@ -220,6 +220,64 @@ impl Format> for MaybeParenthesizeExpression<'_> { } } }, + OptionalParentheses::BestFit => match parenthesize { + Parenthesize::IfBreaksOrIfRequired => { + parenthesize_if_expands(&expression.format().with_options(Parentheses::Never)) + .fmt(f) + } + + Parenthesize::Optional | Parenthesize::IfRequired => { + expression.format().with_options(Parentheses::Never).fmt(f) + } + Parenthesize::IfBreaks => { + let group_id = f.group_id("optional_parentheses"); + let f = &mut WithNodeLevel::new(NodeLevel::Expression(Some(group_id)), f); + let mut format_expression = expression + .format() + .with_options(Parentheses::Never) + .memoized(); + + // Don't use best fitting if it is known that the expression can never fit + if format_expression.inspect(f)?.will_break() { + // The group here is necessary because `format_expression` may contain IR elements + // that refer to the group id + group(&format_expression) + .with_group_id(Some(group_id)) + .should_expand(true) + .fmt(f) + } else { + // Only add parentheses if it makes the expression fit on the line. + // Using the flat version as the most expanded version gives a left-to-right splitting behavior + // which differs from when using regular groups, because they split right-to-left. + best_fitting![ + // --------------------------------------------------------------------- + // Variant 1: + // Try to fit the expression without any parentheses + group(&format_expression).with_group_id(Some(group_id)), + // --------------------------------------------------------------------- + // Variant 2: + // Try to fit the expression by adding parentheses and indenting the expression. + group(&format_args![ + text("("), + soft_block_indent(&format_expression), + text(")") + ]) + .with_group_id(Some(group_id)) + .should_expand(true), + // --------------------------------------------------------------------- + // Variant 3: Fallback, no parentheses + // Expression doesn't fit regardless of adding the parentheses. Remove the parentheses again. + group(&format_expression) + .with_group_id(Some(group_id)) + .should_expand(true) + ] + // Measure all lines, to avoid that the printer decides that this fits right after hitting + // the `(`. + .with_mode(BestFittingMode::AllLines) + .fmt(f) + } + } + }, OptionalParentheses::Never => match parenthesize { Parenthesize::IfBreaksOrIfRequired => { parenthesize_if_expands(&expression.format().with_options(Parentheses::Never)) @@ -230,6 +288,7 @@ impl Format> for MaybeParenthesizeExpression<'_> { expression.format().with_options(Parentheses::Never).fmt(f) } }, + OptionalParentheses::Always => { expression.format().with_options(Parentheses::Always).fmt(f) } diff --git a/crates/ruff_python_formatter/src/expression/parentheses.rs b/crates/ruff_python_formatter/src/expression/parentheses.rs index 748583ae0c..0ba3bc2a70 100644 --- a/crates/ruff_python_formatter/src/expression/parentheses.rs +++ b/crates/ruff_python_formatter/src/expression/parentheses.rs @@ -1,5 +1,5 @@ use ruff_formatter::prelude::tag::Condition; -use ruff_formatter::{format_args, write, Argument, Arguments}; +use ruff_formatter::{format_args, write, Argument, Arguments, FormatContext, FormatOptions}; use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::{ExpressionRef, Ranged}; use ruff_python_trivia::{first_non_trivia_token, SimpleToken, SimpleTokenKind, SimpleTokenizer}; @@ -15,14 +15,37 @@ pub(crate) enum OptionalParentheses { /// Add parentheses if the expression expands over multiple lines Multiline, - /// Always set parentheses regardless if the expression breaks or if they were + /// Always set parentheses regardless if the expression breaks or if they are /// present in the source. Always, - /// Never add parentheses + /// Add parentheses if it helps to make this expression fit. Otherwise never add parentheses. + /// This mode should only be used for expressions that don't have their own split points, e.g. identifiers, + /// or constants. + BestFit, + + /// Never add parentheses. Use it for expressions that have their own parentheses or if the expression body always spans multiple lines (multiline strings). Never, } +pub(super) fn should_use_best_fit(value: T, context: &PyFormatContext) -> bool +where + T: Ranged, +{ + let text_len = context.source()[value.range()].len(); + + // Only use best fits if: + // * The text is longer than 5 characters: + // This is to align the behavior with `True` and `False`, that don't use best fits and are 5 characters long. + // It allows to avoid [`OptionalParentheses::BestFit`] for most numbers and common identifiers like `self`. + // The downside is that it can result in short values not being parenthesized if they exceed the line width. + // This is considered an edge case not worth the performance penalty and IMO, breaking an identifier + // of 5 characters to avoid it exceeding the line width by 1 reduces the readability. + // * The text is know to never fit: The text can never fit even when parenthesizing if it is longer + // than the configured line width (minus indent). + text_len > 5 && text_len < context.options().line_width().value() as usize +} + pub(crate) trait NeedsParentheses { /// Determines if this object needs optional parentheses or if it is safe to omit the parentheses. fn needs_parentheses( diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index 39ac5ed30d..4303eac0d8 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -249,12 +249,10 @@ if True: #[test] fn quick_test() { let src = r#" -@MyDecorator(list = a) # fmt: skip -# trailing comment -class Test: - pass - - +for converter in connection.ops.get_db_converters( + expression +) + expression.get_db_converters(connection): + ... "#; // Tokenize once let mut tokens = Vec::new(); @@ -291,9 +289,10 @@ class Test: assert_eq!( printed.as_code(), - r#"while True: - if something.changed: - do.stuff() # trailing comment + r#"for converter in connection.ops.get_db_converters( + expression +) + expression.get_db_converters(connection): + ... "# ); } diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__long_strings_flag_disabled.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__long_strings_flag_disabled.py.snap index 6e2a6ac805..8be3c1f3ea 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__long_strings_flag_disabled.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__long_strings_flag_disabled.py.snap @@ -304,23 +304,7 @@ long_unmergable_string_with_pragma = ( ```diff --- Black +++ Ruff -@@ -143,9 +143,13 @@ - ) - ) - --fstring = f"f-strings definitely make things more {difficult} than they need to be for {{black}}. But boy they sure are handy. The problem is that some lines will need to have the 'f' whereas others do not. This {line}, for example, needs one." -+fstring = ( -+ f"f-strings definitely make things more {difficult} than they need to be for {{black}}. But boy they sure are handy. The problem is that some lines will need to have the 'f' whereas others do not. This {line}, for example, needs one." -+) - --fstring_with_no_fexprs = f"Some regular string that needs to get split certainly but is NOT an fstring by any means whatsoever." -+fstring_with_no_fexprs = ( -+ f"Some regular string that needs to get split certainly but is NOT an fstring by any means whatsoever." -+) - - comment_string = "Long lines with inline comments should have their comments appended to the reformatted string's enclosing right parentheses." # This comment gets thrown to the top. - -@@ -165,13 +169,9 @@ +@@ -165,13 +165,9 @@ triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched.""" @@ -336,7 +320,7 @@ long_unmergable_string_with_pragma = ( "formatting" ) -@@ -221,8 +221,8 @@ +@@ -221,8 +217,8 @@ func_with_bad_comma( ( "This is a really long string argument to a function that has a trailing comma" @@ -347,17 +331,6 @@ long_unmergable_string_with_pragma = ( ) func_with_bad_parens_that_wont_fit_in_one_line( -@@ -274,7 +274,9 @@ - yield "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 = f"This is a {{really}} long string that needs to be split without a doubt (i.e. most definitely). In short, this {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 four." -+x = ( -+ f"This is a {{really}} long string that needs to be split without a doubt (i.e. most definitely). In short, this {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 four." -+) - - 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 ``` ## Ruff Output @@ -508,13 +481,9 @@ old_fmt_string3 = ( ) ) -fstring = ( - f"f-strings definitely make things more {difficult} than they need to be for {{black}}. But boy they sure are handy. The problem is that some lines will need to have the 'f' whereas others do not. This {line}, for example, needs one." -) +fstring = f"f-strings definitely make things more {difficult} than they need to be for {{black}}. But boy they sure are handy. The problem is that some lines will need to have the 'f' whereas others do not. This {line}, for example, needs one." -fstring_with_no_fexprs = ( - f"Some regular string that needs to get split certainly but is NOT an fstring by any means whatsoever." -) +fstring_with_no_fexprs = f"Some regular string that needs to get split certainly but is NOT an fstring by any means whatsoever." comment_string = "Long lines with inline comments should have their comments appended to the reformatted string's enclosing right parentheses." # This comment gets thrown to the top. @@ -639,9 +608,7 @@ def foo(): yield "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 = ( - f"This is a {{really}} long string that needs to be split without a doubt (i.e. most definitely). In short, this {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 four." -) +x = f"This is a {{really}} long string that needs to be split without a doubt (i.e. most definitely). In short, this {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 four." 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 diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_parens.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_parens.py.snap deleted file mode 100644 index 9cb91d4c09..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_parens.py.snap +++ /dev/null @@ -1,281 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/remove_parens.py ---- -## Input - -```py -x = (1) -x = (1.2) - -data = ( - "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -).encode() - -async def show_status(): - while True: - try: - if report_host: - data = ( - f"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - ).encode() - except Exception as e: - pass - -def example(): - return (("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")) - - -def example1(): - return ((1111111111111111111111111111111111111111111111111111111111111111111111111111111111111)) - - -def example1point5(): - return ((((((1111111111111111111111111111111111111111111111111111111111111111111111111111111111111)))))) - - -def example2(): - return (("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")) - - -def example3(): - return ((1111111111111111111111111111111111111111111111111111111111111111111111111111111)) - - -def example4(): - return ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((True)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) - - -def example5(): - return ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((())))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) - - -def example6(): - return ((((((((({a:a for a in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]}))))))))) - - -def example7(): - return ((((((((({a:a for a in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20000000000000000000]}))))))))) - - -def example8(): - return (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((None))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -11,8 +11,10 @@ - try: - if report_host: - data = ( -- f"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -- ).encode() -+ ( -+ f"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -+ ).encode() -+ ) - except Exception as e: - pass - -@@ -30,15 +32,11 @@ - - - def example2(): -- return ( -- "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -- ) -+ return "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - - - def example3(): -- return ( -- 1111111111111111111111111111111111111111111111111111111111111111111111111111111 -- ) -+ return 1111111111111111111111111111111111111111111111111111111111111111111111111111111 - - - def example4(): -``` - -## Ruff Output - -```py -x = 1 -x = 1.2 - -data = ( - "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -).encode() - - -async def show_status(): - while True: - try: - if report_host: - data = ( - ( - f"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - ).encode() - ) - except Exception as e: - pass - - -def example(): - return "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - - -def example1(): - return 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111 - - -def example1point5(): - return 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111 - - -def example2(): - return "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - - -def example3(): - return 1111111111111111111111111111111111111111111111111111111111111111111111111111111 - - -def example4(): - return True - - -def example5(): - return () - - -def example6(): - return {a: a for a in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]} - - -def example7(): - return { - a: a - for a in [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20000000000000000000, - ] - } - - -def example8(): - return None -``` - -## Black Output - -```py -x = 1 -x = 1.2 - -data = ( - "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -).encode() - - -async def show_status(): - while True: - try: - if report_host: - data = ( - f"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - ).encode() - except Exception as e: - pass - - -def example(): - return "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - - -def example1(): - return 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111 - - -def example1point5(): - return 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111 - - -def example2(): - return ( - "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - ) - - -def example3(): - return ( - 1111111111111111111111111111111111111111111111111111111111111111111111111111111 - ) - - -def example4(): - return True - - -def example5(): - return () - - -def example6(): - return {a: a for a in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]} - - -def example7(): - return { - a: a - for a in [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20000000000000000000, - ] - } - - -def example8(): - return None -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_commas_in_leading_parts.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_commas_in_leading_parts.py.snap index 6a978187fc..3abf27d802 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_commas_in_leading_parts.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_commas_in_leading_parts.py.snap @@ -56,14 +56,6 @@ assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( # Example from https://github.com/psf/black/issues/3229 -@@ -45,6 +43,4 @@ - # Regression test for https://github.com/psf/black/issues/3414. - assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( - xxxxxxxxx --).xxxxxxxxxxxxxxxxxx(), ( -- "xxx {xxxxxxxxx} xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" --) -+).xxxxxxxxxxxxxxxxxx(), "xxx {xxxxxxxxx} xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ``` ## Ruff Output @@ -114,7 +106,9 @@ assert ( # Regression test for https://github.com/psf/black/issues/3414. assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( xxxxxxxxx -).xxxxxxxxxxxxxxxxxx(), "xxx {xxxxxxxxx} xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +).xxxxxxxxxxxxxxxxxx(), ( + "xxx {xxxxxxxxx} xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +) ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__unary.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__unary.py.snap index e0325c897b..b9d8bd127e 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__unary.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__unary.py.snap @@ -146,6 +146,12 @@ if not \ # Regression: https://github.com/astral-sh/ruff/issues/5338 if a and not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: ... + +if ( + not + # comment + a): + ... ``` ## Output @@ -304,6 +310,13 @@ if ( & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ): ... + +if ( + not + # comment + a +): + ... ``` 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 new file mode 100644 index 0000000000..354d07487d --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__unsplittable.py.snap @@ -0,0 +1,126 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unsplittable.py +--- +## Input +```py +x = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + +x_aa = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +xxxxx = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + +while ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +): + pass + +while aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: + pass + +# Only applies in `Parenthesize::IfBreaks` positions +raise aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + +raise ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +) + +raise a from aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +raise a from aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + +# Can never apply on expression statement level +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + +# Is it only relevant for items that can't break + +aaaaaaa = 111111111111111111111111111111111111111111111111111111111111111111111111111111 +aaaaaaa = ( + 1111111111111111111111111111111111111111111111111111111111111111111111111111111 +) + +aaaaaaa = """111111111111111111111111111111111111111111111111111111111111111111111111111 +1111111111111111111111111111111111111111111111111111111111111111111111111111111111111""" + +# Never parenthesize multiline strings +aaaaaaa = ( + """1111111111111111111111111111111111111111111111111111111111111111111111111111 +1111111111111111111111111111111111111111111111111111111111111111111111111111111111111""" +) + + + +aaaaaaaa = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbb +aaaaaaaa = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + +aaaaaaaa = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + + +for converter in connection.ops.get_db_converters( + expression +) + expression.get_db_converters(connection): + ... +``` + +## Output +```py +x = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + +x_aa = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +xxxxx = ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +) + +while ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +): + pass + +while aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: + pass + +# Only applies in `Parenthesize::IfBreaks` positions +raise aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + +raise ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +) + +raise a from aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +raise a from aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + +# Can never apply on expression statement level +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + +# Is it only relevant for items that can't break + +aaaaaaa = 111111111111111111111111111111111111111111111111111111111111111111111111111111 +aaaaaaa = ( + 1111111111111111111111111111111111111111111111111111111111111111111111111111111 +) + +aaaaaaa = """111111111111111111111111111111111111111111111111111111111111111111111111111 +1111111111111111111111111111111111111111111111111111111111111111111111111111111111111""" + +# Never parenthesize multiline strings +aaaaaaa = """1111111111111111111111111111111111111111111111111111111111111111111111111111 +1111111111111111111111111111111111111111111111111111111111111111111111111111111111111""" + + +aaaaaaaa = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbb +aaaaaaaa = ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +) + +aaaaaaaa = ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +) + + +for converter in connection.ops.get_db_converters( + expression +) + expression.get_db_converters(connection): + ... +``` + + +