astral-sh / ruff

An extremely fast Python linter and code formatter, written in Rust.
https://docs.astral.sh/ruff
MIT License
32.16k stars 1.08k forks source link

ruff formatter: one call per line for chained method calls #8598

Open MartinBernstorff opened 11 months ago

MartinBernstorff commented 11 months ago

This is something I miss tremendously from the rust ecosystem. One example:

image

When formatted becomes:

image

I think the first example is much more readable, and would love a Python formatter which supported it ❤️

Meta: I was uncertain about where to make a feature request for the ruff formatter, hope this is the right place!

MichaReiser commented 11 months ago

Hy @MartinBernstorff

This is something that I also noticed coming from JS where Prettier formats call chains as outlined by you above (except without the outer parentheses).

My reasoning for Black/Ruff's call chain formatting is that Black tries to avoid parenthesizing expressions which is more idiomatic (I think):

if a + long_call(
    arg1, 
    arg2
): 
    pass

instead of

if (
    a + 
    long_call(
        arg1, 
        arg2
    )
): 
    pass

The way this is implemented is that anything inside of parentheses gets a higher split priority (try to split the content inside parentheses first).

I'm not saying that we can't improve the formatting. I'm just trying to explain where I believe black's formatting decision is coming from.

MartinBernstorff commented 11 months ago

@MichaReiser Appreciate the context! I completely agree your first example is more readable, so there's some work on defining the goal here.

From a very brief consideration, I can only come up with chained method calls as an example of my desired splitting, so perhaps this is a special case? Love to lean on your experience here!

nick4u commented 8 months ago

I would love to see some option to preserve chain methods-call-per-line SQLAlchemy code is so much more readable when those new lines are preserved - it almost looks like real SQL

some_query = (
  select(whatever.id, x.something)
  .join(x, x.y == whatever.y)
  .where(x > 12)
  .order_by(whatever.id)
)

anyway - ruff rules!

Red-Eyed commented 7 months ago

Hello, first of all, thank you for a great piece of software!

Came here to ask for a similar functionality mentioned above: I would like to see chaining methods on new line:

It is heavily used in the expression library:

(Seq(find_node(labels, polygons_predicate))
    .choose(lambda x: x)
    .map(parse_points)
    .choose(lambda x: x),
)
mattharrison commented 7 months ago

As a big proponent of chaining in Pandas and Polars, I would love to see some options to help with code readability with these libraries. Starting each line with a period makes the code easier to understand.

mchccc commented 7 months ago

As a sort of workaround, you can add comments in between the lines:

        plot_df = (
            self.data
            # Round costs
            .assign(cost=np.round((self.data.cost / 1000), decimals=0))
        )

One might even argue it helps more with readability ;)

thomas-mckay commented 5 months ago

As mentioned by nick4u, this would be a great feature for SQLAlchemy, and I'll add Django to the list:

result = (
    SomeModel.objects
    .filter(...)
    .annotate(...)
    .prefetch_related(...)
    .order_by(...)
)

We format like this naturally at my current job, and the main blocking-point of adopting a formatter is that it mangles this syntax regardless of line length (i.e.: the formatter will reformat even code that is within line-length limits). To be fair, the larger pain-point is that formatters in general don't care about what the developer's intent was regarding readability, it just assumes it knows better (or that the dev didn't care to begin with). So both a carefully-crafted, readable piece of code and a long unreadable string will be overridden regardless. The magic-comma is a big step in the right direction, as it lets developers tell the formatter what is more readable. Thankfully, that covers a lot of cases, but not all.

As tmke8 commented on #9577, maybe the solution to circumvent the complexity of implementing this type of formatting, at least for now, would be for the formatter to understand "magic-parenthesis". In other words, if it encounters this formatting, apply formatting within the parenthesis, but don't remove them. The example above would be left unchanged, but

result = (
    SomeModel.objects
    .filter(some_very_long_column_name_1__startswith='some_very_long_value', some_very_long_column_name_2__startswith='some_very_long_value')
    .order_by(...)
)

would become

result = (
    SomeModel.objects
    .filter(
        some_very_long_column_name_1__startswith='some_very_long_value',
        some_very_long_column_name_2__startswith='some_very_long_value',
    )
    .order_by(...)
)

Also, this would have a great side-effect for multi-line conditions, I think:

Currently, this code:

if (
    self.task
    and not self.task.is_canceled
    and not self.task.is_finished
): ...

gets auto-formatted to

if self.task and not self.task.is_canceled and not self.task.is_finished: ...

But with "magic-parenthesis", the first formatting would be left as-is.

PS: I realize, this comment may be taking the issue in a different direction. If so, let me know and I'll open a new one. PPS: Thank you for Ruff :heart:

parikls commented 5 months ago

+1 for this. using such code-style constantly for queries for both alchemy and django

shner-elmo commented 4 months ago

Great suggestion for a great linter!

Has there been any progress so far on this feature?

aayushchhabra1999 commented 4 months ago

+1 Need this for method chaining and sqlalchemy formatting.

CC: @MichaReiser @charliermarsh - Any work or plans for this anytime soon?

the-infinity commented 4 months ago

+1 too, also at SQLAlchemy, this would really improve readability if you have this option.

yamplum commented 2 months ago

Just wanted to raise another voice in support of this feature. It seems like Black supports this style of formatting? https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#call-chains

I would really prefer to stick with a Rust-based tool but this might be worth switching over.

MichaReiser commented 2 months ago

Just wanted to raise another voice in support of this feature. It seems like Black supports this style of formatting? black.readthedocs.io/en/stable/the_black_code_style/current_style.html#call-chains

Ruff should support call-chain formatting the same as Black. If there are cases where Black applies call-chain formatting and Ruff doesn't, then that's a bug.

Black Ruff

yamplum commented 2 months ago

Ruff should support call-chain formatting the same as Black. If there are cases where Black applies call-chain formatting and Ruff doesn't, than that's a bug.

Sorry, you're right — it seems that Ruff matches Black here, however both "fail" in this manner when there is only one method in the "chain", like here. It would be great if cases like that could also be formatted sanely.

MichaReiser commented 2 months ago

Sorry, you're right — it seems that Ruff matches Black here, however both "fail" in this manner when there is only one method in the "chain", like here. It would be great if cases like that could also be formatted sanely.

No worries. I do agree that better call chain formatting would be great.