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 aded2ec2a4a73..69f65c20c573a 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 @@ -313,4 +313,5 @@ _ = ( 'This string should change its quotes to double quotes' f'This string uses double quotes in an expression {"woah"}' + f'This f-string does not use any quotes.' ) diff --git a/crates/ruff_python_formatter/src/other/f_string.rs b/crates/ruff_python_formatter/src/other/f_string.rs index 2b2e1f0449c60..6f46176a27888 100644 --- a/crates/ruff_python_formatter/src/other/f_string.rs +++ b/crates/ruff_python_formatter/src/other/f_string.rs @@ -1,10 +1,12 @@ +use crate::prelude::*; +use crate::preview::{ + is_f_string_formatting_enabled, is_f_string_implicit_concatenated_string_literal_quotes_enabled, +}; +use crate::string::{Quoting, StringNormalizer, StringQuotes}; use ruff_formatter::write; use ruff_python_ast::{AnyStringFlags, FString, StringFlags}; use ruff_source_file::Locator; - -use crate::prelude::*; -use crate::preview::is_f_string_formatting_enabled; -use crate::string::{Quoting, StringNormalizer, StringQuotes}; +use ruff_text_size::Ranged; use super::f_string_element::FormatFStringElement; @@ -29,8 +31,17 @@ impl Format> for FormatFString<'_> { fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { let locator = f.context().locator(); + // If the preview style is enabled, make the decision on what quotes to use locally for each + // f-string instead of globally for the entire f-string expression. + let quoting = + if is_f_string_implicit_concatenated_string_literal_quotes_enabled(f.context()) { + f_string_quoting(self.value, &locator) + } else { + self.quoting + }; + let normalizer = StringNormalizer::from_context(f.context()) - .with_quoting(self.quoting) + .with_quoting(quoting) .with_preferred_quote_style(f.options().quote_style()); // If f-string formatting is disabled (not in preview), then we will @@ -140,3 +151,20 @@ impl FStringLayout { matches!(self, FStringLayout::Multiline) } } + +fn f_string_quoting(f_string: &FString, locator: &Locator) -> Quoting { + let triple_quoted = f_string.flags.is_triple_quoted(); + + if f_string.elements.expressions().any(|expression| { + let string_content = locator.slice(expression.range()); + if triple_quoted { + string_content.contains(r#"""""#) || string_content.contains("'''") + } else { + string_content.contains(['"', '\'']) + } + }) { + Quoting::Preserve + } else { + Quoting::CanChange + } +} diff --git a/crates/ruff_python_formatter/src/other/string_literal.rs b/crates/ruff_python_formatter/src/other/string_literal.rs index 4f882f6ada09d..7aee127d4b06e 100644 --- a/crates/ruff_python_formatter/src/other/string_literal.rs +++ b/crates/ruff_python_formatter/src/other/string_literal.rs @@ -48,8 +48,11 @@ impl StringLiteralKind { StringLiteralKind::String | StringLiteralKind::Docstring => Quoting::CanChange, #[allow(deprecated)] StringLiteralKind::InImplicitlyConcatenatedFString(quoting) => { + // Allow string literals to pick the "optimal" quote character + // even if any other fstring in the implicit concatenation uses an expression + // containing a quote character. // TODO: Remove StringLiteralKind::InImplicitlyConcatenatedFString when promoting - // this style to stable + // this style to stable and remove the layout from `AnyStringPart::String`. if is_f_string_implicit_concatenated_string_literal_quotes_enabled(context) { Quoting::CanChange } else { 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 4c91a8640f54a..5faebb836e37d 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 @@ -319,6 +319,7 @@ hello { _ = ( 'This string should change its quotes to double quotes' f'This string uses double quotes in an expression {"woah"}' + f'This f-string does not use any quotes.' ) ``` @@ -662,6 +663,7 @@ hello { _ = ( "This string should change its quotes to double quotes" f'This string uses double quotes in an expression {"woah"}' + f"This f-string does not use any quotes." ) ``` @@ -993,6 +995,7 @@ hello { _ = ( 'This string should change its quotes to double quotes' f'This string uses double quotes in an expression {"woah"}' + f'This f-string does not use any quotes.' ) ``` @@ -1300,7 +1303,7 @@ _ = ( # comment 27 # comment 28 } woah {x}" -@@ -287,26 +299,26 @@ +@@ -287,27 +299,27 @@ if indent2: foo = f"""hello world hello { @@ -1342,5 +1345,7 @@ _ = ( - 'This string should change its quotes to double quotes' + "This string should change its quotes to double quotes" f'This string uses double quotes in an expression {"woah"}' +- f'This f-string does not use any quotes.' ++ f"This f-string does not use any quotes." ) ```