diff --git a/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap b/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap index a4669d8b15c284..77bdfea7fa43ae 100644 --- a/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap +++ b/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap @@ -382,6 +382,7 @@ formatter.line_ending = auto formatter.indent_style = space formatter.indent_width = 4 formatter.quote_style = double +formatter.f_string_consistent_quotes = disabled formatter.magic_trailing_comma = respect formatter.docstring_code_format = disabled formatter.docstring_code_line_width = dynamic diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/f_string_consistent_quotes.disabled.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/f_string_consistent_quotes.disabled.options.json new file mode 100644 index 00000000000000..eec82c21fb2ccd --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/f_string_consistent_quotes.disabled.options.json @@ -0,0 +1,5 @@ +{ + "target_version": "py312", + "quote_style": "double", + "f_string_consistent_quotes": "disabled" +} \ No newline at end of file diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/f_string_consistent_quotes.double.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/f_string_consistent_quotes.double.options.json new file mode 100644 index 00000000000000..0e33ba578973a1 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/f_string_consistent_quotes.double.options.json @@ -0,0 +1,5 @@ +{ + "target_version": "py312", + "quote_style": "double", + "f_string_consistent_quotes": "enabled" +} \ No newline at end of file diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/f_string_consistent_quotes.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/f_string_consistent_quotes.py new file mode 100644 index 00000000000000..5117d848e5f177 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/f_string_consistent_quotes.py @@ -0,0 +1,38 @@ +# Test consistent quotes in f-strings with PEP 701 (Python 3.12+) + +# Basic cases +basic_double = f"Value: {x}" +basic_single = f'Value: {x}' + +# Nested quotes +nested_single_in_double = f"He said: '{greeting}'" +nested_double_in_single = f'She replied: "{response}"' + +# Double quotes within expressions +expr_with_double = f"Value: {f"nested {x}"}" +expr_with_single = f"Value: {f'nested {x}'}" + +# Complex nested expressions +complex_nested = f"Outer: {f'Middle: {f"Inner: {x}"}'}" + +# Quotes that would need to be escaped without PEP 701 +would_need_escape = f"Contains a double quote: {word} says \"{quote}\"" + +# String with quotes in expression +quoted_in_expr = f"Result: {get_value(key="test", default="none")}" + +# Multiple f-strings in one expression +multiple = f"First: {x}" + f"Second: {y}" + +# Mix of raw and regular f-strings +raw_mix = rf"Raw: {x}\n" + f"Regular: {y}" + +# Multiline f-strings +multiline = f""" +Multiple +lines +with {value} +""" + +# Complex real-world example +complex_example = f"User {user['name']} logged in at {format_time(timestamp, fmt="HH:MM:SS")}" \ No newline at end of file diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/f_string_consistent_quotes.py311.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/f_string_consistent_quotes.py311.options.json new file mode 100644 index 00000000000000..e24edc091b444d --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/f_string_consistent_quotes.py311.options.json @@ -0,0 +1,5 @@ +{ + "target_version": "py311", + "quote_style": "double", + "f_string_consistent_quotes": "enabled" +} \ No newline at end of file diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/f_string_consistent_quotes.single.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/f_string_consistent_quotes.single.options.json new file mode 100644 index 00000000000000..9f4f47bee10639 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/f_string_consistent_quotes.single.options.json @@ -0,0 +1,5 @@ +{ + "target_version": "py312", + "quote_style": "single", + "f_string_consistent_quotes": "enabled" +} \ No newline at end of file diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index c6f265792b5a46..9d35a972a43b87 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -14,8 +14,8 @@ use crate::comments::{ }; pub use crate::context::PyFormatContext; pub use crate::options::{ - DocstringCode, DocstringCodeLineWidth, MagicTrailingComma, PreviewMode, PyFormatOptions, - QuoteStyle, + DocstringCode, DocstringCodeLineWidth, FStringConsistentQuotes, MagicTrailingComma, + PreviewMode, PyFormatOptions, QuoteStyle, }; use crate::range::is_logical_line; pub use crate::shared_traits::{AsFormat, FormattedIter, FormattedIterExt, IntoFormat}; diff --git a/crates/ruff_python_formatter/src/options.rs b/crates/ruff_python_formatter/src/options.rs index 1357d53590d35b..0b0e95db816f68 100644 --- a/crates/ruff_python_formatter/src/options.rs +++ b/crates/ruff_python_formatter/src/options.rs @@ -62,6 +62,13 @@ pub struct PyFormatOptions { /// Whether preview style formatting is enabled or not preview: PreviewMode, + + /// Controls the quote style for f-strings in Python 3.12+. When enabled, + /// f-strings will use consistent quotes (following quote-style) even when nesting quotes, + /// leveraging Python 3.12's enhanced string handling capabilities. + /// When disabled (default), Ruff will alternate quotes inside f-strings for compatibility with + /// older Python versions. + f_string_consistent_quotes: FStringConsistentQuotes, } fn default_line_width() -> LineWidth { @@ -91,6 +98,7 @@ impl Default for PyFormatOptions { docstring_code: DocstringCode::default(), docstring_code_line_width: DocstringCodeLineWidth::default(), preview: PreviewMode::default(), + f_string_consistent_quotes: FStringConsistentQuotes::default(), } } } @@ -144,6 +152,10 @@ impl PyFormatOptions { self.preview } + pub const fn f_string_consistent_quotes(&self) -> FStringConsistentQuotes { + self.f_string_consistent_quotes + } + #[must_use] pub fn with_target_version(mut self, target_version: ast::PythonVersion) -> Self { self.target_version = target_version; @@ -204,6 +216,15 @@ impl PyFormatOptions { self } + #[must_use] + pub fn with_f_string_consistent_quotes( + mut self, + consistent_quotes: FStringConsistentQuotes, + ) -> Self { + self.f_string_consistent_quotes = consistent_quotes; + self + } + #[must_use] pub fn with_source_map_generation(mut self, source_map: SourceMapGeneration) -> Self { self.source_map_generation = source_map; @@ -347,6 +368,31 @@ impl fmt::Display for PreviewMode { } } +#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, CacheKey)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub enum FStringConsistentQuotes { + #[default] + Disabled, + Enabled, +} + +impl FStringConsistentQuotes { + pub const fn is_enabled(self) -> bool { + matches!(self, FStringConsistentQuotes::Enabled) + } +} + +impl fmt::Display for FStringConsistentQuotes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Disabled => write!(f, "disabled"), + Self::Enabled => write!(f, "enabled"), + } + } +} + #[derive(Copy, Clone, Debug, Eq, PartialEq, Default, CacheKey)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] diff --git a/crates/ruff_python_formatter/src/string/normalize.rs b/crates/ruff_python_formatter/src/string/normalize.rs index 0c3e0cb6f39758..0bc583885d62a0 100644 --- a/crates/ruff_python_formatter/src/string/normalize.rs +++ b/crates/ruff_python_formatter/src/string/normalize.rs @@ -47,16 +47,28 @@ 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(); - // For f-strings prefer alternating the quotes unless The outer string is triple quoted and the inner isn't. + // For f-strings in Python 3.12+, use consistent quotes if enabled. + // Otherwise, alternate quotes for compatibility with older Python versions. if let FStringState::InsideExpressionElement(parent_context) = self.context.f_string_state() { let parent_flags = parent_context.f_string().flags(); + let consistent_quotes = self + .context + .options() + .f_string_consistent_quotes() + .is_enabled(); if !parent_flags.is_triple_quoted() || string.flags().is_triple_quoted() { + // When f_string_consistent_quotes is enabled AND we're targeting Python 3.12+, + // use the preferred quote style consistently + if supports_pep_701 && consistent_quotes && !preferred_quote_style.is_preserve() { + return preferred_quote_style; + } + // Otherwise, use alternating quotes for compatibility // This logic is even necessary when using preserve and the target python version doesn't support PEP701 because // we might end up joining two f-strings that have different quote styles, in which case we need to alternate the quotes // for inner strings to avoid a syntax error: `string = "this is my string with " f'"{params.get("mine")}"'` - if !preferred_quote_style.is_preserve() || !supports_pep_701 { + else if !preferred_quote_style.is_preserve() || !supports_pep_701 { return QuoteStyle::from(parent_flags.quote_style().opposite()); } } diff --git a/crates/ruff_python_formatter/tests/fixtures.rs b/crates/ruff_python_formatter/tests/fixtures.rs index 75bef10def55cd..2bc6d8ef4e1566 100644 --- a/crates/ruff_python_formatter/tests/fixtures.rs +++ b/crates/ruff_python_formatter/tests/fixtures.rs @@ -477,7 +477,8 @@ docstring-code = {docstring_code:?} docstring-code-line-width = {docstring_code_line_width:?} preview = {preview:?} target_version = {target_version} -source_type = {source_type:?}"#, +source_type = {source_type:?} +f-string-consistent-quotes = {f_string_consistent_quotes:?}"#, indent_style = self.0.indent_style(), indent_width = self.0.indent_width().value(), line_width = self.0.line_width().value(), @@ -488,7 +489,8 @@ source_type = {source_type:?}"#, docstring_code_line_width = self.0.docstring_code_line_width(), preview = self.0.preview(), target_version = self.0.target_version(), - source_type = self.0.source_type() + source_type = self.0.source_type(), + f_string_consistent_quotes = self.0.f_string_consistent_quotes() ) } } diff --git a/crates/ruff_python_formatter/tests/snapshots/format@blank_line_before_class_docstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@blank_line_before_class_docstring.py.snap index 58cfb7e4926104..3cd510e9966ef3 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@blank_line_before_class_docstring.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@blank_line_before_class_docstring.py.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs +assertion_line: 267 input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/blank_line_before_class_docstring.py --- ## Input @@ -58,6 +59,7 @@ docstring-code-line-width = "dynamic" preview = Enabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@docstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@docstring.py.snap index 9ca83e81deeb25..6588f40d76e2d9 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@docstring.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@docstring.py.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs +assertion_line: 267 input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring.py --- ## Input @@ -177,6 +178,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -353,6 +355,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -529,6 +532,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -705,6 +709,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -881,6 +886,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples.py.snap index 024d662e0ae6be..287d986ad7299b 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples.py.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs +assertion_line: 267 input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples.py --- ## Input @@ -1370,6 +1371,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -2742,6 +2744,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -4114,6 +4117,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -5486,6 +5490,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -6858,6 +6863,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -8223,6 +8229,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -9588,6 +9595,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -10962,6 +10970,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -12327,6 +12336,7 @@ docstring-code-line-width = 60 preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -13701,6 +13711,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_crlf.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_crlf.py.snap index 9b1dc4ba525f8e..1000f64c909f0b 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_crlf.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_crlf.py.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs +assertion_line: 267 input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples_crlf.py --- ## Input @@ -29,6 +30,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_dynamic_line_width.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_dynamic_line_width.py.snap index 7112865500923d..5d06b8ecb35a8d 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_dynamic_line_width.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_dynamic_line_width.py.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs +assertion_line: 267 input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples_dynamic_line_width.py --- ## Input @@ -310,6 +311,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -881,6 +883,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -1427,6 +1430,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -1998,6 +2002,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@docstring_tab_indentation.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@docstring_tab_indentation.py.snap index a7e5f5d84fc470..27535830b83f17 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@docstring_tab_indentation.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@docstring_tab_indentation.py.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs +assertion_line: 267 input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_tab_indentation.py --- ## Input @@ -92,6 +93,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -186,6 +188,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__bytes.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__bytes.py.snap index 0e9e65cf924c13..113832963b990a 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__bytes.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__bytes.py.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs +assertion_line: 267 input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/bytes.py --- ## Input @@ -142,6 +143,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -298,6 +300,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python 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 b357a01b9f6cb2..e37bd5b196730c 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 @@ -1,5 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs +assertion_line: 267 input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py --- ## Input @@ -753,6 +754,7 @@ docstring-code-line-width = "dynamic" preview = Enabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -1553,6 +1555,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring_preview.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring_preview.py.snap index 508eda3e94cb70..383806e0ab1345 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring_preview.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring_preview.py.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs +assertion_line: 267 input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring_preview.py --- ## Input @@ -40,6 +41,7 @@ docstring-code-line-width = "dynamic" preview = Enabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__join_implicit_concatenated_string_preserve.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__join_implicit_concatenated_string_preserve.py.snap index a9d9f10827ea8a..c4d337e16c33c9 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__join_implicit_concatenated_string_preserve.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__join_implicit_concatenated_string_preserve.py.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs +assertion_line: 267 input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string_preserve.py --- ## Input @@ -42,6 +43,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -83,6 +85,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.12 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap index 953b2c34cbc8bd..d0dda297f310ae 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs +assertion_line: 267 input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py --- ## Input @@ -234,6 +235,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -484,6 +486,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@f_string_consistent_quotes.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@f_string_consistent_quotes.py.snap new file mode 100644 index 00000000000000..277dd0c20cc38e --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@f_string_consistent_quotes.py.snap @@ -0,0 +1,89 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +assertion_line: 267 +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/f_string_consistent_quotes.py +--- +## Input +```python +# Test consistent quotes in f-strings with PEP 701 (Python 3.12+) + +# Basic cases +basic_double = f"Value: {x}" +basic_single = f'Value: {x}' + +# Nested quotes +nested_single_in_double = f"He said: '{greeting}'" +nested_double_in_single = f'She replied: "{response}"' + +# Double quotes within expressions +expr_with_double = f"Value: {f"nested {x}"}" +expr_with_single = f"Value: {f'nested {x}'}" + +# Complex nested expressions +complex_nested = f"Outer: {f'Middle: {f"Inner: {x}"}'}" + +# Quotes that would need to be escaped without PEP 701 +would_need_escape = f"Contains a double quote: {word} says \"{quote}\"" + +# String with quotes in expression +quoted_in_expr = f"Result: {get_value(key="test", default="none")}" + +# Multiple f-strings in one expression +multiple = f"First: {x}" + f"Second: {y}" + +# Mix of raw and regular f-strings +raw_mix = rf"Raw: {x}\n" + f"Regular: {y}" + +# Multiline f-strings +multiline = f""" +Multiple +lines +with {value} +""" + +# Complex real-world example +complex_example = f"User {user['name']} logged in at {format_time(timestamp, fmt="HH:MM:SS")}"``` + +## Output +```python +# Test consistent quotes in f-strings with PEP 701 (Python 3.12+) + +# Basic cases +basic_double = f"Value: {x}" +basic_single = f"Value: {x}" + +# Nested quotes +nested_single_in_double = f"He said: '{greeting}'" +nested_double_in_single = f'She replied: "{response}"' + +# Double quotes within expressions +expr_with_double = f"Value: {f'nested {x}'}" +expr_with_single = f"Value: {f'nested {x}'}" + +# Complex nested expressions +complex_nested = f"Outer: {f'Middle: {f"Inner: {x}"}'}" + +# Quotes that would need to be escaped without PEP 701 +would_need_escape = f'Contains a double quote: {word} says "{quote}"' + +# String with quotes in expression +quoted_in_expr = f"Result: {get_value(key='test', default='none')}" + +# Multiple f-strings in one expression +multiple = f"First: {x}" + f"Second: {y}" + +# Mix of raw and regular f-strings +raw_mix = rf"Raw: {x}\n" + f"Regular: {y}" + +# Multiline f-strings +multiline = f""" +Multiple +lines +with {value} +""" + +# Complex real-world example +complex_example = ( + f"User {user['name']} logged in at {format_time(timestamp, fmt='HH:MM:SS')}" +) +``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__fmt_off_docstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__fmt_off_docstring.py.snap index c7775aba83819e..693c5830bddc9a 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__fmt_off_docstring.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__fmt_off_docstring.py.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs +assertion_line: 267 input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/fmt_off_docstring.py --- ## Input @@ -39,6 +40,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -77,6 +79,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__indent.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__indent.py.snap index 8fd14d663462f9..521282657400c6 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__indent.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__indent.py.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs +assertion_line: 267 input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/indent.py --- ## Input @@ -20,6 +21,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -39,6 +41,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -58,6 +61,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__mixed_space_and_tab.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__mixed_space_and_tab.py.snap index 99bde1d1d63ec8..ac67feba41b595 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__mixed_space_and_tab.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__mixed_space_and_tab.py.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs +assertion_line: 267 input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/mixed_space_and_tab.py --- ## Input @@ -35,6 +36,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -70,6 +72,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -105,6 +108,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@notebook_docstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@notebook_docstring.py.snap index 6018c9fc26a27e..73b9a40fc20732 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@notebook_docstring.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@notebook_docstring.py.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs +assertion_line: 267 input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/notebook_docstring.py --- ## Input @@ -26,6 +27,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Ipynb +f-string-consistent-quotes = Disabled ``` ```python @@ -50,6 +52,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap index 3557b8c4781b53..0626594b94db0a 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs +assertion_line: 267 input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/preview.py --- ## Input @@ -86,6 +87,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -169,6 +171,7 @@ docstring-code-line-width = "dynamic" preview = Enabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@quote_style.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@quote_style.py.snap index b79b0ead6e0e9c..e73af5c2121f54 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@quote_style.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@quote_style.py.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs +assertion_line: 267 input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/quote_style.py --- ## Input @@ -70,6 +71,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -144,6 +146,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -218,6 +221,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@range_formatting__docstring_code_examples.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@range_formatting__docstring_code_examples.py.snap index 0cb72a9fb45f8b..66f3d2fe012e56 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@range_formatting__docstring_code_examples.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@range_formatting__docstring_code_examples.py.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs +assertion_line: 267 input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/docstring_code_examples.py --- ## Input @@ -123,6 +124,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -275,6 +277,7 @@ docstring-code-line-width = 88 preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@range_formatting__indent.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@range_formatting__indent.py.snap index 669f8f5eb52ac0..4d3a61a3bcf52a 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@range_formatting__indent.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@range_formatting__indent.py.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs +assertion_line: 267 input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/indent.py --- ## Input @@ -83,6 +84,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -162,6 +164,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -241,6 +244,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@range_formatting__stub.pyi.snap b/crates/ruff_python_formatter/tests/snapshots/format@range_formatting__stub.pyi.snap index d2ec8f05d87f52..3d39d0f8711210 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@range_formatting__stub.pyi.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@range_formatting__stub.pyi.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs +assertion_line: 267 input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/stub.pyi --- ## Input @@ -36,6 +37,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Stub +f-string-consistent-quotes = Disabled ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap index 12521c94333fb7..dc902ce94d1df3 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs +assertion_line: 267 input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.py --- ## Input @@ -53,6 +54,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -111,6 +113,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__with.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__with.py.snap index d72ffe623ab358..22a30edff813e5 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__with.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__with.py.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs +assertion_line: 267 input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/with.py --- ## Input @@ -389,6 +390,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.8 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -891,6 +893,7 @@ docstring-code-line-width = "dynamic" preview = Enabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__with_39.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__with_39.py.snap index f38e1658a203dd..2e6b2630116053 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__with_39.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__with_39.py.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs +assertion_line: 267 input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/with_39.py --- ## Input @@ -112,6 +113,7 @@ docstring-code-line-width = "dynamic" preview = Enabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@stub_files__blank_line_after_nested_stub_class.pyi.snap b/crates/ruff_python_formatter/tests/snapshots/format@stub_files__blank_line_after_nested_stub_class.pyi.snap index 1d7b8c76363cae..3ec20de8f0fca5 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@stub_files__blank_line_after_nested_stub_class.pyi.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@stub_files__blank_line_after_nested_stub_class.pyi.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs +assertion_line: 267 input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/blank_line_after_nested_stub_class.pyi --- ## Input @@ -203,6 +204,7 @@ docstring-code-line-width = "dynamic" preview = Enabled target_version = 3.9 source_type = Stub +f-string-consistent-quotes = Disabled ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@stub_files__blank_line_after_nested_stub_class_eof.pyi.snap b/crates/ruff_python_formatter/tests/snapshots/format@stub_files__blank_line_after_nested_stub_class_eof.pyi.snap index 55485257f392e9..403a1ca453d537 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@stub_files__blank_line_after_nested_stub_class_eof.pyi.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@stub_files__blank_line_after_nested_stub_class_eof.pyi.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs +assertion_line: 267 input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/blank_line_after_nested_stub_class_eof.pyi --- ## Input @@ -37,6 +38,7 @@ docstring-code-line-width = "dynamic" preview = Enabled target_version = 3.9 source_type = Stub +f-string-consistent-quotes = Disabled ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@tab_width.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@tab_width.py.snap index ab1caa6805825d..ecd08405eb615e 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@tab_width.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@tab_width.py.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs +assertion_line: 267 input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/tab_width.py --- ## Input @@ -28,6 +29,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -55,6 +57,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python @@ -85,6 +88,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.9 source_type = Python +f-string-consistent-quotes = Disabled ``` ```python diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index dc7e463fa5a4f5..ea657abd50b6c0 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -200,6 +200,16 @@ impl Configuration { ruff_formatter::IndentWidth::from(NonZeroU8::from(tab_size)) }), quote_style, + f_string_consistent_quotes: format + .f_string_consistent_quotes + .map(|enabled| { + if enabled { + ruff_python_formatter::FStringConsistentQuotes::Enabled + } else { + ruff_python_formatter::FStringConsistentQuotes::Disabled + } + }) + .unwrap_or(format_defaults.f_string_consistent_quotes), magic_trailing_comma: format .magic_trailing_comma .unwrap_or(format_defaults.magic_trailing_comma), @@ -1186,6 +1196,7 @@ pub struct FormatConfiguration { pub indent_style: Option, pub quote_style: Option, + pub f_string_consistent_quotes: Option, pub magic_trailing_comma: Option, pub line_ending: Option, pub docstring_code_format: Option, @@ -1211,6 +1222,7 @@ impl FormatConfiguration { preview: options.preview.map(PreviewMode::from), indent_style: options.indent_style, quote_style: options.quote_style, + f_string_consistent_quotes: options.f_string_consistent_quotes, magic_trailing_comma: options.skip_magic_trailing_comma.map(|skip| { if skip { MagicTrailingComma::Ignore @@ -1239,6 +1251,9 @@ impl FormatConfiguration { extension: self.extension.or(config.extension), indent_style: self.indent_style.or(config.indent_style), quote_style: self.quote_style.or(config.quote_style), + f_string_consistent_quotes: self + .f_string_consistent_quotes + .or(config.f_string_consistent_quotes), magic_trailing_comma: self.magic_trailing_comma.or(config.magic_trailing_comma), line_ending: self.line_ending.or(config.line_ending), docstring_code_format: self.docstring_code_format.or(config.docstring_code_format), diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 01d9ada89cf3c8..680b9195a37ba4 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -3429,6 +3429,36 @@ pub struct FormatOptions { )] pub quote_style: Option, + /// Controls the quote style for f-strings in Python 3.12+. When enabled, + /// f-strings will use consistent quotes (following quote-style) even when nesting strings inside + /// f-string expressions, leveraging Python 3.12's PEP 701 which allows using the same quote character + /// in expressions as the outer f-string. + /// When disabled (default), Ruff will alternate quotes inside f-strings for compatibility with + /// older Python versions. + /// + /// For example, with `f-string-consistent-quotes = true` and `quote-style = "double"` in Python 3.12+: + /// + /// ```python + /// f"Result: {data["key"]}" # Same quotes in f-string and expression + /// ``` + /// + /// With `f-string-consistent-quotes = false` (default): + /// + /// ```python + /// f"Result: {data['key']}" # Alternate quotes for compatibility + /// ``` + /// + /// Note: This setting has no effect when targeting Python versions below 3.12. + #[option( + default = "false", + value_type = "bool", + example = r#" + # Use consistent quotes in f-strings (Python 3.12+ only). + f-string-consistent-quotes = true + "# + )] + pub f_string_consistent_quotes: Option, + /// Ruff uses existing trailing commas as an indication that short lines should be left separate. /// If this option is set to `true`, the magic trailing comma is ignored. /// diff --git a/crates/ruff_workspace/src/settings.rs b/crates/ruff_workspace/src/settings.rs index 9ca5ef91682052..f700895ac45d67 100644 --- a/crates/ruff_workspace/src/settings.rs +++ b/crates/ruff_workspace/src/settings.rs @@ -11,8 +11,8 @@ use ruff_linter::settings::LinterSettings; use ruff_macros::CacheKey; use ruff_python_ast::{PySourceType, PythonVersion}; use ruff_python_formatter::{ - DocstringCode, DocstringCodeLineWidth, MagicTrailingComma, PreviewMode, PyFormatOptions, - QuoteStyle, + DocstringCode, DocstringCodeLineWidth, FStringConsistentQuotes, MagicTrailingComma, + PreviewMode, PyFormatOptions, QuoteStyle, }; use ruff_source_file::find_newline; use std::fmt; @@ -184,6 +184,8 @@ pub struct FormatterSettings { pub quote_style: QuoteStyle, + pub f_string_consistent_quotes: FStringConsistentQuotes, + pub magic_trailing_comma: MagicTrailingComma, pub line_ending: LineEnding, @@ -229,6 +231,7 @@ impl FormatterSettings { .with_indent_style(self.indent_style) .with_indent_width(self.indent_width) .with_quote_style(self.quote_style) + .with_f_string_consistent_quotes(self.f_string_consistent_quotes) .with_magic_trailing_comma(self.magic_trailing_comma) .with_preview(self.preview) .with_line_ending(line_ending) @@ -264,6 +267,7 @@ impl Default for FormatterSettings { indent_style: default_options.indent_style(), indent_width: default_options.indent_width(), quote_style: default_options.quote_style(), + f_string_consistent_quotes: default_options.f_string_consistent_quotes(), magic_trailing_comma: default_options.magic_trailing_comma(), docstring_code_format: default_options.docstring_code(), docstring_code_line_width: default_options.docstring_code_line_width(), @@ -287,6 +291,7 @@ impl fmt::Display for FormatterSettings { self.indent_style, self.indent_width, self.quote_style, + self.f_string_consistent_quotes, self.magic_trailing_comma, self.docstring_code_format, self.docstring_code_line_width, diff --git a/docs/formatter.md b/docs/formatter.md index 784874c7523dcc..bf1bfe5aa5fc30 100644 --- a/docs/formatter.md +++ b/docs/formatter.md @@ -225,6 +225,26 @@ def f(x): [literal blocks]: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#literal-blocks [`code-block` and `sourcecode` directives]: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block +## F-string quotes + +Ruff supports consistent quoting in f-strings when targeting Python 3.12 or later, which introduced [PEP 701](https://peps.python.org/pep-0701/). This enhancement allows the same quote character to be used both in the outer f-string and inside expressions - something that wasn't possible before Python 3.12. + +With `f-string-consistent-quotes = true` and assuming `quote-style = "double"`: + +```python +# In Python 3.12+ with f-string-consistent-quotes enabled: +f"User {data["name"]} logged in at {time}" # Same quotes in f-string and expression +``` + +With `f-string-consistent-quotes = false` (default) or in Python versions before 3.12: + +```python +# Default behavior for compatibility: +f"User {data['name']} logged in at {time}" # Alternate quotes for compatibility +``` + +This setting has no effect when targeting Python versions below 3.12. When enabled, this feature produces more consistent and readable code by following your preferred quote style throughout f-strings, even in nested expressions. + ## Format suppression Like Black, Ruff supports `# fmt: on`, `# fmt: off`, and `# fmt: skip` pragma comments, which can @@ -443,8 +463,7 @@ f'{1=:"foo}' f"{1=:"foo}" ``` -For nested f-strings, Ruff alternates quote styles, starting with the [configured quote style] for the -outermost f-string. For example, consider the following f-string: +By default, or when targeting Python versions below 3.12, Ruff alternates quote styles for nested f-strings, starting with the [configured quote style] for the outermost f-string. For example, consider the following f-string: ```python # format.quote-style = "double" @@ -452,12 +471,18 @@ outermost f-string. For example, consider the following f-string: f"outer f-string {f"nested f-string {f"another nested f-string"} end"} end" ``` -Ruff formats it as: +With default settings, Ruff formats it as: ```python f"outer f-string {f'nested f-string {f"another nested f-string"} end'} end" ``` +When targeting Python 3.12+ and with `f-string-consistent-quotes = true`, Ruff will maintain consistent quote styles: + +```python +f"outer f-string {f"nested f-string {f"another nested f-string"} end"} end" +``` + #### Line breaks Starting with Python 3.12 ([PEP 701](https://peps.python.org/pep-0701/)), the expression parts of an f-string can diff --git a/ruff.schema.json b/ruff.schema.json index 57dd60a7c45f07..899e2f7e0d175b 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1497,6 +1497,13 @@ "type": "string" } }, + "f-string-consistent-quotes": { + "description": "Controls the quote style for f-strings in Python 3.12+. When enabled, f-strings will use consistent quotes (following quote-style) even when nesting strings inside f-string expressions, leveraging Python 3.12's PEP 701 which allows using the same quote character in expressions as the outer f-string. When disabled (default), Ruff will alternate quotes inside f-strings for compatibility with older Python versions.\n\nFor example, with `f-string-consistent-quotes = true` and `quote-style = \"double\"` in Python 3.12+:\n\n```python f\"Result: {data[\"key\"]}\" # Same quotes in f-string and expression ```\n\nWith `f-string-consistent-quotes = false` (default):\n\n```python f\"Result: {data['key']}\" # Alternate quotes for compatibility ```\n\nNote: This setting has no effect when targeting Python versions below 3.12.", + "type": [ + "boolean", + "null" + ] + }, "indent-style": { "description": "Whether to use spaces or tabs for indentation.\n\n`indent-style = \"space\"` (default):\n\n```python def f(): print(\"Hello\") # Spaces indent the `print` statement. ```\n\n`indent-style = \"tab\"`:\n\n```python def f(): print(\"Hello\") # A tab `\\t` indents the `print` statement. ```\n\nPEP 8 recommends using spaces for [indentation](https://peps.python.org/pep-0008/#indentation). We care about accessibility; if you do not need tabs for accessibility, we do not recommend you use them.\n\nSee [`indent-width`](#indent-width) to configure the number of spaces per indentation and the tab width.", "anyOf": [