Skip to content

Commit 62c5f74

Browse files
committed
Fluent style
1 parent 2a65e6f commit 62c5f74

12 files changed

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

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

+75-19
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
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: Option<Parentheses>,
13+
}
14+
15+
impl FormatRuleWithOptions<ExprAttribute, PyFormatContext<'_>> for FormatExprAttribute {
16+
type Options = Option<Parentheses>;
17+
18+
fn with_options(mut self, options: Self::Options) -> Self {
19+
self.parentheses = options;
20+
self
21+
}
22+
}
1323

1424
impl FormatNodeRule<ExprAttribute> for FormatExprAttribute {
1525
fn fmt_fields(&self, item: &ExprAttribute, f: &mut PyFormatter) -> FormatResult<()> {
@@ -37,11 +47,22 @@ impl FormatNodeRule<ExprAttribute> for FormatExprAttribute {
3747

3848
if needs_parentheses {
3949
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)?;
50+
} else if self.parentheses == Some(Parentheses::FluentStyle) {
51+
// Fluent style: We need to pass the parenthese on to inner attributes or call chains
52+
match value.as_ref() {
53+
Expr::Attribute(_) => value
54+
.format()
55+
.with_options(Parentheses::FluentStyle)
56+
.fmt(f)?,
57+
Expr::Call(_) | Expr::Subscript(_) => {
58+
value
59+
.format()
60+
.with_options(Parentheses::FluentStyle)
61+
.fmt(f)?;
62+
soft_line_break().fmt(f)?
63+
}
64+
_ => value.format().fmt(f)?,
65+
}
4566
} else {
4667
value.format().fmt(f)?;
4768
}
@@ -50,16 +71,51 @@ impl FormatNodeRule<ExprAttribute> for FormatExprAttribute {
5071
hard_line_break().fmt(f)?;
5172
}
5273

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-
)
74+
if self.parentheses == Some(Parentheses::FluentStyle) {
75+
// Fluent style has line breaks before the dot
76+
// ```python
77+
// blogs3 = (
78+
// Blog.objects.filter(
79+
// entry__headline__contains="Lennon",
80+
// )
81+
// .filter(
82+
// entry__pub_date__year=2008,
83+
// )
84+
// .filter(
85+
// entry__pub_date__year=2008,
86+
// )
87+
// )
88+
// ```
89+
write!(
90+
f,
91+
[
92+
(!leading_attribute_comments.is_empty()).then_some(hard_line_break()),
93+
leading_comments(leading_attribute_comments),
94+
text("."),
95+
trailing_comments(trailing_dot_comments),
96+
attr.format()
97+
]
98+
)
99+
} else {
100+
// Regular style
101+
// ```python
102+
// blogs2 = Blog.objects.filter(
103+
// entry__headline__contains="Lennon",
104+
// ).filter(
105+
// entry__pub_date__year=2008,
106+
// )
107+
// ```
108+
write!(
109+
f,
110+
[
111+
text("."),
112+
trailing_comments(trailing_dot_comments),
113+
(!leading_attribute_comments.is_empty()).then_some(hard_line_break()),
114+
leading_comments(leading_attribute_comments),
115+
attr.format()
116+
]
117+
)
118+
}
63119
}
64120

65121
fn fmt_dangling_comments(

crates/ruff_python_formatter/src/expression/expr_call.rs

+33-15
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,18 @@ use crate::prelude::*;
1413
use crate::FormatNodeRule;
1514

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

1929
impl FormatNodeRule<ExprCall> for FormatExprCall {
2030
fn fmt_fields(&self, item: &ExprCall, f: &mut PyFormatter) -> FormatResult<()> {
@@ -25,6 +35,19 @@ impl FormatNodeRule<ExprCall> for FormatExprCall {
2535
keywords,
2636
} = item;
2737

38+
if self.parentheses == Some(Parentheses::FluentStyle) {
39+
// Fluent style: We need to pass the parenthese on to inner attributes or call chains
40+
match func.as_ref() {
41+
Expr::Attribute(_) | Expr::Call(_) | Expr::Subscript(_) => func
42+
.format()
43+
.with_options(Parentheses::FluentStyle)
44+
.fmt(f)?,
45+
_ => func.format().fmt(f)?,
46+
}
47+
} else {
48+
func.format().fmt(f)?;
49+
}
50+
2851
// We have a case with `f()` without any argument, which is a special case because we can
2952
// have a comment with no node attachment inside:
3053
// ```python
@@ -36,14 +59,11 @@ impl FormatNodeRule<ExprCall> for FormatExprCall {
3659
let comments = f.context().comments().clone();
3760
return write!(
3861
f,
39-
[
40-
func.format(),
41-
empty_parenthesized_with_dangling_comments(
42-
text("("),
43-
comments.dangling_comments(item),
44-
text(")"),
45-
)
46-
]
62+
[empty_parenthesized_with_dangling_comments(
63+
text("("),
64+
comments.dangling_comments(item),
65+
text(")"),
66+
)]
4767
);
4868
}
4969

@@ -88,7 +108,6 @@ impl FormatNodeRule<ExprCall> for FormatExprCall {
88108
write!(
89109
f,
90110
[
91-
func.format(),
92111
// The outer group is for things like
93112
// ```python
94113
// get_collection(
@@ -104,7 +123,6 @@ impl FormatNodeRule<ExprCall> for FormatExprCall {
104123
// hey_this_is_a_very_long_call, it_has_funny_attributes_asdf_asdf, really=True
105124
// )
106125
// ```
107-
// TODO(konstin): Doesn't work see wrongly formatted test
108126
parenthesized("(", &group(&all_args), ")")
109127
]
110128
)

0 commit comments

Comments
 (0)