From ecaec09c9f073eed95878ac95cc55a71a2430dde Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 27 Oct 2023 22:30:18 -0400 Subject: [PATCH] Implement multiline dictionary and list indentation --- ...th_braces_and_square_brackets.options.json | 3 + ..._parens_with_braces_and_square_brackets.py | 141 +++++ ..._with_braces_and_square_brackets.py.expect | 159 +++++ .../test/fixtures/ruff/expression/hug.py | 116 ++++ crates/ruff_python_formatter/src/builders.rs | 27 +- .../src/expression/mod.rs | 101 ++- .../src/expression/parentheses.rs | 21 +- .../src/other/arguments.rs | 83 ++- ...ns_with_braces_and_square_brackets.py.snap | 543 ++++++++++++++++ .../snapshots/format@expression__hug.py.snap | 599 ++++++++++++++++++ ...t@expression__split_empty_brackets.py.snap | 20 + 11 files changed, 1798 insertions(+), 15 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/preview_hug_parens_with_braces_and_square_brackets.options.json create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/preview_hug_parens_with_braces_and_square_brackets.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/preview_hug_parens_with_braces_and_square_brackets.py.expect create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/hug.py create mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__preview_hug_parens_with_braces_and_square_brackets.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@expression__hug.py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/preview_hug_parens_with_braces_and_square_brackets.options.json b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/preview_hug_parens_with_braces_and_square_brackets.options.json new file mode 100644 index 00000000000000..c106a9c8f36ea1 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/preview_hug_parens_with_braces_and_square_brackets.options.json @@ -0,0 +1,3 @@ +{ + "preview": "enabled" +} diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/preview_hug_parens_with_braces_and_square_brackets.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/preview_hug_parens_with_braces_and_square_brackets.py new file mode 100644 index 00000000000000..eff37f23c008b2 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/preview_hug_parens_with_braces_and_square_brackets.py @@ -0,0 +1,141 @@ +def foo_brackets(request): + return JsonResponse( + { + "var_1": foo, + "var_2": bar, + } + ) + +def foo_square_brackets(request): + return JsonResponse( + [ + "var_1", + "var_2", + ] + ) + +func({"a": 37, "b": 42, "c": 927, "aaaaaaaaaaaaaaaaaaaaaaaaa": 11111111111111111111111111111111111111111}) + +func(["random_string_number_one","random_string_number_two","random_string_number_three","random_string_number_four"]) + +func( + { + # expand me + 'a':37, + 'b':42, + 'c':927 + } +) + +func( + [ + 'a', + 'b', + 'c', + ] +) + +func( + [ + 'a', + 'b', + 'c', + ], +) + +func( # a + [ # b + "c", # c + "d", # d + "e", # e + ] # f +) # g + +func( # a + { # b + "c": 1, # c + "d": 2, # d + "e": 3, # e + } # f +) # g + +func( + # preserve me + [ + "c", + "d", + "e", + ] +) + +func( + [ # preserve me but hug brackets + "c", + "d", + "e", + ] +) + +func( + [ + # preserve me but hug brackets + "c", + "d", + "e", + ] +) + +func( + [ + "c", + # preserve me but hug brackets + "d", + "e", + ] +) + +func( + [ + "c", + "d", + "e", + # preserve me but hug brackets + ] +) + +func( + [ + "c", + "d", + "e", + ] # preserve me but hug brackets +) + +func( + [ + "c", + "d", + "e", + ] + # preserve me +) + +func([x for x in "short line"]) +func([x for x in "long line long line long line long line long line long line long line"]) +func([x for x in [x for x in "long line long line long line long line long line long line long line"]]) + +func({"short line"}) +func({"long line", "long long line", "long long long line", "long long long long line", "long long long long long line"}) +func({{"long line", "long long line", "long long long line", "long long long long line", "long long long long long line"}}) + +foooooooooooooooooooo( + [{c: n + 1 for c in range(256)} for n in range(100)] + [{}], {size} +) + +baaaaaaaaaaaaar( + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], {x}, "a string", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +) + +foo(*["long long long long long line", "long long long long long line", "long long long long long line"]) + +foo(*[str(i) for i in range(100000000000000000000000000000000000000000000000000000000000)]) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/preview_hug_parens_with_braces_and_square_brackets.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/preview_hug_parens_with_braces_and_square_brackets.py.expect new file mode 100644 index 00000000000000..963fb7c4040a4b --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/preview_hug_parens_with_braces_and_square_brackets.py.expect @@ -0,0 +1,159 @@ +def foo_brackets(request): + return JsonResponse({ + "var_1": foo, + "var_2": bar, + }) + + +def foo_square_brackets(request): + return JsonResponse([ + "var_1", + "var_2", + ]) + + +func({ + "a": 37, + "b": 42, + "c": 927, + "aaaaaaaaaaaaaaaaaaaaaaaaa": 11111111111111111111111111111111111111111, +}) + +func([ + "random_string_number_one", + "random_string_number_two", + "random_string_number_three", + "random_string_number_four", +]) + +func({ + # expand me + "a": 37, + "b": 42, + "c": 927, +}) + +func([ + "a", + "b", + "c", +]) + +func( + [ + "a", + "b", + "c", + ], +) + +func([ # a # b + "c", # c + "d", # d + "e", # e +]) # f # g + +func({ # a # b + "c": 1, # c + "d": 2, # d + "e": 3, # e +}) # f # g + +func( + # preserve me + [ + "c", + "d", + "e", + ] +) + +func([ # preserve me but hug brackets + "c", + "d", + "e", +]) + +func([ + # preserve me but hug brackets + "c", + "d", + "e", +]) + +func([ + "c", + # preserve me but hug brackets + "d", + "e", +]) + +func([ + "c", + "d", + "e", + # preserve me but hug brackets +]) + +func([ + "c", + "d", + "e", +]) # preserve me but hug brackets + +func( + [ + "c", + "d", + "e", + ] + # preserve me +) + +func([x for x in "short line"]) +func([ + x for x in "long line long line long line long line long line long line long line" +]) +func([ + x + for x in [ + x + for x in "long line long line long line long line long line long line long line" + ] +]) + +func({"short line"}) +func({ + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", +}) +func({ + { + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", + } +}) + +foooooooooooooooooooo( + [{c: n + 1 for c in range(256)} for n in range(100)] + [{}], {size} +) + +baaaaaaaaaaaaar( + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], {x}, "a string", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +) + +foo(*[ + "long long long long long line", + "long long long long long line", + "long long long long long line", +]) + +foo(*[ + str(i) for i in range(100000000000000000000000000000000000000000000000000000000000) +]) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/hug.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/hug.py new file mode 100644 index 00000000000000..e270608432909e --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/hug.py @@ -0,0 +1,116 @@ +# Preview style: hug brackets to call parentheses. +func([1, 2, 3,]) + +func( # comment +[1, 2, 3,]) + +func( + # comment +[1, 2, 3,]) + +func([1, 2, 3,] # comment +) + +func([1, 2, 3,] + # comment +) + +func([ # comment + 1, 2, 3,] +) + +func(([1, 2, 3,])) + +# Ensure that comprehensions hug too. +func([(x, y,) for (x, y) in z]) + +# Ensure that dictionaries hug too. +func({1: 2, 3: 4, 5: 6,}) + +# Ensure that the same behavior is applied to parenthesized expressions. +([1, 2, 3,]) + +( # comment + [1, 2, 3,]) + +( + [ # comment + 1, 2, 3,]) + +# Ensure that starred arguments are also hugged. +foo( + *[ + a_long_function_name(a_long_variable_name) + for a_long_variable_name in some_generator + ] +) + +foo( + * # comment + [ + a_long_function_name(a_long_variable_name) + for a_long_variable_name in some_generator + ] +) + +foo( + **[ + a_long_function_name(a_long_variable_name) + for a_long_variable_name in some_generator + ] +) + +foo( + ** # comment + [ + a_long_function_name(a_long_variable_name) + for a_long_variable_name in some_generator + ] +) + +# Ensure that multi-argument calls are _not_ hugged. +func([1, 2, 3,], bar) + +func([(x, y,) for (x, y) in z], bar) + +# Ensure that return type annotations (which use `parenthesize_if_expands`) are also hugged. +def func() -> [1, 2, 3,]: + pass + +def func() -> ([1, 2, 3,]): + pass + +def func() -> ([1, 2, 3,]): + pass + +def func() -> ( # comment + [1, 2, 3,]): + pass + +def func() -> ( + [1, 2, 3,] # comment +): + pass + +def func() -> ( + [1, 2, 3,] + # comment +): + pass + + +foo( + ( + 1, + 2, + 3, + ) +) + +foo([ + [ + 1, + 2, + 3, + ] +]) diff --git a/crates/ruff_python_formatter/src/builders.rs b/crates/ruff_python_formatter/src/builders.rs index 581fdc5194a667..e4e2909a4a6dde 100644 --- a/crates/ruff_python_formatter/src/builders.rs +++ b/crates/ruff_python_formatter/src/builders.rs @@ -1,4 +1,4 @@ -use ruff_formatter::{format_args, write, Argument, Arguments}; +use ruff_formatter::{write, Argument, Arguments}; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::context::{NodeLevel, WithNodeLevel}; @@ -12,11 +12,20 @@ where { ParenthesizeIfExpands { inner: Argument::new(content), + indent: true, } } pub(crate) struct ParenthesizeIfExpands<'a, 'ast> { inner: Argument<'a, PyFormatContext<'ast>>, + indent: bool, +} + +impl ParenthesizeIfExpands<'_, '_> { + pub(crate) fn with_indent(mut self, indent: bool) -> Self { + self.indent = indent; + self + } } impl<'ast> Format> for ParenthesizeIfExpands<'_, 'ast> { @@ -26,11 +35,17 @@ impl<'ast> Format> for ParenthesizeIfExpands<'_, 'ast> { write!( f, - [group(&format_args![ - if_group_breaks(&token("(")), - soft_block_indent(&Arguments::from(&self.inner)), - if_group_breaks(&token(")")), - ])] + [group(&format_with(|f| { + if_group_breaks(&token("(")).fmt(f)?; + + if self.indent { + soft_block_indent(&Arguments::from(&self.inner)).fmt(f)?; + } else { + Arguments::from(&self.inner).fmt(f)?; + }; + + if_group_breaks(&token(")")).fmt(f) + }))] ) } } diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index 86fae5137a448d..c5bde4b1abeacb 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -2,7 +2,7 @@ use std::cmp::Ordering; use std::slice; use ruff_formatter::{ - write, FormatOwnedWithRule, FormatRefWithRule, FormatRule, FormatRuleWithOptions, + write, FormatContext, FormatOwnedWithRule, FormatRefWithRule, FormatRule, FormatRuleWithOptions, }; use ruff_python_ast as ast; use ruff_python_ast::parenthesize::parentheses_iterator; @@ -126,10 +126,13 @@ impl FormatRule> for FormatExpr { Parentheses::Never => false, }; if parenthesize { - let comment = f.context().comments().clone(); - let node_comments = comment.leading_dangling_trailing(expression); + let comments = f.context().comments().clone(); + let node_comments = comments.leading_dangling_trailing(expression); if !node_comments.has_leading() && !node_comments.has_trailing() { - parenthesized("(", &format_expr, ")").fmt(f) + let hug = f.options().preview().is_enabled() && is_expression_huggable(expression); + parenthesized("(", &format_expr, ")") + .with_indent(!hug) + .fmt(f) } else { format_with_parentheses_comments(expression, &node_comments, f) } @@ -400,33 +403,45 @@ impl Format> for MaybeParenthesizeExpression<'_> { match needs_parentheses { OptionalParentheses::Multiline => match parenthesize { Parenthesize::IfBreaksOrIfRequired => { + let indent = !(f.context().options().preview().is_enabled() + && is_expression_huggable(expression)); parenthesize_if_expands(&expression.format().with_options(Parentheses::Never)) + .with_indent(indent) .fmt(f) } + Parenthesize::IfRequired => { expression.format().with_options(Parentheses::Never).fmt(f) } + Parenthesize::Optional | Parenthesize::IfBreaks => { if can_omit_optional_parentheses(expression, f.context()) { optional_parentheses(&expression.format().with_options(Parentheses::Never)) .fmt(f) } else { + let indent = !(f.context().options().preview().is_enabled() + && is_expression_huggable(expression)); parenthesize_if_expands( &expression.format().with_options(Parentheses::Never), ) + .with_indent(indent) .fmt(f) } } }, OptionalParentheses::BestFit => match parenthesize { Parenthesize::IfBreaksOrIfRequired => { + let indent = !(f.context().options().preview().is_enabled() + && is_expression_huggable(expression)); parenthesize_if_expands(&expression.format().with_options(Parentheses::Never)) + .with_indent(indent) .fmt(f) } Parenthesize::Optional | Parenthesize::IfRequired => { expression.format().with_options(Parentheses::Never).fmt(f) } + Parenthesize::IfBreaks => { // Is the expression the last token in the parent statement. // Excludes `await` and `yield` for which Black doesn't seem to apply the layout? @@ -533,7 +548,10 @@ impl Format> for MaybeParenthesizeExpression<'_> { }, OptionalParentheses::Never => match parenthesize { Parenthesize::IfBreaksOrIfRequired => { + let indent = !(f.context().options().preview().is_enabled() + && is_expression_huggable(expression)); parenthesize_if_expands(&expression.format().with_options(Parentheses::Never)) + .with_indent(indent) .fmt(f) } @@ -1119,6 +1137,81 @@ pub(crate) fn has_own_parentheses( } } +/// Returns `true` if the expression can hug directly to enclosing parentheses. +/// +/// For example, in preview style, given: +/// ```python +/// ([1, 2, 3,]) +/// ``` +/// +/// We want to format it as: +/// ```python +/// ([ +/// 1, +/// 2, +/// 3, +/// ]) +/// ``` +/// +/// As opposed to: +/// ```python +/// ( +/// [ +/// 1, +/// 2, +/// 3, +/// ] +/// ) +/// ``` +pub(crate) fn is_expression_huggable(expr: &Expr) -> bool { + match expr { + Expr::Tuple(_) + | Expr::List(_) + | Expr::Set(_) + | Expr::Dict(_) + | Expr::ListComp(_) + | Expr::SetComp(_) + | Expr::DictComp(_) => true, + + Expr::Starred(ast::ExprStarred { value, .. }) => matches!( + value.as_ref(), + Expr::Tuple(_) + | Expr::List(_) + | Expr::Set(_) + | Expr::Dict(_) + | Expr::ListComp(_) + | Expr::SetComp(_) + | Expr::DictComp(_) + ), + + Expr::BoolOp(_) + | Expr::NamedExpr(_) + | Expr::BinOp(_) + | Expr::UnaryOp(_) + | Expr::Lambda(_) + | Expr::IfExp(_) + | Expr::GeneratorExp(_) + | Expr::Await(_) + | Expr::Yield(_) + | Expr::YieldFrom(_) + | Expr::Compare(_) + | Expr::Call(_) + | Expr::FormattedValue(_) + | Expr::FString(_) + | Expr::Attribute(_) + | Expr::Subscript(_) + | Expr::Name(_) + | Expr::Slice(_) + | Expr::IpyEscapeCommand(_) + | Expr::StringLiteral(_) + | Expr::BytesLiteral(_) + | Expr::NumberLiteral(_) + | Expr::BooleanLiteral(_) + | Expr::NoneLiteral(_) + | Expr::EllipsisLiteral(_) => false, + } +} + /// The precedence of [python operators](https://docs.python.org/3/reference/expressions.html#operator-precedence) from /// highest to lowest priority. /// diff --git a/crates/ruff_python_formatter/src/expression/parentheses.rs b/crates/ruff_python_formatter/src/expression/parentheses.rs index d05b9dcbd7d5f2..defcdf54c16211 100644 --- a/crates/ruff_python_formatter/src/expression/parentheses.rs +++ b/crates/ruff_python_formatter/src/expression/parentheses.rs @@ -84,6 +84,7 @@ pub enum Parentheses { Never, } +/// Returns `true` if the [`ExpressionRef`] is enclosed by parentheses in the source code. pub(crate) fn is_expression_parenthesized( expr: ExpressionRef, comment_ranges: &CommentRanges, @@ -125,6 +126,7 @@ where FormatParenthesized { left, comments: &[], + indent: true, content: Argument::new(content), right, } @@ -133,6 +135,7 @@ where pub(crate) struct FormatParenthesized<'content, 'ast> { left: &'static str, comments: &'content [SourceComment], + indent: bool, content: Argument<'content, PyFormatContext<'ast>>, right: &'static str, } @@ -153,6 +156,11 @@ impl<'content, 'ast> FormatParenthesized<'content, 'ast> { ) -> FormatParenthesized<'content, 'ast> { FormatParenthesized { comments, ..self } } + + /// Whether to indent the content within the parentheses. + pub(crate) fn with_indent(self, indent: bool) -> FormatParenthesized<'content, 'ast> { + FormatParenthesized { indent, ..self } + } } impl<'ast> Format> for FormatParenthesized<'_, 'ast> { @@ -160,10 +168,15 @@ impl<'ast> Format> for FormatParenthesized<'_, 'ast> { let current_level = f.context().node_level(); let content = format_with(|f| { - group(&format_args![ - dangling_open_parenthesis_comments(self.comments), - soft_block_indent(&Arguments::from(&self.content)) - ]) + group(&format_with(|f| { + dangling_open_parenthesis_comments(self.comments).fmt(f)?; + if self.indent { + soft_block_indent(&Arguments::from(&self.content)).fmt(f)?; + } else { + Arguments::from(&self.content).fmt(f)?; + } + Ok(()) + })) .fmt(f) }); diff --git a/crates/ruff_python_formatter/src/other/arguments.rs b/crates/ruff_python_formatter/src/other/arguments.rs index 5baa9fa741c46e..f8e8dcabd21958 100644 --- a/crates/ruff_python_formatter/src/other/arguments.rs +++ b/crates/ruff_python_formatter/src/other/arguments.rs @@ -1,12 +1,15 @@ -use ruff_formatter::write; +use ruff_formatter::{write, FormatContext}; use ruff_python_ast::{ArgOrKeyword, Arguments, Expr}; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::comments::SourceComment; use crate::expression::expr_generator_exp::GeneratorExpParentheses; +use crate::expression::is_expression_huggable; use crate::expression::parentheses::{empty_parenthesized, parenthesized, Parentheses}; +use crate::other::commas; use crate::prelude::*; +use crate::PyFormatOptions; #[derive(Default)] pub struct FormatArguments; @@ -85,6 +88,19 @@ impl FormatNodeRule for FormatArguments { let comments = f.context().comments().clone(); let dangling_comments = comments.dangling(item); + // In preview, some arguments are huggable, in that we can omit indentation between the + // call parentheses and the argument, e.g.: + // ```python + // f([ + // 1, + // 2, + // 3, + // ]) + // ``` + let hug = f.options().preview().is_enabled() + && dangling_comments.is_empty() + && is_argument_huggable(item, f.context().options(), f.context()); + write!( f, [ @@ -104,6 +120,7 @@ impl FormatNodeRule for FormatArguments { // ) // ``` parenthesized("(", &group(&all_arguments), ")") + .with_indent(!hug) .with_dangling_comments(dangling_comments) ] ) @@ -143,3 +160,67 @@ fn is_single_argument_parenthesized(argument: &Expr, call_end: TextSize, source: false } +/// Returns `true` if the arguments can hug directly to the enclosing parentheses in the call. +/// +/// For example, in preview style, given: +/// ```python +/// func([1, 2, 3,]) +/// ``` +/// +/// We want to format it as: +/// ```python +/// func([ +/// 1, +/// 2, +/// 3, +/// ]) +/// ``` +/// +/// As opposed to: +/// ```python +/// func( +/// [ +/// 1, +/// 2, +/// 3, +/// ] +/// ) +/// ``` +/// +/// Hugging should only be applied to single-argument collections, like lists, or starred versions +/// of those collections. +fn is_argument_huggable( + item: &Arguments, + options: &PyFormatOptions, + context: &PyFormatContext, +) -> bool { + // Find the lone argument or `**kwargs` keyword. + let arg = match (item.args.as_slice(), item.keywords.as_slice()) { + ([arg], []) => arg, + ([], [keyword]) if keyword.arg.is_none() && !context.comments().has(keyword) => { + &keyword.value + } + _ => return false, + }; + + if !is_expression_huggable(arg) { + return false; + } + + // If the expression has leading or trailing comments, then we can't hug it. + let comments = context.comments().leading_dangling_trailing(arg); + if comments.has_leading() { + return false; + } + if comments.has_trailing() { + return false; + } + + if options.magic_trailing_comma().is_respect() + && commas::has_magic_trailing_comma(TextRange::new(arg.end(), item.end()), options, context) + { + return false; + } + + true +} diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__preview_hug_parens_with_braces_and_square_brackets.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__preview_hug_parens_with_braces_and_square_brackets.py.snap new file mode 100644 index 00000000000000..df9471aac22dc2 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__preview_hug_parens_with_braces_and_square_brackets.py.snap @@ -0,0 +1,543 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/preview_hug_parens_with_braces_and_square_brackets.py +--- +## Input + +```python +def foo_brackets(request): + return JsonResponse( + { + "var_1": foo, + "var_2": bar, + } + ) + +def foo_square_brackets(request): + return JsonResponse( + [ + "var_1", + "var_2", + ] + ) + +func({"a": 37, "b": 42, "c": 927, "aaaaaaaaaaaaaaaaaaaaaaaaa": 11111111111111111111111111111111111111111}) + +func(["random_string_number_one","random_string_number_two","random_string_number_three","random_string_number_four"]) + +func( + { + # expand me + 'a':37, + 'b':42, + 'c':927 + } +) + +func( + [ + 'a', + 'b', + 'c', + ] +) + +func( + [ + 'a', + 'b', + 'c', + ], +) + +func( # a + [ # b + "c", # c + "d", # d + "e", # e + ] # f +) # g + +func( # a + { # b + "c": 1, # c + "d": 2, # d + "e": 3, # e + } # f +) # g + +func( + # preserve me + [ + "c", + "d", + "e", + ] +) + +func( + [ # preserve me but hug brackets + "c", + "d", + "e", + ] +) + +func( + [ + # preserve me but hug brackets + "c", + "d", + "e", + ] +) + +func( + [ + "c", + # preserve me but hug brackets + "d", + "e", + ] +) + +func( + [ + "c", + "d", + "e", + # preserve me but hug brackets + ] +) + +func( + [ + "c", + "d", + "e", + ] # preserve me but hug brackets +) + +func( + [ + "c", + "d", + "e", + ] + # preserve me +) + +func([x for x in "short line"]) +func([x for x in "long line long line long line long line long line long line long line"]) +func([x for x in [x for x in "long line long line long line long line long line long line long line"]]) + +func({"short line"}) +func({"long line", "long long line", "long long long line", "long long long long line", "long long long long long line"}) +func({{"long line", "long long line", "long long long line", "long long long long line", "long long long long long line"}}) + +foooooooooooooooooooo( + [{c: n + 1 for c in range(256)} for n in range(100)] + [{}], {size} +) + +baaaaaaaaaaaaar( + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], {x}, "a string", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +) + +foo(*["long long long long long line", "long long long long long line", "long long long long long line"]) + +foo(*[str(i) for i in range(100000000000000000000000000000000000000000000000000000000000)]) +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -47,17 +47,21 @@ + ], + ) + +-func([ # a # b +- "c", # c +- "d", # d +- "e", # e +-]) # f # g ++func( # a ++ [ # b ++ "c", # c ++ "d", # d ++ "e", # e ++ ] # f ++) # g + +-func({ # a # b +- "c": 1, # c +- "d": 2, # d +- "e": 3, # e +-}) # f # g ++func( # a ++ { # b ++ "c": 1, # c ++ "d": 2, # d ++ "e": 3, # e ++ } # f ++) # g + + func( + # preserve me +@@ -95,11 +99,13 @@ + # preserve me but hug brackets + ]) + +-func([ +- "c", +- "d", +- "e", +-]) # preserve me but hug brackets ++func( ++ [ ++ "c", ++ "d", ++ "e", ++ ] # preserve me but hug brackets ++) + + func( + [ +``` + +## Ruff Output + +```python +def foo_brackets(request): + return JsonResponse({ + "var_1": foo, + "var_2": bar, + }) + + +def foo_square_brackets(request): + return JsonResponse([ + "var_1", + "var_2", + ]) + + +func({ + "a": 37, + "b": 42, + "c": 927, + "aaaaaaaaaaaaaaaaaaaaaaaaa": 11111111111111111111111111111111111111111, +}) + +func([ + "random_string_number_one", + "random_string_number_two", + "random_string_number_three", + "random_string_number_four", +]) + +func({ + # expand me + "a": 37, + "b": 42, + "c": 927, +}) + +func([ + "a", + "b", + "c", +]) + +func( + [ + "a", + "b", + "c", + ], +) + +func( # a + [ # b + "c", # c + "d", # d + "e", # e + ] # f +) # g + +func( # a + { # b + "c": 1, # c + "d": 2, # d + "e": 3, # e + } # f +) # g + +func( + # preserve me + [ + "c", + "d", + "e", + ] +) + +func([ # preserve me but hug brackets + "c", + "d", + "e", +]) + +func([ + # preserve me but hug brackets + "c", + "d", + "e", +]) + +func([ + "c", + # preserve me but hug brackets + "d", + "e", +]) + +func([ + "c", + "d", + "e", + # preserve me but hug brackets +]) + +func( + [ + "c", + "d", + "e", + ] # preserve me but hug brackets +) + +func( + [ + "c", + "d", + "e", + ] + # preserve me +) + +func([x for x in "short line"]) +func([ + x for x in "long line long line long line long line long line long line long line" +]) +func([ + x + for x in [ + x + for x in "long line long line long line long line long line long line long line" + ] +]) + +func({"short line"}) +func({ + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", +}) +func({ + { + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", + } +}) + +foooooooooooooooooooo( + [{c: n + 1 for c in range(256)} for n in range(100)] + [{}], {size} +) + +baaaaaaaaaaaaar( + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], {x}, "a string", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +) + +foo(*[ + "long long long long long line", + "long long long long long line", + "long long long long long line", +]) + +foo(*[ + str(i) for i in range(100000000000000000000000000000000000000000000000000000000000) +]) +``` + +## Black Output + +```python +def foo_brackets(request): + return JsonResponse({ + "var_1": foo, + "var_2": bar, + }) + + +def foo_square_brackets(request): + return JsonResponse([ + "var_1", + "var_2", + ]) + + +func({ + "a": 37, + "b": 42, + "c": 927, + "aaaaaaaaaaaaaaaaaaaaaaaaa": 11111111111111111111111111111111111111111, +}) + +func([ + "random_string_number_one", + "random_string_number_two", + "random_string_number_three", + "random_string_number_four", +]) + +func({ + # expand me + "a": 37, + "b": 42, + "c": 927, +}) + +func([ + "a", + "b", + "c", +]) + +func( + [ + "a", + "b", + "c", + ], +) + +func([ # a # b + "c", # c + "d", # d + "e", # e +]) # f # g + +func({ # a # b + "c": 1, # c + "d": 2, # d + "e": 3, # e +}) # f # g + +func( + # preserve me + [ + "c", + "d", + "e", + ] +) + +func([ # preserve me but hug brackets + "c", + "d", + "e", +]) + +func([ + # preserve me but hug brackets + "c", + "d", + "e", +]) + +func([ + "c", + # preserve me but hug brackets + "d", + "e", +]) + +func([ + "c", + "d", + "e", + # preserve me but hug brackets +]) + +func([ + "c", + "d", + "e", +]) # preserve me but hug brackets + +func( + [ + "c", + "d", + "e", + ] + # preserve me +) + +func([x for x in "short line"]) +func([ + x for x in "long line long line long line long line long line long line long line" +]) +func([ + x + for x in [ + x + for x in "long line long line long line long line long line long line long line" + ] +]) + +func({"short line"}) +func({ + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", +}) +func({ + { + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", + } +}) + +foooooooooooooooooooo( + [{c: n + 1 for c in range(256)} for n in range(100)] + [{}], {size} +) + +baaaaaaaaaaaaar( + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], {x}, "a string", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +) + +foo(*[ + "long long long long long line", + "long long long long long line", + "long long long long long line", +]) + +foo(*[ + str(i) for i in range(100000000000000000000000000000000000000000000000000000000000) +]) +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__hug.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__hug.py.snap new file mode 100644 index 00000000000000..6ec8ba34aea2f5 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__hug.py.snap @@ -0,0 +1,599 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/hug.py +--- +## Input +```python +# Preview style: hug brackets to call parentheses. +func([1, 2, 3,]) + +func( # comment +[1, 2, 3,]) + +func( + # comment +[1, 2, 3,]) + +func([1, 2, 3,] # comment +) + +func([1, 2, 3,] + # comment +) + +func([ # comment + 1, 2, 3,] +) + +func(([1, 2, 3,])) + +# Ensure that comprehensions hug too. +func([(x, y,) for (x, y) in z]) + +# Ensure that dictionaries hug too. +func({1: 2, 3: 4, 5: 6,}) + +# Ensure that the same behavior is applied to parenthesized expressions. +([1, 2, 3,]) + +( # comment + [1, 2, 3,]) + +( + [ # comment + 1, 2, 3,]) + +# Ensure that starred arguments are also hugged. +foo( + *[ + a_long_function_name(a_long_variable_name) + for a_long_variable_name in some_generator + ] +) + +foo( + * # comment + [ + a_long_function_name(a_long_variable_name) + for a_long_variable_name in some_generator + ] +) + +foo( + **[ + a_long_function_name(a_long_variable_name) + for a_long_variable_name in some_generator + ] +) + +foo( + ** # comment + [ + a_long_function_name(a_long_variable_name) + for a_long_variable_name in some_generator + ] +) + +# Ensure that multi-argument calls are _not_ hugged. +func([1, 2, 3,], bar) + +func([(x, y,) for (x, y) in z], bar) + +# Ensure that return type annotations (which use `parenthesize_if_expands`) are also hugged. +def func() -> [1, 2, 3,]: + pass + +def func() -> ([1, 2, 3,]): + pass + +def func() -> ([1, 2, 3,]): + pass + +def func() -> ( # comment + [1, 2, 3,]): + pass + +def func() -> ( + [1, 2, 3,] # comment +): + pass + +def func() -> ( + [1, 2, 3,] + # comment +): + pass + + +foo( + ( + 1, + 2, + 3, + ) +) + +foo([ + [ + 1, + 2, + 3, + ] +]) +``` + +## Output +```python +# Preview style: hug brackets to call parentheses. +func( + [ + 1, + 2, + 3, + ] +) + +func( # comment + [ + 1, + 2, + 3, + ] +) + +func( + # comment + [ + 1, + 2, + 3, + ] +) + +func( + [ + 1, + 2, + 3, + ] # comment +) + +func( + [ + 1, + 2, + 3, + ] + # comment +) + +func( + [ # comment + 1, + 2, + 3, + ] +) + +func( + ( + [ + 1, + 2, + 3, + ] + ) +) + +# Ensure that comprehensions hug too. +func( + [ + ( + x, + y, + ) + for (x, y) in z + ] +) + +# Ensure that dictionaries hug too. +func( + { + 1: 2, + 3: 4, + 5: 6, + } +) + +# Ensure that the same behavior is applied to parenthesized expressions. +( + [ + 1, + 2, + 3, + ] +) + +( # comment + [ + 1, + 2, + 3, + ] +) + +( + [ # comment + 1, + 2, + 3, + ] +) + +# Ensure that starred arguments are also hugged. +foo( + *[ + a_long_function_name(a_long_variable_name) + for a_long_variable_name in some_generator + ] +) + +foo( + # comment + *[ + a_long_function_name(a_long_variable_name) + for a_long_variable_name in some_generator + ] +) + +foo( + **[ + a_long_function_name(a_long_variable_name) + for a_long_variable_name in some_generator + ] +) + +foo( + # comment + **[ + a_long_function_name(a_long_variable_name) + for a_long_variable_name in some_generator + ] +) + +# Ensure that multi-argument calls are _not_ hugged. +func( + [ + 1, + 2, + 3, + ], + bar, +) + +func( + [ + ( + x, + y, + ) + for (x, y) in z + ], + bar, +) + + +# Ensure that return type annotations (which use `parenthesize_if_expands`) are also hugged. +def func() -> ( + [ + 1, + 2, + 3, + ] +): + pass + + +def func() -> ( + [ + 1, + 2, + 3, + ] +): + pass + + +def func() -> ( + [ + 1, + 2, + 3, + ] +): + pass + + +def func() -> ( # comment + [ + 1, + 2, + 3, + ] +): + pass + + +def func() -> ( + [ + 1, + 2, + 3, + ] # comment +): + pass + + +def func() -> ( + [ + 1, + 2, + 3, + ] + # comment +): + pass + + +foo( + ( + 1, + 2, + 3, + ) +) + +foo( + [ + [ + 1, + 2, + 3, + ] + ] +) +``` + + +## Preview changes +```diff +--- Stable ++++ Preview +@@ -1,11 +1,9 @@ + # Preview style: hug brackets to call parentheses. +-func( +- [ +- 1, +- 2, +- 3, +- ] +-) ++func([ ++ 1, ++ 2, ++ 3, ++]) + + func( # comment + [ +@@ -41,52 +39,40 @@ + # comment + ) + +-func( +- [ # comment +- 1, +- 2, +- 3, +- ] +-) ++func([ # comment ++ 1, ++ 2, ++ 3, ++]) ++ ++func(([ ++ 1, ++ 2, ++ 3, ++])) + +-func( ++# Ensure that comprehensions hug too. ++func([ + ( +- [ +- 1, +- 2, +- 3, +- ] ++ x, ++ y, + ) +-) ++ for (x, y) in z ++]) + +-# Ensure that comprehensions hug too. +-func( +- [ +- ( +- x, +- y, +- ) +- for (x, y) in z +- ] +-) +- + # Ensure that dictionaries hug too. +-func( +- { +- 1: 2, +- 3: 4, +- 5: 6, +- } +-) ++func({ ++ 1: 2, ++ 3: 4, ++ 5: 6, ++}) + + # Ensure that the same behavior is applied to parenthesized expressions. +-( +- [ +- 1, +- 2, +- 3, +- ] +-) ++([ ++ 1, ++ 2, ++ 3, ++]) + + ( # comment + [ +@@ -96,21 +82,17 @@ + ] + ) + +-( +- [ # comment +- 1, +- 2, +- 3, +- ] +-) ++([ # comment ++ 1, ++ 2, ++ 3, ++]) + + # Ensure that starred arguments are also hugged. +-foo( +- *[ +- a_long_function_name(a_long_variable_name) +- for a_long_variable_name in some_generator +- ] +-) ++foo(*[ ++ a_long_function_name(a_long_variable_name) ++ for a_long_variable_name in some_generator ++]) + + foo( + # comment +@@ -120,12 +102,10 @@ + ] + ) + +-foo( +- **[ +- a_long_function_name(a_long_variable_name) +- for a_long_variable_name in some_generator +- ] +-) ++foo(**[ ++ a_long_function_name(a_long_variable_name) ++ for a_long_variable_name in some_generator ++]) + + foo( + # comment +@@ -158,33 +138,27 @@ + + + # Ensure that return type annotations (which use `parenthesize_if_expands`) are also hugged. +-def func() -> ( +- [ +- 1, +- 2, +- 3, +- ] +-): ++def func() -> ([ ++ 1, ++ 2, ++ 3, ++]): + pass + + +-def func() -> ( +- [ +- 1, +- 2, +- 3, +- ] +-): ++def func() -> ([ ++ 1, ++ 2, ++ 3, ++]): + pass + + +-def func() -> ( +- [ +- 1, +- 2, +- 3, +- ] +-): ++def func() -> ([ ++ 1, ++ 2, ++ 3, ++]): + pass + + +@@ -219,20 +193,16 @@ + pass + + +-foo( +- ( ++foo(( ++ 1, ++ 2, ++ 3, ++)) ++ ++foo([ ++ [ + 1, + 2, + 3, +- ) +-) +- +-foo( +- [ +- [ +- 1, +- 2, +- 3, +- ] + ] +-) ++]) +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__split_empty_brackets.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__split_empty_brackets.py.snap index 810964acc19a2f..2c9fb1d3808165 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__split_empty_brackets.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__split_empty_brackets.py.snap @@ -194,4 +194,24 @@ response = await sync_to_async( ``` +## Preview changes +```diff +--- Stable ++++ Preview +@@ -62,9 +62,9 @@ + 1 + }.unicodedata.normalize("NFKCNFKCNFKCNFKCNFKC", s2).casefold() + +-ct_match = ([]).unicodedata.normalize("NFKC", s1).casefold() == ( +- [] +-).unicodedata.normalize("NFKCNFKCNFKCNFKCNFKC", s2).casefold() ++ct_match = ([]).unicodedata.normalize( ++ "NFKC", s1 ++).casefold() == ([]).unicodedata.normalize("NFKCNFKCNFKCNFKCNFKC", s2).casefold() + + return await self.http_client.fetch( + f"http://127.0.0.1:{self.port}{path}", +``` + +