Compare commits

...

10 Commits

Author SHA1 Message Date
Brent Westbrook
e18ead10e1 docs 2025-10-21 12:24:50 -04:00
Brent Westbrook
e6917b0897 revert now-unnecessary comprehension check 2025-10-21 12:24:50 -04:00
Brent Westbrook
07eef98b15 preserve parentheses with Parenthesize::IfBreaksComprehension 2025-10-21 12:24:50 -04:00
Brent Westbrook
b45fa03492 try a separate Parenthesize variant 2025-10-21 12:24:50 -04:00
Brent Westbrook
545df16091 avoid extra parentheses for nested collections 2025-10-21 12:24:50 -04:00
Brent Westbrook
1d61766826 accept snapshots 2025-10-21 12:24:50 -04:00
Brent Westbrook
c1ba13e3e9 retain parentheses for if exprs 2025-10-21 12:24:50 -04:00
Brent Westbrook
9589542c60 passing simple test case
```py
[a for graph_path_expression in refined_constraint.condition_as_predicate.variables]
```

```shell
$ cargo run -p ruff -- format --check --preview --no-cache --config "line-length=79" fmt.py
unformatted: File would be reformatted
 --> fmt.py:1:1
  - [a for graph_path_expression in refined_constraint.condition_as_predicate.variables]
1 + [
2 +     a
3 +     for graph_path_expression in (
4 +         refined_constraint.condition_as_predicate.variables
5 +     )
6 + ]
```
2025-10-21 12:24:49 -04:00
Brent Westbrook
5329c8ca40 add preview function 2025-10-21 12:24:49 -04:00
Brent Westbrook
615e1a17eb Implement the wrap_comprehension_in preview style from Black
Summary
--

This PR implements the `wrap_comprehension_in` style added in
https://github.com/psf/black/pull/4699. This wraps `in` clauses in
comprehensions if they get too long. Using some examples from the upstream
issue, this code:

```py
[a for graph_path_expression in refined_constraint.condition_as_predicate.variables]

[
    a
    for graph_path_expression
    in refined_constraint.condition_as_predicate.variables
]
```

is currently formatted to:

```py
[
    a
    for graph_path_expression in refined_constraint.condition_as_predicate.variables
]

[
    a
    for graph_path_expression in refined_constraint.condition_as_predicate.variables
]
```

even if the second line of the comprehension exceeds the configured line length.

In preview, black will now break these lines by parenthesizing the expression
following `in`:

```py
[
    a
    for graph_path_expression in (
        refined_constraint.condition_as_predicate.variables
    )
]

[
    a
    for graph_path_expression in (
        refined_constraint.condition_as_predicate.variables
    )
]
```

I actually kind of like the alternative formatting mentioned on the original
Black issue and in our #12870 which would be more like:

```py
[
    a
    for graph_path_expression
	in refined_constraint.condition_as_predicate.variables
]
```

but I think I'm in the minority there.

Test Plan
--

Existing Black compatibility tests showing fewer differences
2025-10-21 12:06:27 -04:00
9 changed files with 254 additions and 109 deletions

View File

