ruff icon indicating copy to clipboard operation
ruff copied to clipboard

ruff formatter: one call per line for chained method calls

Open MartinBernstorff opened this issue 1 year ago • 8 comments

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!

MartinBernstorff avatar Nov 10 '23 11:11 MartinBernstorff

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.

MichaReiser avatar Nov 27 '23 11:11 MichaReiser

@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!

MartinBernstorff avatar Nov 27 '23 12:11 MartinBernstorff

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!

nick4u avatar Feb 21 '24 07:02 nick4u

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),
)

Red-Eyed avatar Mar 01 '24 09:03 Red-Eyed

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.

mattharrison avatar Mar 20 '24 21:03 mattharrison

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 ;)

mchccc avatar Mar 28 '24 10:03 mchccc

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:

thomas-mckay avatar May 10 '24 18:05 thomas-mckay

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

parikls avatar May 23 '24 16:05 parikls

Great suggestion for a great linter!

Has there been any progress so far on this feature?

shner-elmo avatar Jun 01 '24 22:06 shner-elmo

+1 Need this for method chaining and sqlalchemy formatting.

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

aayushchhabra1999 avatar Jun 02 '24 19:06 aayushchhabra1999

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

the-infinity avatar Jun 12 '24 10:06 the-infinity

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.

yamplum avatar Aug 13 '24 18:08 yamplum

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

MichaReiser avatar Aug 14 '24 06:08 MichaReiser

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.

yamplum avatar Aug 14 '24 07:08 yamplum

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.

MichaReiser avatar Aug 14 '24 07:08 MichaReiser