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

@ -0,0 +1,157 @@
# Test cases for call chains and optional parentheses, with and without fluent style
raise OsError("") from a.aaaaa(
aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa
).a(aaaa)
raise OsError(
"sökdjffffsldkfjlhsakfjhalsökafhsöfdahsödfjösaaksjdllllllllllllll"
) from a.aaaaa(
aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa
).a(
aaaa
)
a1 = Blog.objects.filter(entry__headline__contains="Lennon").filter(
entry__pub_date__year=2008
)
a2 = Blog.objects.filter(
entry__headline__contains="Lennon",
).filter(
entry__pub_date__year=2008,
)
raise OsError("") from (
Blog.objects.filter(
entry__headline__contains="Lennon",
)
.filter(
entry__pub_date__year=2008,
)
.filter(
entry__pub_date__year=2008,
)
)
raise OsError("sökdjffffsldkfjlhsakfjhalsökafhsöfdahsödfjösaaksjdllllllllllllll") from (
Blog.objects.filter(
entry__headline__contains="Lennon",
)
.filter(
entry__pub_date__year=2008,
)
.filter(
entry__pub_date__year=2008,
)
)
# Break only after calls and indexing
b1 = (
session.query(models.Customer.id)
.filter(
models.Customer.account_id == account_id, models.Customer.email == email_address
)
.count()
)
b2 = (
Blog.objects.filter(
entry__headline__contains="Lennon",
)
.limit_results[:10]
.filter(
entry__pub_date__month=10,
)
)
# Nested call chains
c1 = (
Blog.objects.filter(
entry__headline__contains="Lennon",
).filter(
entry__pub_date__year=2008,
)
+ Blog.objects.filter(
entry__headline__contains="McCartney",
)
.limit_results[:10]
.filter(
entry__pub_date__year=2010,
)
).all()
# Test different cases with trailing end of line comments:
# * fluent style, fits: no parentheses -> ignore the expand_parent
# * fluent style, doesn't fit: break all soft line breaks
# * default, fits: no parentheses
# * default, doesn't fit: parentheses but no soft line breaks
# Fits, either style
d11 = x.e().e().e() #
d12 = (x.e().e().e()) #
d13 = (
x.e() #
.e()
.e()
)
# Doesn't fit, default
d2 = (
x.e().esadjkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkfsdddd() #
)
# Doesn't fit, fluent style
d3 = (
x.e() #
.esadjkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk()
.esadjkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk()
)
# Don't drop the bin op parentheses
e1 = (1 + 2).w().t()
e2 = (1 + 2)().w().t()
e3 = (1 + 2)[1].w().t()
# Treat preserved parentheses correctly
f1 = (b().c()).d(1,)
f2 = b().c().d(1,)
f3 = (b).c().d(1,)
f4 = (a)(b).c(1,)
f5 = (a.b()).c(1,)
# Indent in the parentheses without breaking
g1 = (
queryset.distinct().order_by(field.name).values_list(field_name_flat_long_long=True)
)
# Fluent style in subexpressions
if (
not a()
.b()
.cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc()
):
pass
h2 = (
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
+ ccccccccccccccccccccccccc()
.dddddddddddddddddddddd()
.eeeeeeeeee()
.ffffffffffffffffffffff()
)
# Parentheses aren't allowed on statement level, don't use fluent style here
if True:
(alias).filter(content_typeold_content_type).update(
content_typenew_contesadfasfdant_type
)
zero(
one,
).two(
three,
).four(
five,
)

View file

@ -52,7 +52,7 @@ f(
hey_this_is_a_very_long_call=1, it_has_funny_attributes_asdf_asdf=1, too_long_for_the_line=1, really=True
)
# TODO(konstin): Call chains/fluent interface (https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#call-chains)
# Call chains/fluent interface (https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#call-chains)
result = (
session.query(models.Customer.id)
.filter(