Call chain formatting in fluent style (#6151)

Implement fluent style/call chains. See the `call_chains.py` formatting
for examples.

This isn't fully like black because in `raise A from B` they allow `A`
breaking can influence the formatting of `B` even if it is already
multiline.

Similarity index:

| project      | main  | PR    |
|--------------|-------|-------|
| build        | ???   | 0.753 |
| django       | 0.991 | 0.998 |
| transformers | 0.993 | 0.994 |
| typeshed     | 0.723 | 0.723 |
| warehouse    | 0.978 | 0.994 |
| zulip        | 0.992 | 0.994 |

Call chain formatting is affected by
https://github.com/astral-sh/ruff/issues/627, but i'm cutting scope
here.

Closes #5343

**Test Plan**:
 * Added a dedicated call chains test file
 * The ecosystem checks found some bugs
 * I manually check django and zulip formatting

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
konsti 2023-08-04 15:58:01 +02:00 committed by GitHub
parent 35bdbe43a8
commit 99baad12d8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 917 additions and 517 deletions

View file

@ -1,13 +1,25 @@
use ruff_formatter::write;
use crate::expression::CallChainLayout;
use ruff_formatter::FormatRuleWithOptions;
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::ExprCall;
use ruff_python_ast::{Expr, ExprCall};
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
use crate::prelude::*;
use crate::FormatNodeRule;
#[derive(Default)]
pub struct FormatExprCall;
pub struct FormatExprCall {
call_chain_layout: CallChainLayout,
}
impl FormatRuleWithOptions<ExprCall, PyFormatContext<'_>> for FormatExprCall {
type Options = CallChainLayout;
fn with_options(mut self, options: Self::Options) -> Self {
self.call_chain_layout = options;
self
}
}
impl FormatNodeRule<ExprCall> for FormatExprCall {
fn fmt_fields(&self, item: &ExprCall, f: &mut PyFormatter) -> FormatResult<()> {
@ -17,7 +29,32 @@ impl FormatNodeRule<ExprCall> for FormatExprCall {
arguments,
} = item;
write!(f, [func.format(), arguments.format()])
let call_chain_layout = self.call_chain_layout.apply_in_node(item, f);
let fmt_inner = format_with(|f| {
match func.as_ref() {
Expr::Attribute(expr) => expr.format().with_options(call_chain_layout).fmt(f)?,
Expr::Call(expr) => expr.format().with_options(call_chain_layout).fmt(f)?,
Expr::Subscript(expr) => expr.format().with_options(call_chain_layout).fmt(f)?,
_ => func.format().fmt(f)?,
}
arguments.format().fmt(f)
});
// Allow to indent the parentheses while
// ```python
// g1 = (
// queryset.distinct().order_by(field.name).values_list(field_name_flat_long_long=True)
// )
// ```
if call_chain_layout == CallChainLayout::Fluent
&& self.call_chain_layout == CallChainLayout::Default
{
group(&fmt_inner).fmt(f)
} else {
fmt_inner.fmt(f)
}
}
}
@ -27,6 +64,12 @@ impl NeedsParentheses for ExprCall {
_parent: AnyNodeRef,
context: &PyFormatContext,
) -> OptionalParentheses {
self.func.needs_parentheses(self.into(), context)
if CallChainLayout::from_expression(self.into(), context.source())
== CallChainLayout::Fluent
{
OptionalParentheses::Multiline
} else {
self.func.needs_parentheses(self.into(), context)
}
}
}