From 019d9aebe9742607cbac7d64bec61d3ab1dd8bd9 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 30 Nov 2023 21:11:14 -0500 Subject: [PATCH] Implement multiline dictionary and list hugging for preview style (#8293) ## Summary This PR implement's Black's new single-argument hugging for lists, sets, and dictionaries under preview style. For example, this: ```python foo( [ 1, 2, 3, ] ) ``` Would instead now be formatted as: ```python foo([ 1, 2, 3, ]) ``` A couple notes: - This doesn't apply when the argument has a magic trailing comma. - This _does_ apply when the argument is starred or double-starred. - We don't apply this when there are comments before or after the argument, though Black does in some cases (and moves the comments outside the call parentheses). It doesn't say it in the originating PR (https://github.com/psf/black/pull/3964), but I think this also applies to parenthesized expressions? At least, it does in my testing of preview vs. stable, though it's possible that behavior predated the linked PR. See: #8279. ## Test Plan Before: | project | similarity index | total files | changed files | |----------------|------------------:|------------------:|------------------:| | cpython | 0.75804 | 1799 | 1648 | | django | 0.99984 | 2772 | 34 | | home-assistant | 0.99963 | 10596 | 146 | | poetry | 0.99925 | 317 | 12 | | transformers | 0.99967 | 2657 | 322 | | twine | 1.00000 | 33 | 0 | | typeshed | 0.99980 | 3669 | 18 | | warehouse | 0.99977 | 654 | 13 | | zulip | 0.99970 | 1459 | 21 | After: | project | similarity index | total files | changed files | |----------------|------------------:|------------------:|------------------:| | cpython | 0.75804 | 1799 | 1648 | | django | 0.99984 | 2772 | 34 | | home-assistant | 0.99963 | 10596 | 146 | | poetry | 0.96215 | 317 | 34 | | transformers | 0.99967 | 2657 | 322 | | twine | 1.00000 | 33 | 0 | | typeshed | 0.99980 | 3669 | 18 | | warehouse | 0.99977 | 654 | 13 | | zulip | 0.99970 | 1459 | 21 | --- ...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 | 153 ++++ crates/ruff_python_formatter/src/builders.rs | 27 +- .../src/expression/mod.rs | 95 ++- .../src/expression/parentheses.rs | 21 +- .../src/other/arguments.rs | 70 +- ...ns_with_braces_and_square_brackets.py.snap | 543 +++++++++++++ .../snapshots/format@expression__hug.py.snap | 752 ++++++++++++++++++ ...t@expression__split_empty_brackets.py.snap | 20 + 11 files changed, 1969 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 0000000000000..c106a9c8f36ea --- /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 0000000000000..eff37f23c008b --- /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 0000000000000..963fb7c4040a4 --- /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 0000000000000..bbd41b51a8ba7 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/hug.py @@ -0,0 +1,153 @@ +# 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,])) + + +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 + +# Ensure that nested lists are hugged. +func([ + [ + 1, + 2, + 3, + ] +]) + + +func([ + # comment + [ + 1, + 2, + 3, + ] +]) + +func([ + [ + 1, + 2, + 3, + ] + # comment +]) + +func([ + [ # comment + 1, + 2, + 3, + ] +]) + + +func([ # comment + [ + 1, + 2, + 3, + ] +]) diff --git a/crates/ruff_python_formatter/src/builders.rs b/crates/ruff_python_formatter/src/builders.rs index 581fdc5194a66..e4e2909a4a6dd 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 86fae5137a448..b9dc9e8520a04 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -23,6 +23,7 @@ use crate::expression::parentheses::{ OptionalParentheses, Parentheses, Parenthesize, }; use crate::prelude::*; +use crate::PyFormatOptions; mod binary_like; pub(crate) mod expr_attribute; @@ -126,10 +127,12 @@ 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) + parenthesized("(", &format_expr, ")") + .with_indent(!is_expression_huggable(expression, f.options())) + .fmt(f) } else { format_with_parentheses_comments(expression, &node_comments, f) } @@ -403,9 +406,11 @@ impl Format> for MaybeParenthesizeExpression<'_> { parenthesize_if_expands(&expression.format().with_options(Parentheses::Never)) .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)) @@ -427,6 +432,7 @@ impl Format> for MaybeParenthesizeExpression<'_> { 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? @@ -534,6 +540,7 @@ impl Format> for MaybeParenthesizeExpression<'_> { OptionalParentheses::Never => match parenthesize { Parenthesize::IfBreaksOrIfRequired => { parenthesize_if_expands(&expression.format().with_options(Parentheses::Never)) + .with_indent(!is_expression_huggable(expression, f.options())) .fmt(f) } @@ -1119,6 +1126,86 @@ pub(crate) fn has_own_parentheses( } } +/// Returns `true` if the expression can hug directly to enclosing parentheses, as in Black's +/// `hug_parens_with_braces_and_square_brackets` preview style behavior. +/// +/// 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, options: &PyFormatOptions) -> bool { + if !options.preview().is_enabled() { + return false; + } + + 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. /// @@ -1144,7 +1231,7 @@ enum OperatorPrecedence { Conditional, } -impl From for OperatorPrecedence { +impl From for OperatorPrecedence { fn from(value: Operator) -> Self { match value { Operator::Add | Operator::Sub => OperatorPrecedence::Additive, diff --git a/crates/ruff_python_formatter/src/expression/parentheses.rs b/crates/ruff_python_formatter/src/expression/parentheses.rs index d05b9dcbd7d5f..971d913146609 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 || !self.comments.is_empty() { + 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 5baa9fa741c46..a48596cac0eb3 100644 --- a/crates/ruff_python_formatter/src/other/arguments.rs +++ b/crates/ruff_python_formatter/src/other/arguments.rs @@ -1,11 +1,13 @@ -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::*; #[derive(Default)] @@ -104,6 +106,7 @@ impl FormatNodeRule for FormatArguments { // ) // ``` parenthesized("(", &group(&all_arguments), ")") + .with_indent(!is_argument_huggable(item, f.context())) .with_dangling_comments(dangling_comments) ] ) @@ -143,3 +146,68 @@ 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, as +/// in Black's `hug_parens_with_braces_and_square_brackets` preview style behavior. +/// +/// 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, context: &PyFormatContext) -> bool { + let options = context.options(); + if !options.preview().is_enabled() { + return false; + } + + // 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 the expression itself isn't huggable, then we can't hug it. + if !is_expression_huggable(arg, options) { + 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() || comments.has_trailing() { + return false; + } + + // If the expression has a trailing comma, then we can't hug it. + 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 0000000000000..df9471aac22dc --- /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 0000000000000..f9a4ca0ba5be8 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__hug.py.snap @@ -0,0 +1,752 @@ +--- +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,])) + + +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 + +# Ensure that nested lists are hugged. +func([ + [ + 1, + 2, + 3, + ] +]) + + +func([ + # comment + [ + 1, + 2, + 3, + ] +]) + +func([ + [ + 1, + 2, + 3, + ] + # comment +]) + +func([ + [ # comment + 1, + 2, + 3, + ] +]) + + +func([ # comment + [ + 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, + ] + ) +) + + +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 + + +# Ensure that nested lists are hugged. +func( + [ + [ + 1, + 2, + 3, + ] + ] +) + + +func( + [ + # comment + [ + 1, + 2, + 3, + ] + ] +) + +func( + [ + [ + 1, + 2, + 3, + ] + # comment + ] +) + +func( + [ + [ # comment + 1, + 2, + 3, + ] + ] +) + + +func( + [ # comment + [ + 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,61 +39,47 @@ + # comment + ) + +-func( +- [ # comment +- 1, +- 2, +- 3, +- ] +-) ++func([ # comment ++ 1, ++ 2, ++ 3, ++]) + +-func( +- ( +- [ +- 1, +- 2, +- 3, +- ] +- ) +-) ++func(([ ++ 1, ++ 2, ++ 3, ++])) + + +-func( +- ( +- 1, +- 2, +- 3, +- ) +-) ++func(( ++ 1, ++ 2, ++ 3, ++)) + + # Ensure that comprehensions hug too. +-func( +- [ +- ( +- x, +- y, +- ) +- for (x, y) in z +- ] +-) ++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 + [ +@@ -105,21 +89,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 +@@ -129,12 +109,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 +@@ -167,33 +145,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 + + +@@ -229,56 +201,46 @@ + + + # Ensure that nested lists are hugged. +-func( ++func([ + [ +- [ +- 1, +- 2, +- 3, +- ] ++ 1, ++ 2, ++ 3, + ] +-) ++]) + + +-func( ++func([ ++ # comment + [ +- # comment +- [ +- 1, +- 2, +- 3, +- ] ++ 1, ++ 2, ++ 3, + ] +-) ++]) + +-func( ++func([ + [ +- [ +- 1, +- 2, +- 3, +- ] +- # comment ++ 1, ++ 2, ++ 3, + ] +-) ++ # comment ++]) + +-func( +- [ +- [ # comment +- 1, +- 2, +- 3, +- ] ++func([ ++ [ # comment ++ 1, ++ 2, ++ 3, + ] +-) ++]) + + +-func( +- [ # comment +- [ +- 1, +- 2, +- 3, +- ] ++func([ # comment ++ [ ++ 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 810964acc19a2..2c9fb1d380816 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}", +``` + +