Skip to content

Commit 202c7f0

Browse files
committed
Call chain formatting in fluent style
1 parent de898c5 commit 202c7f0

13 files changed

+590
-510
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Test cases for call chains and optional parentheses, with and without fluent style
2+
3+
raise OsError("") from a.aaaaa(
4+
aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa
5+
).a(aaaa)
6+
7+
raise OsError(
8+
"sökdjffffsldkfjlhsakfjhalsökafhsöfdahsödfjösaaksjdllllllllllllll"
9+
) from a.aaaaa(
10+
aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa
11+
).a(
12+
aaaa
13+
)
14+
15+
blogs1 = Blog.objects.filter(entry__headline__contains="Lennon").filter(
16+
entry__pub_date__year=2008
17+
)
18+
19+
blogs2 = Blog.objects.filter(
20+
entry__headline__contains="Lennon",
21+
).filter(
22+
entry__pub_date__year=2008,
23+
)
24+
25+
raise OsError("") from (
26+
Blog.objects.filter(
27+
entry__headline__contains="Lennon",
28+
)
29+
.filter(
30+
entry__pub_date__year=2008,
31+
)
32+
.filter(
33+
entry__pub_date__year=2008,
34+
)
35+
)
36+
37+
raise OsError("sökdjffffsldkfjlhsakfjhalsökafhsöfdahsödfjösaaksjdllllllllllllll") from (
38+
Blog.objects.filter(
39+
entry__headline__contains="Lennon",
40+
)
41+
.filter(
42+
entry__pub_date__year=2008,
43+
)
44+
.filter(
45+
entry__pub_date__year=2008,
46+
)
47+
)
48+
49+
# Break only after calls and indexing
50+
result = (
51+
session.query(models.Customer.id)
52+
.filter(
53+
models.Customer.account_id == account_id, models.Customer.email == email_address
54+
)
55+
.count()
56+
)
57+
58+
raise (
59+
Blog.objects.filter(
60+
entry__headline__contains="Lennon",
61+
)
62+
.limit_results[:10]
63+
.filter(
64+
entry__pub_date__month=10,
65+
)
66+
)
67+
68+
# Nested call chains
69+
blogs = (
70+
Blog.objects.filter(
71+
entry__headline__contains="Lennon",
72+
).filter(
73+
entry__pub_date__year=2008,
74+
)
75+
+ Blog.objects.filter(
76+
entry__headline__contains="McCartney",
77+
)
78+
.limit_results[:10]
79+
.filter(
80+
entry__pub_date__year=2010,
81+
)
82+
).all()

crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/call.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def f(*args, **kwargs):
5252
hey_this_is_a_very_long_call=1, it_has_funny_attributes_asdf_asdf=1, too_long_for_the_line=1, really=True
5353
)
5454

55-
# TODO(konstin): Call chains/fluent interface (https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#call-chains)
55+
# Call chains/fluent interface (https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#call-chains)
5656
result = (
5757
session.query(models.Customer.id)
5858
.filter(

crates/ruff_python_formatter/src/expression/expr_attribute.rs

+72-19
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
1-
use ruff_python_ast::{Constant, Expr, ExprAttribute, ExprConstant};
2-
3-
use ruff_formatter::write;
1+
use ruff_formatter::{write, FormatRuleWithOptions};
42
use ruff_python_ast::node::AnyNodeRef;
3+
use ruff_python_ast::{Constant, Expr, ExprAttribute, ExprConstant};
54

65
use crate::comments::{leading_comments, trailing_comments};
76
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses, Parentheses};
87
use crate::prelude::*;
98
use crate::FormatNodeRule;
109

1110
#[derive(Default)]
12-
pub struct FormatExprAttribute;
11+
pub struct FormatExprAttribute {
12+
/// Parentheses for fluent style
13+
parentheses: Option<Parentheses>,
14+
}
15+
16+
impl FormatRuleWithOptions<ExprAttribute, PyFormatContext<'_>> for FormatExprAttribute {
17+
type Options = Option<Parentheses>;
18+
19+
fn with_options(mut self, options: Self::Options) -> Self {
20+
self.parentheses = options;
21+
self
22+
}
23+
}
1324