@@ -359,12 +359,14 @@ impl Format<PyFormatContext<'_>> for MaybeParenthesizeExpression<'_> {
parenthesize,
} = self;
let preserve_parentheses = parenthesize.is_optional()
&& is_expression_parenthesized(
(*expression).into(),
f.context().comments().ranges(),
f.context().source(),
);
let preserve_parentheses = matches!(
parenthesize,
Parenthesize::Optional | Parenthesize::IfBreaksComprehension
) && is_expression_parenthesized(
(*expression).into(),
f.context().comments().ranges(),
f.context().source(),
);
// If we want to preserve parentheses, short-circuit.
if preserve_parentheses {
@@ -387,7 +389,9 @@ impl Format<PyFormatContext<'_>> for MaybeParenthesizeExpression<'_> {
// Therefore, it is unnecessary to add an additional pair of parentheses if an outer expression
// is parenthesized. Unless, it's the `Parenthesize::IfBreaksParenthesizedNested` layout
// where parenthesizing nested `maybe_parenthesized_expression` is explicitly desired.
_ if f.context().node_level().is_parenthesized() => {
_ if f.context().node_level().is_parenthesized()
&& !matches!(parenthesize, Parenthesize::IfBreaksComprehension) =>
{
return if matches!(parenthesize, Parenthesize::IfBreaksParenthesizedNested) {
parenthesize_if_expands(&expression.format().with_options(Parentheses::Never))
.with_indent(!is_expression_huggable(expression, f.context()))
@@ -408,7 +412,8 @@ impl Format<PyFormatContext<'_>> for MaybeParenthesizeExpression<'_> {
Parenthesize::Optional
| Parenthesize::IfBreaks
| Parenthesize::IfBreaksParenthesized
| Parenthesize::IfBreaksParenthesizedNested => {
| Parenthesize::IfBreaksParenthesizedNested
| Parenthesize::IfBreaksComprehension => {
if can_omit_optional_parentheses(expression, f.context()) {
optional_parentheses(&unparenthesized).fmt(f)
} else {
@@ -417,7 +422,9 @@ impl Format<PyFormatContext<'_>> for MaybeParenthesizeExpression<'_> {
}
},
OptionalParentheses::BestFit => match parenthesize {
Parenthesize::IfBreaksParenthesized | Parenthesize::IfBreaksParenthesizedNested => {
Parenthesize::IfBreaksParenthesized
| Parenthesize::IfBreaksParenthesizedNested
| Parenthesize::IfBreaksComprehension => {
// Can-omit layout is relevant for `"abcd".call`. We don't want to add unnecessary
// parentheses in this case.
if can_omit_optional_parentheses(expression, f.context()) {
@@ -448,7 +455,8 @@ impl Format<PyFormatContext<'_>> for MaybeParenthesizeExpression<'_> {
| Parenthesize::IfBreaks
| Parenthesize::IfRequired
| Parenthesize::IfBreaksParenthesized
| Parenthesize::IfBreaksParenthesizedNested => unparenthesized.fmt(f),
| Parenthesize::IfBreaksParenthesizedNested
| Parenthesize::IfBreaksComprehension => unparenthesized.fmt(f),
},
OptionalParentheses::Always => {
@@ -989,6 +997,10 @@ impl OwnParentheses {
const fn is_non_empty(self) -> bool {
matches!(self, OwnParentheses::NonEmpty)
}
pub(crate) const fn is_empty(self) -> bool {
matches!(self, OwnParentheses::Empty)
}
}
/// Returns the [`OwnParentheses`] value for a given [`Expr`], to indicate whether it has its

View File

@@ -69,12 +69,12 @@ pub(crate) enum Parenthesize {
/// [`maybe_parenthesized_expression`] calls unlike other layouts that always omit parentheses
/// when outer parentheses are present.
IfBreaksParenthesizedNested,
}
impl Parenthesize {
pub(crate) const fn is_optional(self) -> bool {
matches!(self, Parenthesize::Optional)
}
/// Parenthesize the expression if it doesn't fit on a line or if the expression is
/// parenthesized in the source code. This is similar to `Self::Optional` except that this
/// variant will also include parentheses in nested calls, like
/// `Self::IfBreaksParenthesizedNested`.
IfBreaksComprehension,
}
/// Whether it is necessary to add parentheses around an expression.

View File

@@ -5,8 +5,10 @@ use ruff_text_size::{Ranged, TextRange};
use crate::comments::{leading_comments, trailing_comments};
use crate::expression::expr_tuple::TupleParentheses;
use crate::expression::parentheses::is_expression_parenthesized;
use crate::expression::parentheses::{Parenthesize, is_expression_parenthesized};
use crate::expression::{OwnParentheses, has_own_parentheses, maybe_parenthesize_expression};
use crate::prelude::*;
use crate::preview::is_wrap_comprehension_in_enabled;
#[derive(Default)]
pub struct FormatComprehension;
@@ -104,14 +106,32 @@ impl FormatNodeRule<Comprehension> for FormatComprehension {
leading_comments(before_in_comments),
token("in"),
trailing_comments(trailing_in_comments),
Spacer {
expression: iter,
preserve_parentheses: true
},
iter.format(),
]
)?;
if is_wrap_comprehension_in_enabled(f.context())
&& has_own_parentheses(iter, f.context()).is_none_or(OwnParentheses::is_empty)
{
write!(
f,
[
space(),
maybe_parenthesize_expression(iter, item, Parenthesize::IfBreaksComprehension)
]
)?;
} else {
write!(
f,
[
Spacer {
expression: iter,
preserve_parentheses: true
},
iter.format(),
]
)?;
}
if !ifs.is_empty() {
let joined = format_with(|f| {
let mut joiner = f.join_with(soft_line_break_or_space());

View File

@@ -36,3 +36,9 @@ pub(crate) const fn is_remove_parens_around_except_types_enabled(
) -> bool {
context.is_preview()
}
/// Returns `true` if the [`wrap_comprehension_in`](https://github.com/astral-sh/ruff/pull/TODO)
/// preview style is enabled.
pub(crate) const fn is_wrap_comprehension_in_enabled(context: &PyFormatContext) -> bool {
context.is_preview()
}

View File

@@ -81,83 +81,18 @@ dict_with_really_long_names = {
```diff
--- Black
+++ Ruff
@@ -1,20 +1,14 @@
@@ -46,7 +46,9 @@
[
a
- for graph_path_expression in (
- refined_constraint.condition_as_predicate.variables
- )
+ for graph_path_expression in refined_constraint.condition_as_predicate.variables
]
[
a
- for graph_path_expression in (
- refined_constraint.condition_as_predicate.variables
- )
+ for graph_path_expression in refined_constraint.condition_as_predicate.variables
]
[
a
- for graph_path_expression in (
- refined_constraint.condition_as_predicate.variables
- )
+ for graph_path_expression in refined_constraint.condition_as_predicate.variables
]
[
a
@@ -25,9 +19,7 @@
[
(foobar_very_long_key, foobar_very_long_value)
- for foobar_very_long_key, foobar_very_long_value in (
- foobar_very_long_dictionary.items()
- )
+ for foobar_very_long_key, foobar_very_long_value in foobar_very_long_dictionary.items()
]
# Don't split the `in` if it's not too long
@@ -47,9 +39,7 @@
[
x
for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
- for y in (
- xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
- )
+ for y in xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
]
]
@@ -58,31 +48,23 @@
graph_path_expression
for refined_constraint in self._local_constraint_refinements.values()
if refined_constraint is not None
- for graph_path_expression in (
- refined_constraint.condition_as_predicate.variables
- )
+ for graph_path_expression in refined_constraint.condition_as_predicate.variables
]
# Dictionary comprehensions
dict_with_really_long_names = {
really_really_long_key_name: an_even_longer_really_really_long_key_value
- for really_really_long_key_name, an_even_longer_really_really_long_key_value in (
- really_really_really_long_dict_name.items()
- )
+ for really_really_long_key_name, an_even_longer_really_really_long_key_value in really_really_really_long_dict_name.items()
}
{
key_with_super_really_long_name: key_with_super_really_long_name
- for key_with_super_really_long_name in (
- dictionary_with_super_really_long_name
- )
+ for key_with_super_really_long_name in dictionary_with_super_really_long_name
}
{
key_with_super_really_long_name: key_with_super_really_long_name
- for key_with_super_really_long_name in (
- dictionary_with_super_really_long_name
- )
+ for key_with_super_really_long_name in dictionary_with_super_really_long_name
- for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
+ for x in (
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
+ )
for y in (
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
)
@@ -84,5 +86,5 @@
}
{
key_with_super_really_long_name: key_with_super_really_long_name
@@ -171,15 +106,21 @@ dict_with_really_long_names = {
```python
[
a
for graph_path_expression in refined_constraint.condition_as_predicate.variables
for graph_path_expression in (
refined_constraint.condition_as_predicate.variables
)
]
[
a
for graph_path_expression in refined_constraint.condition_as_predicate.variables
for graph_path_expression in (
refined_constraint.condition_as_predicate.variables
)
]
[
a
for graph_path_expression in refined_constraint.condition_as_predicate.variables
for graph_path_expression in (
refined_constraint.condition_as_predicate.variables
)
]
[
a
@@ -190,7 +131,9 @@ dict_with_really_long_names = {
[
(foobar_very_long_key, foobar_very_long_value)
for foobar_very_long_key, foobar_very_long_value in foobar_very_long_dictionary.items()
for foobar_very_long_key, foobar_very_long_value in (
foobar_very_long_dictionary.items()
)
]
# Don't split the `in` if it's not too long
@@ -209,8 +152,12 @@ expected = [i for i in (a if b else c)]
[
[
x
for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
for y in xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
for x in (
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
)
for y in (
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
)
]
]
@@ -219,21 +166,29 @@ graph_path_expressions_in_local_constraint_refinements = [
graph_path_expression
for refined_constraint in self._local_constraint_refinements.values()
if refined_constraint is not None
for graph_path_expression in refined_constraint.condition_as_predicate.variables
for graph_path_expression in (
refined_constraint.condition_as_predicate.variables
)
]
# Dictionary comprehensions
dict_with_really_long_names = {
really_really_long_key_name: an_even_longer_really_really_long_key_value
for really_really_long_key_name, an_even_longer_really_really_long_key_value in really_really_really_long_dict_name.items()
for really_really_long_key_name, an_even_longer_really_really_long_key_value in (
really_really_really_long_dict_name.items()
)
}
{
key_with_super_really_long_name: key_with_super_really_long_name
for key_with_super_really_long_name in dictionary_with_super_really_long_name
for key_with_super_really_long_name in (
dictionary_with_super_really_long_name
)
}
{
key_with_super_really_long_name: key_with_super_really_long_name
for key_with_super_really_long_name in dictionary_with_super_really_long_name
for key_with_super_really_long_name in (
dictionary_with_super_really_long_name
)
}
{
key_with_super_really_long_name: key_with_super_really_long_name

View File

@@ -1,7 +1,6 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/dict_comp.py
snapshot_kind: text
---
## Input
```python
@@ -404,3 +403,68 @@ query = {
for key, queries in self._filters.items()
}
```
## Preview changes
```diff
--- Stable
+++ Preview
@@ -20,9 +20,10 @@
# above c
c # c
# above in
- in # in
- # above e
- e # e
+ in ( # in
+ # above e
+ e # e
+ )
# above if
if # if
# above f
@@ -43,7 +44,9 @@
for ccccccccccccccccccccccccccccccccccccccc, ddddddddddddddddddd, [
eeeeeeeeeeeeeeeeeeeeee,
fffffffffffffffffffffffff,
- ] in eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeffffffffffffffffffffffffggggggggggggggggggggghhhhhhhhhhhhhhothermoreeand_even_moreddddddddddddddddddddd
+ ] in (
+ eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeffffffffffffffffffffffffggggggggggggggggggggghhhhhhhhhhhhhhothermoreeand_even_moreddddddddddddddddddddd
+ )
if fffffffffffffffffffffffffffffffffffffffffff
< gggggggggggggggggggggggggggggggggggggggggggggg
< hhhhhhhhhhhhhhhhhhhhhhhhhh
@@ -74,7 +77,9 @@
a,
a,
a,
- ] in this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension
+ ] in (
+ this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension
+ )
}
{
k: v
@@ -99,7 +104,9 @@
a,
a,
a,
- ) in this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension
+ ) in (
+ this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension
+ )
}
# Leading
@@ -126,7 +133,9 @@
a,
a,
a, # Trailing
- ) in this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension # Trailing
+ ) in (
+ this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension
+ ) # Trailing
} # Trailing
# Trailing
```

View File

@@ -1,7 +1,6 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/generator_exp.py
snapshot_kind: text
---
## Input
```python

View File

@@ -1,7 +1,6 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list_comp.py
snapshot_kind: text
---
## Input
```python
@@ -379,3 +378,62 @@ y = [
x
]
```
## Preview changes
```diff
--- Stable
+++ Preview
@@ -20,9 +20,10 @@
# above c
c # c
# above in
- in # in
- # above e
- e # e
+ in ( # in
+ # above e
+ e # e
+ )
# above if
if # if
# above f
@@ -40,7 +41,9 @@
for ccccccccccccccccccccccccccccccccccccccc, ddddddddddddddddddd, [
eeeeeeeeeeeeeeeeeeeeee,
fffffffffffffffffffffffff,
- ] in eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeffffffffffffffffffffffffggggggggggggggggggggghhhhhhhhhhhhhhothermoreeand_even_moreddddddddddddddddddddd
+ ] in (
+ eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeffffffffffffffffffffffffggggggggggggggggggggghhhhhhhhhhhhhhothermoreeand_even_moreddddddddddddddddddddd
+ )
if fffffffffffffffffffffffffffffffffffffffffff
< gggggggggggggggggggggggggggggggggggggggggggggg
< hhhhhhhhhhhhhhhhhhhhhhhhhh
@@ -130,8 +133,10 @@
[
1
- for components in b # pylint: disable=undefined-loop-variable # integer 1 may only have decimal 01-09
- + c # negative decimal
+ for components in ( # pylint: disable=undefined-loop-variable
+ b # integer 1 may only have decimal 01-09
+ + c
+ ) # negative decimal
]
# Parenthesized targets and iterators.
@@ -186,9 +191,10 @@
a
for
# comment
- a in
- # comment
- x
+ a in (
+ # comment
+ x
+ )
if
# asdasd
"askldaklsdnmklasmdlkasmdlkasmdlkasmdasd" != "as,mdnaskldmlkasdmlaksdmlkasdlkasdm"
```

View File

@@ -1,7 +1,6 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/set_comp.py
snapshot_kind: text
---
## Input
```python
@@ -124,3 +123,35 @@ selected_choices = {
if str(v) not in self.choices.field.empty_values
}
```
## Preview changes
```diff
--- Stable
+++ Preview
@@ -20,9 +20,10 @@
# above c
c # c
# above in
- in # in
- # above e
- e # e
+ in ( # in
+ # above e
+ e # e
+ )
# above if
if # if
# above f
@@ -40,7 +41,9 @@
for ccccccccccccccccccccccccccccccccccccccc, ddddddddddddddddddd, [
eeeeeeeeeeeeeeeeeeeeee,
fffffffffffffffffffffffff,
- ] in eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeffffffffffffffffffffffffggggggggggggggggggggghhhhhhhhhhhhhhothermoreeand_even_moreddddddddddddddddddddd
+ ] in (
+ eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeffffffffffffffffffffffffggggggggggggggggggggghhhhhhhhhhhhhhothermoreeand_even_moreddddddddddddddddddddd
+ )
if fffffffffffffffffffffffffffffffffffffffffff
< gggggggggggggggggggggggggggggggggggggggggggggg
< hhhhhhhhhhhhhhhhhhhhhhhhhh
```