Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -748,3 +748,7 @@

# Regression tests for https://github.com/astral-sh/ruff/issues/15536
print(f"{ {}, 1, }")


# The inner quotes should not be changed to double quotes before Python 3.12
f"{f'''{'nested'} inner'''} outer"
14 changes: 13 additions & 1 deletion crates/ruff_python_formatter/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,12 @@ pub(crate) enum InterpolatedStringState {
///
/// The containing `FStringContext` is the surrounding f-string context.
InsideInterpolatedElement(InterpolatedStringContext),
/// The formatter is inside more than one nested f-string, such as in `nested` in:
///
/// ```py
/// f"{f'''{'nested'} inner'''} outer"
/// ```
NestedInterpolatedElement(InterpolatedStringContext),
/// The formatter is outside an f-string.
#[default]
Outside,
Expand All @@ -152,12 +158,18 @@ pub(crate) enum InterpolatedStringState {
impl InterpolatedStringState {
pub(crate) fn can_contain_line_breaks(self) -> Option<bool> {
match self {
InterpolatedStringState::InsideInterpolatedElement(context) => {
InterpolatedStringState::InsideInterpolatedElement(context)
| InterpolatedStringState::NestedInterpolatedElement(context) => {
Some(context.is_multiline())
}
InterpolatedStringState::Outside => None,
}
}

/// Returns `true` if the interpolated string state is [`NestedInterpolatedElement`].
pub(crate) fn is_nested(self) -> bool {
matches!(self, Self::NestedInterpolatedElement(..))
}
}

/// The position of a top-level statement in the module.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,16 @@ impl Format<PyFormatContext<'_>> for FormatInterpolatedElement<'_> {

let item = format_with(|f: &mut PyFormatter| {
// Update the context to be inside the f-string expression element.
let f = &mut WithInterpolatedStringState::new(
InterpolatedStringState::InsideInterpolatedElement(self.context),
f,
);
let state = match f.context().interpolated_string_state() {
InterpolatedStringState::InsideInterpolatedElement(_)
| InterpolatedStringState::NestedInterpolatedElement(_) => {
InterpolatedStringState::NestedInterpolatedElement(self.context)
}
InterpolatedStringState::Outside => {
InterpolatedStringState::InsideInterpolatedElement(self.context)
}
};
let f = &mut WithInterpolatedStringState::new(state, f);

write!(f, [bracket_spacing, expression.format()])?;

Expand Down
9 changes: 8 additions & 1 deletion crates/ruff_python_formatter/src/string/normalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,15 @@ impl<'a, 'src> StringNormalizer<'a, 'src> {
.unwrap_or(self.context.options().quote_style());
let supports_pep_701 = self.context.options().target_version().supports_pep_701();

// Preserve the existing quote style for nested interpolations more than one layer deep, if
// PEP 701 isn't supported.
if !supports_pep_701 && self.context.interpolated_string_state().is_nested() {
return QuoteStyle::Preserve;
}

// For f-strings and t-strings prefer alternating the quotes unless The outer string is triple quoted and the inner isn't.
if let InterpolatedStringState::InsideInterpolatedElement(parent_context) =
if let InterpolatedStringState::InsideInterpolatedElement(parent_context)
| InterpolatedStringState::NestedInterpolatedElement(parent_context) =
self.context.interpolated_string_state()
{
let parent_flags = parent_context.flags();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,11 @@ but none started with prefix {parentdir_prefix}"
f'{{NOT \'a\' "formatted" "value"}}'
f"some f-string with {a} {few():.2f} {formatted.values!r}"
-f'some f-string with {a} {few(""):.2f} {formatted.values!r}'
-f"{f'''{'nested'} inner'''} outer"
+f"some f-string with {a} {few(''):.2f} {formatted.values!r}"
f"{f'''{'nested'} inner'''} outer"
-f"\"{f'{nested} inner'}\" outer"
-f"space between opening braces: { {a for a in (1, 2, 3)}}"
-f'Hello \'{tricky + "example"}\''
+f"some f-string with {a} {few(''):.2f} {formatted.values!r}"
+f"{f'''{"nested"} inner'''} outer"
+f'"{f"{nested} inner"}" outer'
+f"space between opening braces: { {a for a in (1, 2, 3)} }"
+f"Hello '{tricky + 'example'}'"
Expand All @@ -49,7 +48,7 @@ f"{{NOT a formatted value}}"
f'{{NOT \'a\' "formatted" "value"}}'
f"some f-string with {a} {few():.2f} {formatted.values!r}"
f"some f-string with {a} {few(''):.2f} {formatted.values!r}"
f"{f'''{"nested"} inner'''} outer"
f"{f'''{'nested'} inner'''} outer"
f'"{f"{nested} inner"}" outer'
f"space between opening braces: { {a for a in (1, 2, 3)} }"
f"Hello '{tricky + 'example'}'"
Expand All @@ -72,17 +71,3 @@ f'Hello \'{tricky + "example"}\''
f"Tried directories {str(rootdirs)} \
but none started with prefix {parentdir_prefix}"
```

## New Unsupported Syntax Errors

error[invalid-syntax]: Cannot reuse outer quote character in f-strings on Python 3.10 (syntax was added in Python 3.12)
--> fstring.py:6:9
|
4 | f"some f-string with {a} {few():.2f} {formatted.values!r}"
5 | f"some f-string with {a} {few(''):.2f} {formatted.values!r}"
6 | f"{f'''{"nested"} inner'''} outer"
| ^
7 | f'"{f"{nested} inner"}" outer'
8 | f"space between opening braces: { {a for a in (1, 2, 3)} }"
|
warning: Only accept new syntax errors if they are also present in the input. The formatter should not introduce syntax errors.
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,10 @@ print(f"{ # Tuple with multiple elements that doesn't fit on a single line gets

# Regression tests for https://github.com/astral-sh/ruff/issues/15536
print(f"{ {}, 1, }")


# The inner quotes should not be changed to double quotes before Python 3.12
f"{f'''{'nested'} inner'''} outer"
```

## Outputs
Expand Down Expand Up @@ -1532,7 +1536,7 @@ f'{f"""other " """}'
f'{1: hy "user"}'
f'{1:hy "user"}'
f'{1: abcd "{1}" }'
f'{1: abcd "{"aa"}" }'
f'{1: abcd "{'aa'}" }'
f'{1=: "abcd {'aa'}}'
f"{x:a{z:hy \"user\"}} '''"

Expand Down Expand Up @@ -1581,6 +1585,10 @@ print(

# Regression tests for https://github.com/astral-sh/ruff/issues/15536
print(f"{ {}, 1 }")


# The inner quotes should not be changed to double quotes before Python 3.12
f"{f'''{'nested'} inner'''} outer"
```


Expand Down Expand Up @@ -2359,7 +2367,7 @@ f'{f"""other " """}'
f'{1: hy "user"}'
f'{1:hy "user"}'
f'{1: abcd "{1}" }'
f'{1: abcd "{"aa"}" }'
f'{1: abcd "{'aa'}" }'
f'{1=: "abcd {'aa'}}'
f"{x:a{z:hy \"user\"}} '''"

Expand Down Expand Up @@ -2408,6 +2416,10 @@ print(

# Regression tests for https://github.com/astral-sh/ruff/issues/15536
print(f"{ {}, 1 }")


# The inner quotes should not be changed to double quotes before Python 3.12
f"{f'''{'nested'} inner'''} outer"
```


Expand Down