1425
impl FormatNodeRule<ExprAttribute> for FormatExprAttribute {
1526
fn fmt_fields(&self, item: &ExprAttribute, f: &mut PyFormatter) -> FormatResult<()> {
@@ -37,11 +48,18 @@ impl FormatNodeRule<ExprAttribute> for FormatExprAttribute {
3748

3849
if needs_parentheses {
3950
value.format().with_options(Parentheses::Always).fmt(f)?;
40-
} else if let Expr::Attribute(expr_attribute) = value.as_ref() {
41-
// We're in a attribute chain (`a.b.c`). The outermost node adds parentheses if
42-
// required, the inner ones don't need them so we skip the `Expr` formatting that
43-
// normally adds the parentheses.
44-
expr_attribute.format().fmt(f)?;
51+
} else if self.parentheses == Some(Parentheses::FluentStyle) {
52+
// Fluent style: We need to pass the parentheses on to inner attributes or call chains
53+
value
54+
.format()
55+
.with_options(Parentheses::FluentStyle)
56+
.fmt(f)?;
57+
match value.as_ref() {
58+
Expr::Call(_) | Expr::Subscript(_) => {
59+
soft_line_break().fmt(f)?;
60+
}
61+
_ => {}
62+
}
4563
} else {
4664
value.format().fmt(f)?;
4765
}
@@ -50,16 +68,51 @@ impl FormatNodeRule<ExprAttribute> for FormatExprAttribute {
5068
hard_line_break().fmt(f)?;
5169
}
5270

53-
write!(
54-
f,
55-
[
56-
text("."),
57-
trailing_comments(trailing_dot_comments),
58-
(!leading_attribute_comments.is_empty()).then_some(hard_line_break()),
59-
leading_comments(leading_attribute_comments),
60-
attr.format()
61-
]
62-
)
71+
if self.parentheses == Some(Parentheses::FluentStyle) {
72+
// Fluent style has line breaks before the dot
73+
// ```python
74+
// blogs3 = (
75+
// Blog.objects.filter(
76+
// entry__headline__contains="Lennon",
77+
// )
78+
// .filter(
79+
// entry__pub_date__year=2008,
80+
// )
81+
// .filter(
82+
// entry__pub_date__year=2008,
83+
// )
84+
// )
85+
// ```
86+
write!(
87+
f,
88+
[
89+
(!leading_attribute_comments.is_empty()).then_some(hard_line_break()),
90+
leading_comments(leading_attribute_comments),
91+
text("."),
92+
trailing_comments(trailing_dot_comments),
93+
attr.format()
94+
]
95+
)
96+
} else {
97+
// Regular style
98+
// ```python
99+
// blogs2 = Blog.objects.filter(
100+
// entry__headline__contains="Lennon",
101+
// ).filter(
102+
// entry__pub_date__year=2008,
103+
// )
104+
// ```
105+
write!(
106+
f,
107+
[
108+
text("."),
109+
trailing_comments(trailing_dot_comments),
110+
(!leading_attribute_comments.is_empty()).then_some(hard_line_break()),
111+
leading_comments(leading_attribute_comments),
112+
attr.format()
113+
]
114+
)
115+
}
63116
}
64117

65118
fn fmt_dangling_comments(

crates/ruff_python_formatter/src/expression/expr_call.rs

+33-16
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1+
use ruff_formatter::{write, FormatRuleWithOptions};
2+
use ruff_python_ast::node::AnyNodeRef;
13
use ruff_python_ast::{Expr, ExprCall, Ranged};
4+
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
25
use ruff_text_size::{TextRange, TextSize};
36

47
use crate::builders::empty_parenthesized_with_dangling_comments;
5-
use ruff_formatter::write;
6-
use ruff_python_ast::node::AnyNodeRef;
7-
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
8-
98
use crate::expression::expr_generator_exp::GeneratorExpParentheses;
109
use crate::expression::parentheses::{
1110
parenthesized, NeedsParentheses, OptionalParentheses, Parentheses,
@@ -14,7 +13,19 @@ use crate::prelude::*;
1413
use crate::FormatNodeRule;
1514

1615
#[derive(Default)]
17-
pub struct FormatExprCall;
16+
pub struct FormatExprCall {
17+
/// Parentheses for fluent style
18+
parentheses: Option<Parentheses>,
19+
}
20+
21+
impl FormatRuleWithOptions<ExprCall, PyFormatContext<'_>> for FormatExprCall {
22+
type Options = Option<Parentheses>;
23+
24+
fn with_options(mut self, options: Self::Options) -> Self {
25+
self.parentheses = options;
26+
self
27+
}
28+
}
1829

1930
impl FormatNodeRule<ExprCall> for FormatExprCall {
2031
fn fmt_fields(&self, item: &ExprCall, f: &mut PyFormatter) -> FormatResult<()> {
@@ -25,28 +36,36 @@ impl FormatNodeRule<ExprCall> for FormatExprCall {
2536
keywords,
2637
} = item;
2738

39+
if self.parentheses == Some(Parentheses::FluentStyle) {
40+
// Fluent style: We need to pass the parentheses on to inner attributes or call chains
41+
func.format()
42+
.with_options(Parentheses::FluentStyle)
43+
.fmt(f)?;
44+
} else {
45+
func.format().fmt(f)?;
46+
}
47+
2848
// We have a case with `f()` without any argument, which is a special case because we can
2949
// have a comment with no node attachment inside:
3050
// ```python
3151
// f(
3252
// # This function has a dangling comment
3353
// )
3454
// ```
55+
let comments = f.context().comments().clone();
3556
if args.is_empty() && keywords.is_empty() {
36-
let comments = f.context().comments().clone();
3757
return write!(
3858
f,
39-
[
40-
func.format(),
41-
empty_parenthesized_with_dangling_comments(
42-
text("("),
43-
comments.dangling_comments(item),
44-
text(")"),
45-
)
46-
]
59+
[empty_parenthesized_with_dangling_comments(
60+
text("("),
61+
comments.dangling_comments(item),
62+
text(")"),
63+
)]
4764
);
4865
}
4966

67+
debug_assert!(comments.dangling_comments(item).is_empty());
68+
5069
let all_args = format_with(|f: &mut PyFormatter| {
5170
let source = f.context().source();
5271
let mut joiner = f.join_comma_separated(item.end());
@@ -88,7 +107,6 @@ impl FormatNodeRule<ExprCall> for FormatExprCall {
88107
write!(
89108
f,
90109
[
91-
func.format(),
92110
// The outer group is for things like
93111
// ```python
94112
// get_collection(
@@ -104,7 +122,6 @@ impl FormatNodeRule<ExprCall> for FormatExprCall {
104122
// hey_this_is_a_very_long_call, it_has_funny_attributes_asdf_asdf, really=True
105123
// )
106124
// ```
107-
// TODO(konstin): Doesn't work see wrongly formatted test
108125
parenthesized("(", &group(&all_args), ")")
109126
]
110127
)

crates/ruff_python_formatter/src/expression/expr_subscript.rs

+27-7
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
1-
use ruff_python_ast::{Expr, ExprSubscript};
2-
3-
use ruff_formatter::{format_args, write};
1+
use ruff_formatter::{format_args, write, FormatRuleWithOptions};
42
use ruff_python_ast::node::{AnyNodeRef, AstNode};
3+
use ruff_python_ast::{Expr, ExprSubscript};
54

65
use crate::comments::trailing_comments;
76
use crate::context::NodeLevel;
87
use crate::context::PyFormatContext;
98
use crate::expression::expr_tuple::TupleParentheses;
10-
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
9+
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses, Parentheses};
1110
use crate::prelude::*;
1211
use crate::FormatNodeRule;
1312

1413
#[derive(Default)]
15-
pub struct FormatExprSubscript;
14+
pub struct FormatExprSubscript {
15+
parentheses: Option<Parentheses>,
16+
}
17+
18+
impl FormatRuleWithOptions<ExprSubscript, PyFormatContext<'_>> for FormatExprSubscript {
19+
/// Parentheses for fluent style
20+
type Options = Option<Parentheses>;
21+
22+
fn with_options(mut self, options: Self::Options) -> Self {
23+
self.parentheses = options;
24+
self
25+
}
26+
}
1627

1728
impl FormatNodeRule<ExprSubscript> for FormatExprSubscript {
1829
fn fmt_fields(&self, item: &ExprSubscript, f: &mut PyFormatter) -> FormatResult<()> {
@@ -30,15 +41,24 @@ impl FormatNodeRule<ExprSubscript> for FormatExprSubscript {
3041
"The subscript expression must have at most a single comment, the one after the bracket"
3142
);
3243

44+
let format_value = format_with(|f| {
45+
if self.parentheses == Some(Parentheses::FluentStyle) {
46+
// Fluent style: We need to pass the parentheses on to inner attributes or call chains
47+
value.format().with_options(Parentheses::FluentStyle).fmt(f)
48+
} else {
49+
value.format().fmt(f)
50+
}
51+
});
52+
3353
if let NodeLevel::Expression(Some(group_id)) = f.context().node_level() {
3454
// Enforce the optional parentheses for parenthesized values.
3555
f.context_mut().set_node_level(NodeLevel::Expression(None));
36-
let result = value.format().fmt(f);
56+
let result = format_value.fmt(f);
3757
f.context_mut()
3858
.set_node_level(NodeLevel::Expression(Some(group_id)));
3959
result?;
4060
} else {
41-
value.format().fmt(f)?;
61+
format_value.fmt(f)?;
4262
}
4363

4464
let format_slice = format_with(|f: &mut PyFormatter| {

0 commit comments

Comments
 (0)