diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/lambda.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/lambda.py index 1b1c1ee3c2..036ef2c3c8 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/lambda.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/lambda.py @@ -125,6 +125,13 @@ lambda a, /, c: a *x: x ) +( + lambda + # comment + *x, + **y: x +) + ( lambda # comment 1 @@ -196,6 +203,17 @@ lambda: ( # comment x ) +( + lambda # 1 + # 2 + x, # 3 + # 4 + y + : # 5 + # 6 + x +) + ( lambda x, @@ -204,6 +222,71 @@ lambda: ( # comment z ) + +# Leading +lambda x: ( + lambda y: lambda z: x + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + z # Trailing +) # Trailing + + +# Leading +lambda x: lambda y: lambda z: [ + x, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + z +] # Trailing +# Trailing + lambda self, araa, kkkwargs=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs), e=1, f=2, g=2: d # Regression tests for https://github.com/astral-sh/ruff/issues/8179 @@ -228,6 +311,441 @@ def a(): g = 10 ) +def a(): + return b( + c, + d, + e, + f=lambda self, *args, **kwargs: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa( + *args, **kwargs + ) + 1, + ) + +# Additional ecosystem cases from https://github.com/astral-sh/ruff/pull/21385 +class C: + def foo(): + mock_service.return_value.bucket.side_effect = lambda name: ( + source_bucket + if name == source_bucket_name + else storage.Bucket(mock_service, destination_bucket_name) + ) + +class C: + function_dict: Dict[Text, Callable[[CRFToken], Any]] = { + CRFEntityExtractorOptions.POS2: lambda crf_token: crf_token.pos_tag[:2] + if crf_token.pos_tag is not None + else None, + } + +name = re.sub(r"[^\x21\x23-\x5b\x5d-\x7e]...............", lambda m: f"\\{m.group(0)}", p["name"]) + +def foo(): + if True: + if True: + return ( + lambda x: np.exp(cs(np.log(x.to(u.MeV).value))) * u.MeV * u.cm**2 / u.g + ) + +class C: + _is_recognized_dtype: Callable[[DtypeObj], bool] = lambda x: lib.is_np_dtype( + x, "M" + ) or isinstance(x, DatetimeTZDtype) + +class C: + def foo(): + if True: + transaction_count = self._query_txs_for_range( + get_count_fn=lambda from_ts, to_ts, _chain_id=chain_id: db_evmtx.count_transactions_in_range( + chain_id=_chain_id, + from_ts=from_ts, + to_ts=to_ts, + ), + ) + + transaction_count = self._query_txs_for_range( + get_count_fn=lambda from_ts, to_ts, _chain_id=chain_id: db_evmtx.count_transactions_in_range[_chain_id, from_ts, to_ts], + ) + +def ddb(): + sql = ( + lambda var, table, n=N: f""" + CREATE TABLE {table} AS + SELECT ROW_NUMBER() OVER () AS id, {var} + FROM ( + SELECT {var} + FROM RANGE({n}) _ ({var}) + ORDER BY RANDOM() + ) + """ + ) + +long_assignment_target.with_attribute.and_a_slice[with_an_index] = ( # 1 + # 2 + lambda x, y, z: # 3 + # 4 + x + y + z # 5 + # 6 +) + +long_assignment_target.with_attribute.and_a_slice[with_an_index] = ( + lambda x, y, z: x + y + z +) + +long_assignment_target.with_attribute.and_a_slice[with_an_index] = lambda x, y, z: x + y + z + +very_long_variable_name_x, very_long_variable_name_y = lambda a: a + some_very_long_expression, lambda b: b * another_very_long_expression_here + +very_long_variable_name_for_result += lambda x: very_long_function_call_that_should_definitely_be_parenthesized_now(x, more_args, additional_parameters) + + +if 1: + if 2: + if 3: + if self.location in EVM_EVMLIKE_LOCATIONS and database is not None: + exported_dict["notes"] = EVM_ADDRESS_REGEX.sub( + repl=lambda matched_address: self._maybe_add_label_with_address( + database=database, + matched_address=matched_address, + ), + string=exported_dict["notes"], + ) + +class C: + def f(): + return dict( + filter( + lambda intent_response: self.is_retrieval_intent_response( + intent_response + ), + self.responses.items(), + ) + ) + +@pytest.mark.parametrize( + "op", + [ + # Not fluent + param( + lambda left, right: ( + ibis.timestamp("2017-04-01") + ), + ), + # These four are fluent and fit on one line inside the parenthesized + # lambda body + param( + lambda left, right: ( + ibis.timestamp("2017-04-01").cast(dt.date) + ), + ), + param( + lambda left, right: ( + ibis.timestamp("2017-04-01").cast(dt.date).between(left, right) + ), + ), + param(lambda left, right: ibis.timestamp("2017-04-01").cast(dt.date)), + param(lambda left, right: ibis.timestamp("2017-04-01").cast(dt.date).between(left, right)), + # This is too long on one line in the lambda body and gets wrapped + # inside the body. + param( + lambda left, right: ( + ibis.timestamp("2017-04-01").cast(dt.date).between(left, right).between(left, right) + ), + ), + ], +) +def test_string_temporal_compare_between(con, op, left, right): ... + +[ + ( + lambda eval_df, _: MetricValue( + scores=eval_df["prediction"].tolist(), + aggregate_results={"prediction_sum": sum(eval_df["prediction"])}, + ) + ), +] + +# reuses the list parentheses +lambda xxxxxxxxxxxxxxxxxxxx, yyyyyyyyyyyyyyyyyyyy, zzzzzzzzzzzzzzzzzzzz: [xxxxxxxxxxxxxxxxxxxx, yyyyyyyyyyyyyyyyyyyy, zzzzzzzzzzzzzzzzzzzz] + +# adds parentheses around the body +lambda xxxxxxxxxxxxxxxxxxxx, yyyyyyyyyyyyyyyyyyyy, zzzzzzzzzzzzzzzzzzzz: xxxxxxxxxxxxxxxxxxxx + yyyyyyyyyyyyyyyyyyyy + zzzzzzzzzzzzzzzzzzzz + +# removes parentheses around the body +lambda xxxxxxxxxxxxxxxxxxxx: (xxxxxxxxxxxxxxxxxxxx + 1) + +mapper = lambda x: dict_with_default[np.nan if isinstance(x, float) and np.isnan(x) else x] + +lambda x, y, z: ( + x + y + z +) + +lambda x, y, z: ( + x + y + z + # trailing body +) + +lambda x, y, z: ( + x + y + z # trailing eol body +) + +lambda x, y, z: ( + x + y + z +) # trailing lambda + +lambda x, y, z: ( + # leading body + x + y + z +) + +lambda x, y, z: ( # leading eol body + x + y + z +) + +( + lambda name: + source_bucket # trailing eol comment + if name == source_bucket_name + else storage.Bucket(mock_service, destination_bucket_name) +) + +( + lambda name: + # dangling header comment + source_bucket + if name == source_bucket_name + else storage.Bucket(mock_service, destination_bucket_name) +) + +x = ( + lambda name: + # dangling header comment + source_bucket + if name == source_bucket_name + else storage.Bucket(mock_service, destination_bucket_name) +) + +( + lambda name: # dangling header comment + ( + source_bucket + if name == source_bucket_name + else storage.Bucket(mock_service, destination_bucket_name) + ) +) + +( + lambda from_ts, to_ts, _chain_id=chain_id: # dangling eol header comment + db_evmtx.count_transactions_in_range( + chain_id=_chain_id, + from_ts=from_ts, + to_ts=to_ts, + ) +) + +( + lambda from_ts, to_ts, _chain_id=chain_id: + # dangling header comment before call + db_evmtx.count_transactions_in_range( + chain_id=_chain_id, + from_ts=from_ts, + to_ts=to_ts, + ) +) + +( + lambda left, right: + # comment + ibis.timestamp("2017-04-01").cast(dt.date).between(left, right) +) + +( + lambda left, right: + ibis.timestamp("2017-04-01") # comment + .cast(dt.date) + .between(left, right) +) + +( + lambda xxxxxxxxxxxxxxxxxxxx, yyyyyyyyyyyyyyyyyyyy: + # comment + [xxxxxxxxxxxxxxxxxxxx, yyyyyyyyyyyyyyyyyyyy, zzzzzzzzzzzzzzzzzzzz] +) + +( + lambda x, y: + # comment + { + "key": x, + "another": y, + } +) + +( + lambda x, y: + # comment + ( + x, + y, + z + ) +) + +( + lambda x: + # comment + dict_with_default[np.nan if isinstance(x, float) and np.isnan(x) else x] +) + +( + lambda from_ts, to_ts, _chain_id=chain_id: + db_evmtx.count_transactions_in_range[ + # comment + _chain_id, from_ts, to_ts + ] +) + +( + lambda + # comment + *args, **kwargs: + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs) + 1 +) + +( + lambda # comment + *args, **kwargs: + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs) + 1 +) + +( + lambda # comment 1 + # comment 2 + *args, **kwargs: # comment 3 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs) + 1 +) + +( + lambda # comment 1 + *args, **kwargs: # comment 3 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs) + 1 +) + +( + lambda *args, **kwargs: + # comment 1 + ( # comment 2 + # comment 3 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs) + 1 # comment 4 + # comment 5 + ) # comment 6 +) + +( + lambda *brgs, **kwargs: + # comment 1 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa( # comment 2 + # comment 3 + *brgs, **kwargs) + 1 # comment 4 + # comment 5 +) + +( + lambda *crgs, **kwargs: # comment 1 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*crgs, **kwargs) + 1 +) + +( + lambda *drgs, **kwargs: # comment 1 + ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*drgs, **kwargs) + 1 + ) +) + +( + lambda * # comment 1 + ergs, ** + # comment 2 + kwargs # comment 3 + : # comment 4 + ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*ergs, **kwargs) + 1 + ) +) + +( + lambda # 1 + # 2 + left, # 3 + # 4 + right: # 5 + # 6 + ibis.timestamp("2017-04-01").cast(dt.date).between(left, right) +) + +( + lambda x: # outer comment 1 + ( + lambda y: # inner comment 1 + # inner comment 2 + lambda z: ( + # innermost comment + x + y + z + ) + ) +) + +foo( + lambda from_ts, # comment prevents collapsing the parameters to one line + to_ts, _chain_id=chain_id: db_evmtx.count_transactions_in_range( + chain_id=_chain_id, + from_ts=from_ts, + to_ts=to_ts, + ) +) + +foo( + lambda from_ts, # but still wrap the body if it gets too long + to_ts, + _chain_id=chain_id: db_evmtx.count_transactions_in_rangeeeeeeeeeeeeeeeeeeeeeeeeeeeee( + chain_id=_chain_id, + from_ts=from_ts, + to_ts=to_ts, + ) +) + +transform = lambda left, right: ibis.timestamp("2017-04-01").cast(dt.date).between(left, right).between(left, right) # trailing comment + +( + lambda: # comment + 1 +) + +( + lambda # comment + : + 1 +) + +( + lambda: + # comment + 1 +) + +( + lambda: # comment 1 + # comment 2 + 1 +) + +( + lambda # comment 1 + # comment 2 + : # comment 3 + # comment 4 + 1 +) + ( lambda * # comment 2 @@ -271,3 +789,18 @@ def a(): x: x ) + +( + lambda: # dangling-end-of-line + # dangling-own-line + ( # leading-body-end-of-line + x + ) +) + +( + lambda: # dangling-end-of-line + ( # leading-body-end-of-line + x + ) +) diff --git a/crates/ruff_python_formatter/src/builders.rs b/crates/ruff_python_formatter/src/builders.rs index 8d7aeb502b..8d3f701e59 100644 --- a/crates/ruff_python_formatter/src/builders.rs +++ b/crates/ruff_python_formatter/src/builders.rs @@ -1,4 +1,4 @@ -use ruff_formatter::{Argument, Arguments, write}; +use ruff_formatter::{Argument, Arguments, format_args, write}; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::context::{NodeLevel, WithNodeLevel}; @@ -33,20 +33,27 @@ impl<'ast> Format> for ParenthesizeIfExpands<'_, 'ast> { { let mut f = WithNodeLevel::new(NodeLevel::ParenthesizedExpression, f); - write!( - f, - [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) - }))] - ) + if self.indent { + let parens_id = f.group_id("indented_parenthesize_if_expands"); + group(&format_args![ + if_group_breaks(&token("(")), + indent_if_group_breaks( + &format_args![soft_line_break(), &Arguments::from(&self.inner)], + parens_id + ), + soft_line_break(), + if_group_breaks(&token(")")) + ]) + .with_id(Some(parens_id)) + .fmt(&mut f) + } else { + group(&format_args![ + if_group_breaks(&token("(")), + Arguments::from(&self.inner), + if_group_breaks(&token(")")), + ]) + .fmt(&mut f) + } } } } diff --git a/crates/ruff_python_formatter/src/expression/expr_lambda.rs b/crates/ruff_python_formatter/src/expression/expr_lambda.rs index f91666ecf7..faab6ef8c5 100644 --- a/crates/ruff_python_formatter/src/expression/expr_lambda.rs +++ b/crates/ruff_python_formatter/src/expression/expr_lambda.rs @@ -1,15 +1,21 @@ -use ruff_formatter::write; -use ruff_python_ast::AnyNodeRef; -use ruff_python_ast::ExprLambda; +use ruff_formatter::{FormatRuleWithOptions, RemoveSoftLinesBuffer, format_args, write}; +use ruff_python_ast::{AnyNodeRef, Expr, ExprLambda}; use ruff_text_size::Ranged; -use crate::comments::dangling_comments; -use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; +use crate::builders::parenthesize_if_expands; +use crate::comments::{SourceComment, dangling_comments, leading_comments, trailing_comments}; +use crate::expression::parentheses::{ + NeedsParentheses, OptionalParentheses, Parentheses, is_expression_parenthesized, +}; +use crate::expression::{CallChainLayout, has_own_parentheses}; use crate::other::parameters::ParametersParentheses; use crate::prelude::*; +use crate::preview::is_parenthesize_lambda_bodies_enabled; #[derive(Default)] -pub struct FormatExprLambda; +pub struct FormatExprLambda { + layout: ExprLambdaLayout, +} impl FormatNodeRule for FormatExprLambda { fn fmt_fields(&self, item: &ExprLambda, f: &mut PyFormatter) -> FormatResult<()> { @@ -20,13 +26,19 @@ impl FormatNodeRule for FormatExprLambda { body, } = item; + let body = &**body; + let parameters = parameters.as_deref(); + let comments = f.context().comments().clone(); let dangling = comments.dangling(item); + let preview = is_parenthesize_lambda_bodies_enabled(f.context()); write!(f, [token("lambda")])?; - if let Some(parameters) = parameters { - // In this context, a dangling comment can either be a comment between the `lambda` the + // Format any dangling comments before the parameters, but save any dangling comments after + // the parameters/after the header to be formatted with the body below. + let dangling_header_comments = if let Some(parameters) = parameters { + // In this context, a dangling comment can either be a comment between the `lambda` and the // parameters, or a comment between the parameters and the body. let (dangling_before_parameters, dangling_after_parameters) = dangling .split_at(dangling.partition_point(|comment| comment.end() < parameters.start())); @@ -86,7 +98,7 @@ impl FormatNodeRule for FormatExprLambda { // *x: x // ) // ``` - if comments.has_leading(&**parameters) { + if comments.has_leading(parameters) { hard_line_break().fmt(f)?; } else { write!(f, [space()])?; @@ -95,32 +107,90 @@ impl FormatNodeRule for FormatExprLambda { write!(f, [dangling_comments(dangling_before_parameters)])?; } - write!( - f, - [parameters - .format() - .with_options(ParametersParentheses::Never)] - )?; - - write!(f, [token(":")])?; - - if dangling_after_parameters.is_empty() { - write!(f, [space()])?; + // Try to keep the parameters on a single line, unless there are intervening comments. + if preview && !comments.contains_comments(parameters.into()) { + let mut buffer = RemoveSoftLinesBuffer::new(f); + write!( + buffer, + [parameters + .format() + .with_options(ParametersParentheses::Never)] + )?; } else { - write!(f, [dangling_comments(dangling_after_parameters)])?; + write!( + f, + [parameters + .format() + .with_options(ParametersParentheses::Never)] + )?; } + + dangling_after_parameters } else { - write!(f, [token(":")])?; + dangling + }; - // In this context, a dangling comment is a comment between the `lambda` and the body. - if dangling.is_empty() { - write!(f, [space()])?; - } else { - write!(f, [dangling_comments(dangling)])?; - } + write!(f, [token(":")])?; + + if dangling_header_comments.is_empty() { + write!(f, [space()])?; + } else if !preview { + write!(f, [dangling_comments(dangling_header_comments)])?; } - write!(f, [body.format()]) + if !preview { + return body.format().fmt(f); + } + + let fmt_body = FormatBody { + body, + dangling_header_comments, + }; + + match self.layout { + ExprLambdaLayout::Assignment => fits_expanded(&fmt_body).fmt(f), + ExprLambdaLayout::Default => fmt_body.fmt(f), + } + } +} + +#[derive(Debug, Default, Copy, Clone)] +pub enum ExprLambdaLayout { + #[default] + Default, + + /// The [`ExprLambda`] is the direct child of an assignment expression, so it needs to use + /// `fits_expanded` to prefer parenthesizing its own body before the assignment tries to + /// parenthesize the whole lambda. For example, we want this formatting: + /// + /// ```py + /// long_assignment_target = lambda x, y, z: ( + /// x + y + z + /// ) + /// ``` + /// + /// instead of either of these: + /// + /// ```py + /// long_assignment_target = ( + /// lambda x, y, z: ( + /// x + y + z + /// ) + /// ) + /// + /// long_assignment_target = ( + /// lambda x, y, z: x + y + z + /// ) + /// ``` + Assignment, +} + +impl FormatRuleWithOptions> for FormatExprLambda { + type Options = ExprLambdaLayout; + + fn with_options(mut self, options: Self::Options) -> Self { + self.layout = options; + self } } @@ -137,3 +207,266 @@ impl NeedsParentheses for ExprLambda { } } } + +struct FormatBody<'a> { + body: &'a Expr, + + /// Dangling comments attached to the lambda header that should be formatted with the body. + /// + /// These can include both own-line and end-of-line comments. For lambdas with parameters, this + /// means comments after the parameters: + /// + /// ```py + /// ( + /// lambda x, y # 1 + /// # 2 + /// : # 3 + /// # 4 + /// x + y + /// ) + /// ``` + /// + /// Or all dangling comments for lambdas without parameters: + /// + /// ```py + /// ( + /// lambda # 1 + /// # 2 + /// : # 3 + /// # 4 + /// 1 + /// ) + /// ``` + /// + /// In most cases these should formatted within the parenthesized body, as in: + /// + /// ```py + /// ( + /// lambda: ( # 1 + /// # 2 + /// # 3 + /// # 4 + /// 1 + /// ) + /// ) + /// ``` + /// + /// or without `# 2`: + /// + /// ```py + /// ( + /// lambda: ( # 1 # 3 + /// # 4 + /// 1 + /// ) + /// ) + /// ``` + dangling_header_comments: &'a [SourceComment], +} + +impl Format> for FormatBody<'_> { + fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { + let FormatBody { + dangling_header_comments, + body, + } = self; + + let body = *body; + let comments = f.context().comments().clone(); + let body_comments = comments.leading_dangling_trailing(body); + + if !dangling_header_comments.is_empty() { + // Split the dangling header comments into trailing comments formatted with the lambda + // header (1) and leading comments formatted with the body (2, 3, 4). + // + // ```python + // ( + // lambda # 1 + // # 2 + // : # 3 + // # 4 + // y + // ) + // ``` + // + // Note that these are split based on their line position rather than using + // `partition_point` based on a range, for example. + let (trailing_header_comments, leading_body_comments) = dangling_header_comments + .split_at( + dangling_header_comments + .iter() + .position(|comment| comment.line_position().is_own_line()) + .unwrap_or(dangling_header_comments.len()), + ); + + // If the body is parenthesized and has its own leading comments, preserve the + // separation between the dangling lambda comments and the body comments. For + // example, preserve this comment positioning: + // + // ```python + // ( + // lambda: # 1 + // # 2 + // ( # 3 + // x + // ) + // ) + // ``` + // + // 1 and 2 are dangling on the lambda and emitted first, followed by a hard line + // break and the parenthesized body with its leading comments. + // + // However, when removing 2, 1 and 3 can instead be formatted on the same line: + // + // ```python + // ( + // lambda: ( # 1 # 3 + // x + // ) + // ) + // ``` + let comments = f.context().comments(); + if is_expression_parenthesized(body.into(), comments.ranges(), f.context().source()) + && comments.has_leading(body) + { + trailing_comments(dangling_header_comments).fmt(f)?; + + // Note that `leading_body_comments` have already been formatted as part of + // `dangling_header_comments` above, but their presence still determines the spacing + // here. + if leading_body_comments.is_empty() { + space().fmt(f)?; + } else { + hard_line_break().fmt(f)?; + } + + body.format().with_options(Parentheses::Always).fmt(f) + } else { + write!( + f, + [ + space(), + token("("), + trailing_comments(trailing_header_comments), + block_indent(&format_args!( + leading_comments(leading_body_comments), + body.format().with_options(Parentheses::Never) + )), + token(")") + ] + ) + } + } + // If the body has comments, we always want to preserve the parentheses. This also + // ensures that we correctly handle parenthesized comments, and don't need to worry + // about them in the implementation below. + else if body_comments.has_leading() || body_comments.has_trailing_own_line() { + body.format().with_options(Parentheses::Always).fmt(f) + } + // Calls and subscripts require special formatting because they have their own + // parentheses, but they can also have an arbitrary amount of text before the + // opening parenthesis. We want to avoid cases where we keep a long callable on the + // same line as the lambda parameters. For example, `db_evmtx...` in: + // + // ```py + // transaction_count = self._query_txs_for_range( + // get_count_fn=lambda from_ts, to_ts, _chain_id=chain_id: db_evmtx.count_transactions_in_range( + // chain_id=_chain_id, + // from_ts=from_ts, + // to_ts=to_ts, + // ), + // ) + // ``` + // + // should cause the whole lambda body to be parenthesized instead: + // + // ```py + // transaction_count = self._query_txs_for_range( + // get_count_fn=lambda from_ts, to_ts, _chain_id=chain_id: ( + // db_evmtx.count_transactions_in_range( + // chain_id=_chain_id, + // from_ts=from_ts, + // to_ts=to_ts, + // ) + // ), + // ) + // ``` + else if matches!(body, Expr::Call(_) | Expr::Subscript(_)) { + let unparenthesized = body.format().with_options(Parentheses::Never); + if CallChainLayout::from_expression( + body.into(), + comments.ranges(), + f.context().source(), + ) == CallChainLayout::Fluent + { + parenthesize_if_expands(&unparenthesized).fmt(f) + } else { + let unparenthesized = unparenthesized.memoized(); + if unparenthesized.inspect(f)?.will_break() { + expand_parent().fmt(f)?; + } + + best_fitting![ + // body all flat + unparenthesized, + // body expanded + group(&unparenthesized).should_expand(true), + // parenthesized + format_args![token("("), block_indent(&unparenthesized), token(")")] + ] + .fmt(f) + } + } + // For other cases with their own parentheses, such as lists, sets, dicts, tuples, + // etc., we can just format the body directly. Their own formatting results in the + // lambda being formatted well too. For example: + // + // ```py + // lambda xxxxxxxxxxxxxxxxxxxx, yyyyyyyyyyyyyyyyyyyy, zzzzzzzzzzzzzzzzzzzz: [xxxxxxxxxxxxxxxxxxxx, yyyyyyyyyyyyyyyyyyyy, zzzzzzzzzzzzzzzzzzzz] + // ``` + // + // gets formatted as: + // + // ```py + // lambda xxxxxxxxxxxxxxxxxxxx, yyyyyyyyyyyyyyyyyyyy, zzzzzzzzzzzzzzzzzzzz: [ + // xxxxxxxxxxxxxxxxxxxx, + // yyyyyyyyyyyyyyyyyyyy, + // zzzzzzzzzzzzzzzzzzzz + // ] + // ``` + else if has_own_parentheses(body, f.context()).is_some() { + body.format().fmt(f) + } + // Finally, for expressions without their own parentheses, use + // `parenthesize_if_expands` to add parentheses around the body, only if it expands + // across multiple lines. The `Parentheses::Never` here also removes unnecessary + // parentheses around lambda bodies that fit on one line. For example: + // + // ```py + // lambda xxxxxxxxxxxxxxxxxxxx, yyyyyyyyyyyyyyyyyyyy, zzzzzzzzzzzzzzzzzzzz: xxxxxxxxxxxxxxxxxxxx + yyyyyyyyyyyyyyyyyyyy + zzzzzzzzzzzzzzzzzzzz + // ``` + // + // is formatted as: + // + // ```py + // lambda xxxxxxxxxxxxxxxxxxxx, yyyyyyyyyyyyyyyyyyyy, zzzzzzzzzzzzzzzzzzzz: ( + // xxxxxxxxxxxxxxxxxxxx + yyyyyyyyyyyyyyyyyyyy + zzzzzzzzzzzzzzzzzzzz + // ) + // ``` + // + // while + // + // ```py + // lambda xxxxxxxxxxxxxxxxxxxx: (xxxxxxxxxxxxxxxxxxxx + 1) + // ``` + // + // is formatted as: + // + // ```py + // lambda xxxxxxxxxxxxxxxxxxxx: xxxxxxxxxxxxxxxxxxxx + 1 + // ``` + else { + parenthesize_if_expands(&body.format().with_options(Parentheses::Never)).fmt(f) + } + } +} diff --git a/crates/ruff_python_formatter/src/preview.rs b/crates/ruff_python_formatter/src/preview.rs index 9d307390d6..62b6b90033 100644 --- a/crates/ruff_python_formatter/src/preview.rs +++ b/crates/ruff_python_formatter/src/preview.rs @@ -52,3 +52,10 @@ pub(crate) const fn is_avoid_parens_for_long_as_captures_enabled( ) -> bool { context.is_preview() } + +/// Returns `true` if the +/// [`parenthesize_lambda_bodies`](https://github.com/astral-sh/ruff/pull/21385) preview style is +/// enabled. +pub(crate) const fn is_parenthesize_lambda_bodies_enabled(context: &PyFormatContext) -> bool { + context.is_preview() +} diff --git a/crates/ruff_python_formatter/src/statement/stmt_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_assign.rs index 5a16e5e8bf..52ebd710a6 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_assign.rs @@ -9,6 +9,7 @@ use crate::comments::{ Comments, LeadingDanglingTrailingComments, SourceComment, trailing_comments, }; use crate::context::{NodeLevel, WithNodeLevel}; +use crate::expression::expr_lambda::ExprLambdaLayout; use crate::expression::parentheses::{ NeedsParentheses, OptionalParentheses, Parentheses, Parenthesize, is_expression_parenthesized, optional_parentheses, @@ -18,6 +19,7 @@ use crate::expression::{ maybe_parenthesize_expression, }; use crate::other::interpolated_string::InterpolatedStringLayout; +use crate::preview::is_parenthesize_lambda_bodies_enabled; use crate::statement::trailing_semicolon; use crate::string::StringLikeExtensions; use crate::string::implicit::{ @@ -303,12 +305,7 @@ impl Format> for FormatStatementsLastExpression<'_> { && format_implicit_flat.is_none() && format_interpolated_string.is_none() { - return maybe_parenthesize_expression( - value, - *statement, - Parenthesize::IfBreaks, - ) - .fmt(f); + return maybe_parenthesize_value(value, *statement).fmt(f); } let comments = f.context().comments().clone(); @@ -586,11 +583,7 @@ impl Format> for FormatStatementsLastExpression<'_> { space(), operator, space(), - maybe_parenthesize_expression( - value, - *statement, - Parenthesize::IfBreaks - ) + maybe_parenthesize_value(value, *statement) ] ); } @@ -1369,3 +1362,32 @@ fn is_attribute_with_parenthesized_value(target: &Expr, context: &PyFormatContex _ => false, } } + +/// Like [`maybe_parenthesize_expression`] but with special handling for lambdas in preview. +fn maybe_parenthesize_value<'a>( + expression: &'a Expr, + parent: AnyNodeRef<'a>, +) -> MaybeParenthesizeValue<'a> { + MaybeParenthesizeValue { expression, parent } +} + +struct MaybeParenthesizeValue<'a> { + expression: &'a Expr, + parent: AnyNodeRef<'a>, +} + +impl Format> for MaybeParenthesizeValue<'_> { + fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { + let MaybeParenthesizeValue { expression, parent } = self; + + if is_parenthesize_lambda_bodies_enabled(f.context()) + && let Expr::Lambda(lambda) = expression + && !f.context().comments().has_leading(lambda) + { + parenthesize_if_expands(&lambda.format().with_options(ExprLambdaLayout::Assignment)) + .fmt(f) + } else { + maybe_parenthesize_expression(expression, *parent, Parenthesize::IfBreaks).fmt(f) + } + } +} diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings.py.snap index 7b36236110..ca59cec651 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings.py.snap @@ -906,11 +906,10 @@ x = { -) +string_with_escaped_nameescape = "........................................................................... \\N{LAO KO LA}" --msg = lambda x: ( + msg = lambda x: ( - f"this is a very very very very long lambda value {x} that doesn't fit on a" - " single line" -+msg = ( -+ lambda x: f"this is a very very very very long lambda value {x} that doesn't fit on a single line" ++ f"this is a very very very very long lambda value {x} that doesn't fit on a single line" ) dict_with_lambda_values = { @@ -1403,8 +1402,8 @@ string_with_escaped_nameescape = ".............................................. string_with_escaped_nameescape = "........................................................................... \\N{LAO KO LA}" -msg = ( - lambda x: f"this is a very very very very long lambda value {x} that doesn't fit on a single line" +msg = lambda x: ( + f"this is a very very very very long lambda value {x} that doesn't fit on a single line" ) dict_with_lambda_values = { diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_multiline_strings.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_multiline_strings.py.snap index 353d573496..29af6e1b07 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_multiline_strings.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_multiline_strings.py.snap @@ -375,7 +375,7 @@ a = b if """ # Another use case data = yaml.load("""\ a: 1 -@@ -77,19 +106,23 @@ +@@ -77,10 +106,12 @@ b: 2 """, ) @@ -390,19 +390,7 @@ a = b if """ MULTILINE = """ foo - """.replace("\n", "") --generated_readme = lambda project_name: """ -+generated_readme = ( -+ lambda project_name: """ - {} - - - """.strip().format(project_name) -+) - parser.usage += """ - Custom extra help summary. - -@@ -156,16 +189,24 @@ +@@ -156,16 +187,24 @@ 10 LOAD_CONST 0 (None) 12 RETURN_VALUE """ % (_C.__init__.__code__.co_firstlineno + 1,) @@ -433,7 +421,7 @@ a = b if """ [ """cow moos""", -@@ -206,7 +247,9 @@ +@@ -206,7 +245,9 @@ "c" ) @@ -444,7 +432,7 @@ a = b if """ assert some_var == expected_result, """ test -@@ -224,10 +267,8 @@ +@@ -224,10 +265,8 @@ """Sxxxxxxx xxxxxxxx, xxxxxxx xx xxxxxxxxx xxxxxxxxxxxxx xxxxxxx xxxxxxxxx xxx-xxxxxxxxxx xxxxxx xx xxx-xxxxxx""" ), @@ -457,7 +445,7 @@ a = b if """ }, } -@@ -246,14 +287,12 @@ +@@ -246,14 +285,12 @@ a a""" ), @@ -597,13 +585,11 @@ data = yaml.load( MULTILINE = """ foo """.replace("\n", "") -generated_readme = ( - lambda project_name: """ +generated_readme = lambda project_name: """ {} """.strip().format(project_name) -) parser.usage += """ Custom extra help summary. diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__lambda.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__lambda.py.snap index 5997ff539a..82ee1639d7 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__lambda.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__lambda.py.snap @@ -131,6 +131,13 @@ lambda a, /, c: a *x: x ) +( + lambda + # comment + *x, + **y: x +) + ( lambda # comment 1 @@ -202,6 +209,17 @@ lambda: ( # comment x ) +( + lambda # 1 + # 2 + x, # 3 + # 4 + y + : # 5 + # 6 + x +) + ( lambda x, @@ -210,6 +228,71 @@ lambda: ( # comment z ) + +# Leading +lambda x: ( + lambda y: lambda z: x + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + z # Trailing +) # Trailing + + +# Leading +lambda x: lambda y: lambda z: [ + x, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + z +] # Trailing +# Trailing + lambda self, araa, kkkwargs=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs), e=1, f=2, g=2: d # Regression tests for https://github.com/astral-sh/ruff/issues/8179 @@ -234,6 +317,441 @@ def a(): g = 10 ) +def a(): + return b( + c, + d, + e, + f=lambda self, *args, **kwargs: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa( + *args, **kwargs + ) + 1, + ) + +# Additional ecosystem cases from https://github.com/astral-sh/ruff/pull/21385 +class C: + def foo(): + mock_service.return_value.bucket.side_effect = lambda name: ( + source_bucket + if name == source_bucket_name + else storage.Bucket(mock_service, destination_bucket_name) + ) + +class C: + function_dict: Dict[Text, Callable[[CRFToken], Any]] = { + CRFEntityExtractorOptions.POS2: lambda crf_token: crf_token.pos_tag[:2] + if crf_token.pos_tag is not None + else None, + } + +name = re.sub(r"[^\x21\x23-\x5b\x5d-\x7e]...............", lambda m: f"\\{m.group(0)}", p["name"]) + +def foo(): + if True: + if True: + return ( + lambda x: np.exp(cs(np.log(x.to(u.MeV).value))) * u.MeV * u.cm**2 / u.g + ) + +class C: + _is_recognized_dtype: Callable[[DtypeObj], bool] = lambda x: lib.is_np_dtype( + x, "M" + ) or isinstance(x, DatetimeTZDtype) + +class C: + def foo(): + if True: + transaction_count = self._query_txs_for_range( + get_count_fn=lambda from_ts, to_ts, _chain_id=chain_id: db_evmtx.count_transactions_in_range( + chain_id=_chain_id, + from_ts=from_ts, + to_ts=to_ts, + ), + ) + + transaction_count = self._query_txs_for_range( + get_count_fn=lambda from_ts, to_ts, _chain_id=chain_id: db_evmtx.count_transactions_in_range[_chain_id, from_ts, to_ts], + ) + +def ddb(): + sql = ( + lambda var, table, n=N: f""" + CREATE TABLE {table} AS + SELECT ROW_NUMBER() OVER () AS id, {var} + FROM ( + SELECT {var} + FROM RANGE({n}) _ ({var}) + ORDER BY RANDOM() + ) + """ + ) + +long_assignment_target.with_attribute.and_a_slice[with_an_index] = ( # 1 + # 2 + lambda x, y, z: # 3 + # 4 + x + y + z # 5 + # 6 +) + +long_assignment_target.with_attribute.and_a_slice[with_an_index] = ( + lambda x, y, z: x + y + z +) + +long_assignment_target.with_attribute.and_a_slice[with_an_index] = lambda x, y, z: x + y + z + +very_long_variable_name_x, very_long_variable_name_y = lambda a: a + some_very_long_expression, lambda b: b * another_very_long_expression_here + +very_long_variable_name_for_result += lambda x: very_long_function_call_that_should_definitely_be_parenthesized_now(x, more_args, additional_parameters) + + +if 1: + if 2: + if 3: + if self.location in EVM_EVMLIKE_LOCATIONS and database is not None: + exported_dict["notes"] = EVM_ADDRESS_REGEX.sub( + repl=lambda matched_address: self._maybe_add_label_with_address( + database=database, + matched_address=matched_address, + ), + string=exported_dict["notes"], + ) + +class C: + def f(): + return dict( + filter( + lambda intent_response: self.is_retrieval_intent_response( + intent_response + ), + self.responses.items(), + ) + ) + +@pytest.mark.parametrize( + "op", + [ + # Not fluent + param( + lambda left, right: ( + ibis.timestamp("2017-04-01") + ), + ), + # These four are fluent and fit on one line inside the parenthesized + # lambda body + param( + lambda left, right: ( + ibis.timestamp("2017-04-01").cast(dt.date) + ), + ), + param( + lambda left, right: ( + ibis.timestamp("2017-04-01").cast(dt.date).between(left, right) + ), + ), + param(lambda left, right: ibis.timestamp("2017-04-01").cast(dt.date)), + param(lambda left, right: ibis.timestamp("2017-04-01").cast(dt.date).between(left, right)), + # This is too long on one line in the lambda body and gets wrapped + # inside the body. + param( + lambda left, right: ( + ibis.timestamp("2017-04-01").cast(dt.date).between(left, right).between(left, right) + ), + ), + ], +) +def test_string_temporal_compare_between(con, op, left, right): ... + +[ + ( + lambda eval_df, _: MetricValue( + scores=eval_df["prediction"].tolist(), + aggregate_results={"prediction_sum": sum(eval_df["prediction"])}, + ) + ), +] + +# reuses the list parentheses +lambda xxxxxxxxxxxxxxxxxxxx, yyyyyyyyyyyyyyyyyyyy, zzzzzzzzzzzzzzzzzzzz: [xxxxxxxxxxxxxxxxxxxx, yyyyyyyyyyyyyyyyyyyy, zzzzzzzzzzzzzzzzzzzz] + +# adds parentheses around the body +lambda xxxxxxxxxxxxxxxxxxxx, yyyyyyyyyyyyyyyyyyyy, zzzzzzzzzzzzzzzzzzzz: xxxxxxxxxxxxxxxxxxxx + yyyyyyyyyyyyyyyyyyyy + zzzzzzzzzzzzzzzzzzzz + +# removes parentheses around the body +lambda xxxxxxxxxxxxxxxxxxxx: (xxxxxxxxxxxxxxxxxxxx + 1) + +mapper = lambda x: dict_with_default[np.nan if isinstance(x, float) and np.isnan(x) else x] + +lambda x, y, z: ( + x + y + z +) + +lambda x, y, z: ( + x + y + z + # trailing body +) + +lambda x, y, z: ( + x + y + z # trailing eol body +) + +lambda x, y, z: ( + x + y + z +) # trailing lambda + +lambda x, y, z: ( + # leading body + x + y + z +) + +lambda x, y, z: ( # leading eol body + x + y + z +) + +( + lambda name: + source_bucket # trailing eol comment + if name == source_bucket_name + else storage.Bucket(mock_service, destination_bucket_name) +) + +( + lambda name: + # dangling header comment + source_bucket + if name == source_bucket_name + else storage.Bucket(mock_service, destination_bucket_name) +) + +x = ( + lambda name: + # dangling header comment + source_bucket + if name == source_bucket_name + else storage.Bucket(mock_service, destination_bucket_name) +) + +( + lambda name: # dangling header comment + ( + source_bucket + if name == source_bucket_name + else storage.Bucket(mock_service, destination_bucket_name) + ) +) + +( + lambda from_ts, to_ts, _chain_id=chain_id: # dangling eol header comment + db_evmtx.count_transactions_in_range( + chain_id=_chain_id, + from_ts=from_ts, + to_ts=to_ts, + ) +) + +( + lambda from_ts, to_ts, _chain_id=chain_id: + # dangling header comment before call + db_evmtx.count_transactions_in_range( + chain_id=_chain_id, + from_ts=from_ts, + to_ts=to_ts, + ) +) + +( + lambda left, right: + # comment + ibis.timestamp("2017-04-01").cast(dt.date).between(left, right) +) + +( + lambda left, right: + ibis.timestamp("2017-04-01") # comment + .cast(dt.date) + .between(left, right) +) + +( + lambda xxxxxxxxxxxxxxxxxxxx, yyyyyyyyyyyyyyyyyyyy: + # comment + [xxxxxxxxxxxxxxxxxxxx, yyyyyyyyyyyyyyyyyyyy, zzzzzzzzzzzzzzzzzzzz] +) + +( + lambda x, y: + # comment + { + "key": x, + "another": y, + } +) + +( + lambda x, y: + # comment + ( + x, + y, + z + ) +) + +( + lambda x: + # comment + dict_with_default[np.nan if isinstance(x, float) and np.isnan(x) else x] +) + +( + lambda from_ts, to_ts, _chain_id=chain_id: + db_evmtx.count_transactions_in_range[ + # comment + _chain_id, from_ts, to_ts + ] +) + +( + lambda + # comment + *args, **kwargs: + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs) + 1 +) + +( + lambda # comment + *args, **kwargs: + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs) + 1 +) + +( + lambda # comment 1 + # comment 2 + *args, **kwargs: # comment 3 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs) + 1 +) + +( + lambda # comment 1 + *args, **kwargs: # comment 3 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs) + 1 +) + +( + lambda *args, **kwargs: + # comment 1 + ( # comment 2 + # comment 3 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs) + 1 # comment 4 + # comment 5 + ) # comment 6 +) + +( + lambda *brgs, **kwargs: + # comment 1 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa( # comment 2 + # comment 3 + *brgs, **kwargs) + 1 # comment 4 + # comment 5 +) + +( + lambda *crgs, **kwargs: # comment 1 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*crgs, **kwargs) + 1 +) + +( + lambda *drgs, **kwargs: # comment 1 + ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*drgs, **kwargs) + 1 + ) +) + +( + lambda * # comment 1 + ergs, ** + # comment 2 + kwargs # comment 3 + : # comment 4 + ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*ergs, **kwargs) + 1 + ) +) + +( + lambda # 1 + # 2 + left, # 3 + # 4 + right: # 5 + # 6 + ibis.timestamp("2017-04-01").cast(dt.date).between(left, right) +) + +( + lambda x: # outer comment 1 + ( + lambda y: # inner comment 1 + # inner comment 2 + lambda z: ( + # innermost comment + x + y + z + ) + ) +) + +foo( + lambda from_ts, # comment prevents collapsing the parameters to one line + to_ts, _chain_id=chain_id: db_evmtx.count_transactions_in_range( + chain_id=_chain_id, + from_ts=from_ts, + to_ts=to_ts, + ) +) + +foo( + lambda from_ts, # but still wrap the body if it gets too long + to_ts, + _chain_id=chain_id: db_evmtx.count_transactions_in_rangeeeeeeeeeeeeeeeeeeeeeeeeeeeee( + chain_id=_chain_id, + from_ts=from_ts, + to_ts=to_ts, + ) +) + +transform = lambda left, right: ibis.timestamp("2017-04-01").cast(dt.date).between(left, right).between(left, right) # trailing comment + +( + lambda: # comment + 1 +) + +( + lambda # comment + : + 1 +) + +( + lambda: + # comment + 1 +) + +( + lambda: # comment 1 + # comment 2 + 1 +) + +( + lambda # comment 1 + # comment 2 + : # comment 3 + # comment 4 + 1 +) + ( lambda * # comment 2 @@ -277,6 +795,21 @@ def a(): x: x ) + +( + lambda: # dangling-end-of-line + # dangling-own-line + ( # leading-body-end-of-line + x + ) +) + +( + lambda: # dangling-end-of-line + ( # leading-body-end-of-line + x + ) +) ``` ## Output @@ -409,6 +942,12 @@ lambda a, /, c: a *x: x ) +( + lambda + # comment + *x, **y: x +) + ( lambda # comment 1 @@ -476,12 +1015,87 @@ lambda: ( # comment x ) +( + lambda # 1 + # 2 + x, # 3 + # 4 + y: # 5 + # 6 + x +) + ( lambda x, # comment y: z ) + +# Leading +lambda x: ( + lambda y: lambda z: x + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + z # Trailing +) # Trailing + + +# Leading +lambda x: lambda y: lambda z: [ + x, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + z, +] # Trailing +# Trailing + lambda self, araa, kkkwargs=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa( *args, **kwargs ), e=1, f=2, g=2: d @@ -517,6 +1131,473 @@ def a(): ) +def a(): + return b( + c, + d, + e, + f=lambda self, + *args, + **kwargs: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs) + 1, + ) + + +# Additional ecosystem cases from https://github.com/astral-sh/ruff/pull/21385 +class C: + def foo(): + mock_service.return_value.bucket.side_effect = lambda name: ( + source_bucket + if name == source_bucket_name + else storage.Bucket(mock_service, destination_bucket_name) + ) + + +class C: + function_dict: Dict[Text, Callable[[CRFToken], Any]] = { + CRFEntityExtractorOptions.POS2: lambda crf_token: crf_token.pos_tag[:2] + if crf_token.pos_tag is not None + else None, + } + + +name = re.sub( + r"[^\x21\x23-\x5b\x5d-\x7e]...............", lambda m: f"\\{m.group(0)}", p["name"] +) + + +def foo(): + if True: + if True: + return ( + lambda x: np.exp(cs(np.log(x.to(u.MeV).value))) * u.MeV * u.cm**2 / u.g + ) + + +class C: + _is_recognized_dtype: Callable[[DtypeObj], bool] = lambda x: lib.is_np_dtype( + x, "M" + ) or isinstance(x, DatetimeTZDtype) + + +class C: + def foo(): + if True: + transaction_count = self._query_txs_for_range( + get_count_fn=lambda from_ts, + to_ts, + _chain_id=chain_id: db_evmtx.count_transactions_in_range( + chain_id=_chain_id, + from_ts=from_ts, + to_ts=to_ts, + ), + ) + + transaction_count = self._query_txs_for_range( + get_count_fn=lambda from_ts, + to_ts, + _chain_id=chain_id: db_evmtx.count_transactions_in_range[ + _chain_id, from_ts, to_ts + ], + ) + + +def ddb(): + sql = ( + lambda var, table, n=N: f""" + CREATE TABLE {table} AS + SELECT ROW_NUMBER() OVER () AS id, {var} + FROM ( + SELECT {var} + FROM RANGE({n}) _ ({var}) + ORDER BY RANDOM() + ) + """ + ) + + +long_assignment_target.with_attribute.and_a_slice[with_an_index] = ( # 1 + # 2 + lambda x, y, z: # 3 + # 4 + x + y + z # 5 + # 6 +) + +long_assignment_target.with_attribute.and_a_slice[with_an_index] = ( + lambda x, y, z: x + y + z +) + +long_assignment_target.with_attribute.and_a_slice[with_an_index] = ( + lambda x, y, z: x + y + z +) + +very_long_variable_name_x, very_long_variable_name_y = ( + lambda a: a + some_very_long_expression, + lambda b: b * another_very_long_expression_here, +) + +very_long_variable_name_for_result += ( + lambda x: very_long_function_call_that_should_definitely_be_parenthesized_now( + x, more_args, additional_parameters + ) +) + + +if 1: + if 2: + if 3: + if self.location in EVM_EVMLIKE_LOCATIONS and database is not None: + exported_dict["notes"] = EVM_ADDRESS_REGEX.sub( + repl=lambda matched_address: self._maybe_add_label_with_address( + database=database, + matched_address=matched_address, + ), + string=exported_dict["notes"], + ) + + +class C: + def f(): + return dict( + filter( + lambda intent_response: self.is_retrieval_intent_response( + intent_response + ), + self.responses.items(), + ) + ) + + +@pytest.mark.parametrize( + "op", + [ + # Not fluent + param( + lambda left, right: (ibis.timestamp("2017-04-01")), + ), + # These four are fluent and fit on one line inside the parenthesized + # lambda body + param( + lambda left, right: (ibis.timestamp("2017-04-01").cast(dt.date)), + ), + param( + lambda left, right: ( + ibis.timestamp("2017-04-01").cast(dt.date).between(left, right) + ), + ), + param(lambda left, right: ibis.timestamp("2017-04-01").cast(dt.date)), + param( + lambda left, right: ibis.timestamp("2017-04-01") + .cast(dt.date) + .between(left, right) + ), + # This is too long on one line in the lambda body and gets wrapped + # inside the body. + param( + lambda left, right: ( + ibis.timestamp("2017-04-01") + .cast(dt.date) + .between(left, right) + .between(left, right) + ), + ), + ], +) +def test_string_temporal_compare_between(con, op, left, right): ... + + +[ + ( + lambda eval_df, _: MetricValue( + scores=eval_df["prediction"].tolist(), + aggregate_results={"prediction_sum": sum(eval_df["prediction"])}, + ) + ), +] + +# reuses the list parentheses +lambda xxxxxxxxxxxxxxxxxxxx, yyyyyyyyyyyyyyyyyyyy, zzzzzzzzzzzzzzzzzzzz: [ + xxxxxxxxxxxxxxxxxxxx, + yyyyyyyyyyyyyyyyyyyy, + zzzzzzzzzzzzzzzzzzzz, +] + +# adds parentheses around the body +lambda xxxxxxxxxxxxxxxxxxxx, yyyyyyyyyyyyyyyyyyyy, zzzzzzzzzzzzzzzzzzzz: xxxxxxxxxxxxxxxxxxxx + yyyyyyyyyyyyyyyyyyyy + zzzzzzzzzzzzzzzzzzzz + +# removes parentheses around the body +lambda xxxxxxxxxxxxxxxxxxxx: (xxxxxxxxxxxxxxxxxxxx + 1) + +mapper = lambda x: dict_with_default[ + np.nan if isinstance(x, float) and np.isnan(x) else x +] + +lambda x, y, z: (x + y + z) + +lambda x, y, z: ( + x + y + z + # trailing body +) + +lambda x, y, z: ( + x + y + z # trailing eol body +) + +lambda x, y, z: (x + y + z) # trailing lambda + +lambda x, y, z: ( + # leading body + x + y + z +) + +lambda x, y, z: ( # leading eol body + x + y + z +) + +( + lambda name: source_bucket # trailing eol comment + if name == source_bucket_name + else storage.Bucket(mock_service, destination_bucket_name) +) + +( + lambda name: + # dangling header comment + source_bucket + if name == source_bucket_name + else storage.Bucket(mock_service, destination_bucket_name) +) + +x = ( + lambda name: + # dangling header comment + source_bucket + if name == source_bucket_name + else storage.Bucket(mock_service, destination_bucket_name) +) + +( + lambda name: # dangling header comment + ( + source_bucket + if name == source_bucket_name + else storage.Bucket(mock_service, destination_bucket_name) + ) +) + +( + lambda from_ts, to_ts, _chain_id=chain_id: # dangling eol header comment + db_evmtx.count_transactions_in_range( + chain_id=_chain_id, + from_ts=from_ts, + to_ts=to_ts, + ) +) + +( + lambda from_ts, to_ts, _chain_id=chain_id: + # dangling header comment before call + db_evmtx.count_transactions_in_range( + chain_id=_chain_id, + from_ts=from_ts, + to_ts=to_ts, + ) +) + +( + lambda left, right: + # comment + ibis.timestamp("2017-04-01").cast(dt.date).between(left, right) +) + +( + lambda left, right: ibis.timestamp("2017-04-01") # comment + .cast(dt.date) + .between(left, right) +) + +( + lambda xxxxxxxxxxxxxxxxxxxx, yyyyyyyyyyyyyyyyyyyy: + # comment + [xxxxxxxxxxxxxxxxxxxx, yyyyyyyyyyyyyyyyyyyy, zzzzzzzzzzzzzzzzzzzz] +) + +( + lambda x, y: + # comment + { + "key": x, + "another": y, + } +) + +( + lambda x, y: + # comment + (x, y, z) +) + +( + lambda x: + # comment + dict_with_default[np.nan if isinstance(x, float) and np.isnan(x) else x] +) + +( + lambda from_ts, to_ts, _chain_id=chain_id: db_evmtx.count_transactions_in_range[ + # comment + _chain_id, from_ts, to_ts + ] +) + +( + lambda + # comment + *args, **kwargs: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs) + + 1 +) + +( + lambda # comment + *args, **kwargs: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs) + + 1 +) + +( + lambda # comment 1 + # comment 2 + *args, **kwargs: # comment 3 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs) + 1 +) + +( + lambda # comment 1 + *args, **kwargs: # comment 3 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs) + 1 +) + +( + lambda *args, **kwargs: + # comment 1 + ( # comment 2 + # comment 3 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs) + + 1 # comment 4 + # comment 5 + ) # comment 6 +) + +( + lambda *brgs, **kwargs: + # comment 1 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa( # comment 2 + # comment 3 + *brgs, + **kwargs, + ) + + 1 # comment 4 + # comment 5 +) + +( + lambda *crgs, **kwargs: # comment 1 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*crgs, **kwargs) + 1 +) + +( + lambda *drgs, **kwargs: # comment 1 + (aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*drgs, **kwargs) + 1) +) + +( + lambda + # comment 1 + *ergs, + # comment 2 + **kwargs: # comment 3 + # comment 4 + (aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*ergs, **kwargs) + 1) +) + +( + lambda # 1 + # 2 + left, # 3 + # 4 + right: # 5 + # 6 + ibis.timestamp("2017-04-01").cast(dt.date).between(left, right) +) + +( + lambda x: # outer comment 1 + ( + lambda y: # inner comment 1 + # inner comment 2 + lambda z: ( + # innermost comment + x + y + z + ) + ) +) + +foo( + lambda from_ts, # comment prevents collapsing the parameters to one line + to_ts, + _chain_id=chain_id: db_evmtx.count_transactions_in_range( + chain_id=_chain_id, + from_ts=from_ts, + to_ts=to_ts, + ) +) + +foo( + lambda from_ts, # but still wrap the body if it gets too long + to_ts, + _chain_id=chain_id: db_evmtx.count_transactions_in_rangeeeeeeeeeeeeeeeeeeeeeeeeeeeee( + chain_id=_chain_id, + from_ts=from_ts, + to_ts=to_ts, + ) +) + +transform = ( + lambda left, right: ibis.timestamp("2017-04-01") + .cast(dt.date) + .between(left, right) + .between(left, right) +) # trailing comment + +( + lambda: # comment + 1 +) + +( + lambda: # comment + 1 +) + +( + lambda: + # comment + 1 +) + +( + lambda: # comment 1 + # comment 2 + 1 +) + +( + lambda: # comment 1 + # comment 2 + # comment 3 + # comment 4 + 1 +) + ( lambda # comment 2 @@ -553,4 +1634,928 @@ def a(): # comment 1 **x: x ) + +( + lambda: # dangling-end-of-line + # dangling-own-line + ( # leading-body-end-of-line + x + ) +) + +( + lambda: # dangling-end-of-line + ( # leading-body-end-of-line + x + ) +) +``` + + +## Preview changes +```diff +--- Stable ++++ Preview +@@ -27,35 +27,14 @@ + # Trailing + + # Leading +-lambda x: lambda y: lambda z: ( +- x, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- z, ++lambda x: ( ++ lambda y: ( ++ lambda z: (x, y, y, y, y, y, y, y, y, y, y, y, y, y, y, y, y, y, y, y, y, y, z) ++ ) + ) # Trailing + # Trailing + +-a = ( +- lambda: # Dangling ++a = lambda: ( # Dangling + 1 + ) + +@@ -74,7 +53,9 @@ + + # lambda arguments don't have parentheses, so we never add a magic trailing comma ... + def f( +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = lambda x: y, ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = lambda x: ( ++ y ++ ), + ): + pass + +@@ -102,22 +83,25 @@ + + # Dangling comments without parameters. + ( +- lambda: # 3 +- None ++ lambda: ( # 3 ++ None ++ ) + ) + + ( +- lambda: +- # 3 +- None ++ lambda: ( ++ # 3 ++ None ++ ) + ) + + ( +- lambda: # 1 +- # 2 +- # 3 +- # 4 +- None # 5 ++ lambda: ( # 1 ++ # 2 ++ # 3 ++ # 4 ++ None ++ ) # 5 + ) + + ( +@@ -136,16 +120,18 @@ + lambda + # comment 1 + # comment 2 +- *x: +- # comment 3 +- x ++ *x: ( ++ # comment 3 ++ x ++ ) + ) + + ( + lambda # comment 1 + # comment 2 +- *x: # comment 3 +- x ++ *x: ( # comment 3 ++ x ++ ) + ) + + lambda *x: x +@@ -161,30 +147,33 @@ + ) + + ( +- lambda: # comment +- x ++ lambda: ( # comment ++ x ++ ) + ) + + ( +- lambda: +- # comment +- x ++ lambda: ( ++ # comment ++ x ++ ) + ) + + ( +- lambda: # comment +- x ++ lambda: ( # comment ++ x ++ ) + ) + + ( +- lambda: +- # comment +- x ++ lambda: ( ++ # comment ++ x ++ ) + ) + + ( +- lambda: # comment +- ( # comment ++ lambda: ( # comment # comment + x + ) + ) +@@ -192,11 +181,12 @@ + ( + lambda # 1 + # 2 +- x: # 3 +- # 4 +- # 5 +- # 6 +- x ++ x: ( # 3 ++ # 4 ++ # 5 ++ # 6 ++ x ++ ) + ) + + ( +@@ -204,9 +194,10 @@ + # 2 + x, # 3 + # 4 +- y: # 5 +- # 6 +- x ++ y: ( # 5 ++ # 6 ++ x ++ ) + ) + + ( +@@ -218,71 +209,79 @@ + + # Leading + lambda x: ( +- lambda y: lambda z: x +- + y +- + y +- + y +- + y +- + y +- + y +- + y +- + y +- + y +- + y +- + y +- + y +- + y +- + y +- + y +- + y +- + y +- + y +- + y +- + y +- + y +- + z # Trailing ++ lambda y: ( ++ lambda z: ( ++ x ++ + y ++ + y ++ + y ++ + y ++ + y ++ + y ++ + y ++ + y ++ + y ++ + y ++ + y ++ + y ++ + y ++ + y ++ + y ++ + y ++ + y ++ + y ++ + y ++ + y ++ + y ++ + z ++ ) ++ ) # Trailing + ) # Trailing + + + # Leading +-lambda x: lambda y: lambda z: [ +- x, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- y, +- z, +-] # Trailing ++lambda x: ( ++ lambda y: ( ++ lambda z: [ ++ x, ++ y, ++ y, ++ y, ++ y, ++ y, ++ y, ++ y, ++ y, ++ y, ++ y, ++ y, ++ y, ++ y, ++ y, ++ y, ++ y, ++ y, ++ y, ++ y, ++ y, ++ y, ++ y, ++ y, ++ y, ++ y, ++ y, ++ y, ++ y, ++ y, ++ z, ++ ] ++ ) ++) # Trailing + # Trailing + +-lambda self, araa, kkkwargs=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa( +- *args, **kwargs +-), e=1, f=2, g=2: d ++lambda self, araa, kkkwargs=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs), e=1, f=2, g=2: ( ++ d ++) + + + # Regression tests for https://github.com/astral-sh/ruff/issues/8179 +@@ -291,9 +290,9 @@ + c, + d, + e, +- f=lambda self, +- *args, +- **kwargs: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs), ++ f=lambda self, *args, **kwargs: ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs) ++ ), + ) + + +@@ -302,15 +301,9 @@ + c, + d, + e, +- f=lambda self, +- araa, +- kkkwargs, +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, +- args, +- kwargs, +- e=1, +- f=2, +- g=2: d, ++ f=lambda self, araa, kkkwargs, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, args, kwargs, e=1, f=2, g=2: ( ++ d ++ ), + g=10, + ) + +@@ -320,9 +313,9 @@ + c, + d, + e, +- f=lambda self, +- *args, +- **kwargs: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs) + 1, ++ f=lambda self, *args, **kwargs: ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs) + 1 ++ ), + ) + + +@@ -338,9 +331,9 @@ + + class C: + function_dict: Dict[Text, Callable[[CRFToken], Any]] = { +- CRFEntityExtractorOptions.POS2: lambda crf_token: crf_token.pos_tag[:2] +- if crf_token.pos_tag is not None +- else None, ++ CRFEntityExtractorOptions.POS2: lambda crf_token: ( ++ crf_token.pos_tag[:2] if crf_token.pos_tag is not None else None ++ ), + } + + +@@ -352,42 +345,40 @@ + def foo(): + if True: + if True: +- return ( +- lambda x: np.exp(cs(np.log(x.to(u.MeV).value))) * u.MeV * u.cm**2 / u.g ++ return lambda x: ( ++ np.exp(cs(np.log(x.to(u.MeV).value))) * u.MeV * u.cm**2 / u.g + ) + + + class C: +- _is_recognized_dtype: Callable[[DtypeObj], bool] = lambda x: lib.is_np_dtype( +- x, "M" +- ) or isinstance(x, DatetimeTZDtype) ++ _is_recognized_dtype: Callable[[DtypeObj], bool] = lambda x: ( ++ lib.is_np_dtype(x, "M") or isinstance(x, DatetimeTZDtype) ++ ) + + + class C: + def foo(): + if True: + transaction_count = self._query_txs_for_range( +- get_count_fn=lambda from_ts, +- to_ts, +- _chain_id=chain_id: db_evmtx.count_transactions_in_range( +- chain_id=_chain_id, +- from_ts=from_ts, +- to_ts=to_ts, ++ get_count_fn=lambda from_ts, to_ts, _chain_id=chain_id: ( ++ db_evmtx.count_transactions_in_range( ++ chain_id=_chain_id, ++ from_ts=from_ts, ++ to_ts=to_ts, ++ ) + ), + ) + + transaction_count = self._query_txs_for_range( +- get_count_fn=lambda from_ts, +- to_ts, +- _chain_id=chain_id: db_evmtx.count_transactions_in_range[ +- _chain_id, from_ts, to_ts +- ], ++ get_count_fn=lambda from_ts, to_ts, _chain_id=chain_id: ( ++ db_evmtx.count_transactions_in_range[_chain_id, from_ts, to_ts] ++ ), + ) + + + def ddb(): +- sql = ( +- lambda var, table, n=N: f""" ++ sql = lambda var, table, n=N: ( ++ f""" + CREATE TABLE {table} AS + SELECT ROW_NUMBER() OVER () AS id, {var} + FROM ( +@@ -401,18 +392,19 @@ + + long_assignment_target.with_attribute.and_a_slice[with_an_index] = ( # 1 + # 2 +- lambda x, y, z: # 3 +- # 4 +- x + y + z # 5 ++ lambda x, y, z: ( # 3 ++ # 4 ++ x + y + z ++ ) # 5 + # 6 + ) + +-long_assignment_target.with_attribute.and_a_slice[with_an_index] = ( +- lambda x, y, z: x + y + z ++long_assignment_target.with_attribute.and_a_slice[with_an_index] = lambda x, y, z: ( ++ x + y + z + ) + +-long_assignment_target.with_attribute.and_a_slice[with_an_index] = ( +- lambda x, y, z: x + y + z ++long_assignment_target.with_attribute.and_a_slice[with_an_index] = lambda x, y, z: ( ++ x + y + z + ) + + very_long_variable_name_x, very_long_variable_name_y = ( +@@ -420,8 +412,8 @@ + lambda b: b * another_very_long_expression_here, + ) + +-very_long_variable_name_for_result += ( +- lambda x: very_long_function_call_that_should_definitely_be_parenthesized_now( ++very_long_variable_name_for_result += lambda x: ( ++ very_long_function_call_that_should_definitely_be_parenthesized_now( + x, more_args, additional_parameters + ) + ) +@@ -457,12 +449,12 @@ + [ + # Not fluent + param( +- lambda left, right: (ibis.timestamp("2017-04-01")), ++ lambda left, right: ibis.timestamp("2017-04-01"), + ), + # These four are fluent and fit on one line inside the parenthesized + # lambda body + param( +- lambda left, right: (ibis.timestamp("2017-04-01").cast(dt.date)), ++ lambda left, right: ibis.timestamp("2017-04-01").cast(dt.date), + ), + param( + lambda left, right: ( +@@ -471,9 +463,9 @@ + ), + param(lambda left, right: ibis.timestamp("2017-04-01").cast(dt.date)), + param( +- lambda left, right: ibis.timestamp("2017-04-01") +- .cast(dt.date) +- .between(left, right) ++ lambda left, right: ( ++ ibis.timestamp("2017-04-01").cast(dt.date).between(left, right) ++ ) + ), + # This is too long on one line in the lambda body and gets wrapped + # inside the body. +@@ -507,16 +499,18 @@ + ] + + # adds parentheses around the body +-lambda xxxxxxxxxxxxxxxxxxxx, yyyyyyyyyyyyyyyyyyyy, zzzzzzzzzzzzzzzzzzzz: xxxxxxxxxxxxxxxxxxxx + yyyyyyyyyyyyyyyyyyyy + zzzzzzzzzzzzzzzzzzzz ++lambda xxxxxxxxxxxxxxxxxxxx, yyyyyyyyyyyyyyyyyyyy, zzzzzzzzzzzzzzzzzzzz: ( ++ xxxxxxxxxxxxxxxxxxxx + yyyyyyyyyyyyyyyyyyyy + zzzzzzzzzzzzzzzzzzzz ++) + + # removes parentheses around the body +-lambda xxxxxxxxxxxxxxxxxxxx: (xxxxxxxxxxxxxxxxxxxx + 1) ++lambda xxxxxxxxxxxxxxxxxxxx: xxxxxxxxxxxxxxxxxxxx + 1 + + mapper = lambda x: dict_with_default[ + np.nan if isinstance(x, float) and np.isnan(x) else x + ] + +-lambda x, y, z: (x + y + z) ++lambda x, y, z: x + y + z + + lambda x, y, z: ( + x + y + z +@@ -527,7 +521,7 @@ + x + y + z # trailing eol body + ) + +-lambda x, y, z: (x + y + z) # trailing lambda ++lambda x, y, z: x + y + z # trailing lambda + + lambda x, y, z: ( + # leading body +@@ -539,21 +533,23 @@ + ) + + ( +- lambda name: source_bucket # trailing eol comment +- if name == source_bucket_name +- else storage.Bucket(mock_service, destination_bucket_name) ++ lambda name: ( ++ source_bucket # trailing eol comment ++ if name == source_bucket_name ++ else storage.Bucket(mock_service, destination_bucket_name) ++ ) + ) + + ( +- lambda name: +- # dangling header comment +- source_bucket +- if name == source_bucket_name +- else storage.Bucket(mock_service, destination_bucket_name) ++ lambda name: ( ++ # dangling header comment ++ source_bucket ++ if name == source_bucket_name ++ else storage.Bucket(mock_service, destination_bucket_name) ++ ) + ) + +-x = ( +- lambda name: ++x = lambda name: ( + # dangling header comment + source_bucket + if name == source_bucket_name +@@ -561,8 +557,7 @@ + ) + + ( +- lambda name: # dangling header comment +- ( ++ lambda name: ( # dangling header comment + source_bucket + if name == source_bucket_name + else storage.Bucket(mock_service, destination_bucket_name) +@@ -570,61 +565,70 @@ + ) + + ( +- lambda from_ts, to_ts, _chain_id=chain_id: # dangling eol header comment +- db_evmtx.count_transactions_in_range( +- chain_id=_chain_id, +- from_ts=from_ts, +- to_ts=to_ts, ++ lambda from_ts, to_ts, _chain_id=chain_id: ( # dangling eol header comment ++ db_evmtx.count_transactions_in_range( ++ chain_id=_chain_id, ++ from_ts=from_ts, ++ to_ts=to_ts, ++ ) + ) + ) + + ( +- lambda from_ts, to_ts, _chain_id=chain_id: +- # dangling header comment before call +- db_evmtx.count_transactions_in_range( +- chain_id=_chain_id, +- from_ts=from_ts, +- to_ts=to_ts, ++ lambda from_ts, to_ts, _chain_id=chain_id: ( ++ # dangling header comment before call ++ db_evmtx.count_transactions_in_range( ++ chain_id=_chain_id, ++ from_ts=from_ts, ++ to_ts=to_ts, ++ ) + ) + ) + + ( +- lambda left, right: +- # comment +- ibis.timestamp("2017-04-01").cast(dt.date).between(left, right) ++ lambda left, right: ( ++ # comment ++ ibis.timestamp("2017-04-01").cast(dt.date).between(left, right) ++ ) + ) + + ( +- lambda left, right: ibis.timestamp("2017-04-01") # comment +- .cast(dt.date) +- .between(left, right) ++ lambda left, right: ( ++ ibis.timestamp("2017-04-01") # comment ++ .cast(dt.date) ++ .between(left, right) ++ ) + ) + + ( +- lambda xxxxxxxxxxxxxxxxxxxx, yyyyyyyyyyyyyyyyyyyy: +- # comment +- [xxxxxxxxxxxxxxxxxxxx, yyyyyyyyyyyyyyyyyyyy, zzzzzzzzzzzzzzzzzzzz] ++ lambda xxxxxxxxxxxxxxxxxxxx, yyyyyyyyyyyyyyyyyyyy: ( ++ # comment ++ [xxxxxxxxxxxxxxxxxxxx, yyyyyyyyyyyyyyyyyyyy, zzzzzzzzzzzzzzzzzzzz] ++ ) + ) + + ( +- lambda x, y: +- # comment +- { +- "key": x, +- "another": y, +- } ++ lambda x, y: ( ++ # comment ++ { ++ "key": x, ++ "another": y, ++ } ++ ) + ) + + ( +- lambda x, y: +- # comment +- (x, y, z) ++ lambda x, y: ( ++ # comment ++ (x, y, z) ++ ) + ) + + ( +- lambda x: +- # comment +- dict_with_default[np.nan if isinstance(x, float) and np.isnan(x) else x] ++ lambda x: ( ++ # comment ++ dict_with_default[np.nan if isinstance(x, float) and np.isnan(x) else x] ++ ) + ) + + ( +@@ -637,27 +641,31 @@ + ( + lambda + # comment +- *args, **kwargs: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs) +- + 1 ++ *args, **kwargs: ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs) + 1 ++ ) + ) + + ( + lambda # comment +- *args, **kwargs: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs) +- + 1 ++ *args, **kwargs: ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs) + 1 ++ ) + ) + + ( + lambda # comment 1 + # comment 2 +- *args, **kwargs: # comment 3 +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs) + 1 ++ *args, **kwargs: ( # comment 3 ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs) + 1 ++ ) + ) + + ( + lambda # comment 1 +- *args, **kwargs: # comment 3 +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs) + 1 ++ *args, **kwargs: ( # comment 3 ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs) + 1 ++ ) + ) + + ( +@@ -672,25 +680,28 @@ + ) + + ( +- lambda *brgs, **kwargs: +- # comment 1 +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa( # comment 2 +- # comment 3 +- *brgs, +- **kwargs, +- ) +- + 1 # comment 4 ++ lambda *brgs, **kwargs: ( ++ # comment 1 ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa( # comment 2 ++ # comment 3 ++ *brgs, ++ **kwargs, ++ ) ++ + 1 ++ ) # comment 4 + # comment 5 + ) + + ( +- lambda *crgs, **kwargs: # comment 1 +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*crgs, **kwargs) + 1 ++ lambda *crgs, **kwargs: ( # comment 1 ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*crgs, **kwargs) + 1 ++ ) + ) + + ( +- lambda *drgs, **kwargs: # comment 1 +- (aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*drgs, **kwargs) + 1) ++ lambda *drgs, **kwargs: ( # comment 1 ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*drgs, **kwargs) + 1 ++ ) + ) + + ( +@@ -698,9 +709,9 @@ + # comment 1 + *ergs, + # comment 2 +- **kwargs: # comment 3 +- # comment 4 +- (aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*ergs, **kwargs) + 1) ++ **kwargs: ( # comment 3 # comment 4 ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*ergs, **kwargs) + 1 ++ ) + ) + + ( +@@ -708,19 +719,20 @@ + # 2 + left, # 3 + # 4 +- right: # 5 +- # 6 +- ibis.timestamp("2017-04-01").cast(dt.date).between(left, right) ++ right: ( # 5 ++ # 6 ++ ibis.timestamp("2017-04-01").cast(dt.date).between(left, right) ++ ) + ) + + ( +- lambda x: # outer comment 1 +- ( +- lambda y: # inner comment 1 +- # inner comment 2 +- lambda z: ( +- # innermost comment +- x + y + z ++ lambda x: ( # outer comment 1 ++ lambda y: ( # inner comment 1 ++ # inner comment 2 ++ lambda z: ( ++ # innermost comment ++ x + y + z ++ ) + ) + ) + ) +@@ -738,48 +750,52 @@ + foo( + lambda from_ts, # but still wrap the body if it gets too long + to_ts, +- _chain_id=chain_id: db_evmtx.count_transactions_in_rangeeeeeeeeeeeeeeeeeeeeeeeeeeeee( +- chain_id=_chain_id, +- from_ts=from_ts, +- to_ts=to_ts, ++ _chain_id=chain_id: ( ++ db_evmtx.count_transactions_in_rangeeeeeeeeeeeeeeeeeeeeeeeeeeeee( ++ chain_id=_chain_id, ++ from_ts=from_ts, ++ to_ts=to_ts, ++ ) + ) + ) + +-transform = ( +- lambda left, right: ibis.timestamp("2017-04-01") +- .cast(dt.date) +- .between(left, right) +- .between(left, right) ++transform = lambda left, right: ( ++ ibis.timestamp("2017-04-01").cast(dt.date).between(left, right).between(left, right) + ) # trailing comment + + ( +- lambda: # comment +- 1 ++ lambda: ( # comment ++ 1 ++ ) + ) + + ( +- lambda: # comment +- 1 ++ lambda: ( # comment ++ 1 ++ ) + ) + + ( +- lambda: +- # comment +- 1 ++ lambda: ( ++ # comment ++ 1 ++ ) + ) + + ( +- lambda: # comment 1 +- # comment 2 +- 1 ++ lambda: ( # comment 1 ++ # comment 2 ++ 1 ++ ) + ) + + ( +- lambda: # comment 1 +- # comment 2 +- # comment 3 +- # comment 4 +- 1 ++ lambda: ( # comment 1 ++ # comment 2 ++ # comment 3 ++ # comment 4 ++ 1 ++ ) + ) + + ( +@@ -828,8 +844,7 @@ + ) + + ( +- lambda: # dangling-end-of-line +- ( # leading-body-end-of-line ++ lambda: ( # dangling-end-of-line # leading-body-end-of-line + x + ) + ) ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@multiline_string_deviations.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@multiline_string_deviations.py.snap index c4fde9b127..9504e52ae8 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@multiline_string_deviations.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@multiline_string_deviations.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/multiline_string_deviations.py -snapshot_kind: text --- ## Input ```python @@ -106,3 +105,22 @@ generated_readme = ( """.strip().format(project_name) ) ``` + + +## Preview changes +```diff +--- Stable ++++ Preview +@@ -44,10 +44,8 @@ + # this by changing `Lambda::needs_parentheses` to return `BestFit` but it causes + # issues when the lambda has comments. + # Let's keep this as a known deviation for now. +-generated_readme = ( +- lambda project_name: """ ++generated_readme = lambda project_name: """ + {} + + + """.strip().format(project_name) +-) +```