diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py index fd658cc5ce..46322e38f2 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py @@ -748,3 +748,7 @@ 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" diff --git a/crates/ruff_python_formatter/src/context.rs b/crates/ruff_python_formatter/src/context.rs index bc88b987ae..528afc6c71 100644 --- a/crates/ruff_python_formatter/src/context.rs +++ b/crates/ruff_python_formatter/src/context.rs @@ -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, @@ -152,12 +158,18 @@ pub(crate) enum InterpolatedStringState { impl InterpolatedStringState { pub(crate) fn can_contain_line_breaks(self) -> Option { 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. diff --git a/crates/ruff_python_formatter/src/other/interpolated_string_element.rs b/crates/ruff_python_formatter/src/other/interpolated_string_element.rs index 13526c218f..49495aa646 100644 --- a/crates/ruff_python_formatter/src/other/interpolated_string_element.rs +++ b/crates/ruff_python_formatter/src/other/interpolated_string_element.rs @@ -181,10 +181,16 @@ impl Format> 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()])?; diff --git a/crates/ruff_python_formatter/src/string/normalize.rs b/crates/ruff_python_formatter/src/string/normalize.rs index fa60c4a13f..fabd10a029 100644 --- a/crates/ruff_python_formatter/src/string/normalize.rs +++ b/crates/ruff_python_formatter/src/string/normalize.rs @@ -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(); diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__fstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__fstring.py.snap index 6ff1f73fd7..f4140291a5 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__fstring.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__fstring.py.snap @@ -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'}'" @@ -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'}'" @@ -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. diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring.py.snap index c9d90b1764..4460667feb 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring.py.snap @@ -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 @@ -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\"}} '''" @@ -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" ``` @@ -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\"}} '''" @@ -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" ```