From 1ab8a9364aeda2022ea505d99b4284f296dc6c2c Mon Sep 17 00:00:00 2001 From: Matthew Lloyd Date: Mon, 30 Mar 2026 12:25:37 -0400 Subject: [PATCH 1/2] Add nested-string-quote-style formatting option Introduces a new formatter option `nested-string-quote-style` that leverages Python 3.12's PEP 701 to use preferred quotes inside f-string expressions rather than alternating quote styles for compatibility. When set to `preferred` and targeting Python 3.12+, the formatter uses the user's preferred quote style (following the `quote-style` setting) inside f-string expressions. When set to `alternating` (default) or targeting Python versions below 3.12, the formatter continues to alternate quotes. --- ...ires_python_extend_from_shared_config.snap | 1 + .../cli__lint__requires_python_no_tool.snap | 1 + ...quires_python_no_tool_preview_enabled.snap | 1 + ...ython_no_tool_target_version_override.snap | 1 + ..._requires_python_pyproject_toml_above.snap | 1 + ...python_pyproject_toml_above_with_tool.snap | 1 + ...nt__requires_python_ruff_toml_above-2.snap | 1 + ...lint__requires_python_ruff_toml_above.snap | 1 + ...s_python_ruff_toml_no_target_fallback.snap | 1 + ...ow_settings__display_default_settings.snap | 1 + ...isplay_settings_from_nested_directory.snap | 1 + ...ing_nested_string_quote_style.options.json | 22 ++++ .../fstring_nested_string_quote_style.py | 5 + ...ing_nested_string_quote_style.options.json | 12 ++ .../tstring_nested_string_quote_style.py | 5 + crates/ruff_python_formatter/src/lib.rs | 4 +- crates/ruff_python_formatter/src/options.rs | 46 ++++++++ .../src/string/normalize.rs | 17 ++- .../ruff_python_formatter/tests/fixtures.rs | 6 +- ...@blank_line_before_class_docstring.py.snap | 1 + .../tests/snapshots/format@docstring.py.snap | 5 + .../format@docstring_code_examples.py.snap | 10 ++ ...ormat@docstring_code_examples_crlf.py.snap | 1 + ...g_code_examples_dynamic_line_width.py.snap | 4 + .../format@docstring_tab_indentation.py.snap | 2 + .../format@expression__bytes.py.snap | 2 + .../format@expression__fstring.py.snap | 2 + ..._fstring_nested_string_quote_style.py.snap | 111 ++++++++++++++++++ ...format@expression__fstring_preview.py.snap | 1 + ...licit_concatenated_string_preserve.py.snap | 2 + ...format@expression__list_comp_py315.py.snap | 1 + .../format@expression__string.py.snap | 2 + .../format@expression__tstring.py.snap | 1 + ..._tstring_nested_string_quote_style.py.snap | 61 ++++++++++ .../tests/snapshots/format@fluent.py.snap | 1 + ...rmat@fmt_on_off__fmt_off_docstring.py.snap | 2 + .../format@fmt_on_off__indent.py.snap | 3 + ...at@fmt_on_off__mixed_space_and_tab.py.snap | 3 + .../format@notebook_docstring.py.snap | 2 + .../tests/snapshots/format@preview.py.snap | 2 + .../snapshots/format@quote_style.py.snap | 3 + ...ormatting__docstring_code_examples.py.snap | 2 + .../format@range_formatting__indent.py.snap | 3 + .../format@range_formatting__stub.pyi.snap | 1 + .../format@skip_magic_trailing_comma.py.snap | 2 + .../format@statement__lazy_import.py.snap | 1 + .../snapshots/format@statement__try.py.snap | 2 + .../snapshots/format@statement__with.py.snap | 2 + .../format@statement__with_39.py.snap | 1 + ...lank_line_after_nested_stub_class.pyi.snap | 1 + ..._line_after_nested_stub_class_eof.pyi.snap | 1 + .../tests/snapshots/format@tab_width.py.snap | 3 + crates/ruff_workspace/src/configuration.rs | 8 ++ crates/ruff_workspace/src/options.rs | 30 +++++ crates/ruff_workspace/src/settings.rs | 8 +- docs/formatter.md | 14 ++- ruff.schema.json | 18 +++ 57 files changed, 436 insertions(+), 11 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring_nested_string_quote_style.options.json create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring_nested_string_quote_style.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring_nested_string_quote_style.options.json create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring_nested_string_quote_style.py create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@expression__fstring_nested_string_quote_style.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@expression__tstring_nested_string_quote_style.py.snap diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap index ff6b312b0af2a..4ddf4d2f07db9 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap @@ -276,6 +276,7 @@ formatter.line_ending = auto formatter.indent_style = space formatter.indent_width = 4 formatter.quote_style = double +formatter.nested_string_quote_style = alternating formatter.magic_trailing_comma = respect formatter.docstring_code_format = disabled formatter.docstring_code_line_width = dynamic diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap index 8e29e10eb805c..a955da807c974 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap @@ -278,6 +278,7 @@ formatter.line_ending = auto formatter.indent_style = space formatter.indent_width = 4 formatter.quote_style = double +formatter.nested_string_quote_style = alternating formatter.magic_trailing_comma = respect formatter.docstring_code_format = disabled formatter.docstring_code_line_width = dynamic diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap index 712a4532a3f41..2458aefa0307c 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap @@ -285,6 +285,7 @@ formatter.line_ending = auto formatter.indent_style = space formatter.indent_width = 4 formatter.quote_style = double +formatter.nested_string_quote_style = alternating formatter.magic_trailing_comma = respect formatter.docstring_code_format = disabled formatter.docstring_code_line_width = dynamic diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap index a0119beb265c9..cb464b58eb375 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap @@ -280,6 +280,7 @@ formatter.line_ending = auto formatter.indent_style = space formatter.indent_width = 4 formatter.quote_style = double +formatter.nested_string_quote_style = alternating formatter.magic_trailing_comma = respect formatter.docstring_code_format = disabled formatter.docstring_code_line_width = dynamic diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap index b78963d12551c..c7bb7598881ce 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap @@ -277,6 +277,7 @@ formatter.line_ending = auto formatter.indent_style = space formatter.indent_width = 4 formatter.quote_style = double +formatter.nested_string_quote_style = alternating formatter.magic_trailing_comma = respect formatter.docstring_code_format = disabled formatter.docstring_code_line_width = dynamic diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap index 08ec86a558a0a..4047fc27720c0 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap @@ -278,6 +278,7 @@ formatter.line_ending = auto formatter.indent_style = space formatter.indent_width = 4 formatter.quote_style = double +formatter.nested_string_quote_style = alternating formatter.magic_trailing_comma = respect formatter.docstring_code_format = disabled formatter.docstring_code_line_width = dynamic diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap index edfc0a7131b70..2ac72105a077d 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap @@ -276,6 +276,7 @@ formatter.line_ending = auto formatter.indent_style = space formatter.indent_width = 4 formatter.quote_style = double +formatter.nested_string_quote_style = alternating formatter.magic_trailing_comma = respect formatter.docstring_code_format = disabled formatter.docstring_code_line_width = dynamic diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap index 15b224b36eb9d..69b686335fdb3 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap @@ -276,6 +276,7 @@ formatter.line_ending = auto formatter.indent_style = space formatter.indent_width = 4 formatter.quote_style = double +formatter.nested_string_quote_style = alternating formatter.magic_trailing_comma = respect formatter.docstring_code_format = disabled formatter.docstring_code_line_width = dynamic diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap index 8f921dcd1d7ff..701f7e7f68042 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap @@ -276,6 +276,7 @@ formatter.line_ending = auto formatter.indent_style = space formatter.indent_width = 4 formatter.quote_style = double +formatter.nested_string_quote_style = alternating formatter.magic_trailing_comma = respect formatter.docstring_code_format = disabled formatter.docstring_code_line_width = dynamic diff --git a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap index 71af57e6a1351..345d0717222c5 100644 --- a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap +++ b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap @@ -389,6 +389,7 @@ formatter.line_ending = auto formatter.indent_style = space formatter.indent_width = 4 formatter.quote_style = double +formatter.nested_string_quote_style = alternating formatter.magic_trailing_comma = respect formatter.docstring_code_format = disabled formatter.docstring_code_line_width = dynamic diff --git a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap index 24b371035653b..ec94ebf445dd3 100644 --- a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap +++ b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap @@ -397,6 +397,7 @@ formatter.line_ending = auto formatter.indent_style = space formatter.indent_width = 4 formatter.quote_style = double +formatter.nested_string_quote_style = alternating 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/expression/fstring_nested_string_quote_style.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring_nested_string_quote_style.options.json new file mode 100644 index 0000000000000..a67a583779dcd --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring_nested_string_quote_style.options.json @@ -0,0 +1,22 @@ +[ + { + "quote_style": "double", + "target_version": "3.12", + "nested_string_quote_style": "alternating" + }, + { + "quote_style": "double", + "target_version": "3.12", + "nested_string_quote_style": "preferred" + }, + { + "quote_style": "double", + "target_version": "3.11", + "nested_string_quote_style": "alternating" + }, + { + "quote_style": "double", + "target_version": "3.11", + "nested_string_quote_style": "preferred" + } +] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring_nested_string_quote_style.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring_nested_string_quote_style.py new file mode 100644 index 0000000000000..b162908208496 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring_nested_string_quote_style.py @@ -0,0 +1,5 @@ +# Nested string literals inside f-string expressions follow either alternating +# or preferred quote normalization depending on nested-string-quote-style. +f'{ "nested" }' +f'{ "nested" = }' +f'{ ["1", "2"] }' diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring_nested_string_quote_style.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring_nested_string_quote_style.options.json new file mode 100644 index 0000000000000..8f39bf60ed570 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring_nested_string_quote_style.options.json @@ -0,0 +1,12 @@ +[ + { + "quote_style": "double", + "target_version": "3.14", + "nested_string_quote_style": "alternating" + }, + { + "quote_style": "double", + "target_version": "3.14", + "nested_string_quote_style": "preferred" + } +] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring_nested_string_quote_style.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring_nested_string_quote_style.py new file mode 100644 index 0000000000000..ec8c144c5de37 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring_nested_string_quote_style.py @@ -0,0 +1,5 @@ +# Nested string literals inside t-string expressions follow either alternating +# or preferred quote normalization depending on nested-string-quote-style. +t'{ "nested" }' +t'{ "nested" = }' +t'{ ["1", "2"] }' diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index d9bc3bc5db31b..25dad34ef6fc2 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -19,8 +19,8 @@ use crate::comments::{ pub use crate::context::PyFormatContext; pub use crate::db::Db; pub use crate::options::{ - DocstringCode, DocstringCodeLineWidth, MagicTrailingComma, PreviewMode, PyFormatOptions, - QuoteStyle, + DocstringCode, DocstringCodeLineWidth, MagicTrailingComma, NestedStringQuoteStyle, 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 5d19dec9cb85a..a3fcdf1892e92 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 nested strings in Python 3.12+. + /// + /// When set to `preferred`, Ruff will use the configured `quote-style` even for nested + /// strings inside interpolated string expressions. When set to `alternating` (default), Ruff + /// alternates quote styles for nested strings for compatibility with older Python versions. + nested_string_quote_style: NestedStringQuoteStyle, } 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(), + nested_string_quote_style: NestedStringQuoteStyle::default(), } } } @@ -144,6 +152,10 @@ impl PyFormatOptions { self.preview } + pub const fn nested_string_quote_style(&self) -> NestedStringQuoteStyle { + self.nested_string_quote_style + } + #[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_nested_string_quote_style( + mut self, + nested_string_quote_style: NestedStringQuoteStyle, + ) -> Self { + self.nested_string_quote_style = nested_string_quote_style; + self + } + #[must_use] pub fn with_source_map_generation(mut self, source_map: SourceMapGeneration) -> Self { self.source_map_generation = source_map; @@ -352,6 +373,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 NestedStringQuoteStyle { + #[default] + Alternating, + Preferred, +} + +impl NestedStringQuoteStyle { + pub const fn is_preferred(self) -> bool { + matches!(self, NestedStringQuoteStyle::Preferred) + } +} + +impl fmt::Display for NestedStringQuoteStyle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Alternating => write!(f, "alternating"), + Self::Preferred => write!(f, "preferred"), + } + } +} + #[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 150073795034e..31fd83aa907ca 100644 --- a/crates/ruff_python_formatter/src/string/normalize.rs +++ b/crates/ruff_python_formatter/src/string/normalize.rs @@ -53,17 +53,30 @@ impl<'a, 'src> StringNormalizer<'a, 'src> { 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. + // For f-strings and t-strings prefer alternating the quotes unless the outer string is + // triple quoted and the inner isn't. In Python 3.12+, `nested-string-quote-style = + // "preferred"` uses the configured quote style instead. if let InterpolatedStringState::InsideInterpolatedElement(parent_context) | InterpolatedStringState::NestedInterpolatedElement(parent_context) = self.context.interpolated_string_state() { let parent_flags = parent_context.flags(); + let nested_string_quote_style = self.context.options().nested_string_quote_style(); + if !parent_flags.is_triple_quoted() || string.flags().is_triple_quoted() { + // When `nested-string-quote-style = "preferred"` and we're targeting Python + // 3.12+, use the preferred quote style consistently. + if supports_pep_701 + && nested_string_quote_style.is_preferred() + && !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 dbc9d5a11a676..d3945a65ce3b2 100644 --- a/crates/ruff_python_formatter/tests/fixtures.rs +++ b/crates/ruff_python_formatter/tests/fixtures.rs @@ -570,7 +570,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:?} +nested-string-quote-style = {nested_string_quote_style}"#, indent_style = self.0.indent_style(), indent_width = self.0.indent_width().value(), line_width = self.0.line_width().value(), @@ -581,7 +582,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(), + nested_string_quote_style = self.0.nested_string_quote_style() ) } } 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 60177086ceebd..14d4c12d580f6 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 @@ -58,6 +58,7 @@ docstring-code-line-width = "dynamic" preview = Enabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```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 e3117a2e391c0..4dfb6b747a6e8 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@docstring.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@docstring.py.snap @@ -177,6 +177,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -353,6 +354,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -529,6 +531,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -705,6 +708,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -881,6 +885,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```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 26fcfcff6c0ae..502711e87dd90 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 @@ -1370,6 +1370,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -2742,6 +2743,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -4114,6 +4116,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -5486,6 +5489,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -6858,6 +6862,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -8223,6 +8228,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -9588,6 +9594,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -10962,6 +10969,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -12327,6 +12335,7 @@ docstring-code-line-width = 60 preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -13701,6 +13710,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```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 c40ab98413299..3cd2fe42ac7c5 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 @@ -29,6 +29,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```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 628910f153348..004e7d7088473 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 @@ -310,6 +310,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -881,6 +882,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -1427,6 +1429,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -1998,6 +2001,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```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 c6736bcfa9644..c1166e9849ab6 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 @@ -92,6 +92,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -186,6 +187,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```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 7b021391c9319..db137da467b54 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 @@ -142,6 +142,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -298,6 +299,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```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 4460667febb6a..b98df7bdc89bd 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 @@ -774,6 +774,7 @@ docstring-code-line-width = "dynamic" preview = Enabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -1605,6 +1606,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring_nested_string_quote_style.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring_nested_string_quote_style.py.snap new file mode 100644 index 0000000000000..c910703a2523c --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring_nested_string_quote_style.py.snap @@ -0,0 +1,111 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +--- +## Input +```python +# Nested string literals inside f-string expressions follow either alternating +# or preferred quote normalization depending on nested-string-quote-style. +f'{ "nested" }' +f'{ "nested" = }' +f'{ ["1", "2"] }' +``` + +## Outputs +### Output 1 +``` +indent-style = space +line-width = 88 +indent-width = 4 +quote-style = Double +line-ending = LineFeed +magic-trailing-comma = Respect +docstring-code = Disabled +docstring-code-line-width = "dynamic" +preview = Disabled +target_version = 3.12 +source_type = Python +nested-string-quote-style = alternating +``` + +```python +# Nested string literals inside f-string expressions follow either alternating +# or preferred quote normalization depending on nested-string-quote-style. +f"{'nested'}" +f"{ "nested" = }" +f"{['1', '2']}" +``` + + +### Output 2 +``` +indent-style = space +line-width = 88 +indent-width = 4 +quote-style = Double +line-ending = LineFeed +magic-trailing-comma = Respect +docstring-code = Disabled +docstring-code-line-width = "dynamic" +preview = Disabled +target_version = 3.12 +source_type = Python +nested-string-quote-style = preferred +``` + +```python +# Nested string literals inside f-string expressions follow either alternating +# or preferred quote normalization depending on nested-string-quote-style. +f"{"nested"}" +f"{ "nested" = }" +f"{["1", "2"]}" +``` + + +### Output 3 +``` +indent-style = space +line-width = 88 +indent-width = 4 +quote-style = Double +line-ending = LineFeed +magic-trailing-comma = Respect +docstring-code = Disabled +docstring-code-line-width = "dynamic" +preview = Disabled +target_version = 3.11 +source_type = Python +nested-string-quote-style = alternating +``` + +```python +# Nested string literals inside f-string expressions follow either alternating +# or preferred quote normalization depending on nested-string-quote-style. +f"{'nested'}" +f'{ "nested" = }' +f"{['1', '2']}" +``` + + +### Output 4 +``` +indent-style = space +line-width = 88 +indent-width = 4 +quote-style = Double +line-ending = LineFeed +magic-trailing-comma = Respect +docstring-code = Disabled +docstring-code-line-width = "dynamic" +preview = Disabled +target_version = 3.11 +source_type = Python +nested-string-quote-style = preferred +``` + +```python +# Nested string literals inside f-string expressions follow either alternating +# or preferred quote normalization depending on nested-string-quote-style. +f"{'nested'}" +f'{ "nested" = }' +f"{['1', '2']}" +``` 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 052758abce3bf..542fd83227eea 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 @@ -40,6 +40,7 @@ docstring-code-line-width = "dynamic" preview = Enabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```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 526d24e02a00c..9083f26e363f3 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 @@ -42,6 +42,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -83,6 +84,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.12 source_type = Python +nested-string-quote-style = alternating ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__list_comp_py315.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__list_comp_py315.py.snap index 82e4f554cd850..7a34c3c536b3f 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__list_comp_py315.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__list_comp_py315.py.snap @@ -31,6 +31,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.15 source_type = Python +nested-string-quote-style = alternating ``` ```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 0da0050e34f42..9f9dc6a992fc9 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 @@ -234,6 +234,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -484,6 +485,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__tstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__tstring.py.snap index 9e11142b7bf5f..f1a755abdfecc 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__tstring.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__tstring.py.snap @@ -751,6 +751,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.14 source_type = Python +nested-string-quote-style = alternating ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__tstring_nested_string_quote_style.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__tstring_nested_string_quote_style.py.snap new file mode 100644 index 0000000000000..0b32e2143f58d --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__tstring_nested_string_quote_style.py.snap @@ -0,0 +1,61 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +--- +## Input +```python +# Nested string literals inside t-string expressions follow either alternating +# or preferred quote normalization depending on nested-string-quote-style. +t'{ "nested" }' +t'{ "nested" = }' +t'{ ["1", "2"] }' +``` + +## Outputs +### Output 1 +``` +indent-style = space +line-width = 88 +indent-width = 4 +quote-style = Double +line-ending = LineFeed +magic-trailing-comma = Respect +docstring-code = Disabled +docstring-code-line-width = "dynamic" +preview = Disabled +target_version = 3.14 +source_type = Python +nested-string-quote-style = alternating +``` + +```python +# Nested string literals inside t-string expressions follow either alternating +# or preferred quote normalization depending on nested-string-quote-style. +t"{'nested'}" +t"{ "nested" = }" +t"{['1', '2']}" +``` + + +### Output 2 +``` +indent-style = space +line-width = 88 +indent-width = 4 +quote-style = Double +line-ending = LineFeed +magic-trailing-comma = Respect +docstring-code = Disabled +docstring-code-line-width = "dynamic" +preview = Disabled +target_version = 3.14 +source_type = Python +nested-string-quote-style = preferred +``` + +```python +# Nested string literals inside t-string expressions follow either alternating +# or preferred quote normalization depending on nested-string-quote-style. +t"{"nested"}" +t"{ "nested" = }" +t"{["1", "2"]}" +``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fluent.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fluent.py.snap index 73213398d59ec..705231360cb21 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@fluent.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@fluent.py.snap @@ -55,6 +55,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python 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 db1d53e68b7b5..2d8dcee225c47 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 @@ -39,6 +39,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -77,6 +78,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```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 b1c58a7f63cc1..3aa784ecd0019 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 @@ -75,6 +75,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -151,6 +152,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -227,6 +229,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```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 1c52065c5a469..16d0efe6daea6 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 @@ -35,6 +35,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -70,6 +71,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -105,6 +107,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```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 6195b43c2185b..89549ac4b0d82 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 @@ -26,6 +26,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Ipynb +nested-string-quote-style = alternating ``` ```python @@ -50,6 +51,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```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 f268f65783b37..4b40f60291fed 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap @@ -86,6 +86,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -169,6 +170,7 @@ docstring-code-line-width = "dynamic" preview = Enabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```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 a430dc1bb7672..9be871765a343 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 @@ -70,6 +70,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -144,6 +145,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -218,6 +220,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```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 cad5ee4f2ae58..0d774f041f3ff 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 @@ -123,6 +123,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -275,6 +276,7 @@ docstring-code-line-width = 88 preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```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 b40e2fe1acdab..f24600cc4ef9f 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 @@ -88,6 +88,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -174,6 +175,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -260,6 +262,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```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 5610ef79ee1a3..0021e1ad1c528 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 @@ -36,6 +36,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Stub +nested-string-quote-style = alternating ``` ```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 e427d077c72fb..4c959a847da8d 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 @@ -53,6 +53,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -111,6 +112,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__lazy_import.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__lazy_import.py.snap index 74cd81cf8d70e..175d94a0e80b5 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__lazy_import.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__lazy_import.py.snap @@ -31,6 +31,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.15 source_type = Python +nested-string-quote-style = alternating ``` ```python diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__try.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__try.py.snap index ff0e50a0510b2..ee2301f8e4b02 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__try.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__try.py.snap @@ -237,6 +237,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.13 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -523,6 +524,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.14 source_type = Python +nested-string-quote-style = alternating ``` ```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 bb04ae9560584..d80b5d030becb 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 @@ -389,6 +389,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.8 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -820,6 +821,7 @@ docstring-code-line-width = "dynamic" preview = Enabled target_version = 3.9 source_type = Python +nested-string-quote-style = alternating ``` ```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 f38e1658a203d..a9878c6a57031 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 @@ -112,6 +112,7 @@ docstring-code-line-width = "dynamic" preview = Enabled target_version = 3.9 source_type = Python +nested-string-quote-style = alternating ``` ```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 a1efd92a9c8a6..97bcafb74fa0e 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 @@ -203,6 +203,7 @@ docstring-code-line-width = "dynamic" preview = Enabled target_version = 3.10 source_type = Stub +nested-string-quote-style = alternating ``` ```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 9e9dc166fbe73..212548116e43a 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 @@ -37,6 +37,7 @@ docstring-code-line-width = "dynamic" preview = Enabled target_version = 3.10 source_type = Stub +nested-string-quote-style = alternating ``` ```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 51b77d3f16200..2a3925ddaa533 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 @@ -28,6 +28,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -55,6 +56,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python @@ -85,6 +87,7 @@ docstring-code-line-width = "dynamic" preview = Disabled target_version = 3.10 source_type = Python +nested-string-quote-style = alternating ``` ```python diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index 26b4005757609..4dc0dd6c6e600 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -198,6 +198,9 @@ impl Configuration { ruff_formatter::IndentWidth::from(NonZeroU8::from(tab_size)) }), quote_style, + nested_string_quote_style: format + .nested_string_quote_style + .unwrap_or(format_defaults.nested_string_quote_style), magic_trailing_comma: format .magic_trailing_comma .unwrap_or(format_defaults.magic_trailing_comma), @@ -1251,6 +1254,7 @@ pub struct FormatConfiguration { pub indent_style: Option, pub quote_style: Option, + pub nested_string_quote_style: Option, pub magic_trailing_comma: Option, pub line_ending: Option, pub docstring_code_format: Option, @@ -1275,6 +1279,7 @@ impl FormatConfiguration { preview: options.preview.map(PreviewMode::from), indent_style: options.indent_style, quote_style: options.quote_style, + nested_string_quote_style: options.nested_string_quote_style, magic_trailing_comma: options.skip_magic_trailing_comma.map(|skip| { if skip { MagicTrailingComma::Ignore @@ -1302,6 +1307,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), + nested_string_quote_style: self + .nested_string_quote_style + .or(config.nested_string_quote_style), 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 0313fb9fbaffb..419151ed730c8 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -3827,6 +3827,36 @@ pub struct FormatOptions { )] pub quote_style: Option, + /// Controls the quote style for nested strings inside interpolated string expressions in + /// Python 3.12+. + /// + /// - `alternating` (default): Use alternating quotes. + /// - `preferred`: Use the configured [`quote-style`](#format_quote-style). + /// + /// With `nested-string-quote-style = "preferred"` and `quote-style = "double"` in Python 3.12+: + /// + /// ```python + /// f"Result: {data["key"]}" # Same quotes for outer f-string and nested string + /// ``` + /// + /// With `nested-string-quote-style = "alternating"` (default) or in Python versions before + /// 3.12: + /// + /// ```python + /// f"Result: {data['key']}" # Alternating quotes for compatibility + /// ``` + /// + /// Note: This setting has no effect when targeting Python versions below 3.12. + #[option( + default = r#""alternating""#, + value_type = r#""alternating" | "preferred""#, + example = r#" + # Use the configured quote style for nested strings (Python 3.12+ only). + nested-string-quote-style = "preferred" + "# + )] + pub nested_string_quote_style: 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 98befe4b775fd..9490d716fec2f 100644 --- a/crates/ruff_workspace/src/settings.rs +++ b/crates/ruff_workspace/src/settings.rs @@ -11,8 +11,8 @@ use ruff_linter::settings::types::{ use ruff_macros::CacheKey; use ruff_python_ast::{PySourceType, PythonVersion}; use ruff_python_formatter::{ - DocstringCode, DocstringCodeLineWidth, MagicTrailingComma, PreviewMode, PyFormatOptions, - QuoteStyle, + DocstringCode, DocstringCodeLineWidth, MagicTrailingComma, NestedStringQuoteStyle, PreviewMode, + PyFormatOptions, QuoteStyle, }; use ruff_source_file::find_newline; use std::fmt; @@ -190,6 +190,7 @@ pub struct FormatterSettings { pub indent_width: IndentWidth, pub quote_style: QuoteStyle, + pub nested_string_quote_style: NestedStringQuoteStyle, pub magic_trailing_comma: MagicTrailingComma, @@ -236,6 +237,7 @@ impl FormatterSettings { .with_indent_style(self.indent_style) .with_indent_width(self.indent_width) .with_quote_style(self.quote_style) + .with_nested_string_quote_style(self.nested_string_quote_style) .with_magic_trailing_comma(self.magic_trailing_comma) .with_preview(self.preview) .with_line_ending(line_ending) @@ -271,6 +273,7 @@ impl Default for FormatterSettings { indent_style: default_options.indent_style(), indent_width: default_options.indent_width(), quote_style: default_options.quote_style(), + nested_string_quote_style: default_options.nested_string_quote_style(), 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(), @@ -294,6 +297,7 @@ impl fmt::Display for FormatterSettings { self.indent_style, self.indent_width, self.quote_style, + self.nested_string_quote_style, self.magic_trailing_comma, self.docstring_code_format, self.docstring_code_line_width, diff --git a/docs/formatter.md b/docs/formatter.md index cd38b99cd2953..735e5b20f28a6 100644 --- a/docs/formatter.md +++ b/docs/formatter.md @@ -553,8 +553,9 @@ 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" @@ -562,12 +563,19 @@ 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 `nested-string-quote-style = "preferred"`, +Ruff will use the configured quote style for nested strings: + +```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 46f4adee43521..69e4d04545f33 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1613,6 +1613,17 @@ } ] }, + "nested-string-quote-style": { + "description": "Controls the quote style for nested strings inside interpolated string expressions in\nPython 3.12+.\n\n- `alternating` (default): Use alternating quotes.\n- `preferred`: Use the configured [`quote-style`](#format_quote-style).\n\nWith `nested-string-quote-style = \"preferred\"` and `quote-style = \"double\"` in Python 3.12+:\n\n```python\nf\"Result: {data[\"key\"]}\" # Same quotes for outer f-string and nested string\n```\n\nWith `nested-string-quote-style = \"alternating\"` (default) or in Python versions before\n3.12:\n\n```python\nf\"Result: {data['key']}\" # Alternating quotes for compatibility\n```\n\nNote: This setting has no effect when targeting Python versions below 3.12.", + "anyOf": [ + { + "$ref": "#/definitions/NestedStringQuoteStyle" + }, + { + "type": "null" + } + ] + }, "preview": { "description": "Whether to enable the unstable preview style formatting.", "type": [ @@ -2659,6 +2670,13 @@ "NameImports": { "type": "string" }, + "NestedStringQuoteStyle": { + "type": "string", + "enum": [ + "alternating", + "preferred" + ] + }, "OutputFormat": { "type": "string", "enum": [ From eac237223102f42c566e19f666ea20ceb4457072 Mon Sep 17 00:00:00 2001 From: Matthew Lloyd Date: Tue, 31 Mar 2026 11:49:05 -0400 Subject: [PATCH 2/2] Address first round of review comments * Add many tests * Adjust documentation text --- .../fstring_nested_string_quote_style.py | 5 - ...=> nested_string_quote_style.options.json} | 4 +- .../expression/nested_string_quote_style.py | 81 +++ ...ing_nested_string_quote_style.options.json | 12 - .../tstring_nested_string_quote_style.py | 5 - crates/ruff_python_formatter/src/options.rs | 6 +- ..._fstring_nested_string_quote_style.py.snap | 111 ---- ...ression__nested_string_quote_style.py.snap | 543 ++++++++++++++++++ ..._tstring_nested_string_quote_style.py.snap | 61 -- crates/ruff_workspace/src/options.rs | 15 +- ruff.schema.json | 2 +- 11 files changed, 633 insertions(+), 212 deletions(-) delete mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring_nested_string_quote_style.py rename crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/{fstring_nested_string_quote_style.options.json => nested_string_quote_style.options.json} (86%) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/nested_string_quote_style.py delete mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring_nested_string_quote_style.options.json delete mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring_nested_string_quote_style.py delete mode 100644 crates/ruff_python_formatter/tests/snapshots/format@expression__fstring_nested_string_quote_style.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@expression__nested_string_quote_style.py.snap delete mode 100644 crates/ruff_python_formatter/tests/snapshots/format@expression__tstring_nested_string_quote_style.py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring_nested_string_quote_style.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring_nested_string_quote_style.py deleted file mode 100644 index b162908208496..0000000000000 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring_nested_string_quote_style.py +++ /dev/null @@ -1,5 +0,0 @@ -# Nested string literals inside f-string expressions follow either alternating -# or preferred quote normalization depending on nested-string-quote-style. -f'{ "nested" }' -f'{ "nested" = }' -f'{ ["1", "2"] }' diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring_nested_string_quote_style.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/nested_string_quote_style.options.json similarity index 86% rename from crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring_nested_string_quote_style.options.json rename to crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/nested_string_quote_style.options.json index a67a583779dcd..3bc6cb52479dd 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring_nested_string_quote_style.options.json +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/nested_string_quote_style.options.json @@ -1,12 +1,12 @@ [ { "quote_style": "double", - "target_version": "3.12", + "target_version": "3.14", "nested_string_quote_style": "alternating" }, { "quote_style": "double", - "target_version": "3.12", + "target_version": "3.14", "nested_string_quote_style": "preferred" }, { diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/nested_string_quote_style.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/nested_string_quote_style.py new file mode 100644 index 0000000000000..568905bb035ca --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/nested_string_quote_style.py @@ -0,0 +1,81 @@ +# Nested string literals inside interpolated string expressions follow either +# alternating or preferred quote normalization depending on +# nested-string-quote-style. + +# Nested string literals. +f'{ "nested" }' +t'{ "nested" }' + +# Multiple levels of nested interpolated strings. +f'level 1 {f"level 2"}' +t'level 1 {f"level 2"}' +f'''level 1 {f"level 2 {f'level 3'}"}''' +t'''level 1 {f"level 2 {f'level 3'}"}''' +f'level 1 {f"level 2 {f'level 3'}"}' # syntax error pre-3.12 +f'level 1 {f"level 2 {f'level 3 {f"level 4"}'}"}' # syntax error pre-3.12 + +# Nested string literals with equal specifiers (debug expressions). +f'{ "nested" = }' +t'{ "nested" = }' +f"{10 + len('bar')=}" +t"{10 + len('bar')=}" + +# Escape minimization. +f'"double" quotes and {"nested string"}' +t'"double" quotes and {"nested string"}' +f"'single' quotes and {'nested string'}" +t"'single' quotes and {'nested string'}" +f'"double" quotes and {"nested string with \"double\" quotes"}' # syntax error pre-3.12 +t'"double" quotes and {"nested string with \"double\" quotes"}' +f"'single' quotes and {'nested string with \'single\' quotes'}'" # syntax error pre-3.12 +t"'single' quotes and {'nested string with \'single\' quotes'}'" +f'"double" quotes and {"nested string with 'single' quotes"}' # syntax error pre-3.12 +t'"double" quotes and {"nested string with 'single' quotes"}' +f"'single' quotes and {'nested string with "double" quotes'}'" # syntax error pre-3.12 +t"'single' quotes and {'nested string with "double" quotes'}'" + +# Nested strings in lists and dictionaries. +f'{ ["1", "2"] }' +t'{ ["1", "2"] }' +f'{ {"key": [{"inner": "value"}]} }' +t'{ {"key": [{"inner": "value"}]} }' + +# Triple quotes and escaped quotes. +f'''{ "'single'" }''' +t'''{ "'single'" }''' +f'''{ '"double"' }''' +t'''{ '"double"' }''' +f''''single' { "'single'" }''' +t''''single' { "'single'" }''' +f'''"double" { '"double"' }''' +t'''"double" { '"double"' }''' +f''''single' { '"double"' }''' +t''''single' { '"double"' }''' +f'''"double" { "'single'" }''' +t'''"double" { "'single'" }''' + +# Triple quotes and nested f-strings. +f"{f'''{'nested'} inner'''} outer" +t"{t'''{'nested'} inner'''} outer" + +# Outer implicit concatenation. +f'{ "implicit " }' f'{ "concatenation" }' +t'{ "implicit " }' t'{ "concatenation" }' + +# Outer implicit concatenation with escaped quotes. +f'"double" quotes and { "implicit " }' f'{ "concatenation" } with "double" quotes' +t'"double" quotes and { "implicit " }' t'{ "concatenation" } with "double" quotes' +f'\'single\' quotes and { "implicit " }' f'{ "concatenation" } with "double" quotes' +t'\'single\' quotes and { "implicit " }' t'{ "concatenation" } with "double" quotes' +f'"double" quotes and { "implicit " }' f'{ "concatenation" } with \'single\' quotes' +t'"double" quotes and { "implicit " }' t'{ "concatenation" } with \'single\' quotes' +f'\'single\' quotes and { "implicit " }' f'{ "concatenation" } with \'single\' quotes' +t'\'single\' quotes and { "implicit " }' t'{ "concatenation" } with \'single\' quotes' + +# Inner implicit concatenation. +f'{ ("implicit " "concatenation", ["more", "strings"]) }' +t'{ ("implicit " "concatenation", ["more", "strings"]) }' + +# Inner implicit concatenation with escaped quotes. +f'{ ("implicit " "concatenation", ["'single'", "\"double\""]) }' +t'{ ("implicit " "concatenation", ["'single'", "\"double\""]) }' diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring_nested_string_quote_style.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring_nested_string_quote_style.options.json deleted file mode 100644 index 8f39bf60ed570..0000000000000 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring_nested_string_quote_style.options.json +++ /dev/null @@ -1,12 +0,0 @@ -[ - { - "quote_style": "double", - "target_version": "3.14", - "nested_string_quote_style": "alternating" - }, - { - "quote_style": "double", - "target_version": "3.14", - "nested_string_quote_style": "preferred" - } -] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring_nested_string_quote_style.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring_nested_string_quote_style.py deleted file mode 100644 index ec8c144c5de37..0000000000000 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring_nested_string_quote_style.py +++ /dev/null @@ -1,5 +0,0 @@ -# Nested string literals inside t-string expressions follow either alternating -# or preferred quote normalization depending on nested-string-quote-style. -t'{ "nested" }' -t'{ "nested" = }' -t'{ ["1", "2"] }' diff --git a/crates/ruff_python_formatter/src/options.rs b/crates/ruff_python_formatter/src/options.rs index a3fcdf1892e92..d862f95468f08 100644 --- a/crates/ruff_python_formatter/src/options.rs +++ b/crates/ruff_python_formatter/src/options.rs @@ -65,9 +65,9 @@ pub struct PyFormatOptions { /// Controls the quote style for nested strings in Python 3.12+. /// - /// When set to `preferred`, Ruff will use the configured `quote-style` even for nested - /// strings inside interpolated string expressions. When set to `alternating` (default), Ruff - /// alternates quote styles for nested strings for compatibility with older Python versions. + /// When set to `alternating` (default), Ruff will alternate quote styles for nested strings + /// inside interpolated string expressions. When set to `preferred`, Ruff will use + /// the configured `quote-style`. nested_string_quote_style: NestedStringQuoteStyle, } diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring_nested_string_quote_style.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring_nested_string_quote_style.py.snap deleted file mode 100644 index c910703a2523c..0000000000000 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring_nested_string_quote_style.py.snap +++ /dev/null @@ -1,111 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs ---- -## Input -```python -# Nested string literals inside f-string expressions follow either alternating -# or preferred quote normalization depending on nested-string-quote-style. -f'{ "nested" }' -f'{ "nested" = }' -f'{ ["1", "2"] }' -``` - -## Outputs -### Output 1 -``` -indent-style = space -line-width = 88 -indent-width = 4 -quote-style = Double -line-ending = LineFeed -magic-trailing-comma = Respect -docstring-code = Disabled -docstring-code-line-width = "dynamic" -preview = Disabled -target_version = 3.12 -source_type = Python -nested-string-quote-style = alternating -``` - -```python -# Nested string literals inside f-string expressions follow either alternating -# or preferred quote normalization depending on nested-string-quote-style. -f"{'nested'}" -f"{ "nested" = }" -f"{['1', '2']}" -``` - - -### Output 2 -``` -indent-style = space -line-width = 88 -indent-width = 4 -quote-style = Double -line-ending = LineFeed -magic-trailing-comma = Respect -docstring-code = Disabled -docstring-code-line-width = "dynamic" -preview = Disabled -target_version = 3.12 -source_type = Python -nested-string-quote-style = preferred -``` - -```python -# Nested string literals inside f-string expressions follow either alternating -# or preferred quote normalization depending on nested-string-quote-style. -f"{"nested"}" -f"{ "nested" = }" -f"{["1", "2"]}" -``` - - -### Output 3 -``` -indent-style = space -line-width = 88 -indent-width = 4 -quote-style = Double -line-ending = LineFeed -magic-trailing-comma = Respect -docstring-code = Disabled -docstring-code-line-width = "dynamic" -preview = Disabled -target_version = 3.11 -source_type = Python -nested-string-quote-style = alternating -``` - -```python -# Nested string literals inside f-string expressions follow either alternating -# or preferred quote normalization depending on nested-string-quote-style. -f"{'nested'}" -f'{ "nested" = }' -f"{['1', '2']}" -``` - - -### Output 4 -``` -indent-style = space -line-width = 88 -indent-width = 4 -quote-style = Double -line-ending = LineFeed -magic-trailing-comma = Respect -docstring-code = Disabled -docstring-code-line-width = "dynamic" -preview = Disabled -target_version = 3.11 -source_type = Python -nested-string-quote-style = preferred -``` - -```python -# Nested string literals inside f-string expressions follow either alternating -# or preferred quote normalization depending on nested-string-quote-style. -f"{'nested'}" -f'{ "nested" = }' -f"{['1', '2']}" -``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__nested_string_quote_style.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__nested_string_quote_style.py.snap new file mode 100644 index 0000000000000..518d8170fbef5 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__nested_string_quote_style.py.snap @@ -0,0 +1,543 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +--- +## Input +```python +# Nested string literals inside interpolated string expressions follow either +# alternating or preferred quote normalization depending on +# nested-string-quote-style. + +# Nested string literals. +f'{ "nested" }' +t'{ "nested" }' + +# Multiple levels of nested interpolated strings. +f'level 1 {f"level 2"}' +t'level 1 {f"level 2"}' +f'''level 1 {f"level 2 {f'level 3'}"}''' +t'''level 1 {f"level 2 {f'level 3'}"}''' +f'level 1 {f"level 2 {f'level 3'}"}' # syntax error pre-3.12 +f'level 1 {f"level 2 {f'level 3 {f"level 4"}'}"}' # syntax error pre-3.12 + +# Nested string literals with equal specifiers (debug expressions). +f'{ "nested" = }' +t'{ "nested" = }' +f"{10 + len('bar')=}" +t"{10 + len('bar')=}" + +# Escape minimization. +f'"double" quotes and {"nested string"}' +t'"double" quotes and {"nested string"}' +f"'single' quotes and {'nested string'}" +t"'single' quotes and {'nested string'}" +f'"double" quotes and {"nested string with \"double\" quotes"}' # syntax error pre-3.12 +t'"double" quotes and {"nested string with \"double\" quotes"}' +f"'single' quotes and {'nested string with \'single\' quotes'}'" # syntax error pre-3.12 +t"'single' quotes and {'nested string with \'single\' quotes'}'" +f'"double" quotes and {"nested string with 'single' quotes"}' # syntax error pre-3.12 +t'"double" quotes and {"nested string with 'single' quotes"}' +f"'single' quotes and {'nested string with "double" quotes'}'" # syntax error pre-3.12 +t"'single' quotes and {'nested string with "double" quotes'}'" + +# Nested strings in lists and dictionaries. +f'{ ["1", "2"] }' +t'{ ["1", "2"] }' +f'{ {"key": [{"inner": "value"}]} }' +t'{ {"key": [{"inner": "value"}]} }' + +# Triple quotes and escaped quotes. +f'''{ "'single'" }''' +t'''{ "'single'" }''' +f'''{ '"double"' }''' +t'''{ '"double"' }''' +f''''single' { "'single'" }''' +t''''single' { "'single'" }''' +f'''"double" { '"double"' }''' +t'''"double" { '"double"' }''' +f''''single' { '"double"' }''' +t''''single' { '"double"' }''' +f'''"double" { "'single'" }''' +t'''"double" { "'single'" }''' + +# Triple quotes and nested f-strings. +f"{f'''{'nested'} inner'''} outer" +t"{t'''{'nested'} inner'''} outer" + +# Outer implicit concatenation. +f'{ "implicit " }' f'{ "concatenation" }' +t'{ "implicit " }' t'{ "concatenation" }' + +# Outer implicit concatenation with escaped quotes. +f'"double" quotes and { "implicit " }' f'{ "concatenation" } with "double" quotes' +t'"double" quotes and { "implicit " }' t'{ "concatenation" } with "double" quotes' +f'\'single\' quotes and { "implicit " }' f'{ "concatenation" } with "double" quotes' +t'\'single\' quotes and { "implicit " }' t'{ "concatenation" } with "double" quotes' +f'"double" quotes and { "implicit " }' f'{ "concatenation" } with \'single\' quotes' +t'"double" quotes and { "implicit " }' t'{ "concatenation" } with \'single\' quotes' +f'\'single\' quotes and { "implicit " }' f'{ "concatenation" } with \'single\' quotes' +t'\'single\' quotes and { "implicit " }' t'{ "concatenation" } with \'single\' quotes' + +# Inner implicit concatenation. +f'{ ("implicit " "concatenation", ["more", "strings"]) }' +t'{ ("implicit " "concatenation", ["more", "strings"]) }' + +# Inner implicit concatenation with escaped quotes. +f'{ ("implicit " "concatenation", ["'single'", "\"double\""]) }' +t'{ ("implicit " "concatenation", ["'single'", "\"double\""]) }' +``` + +## Outputs +### Output 1 +``` +indent-style = space +line-width = 88 +indent-width = 4 +quote-style = Double +line-ending = LineFeed +magic-trailing-comma = Respect +docstring-code = Disabled +docstring-code-line-width = "dynamic" +preview = Disabled +target_version = 3.14 +source_type = Python +nested-string-quote-style = alternating +``` + +```python +# Nested string literals inside interpolated string expressions follow either +# alternating or preferred quote normalization depending on +# nested-string-quote-style. + +# Nested string literals. +f"{'nested'}" +t"{'nested'}" + +# Multiple levels of nested interpolated strings. +f"level 1 {f'level 2'}" +t"level 1 {f'level 2'}" +f"""level 1 {f"level 2 {f'level 3'}"}""" +t"""level 1 {f"level 2 {f'level 3'}"}""" +f"level 1 {f'level 2 {f"level 3"}'}" # syntax error pre-3.12 +f"level 1 {f'level 2 {f"level 3 {f'level 4'}"}'}" # syntax error pre-3.12 + +# Nested string literals with equal specifiers (debug expressions). +f"{ "nested" = }" +t"{ "nested" = }" +f"{10 + len('bar')=}" +t"{10 + len('bar')=}" + +# Escape minimization. +f'"double" quotes and {"nested string"}' +t'"double" quotes and {"nested string"}' +f"'single' quotes and {'nested string'}" +t"'single' quotes and {'nested string'}" +f'"double" quotes and {'nested string with "double" quotes'}' # syntax error pre-3.12 +t'"double" quotes and {'nested string with "double" quotes'}' +f"'single' quotes and {"nested string with 'single' quotes"}'" # syntax error pre-3.12 +t"'single' quotes and {"nested string with 'single' quotes"}'" +f'"double" quotes and {"nested string with 'single' quotes"}' # syntax error pre-3.12 +t'"double" quotes and {"nested string with 'single' quotes"}' +f"'single' quotes and {'nested string with "double" quotes'}'" # syntax error pre-3.12 +t"'single' quotes and {'nested string with "double" quotes'}'" + +# Nested strings in lists and dictionaries. +f"{['1', '2']}" +t"{['1', '2']}" +f"{ {'key': [{'inner': 'value'}]} }" +t"{ {'key': [{'inner': 'value'}]} }" + +# Triple quotes and escaped quotes. +f"""{"'single'"}""" +t"""{"'single'"}""" +f"""{'"double"'}""" +t"""{'"double"'}""" +f"""'single' {"'single'"}""" +t"""'single' {"'single'"}""" +f""""double" {'"double"'}""" +t""""double" {'"double"'}""" +f"""'single' {'"double"'}""" +t"""'single' {'"double"'}""" +f""""double" {"'single'"}""" +t""""double" {"'single'"}""" + +# Triple quotes and nested f-strings. +f"{f'''{"nested"} inner'''} outer" +t"{t'''{"nested"} inner'''} outer" + +# Outer implicit concatenation. +f"{'implicit '}{'concatenation'}" +t"{'implicit '}{'concatenation'}" + +# Outer implicit concatenation with escaped quotes. +f'"double" quotes and {"implicit "}{"concatenation"} with "double" quotes' +t'"double" quotes and {"implicit "}{"concatenation"} with "double" quotes' +f"'single' quotes and {'implicit '}{'concatenation'} with \"double\" quotes" +t"'single' quotes and {'implicit '}{'concatenation'} with \"double\" quotes" +f"\"double\" quotes and {'implicit '}{'concatenation'} with 'single' quotes" +t"\"double\" quotes and {'implicit '}{'concatenation'} with 'single' quotes" +f"'single' quotes and {'implicit '}{'concatenation'} with 'single' quotes" +t"'single' quotes and {'implicit '}{'concatenation'} with 'single' quotes" + +# Inner implicit concatenation. +f"{('implicit concatenation', ['more', 'strings'])}" +t"{('implicit concatenation', ['more', 'strings'])}" + +# Inner implicit concatenation with escaped quotes. +f"{('implicit concatenation', ["'single'", '"double"'])}" +t"{('implicit concatenation', ["'single'", '"double"'])}" +``` + + +### Output 2 +``` +indent-style = space +line-width = 88 +indent-width = 4 +quote-style = Double +line-ending = LineFeed +magic-trailing-comma = Respect +docstring-code = Disabled +docstring-code-line-width = "dynamic" +preview = Disabled +target_version = 3.14 +source_type = Python +nested-string-quote-style = preferred +``` + +```python +# Nested string literals inside interpolated string expressions follow either +# alternating or preferred quote normalization depending on +# nested-string-quote-style. + +# Nested string literals. +f"{"nested"}" +t"{"nested"}" + +# Multiple levels of nested interpolated strings. +f"level 1 {f"level 2"}" +t"level 1 {f"level 2"}" +f"""level 1 {f"level 2 {f"level 3"}"}""" +t"""level 1 {f"level 2 {f"level 3"}"}""" +f"level 1 {f"level 2 {f"level 3"}"}" # syntax error pre-3.12 +f"level 1 {f"level 2 {f"level 3 {f"level 4"}"}"}" # syntax error pre-3.12 + +# Nested string literals with equal specifiers (debug expressions). +f"{ "nested" = }" +t"{ "nested" = }" +f"{10 + len('bar')=}" +t"{10 + len('bar')=}" + +# Escape minimization. +f'"double" quotes and {"nested string"}' +t'"double" quotes and {"nested string"}' +f"'single' quotes and {"nested string"}" +t"'single' quotes and {"nested string"}" +f'"double" quotes and {'nested string with "double" quotes'}' # syntax error pre-3.12 +t'"double" quotes and {'nested string with "double" quotes'}' +f"'single' quotes and {"nested string with 'single' quotes"}'" # syntax error pre-3.12 +t"'single' quotes and {"nested string with 'single' quotes"}'" +f'"double" quotes and {"nested string with 'single' quotes"}' # syntax error pre-3.12 +t'"double" quotes and {"nested string with 'single' quotes"}' +f"'single' quotes and {'nested string with "double" quotes'}'" # syntax error pre-3.12 +t"'single' quotes and {'nested string with "double" quotes'}'" + +# Nested strings in lists and dictionaries. +f"{["1", "2"]}" +t"{["1", "2"]}" +f"{ {"key": [{"inner": "value"}]} }" +t"{ {"key": [{"inner": "value"}]} }" + +# Triple quotes and escaped quotes. +f"""{"'single'"}""" +t"""{"'single'"}""" +f"""{'"double"'}""" +t"""{'"double"'}""" +f"""'single' {"'single'"}""" +t"""'single' {"'single'"}""" +f""""double" {'"double"'}""" +t""""double" {'"double"'}""" +f"""'single' {'"double"'}""" +t"""'single' {'"double"'}""" +f""""double" {"'single'"}""" +t""""double" {"'single'"}""" + +# Triple quotes and nested f-strings. +f"{f"""{"nested"} inner"""} outer" +t"{t"""{"nested"} inner"""} outer" + +# Outer implicit concatenation. +f"{"implicit "}{"concatenation"}" +t"{"implicit "}{"concatenation"}" + +# Outer implicit concatenation with escaped quotes. +f'"double" quotes and {"implicit "}{"concatenation"} with "double" quotes' +t'"double" quotes and {"implicit "}{"concatenation"} with "double" quotes' +f"'single' quotes and {"implicit "}{"concatenation"} with \"double\" quotes" +t"'single' quotes and {"implicit "}{"concatenation"} with \"double\" quotes" +f"\"double\" quotes and {"implicit "}{"concatenation"} with 'single' quotes" +t"\"double\" quotes and {"implicit "}{"concatenation"} with 'single' quotes" +f"'single' quotes and {"implicit "}{"concatenation"} with 'single' quotes" +t"'single' quotes and {"implicit "}{"concatenation"} with 'single' quotes" + +# Inner implicit concatenation. +f"{("implicit concatenation", ["more", "strings"])}" +t"{("implicit concatenation", ["more", "strings"])}" + +# Inner implicit concatenation with escaped quotes. +f"{("implicit concatenation", ["'single'", '"double"'])}" +t"{("implicit concatenation", ["'single'", '"double"'])}" +``` + + +### Output 3 +``` +indent-style = space +line-width = 88 +indent-width = 4 +quote-style = Double +line-ending = LineFeed +magic-trailing-comma = Respect +docstring-code = Disabled +docstring-code-line-width = "dynamic" +preview = Disabled +target_version = 3.11 +source_type = Python +nested-string-quote-style = alternating +``` + +```python +# Nested string literals inside interpolated string expressions follow either +# alternating or preferred quote normalization depending on +# nested-string-quote-style. + +# Nested string literals. +f"{'nested'}" +t"{'nested'}" + +# Multiple levels of nested interpolated strings. +f"level 1 {f'level 2'}" +t"level 1 {f'level 2'}" +f"""level 1 {f"level 2 {f'level 3'}"}""" +t"""level 1 {f"level 2 {f'level 3'}"}""" +f"level 1 {f'level 2 {f'level 3'}'}" # syntax error pre-3.12 +f"level 1 {f'level 2 {f'level 3 {f"level 4"}'}'}" # syntax error pre-3.12 + +# Nested string literals with equal specifiers (debug expressions). +f'{ "nested" = }' +t"{ "nested" = }" +f"{10 + len('bar')=}" +t"{10 + len('bar')=}" + +# Escape minimization. +f'"double" quotes and {"nested string"}' +t'"double" quotes and {"nested string"}' +f"'single' quotes and {'nested string'}" +t"'single' quotes and {'nested string'}" +f'"double" quotes and {'nested string with "double" quotes'}' # syntax error pre-3.12 +t'"double" quotes and {'nested string with "double" quotes'}' +f"'single' quotes and {"nested string with 'single' quotes"}'" # syntax error pre-3.12 +t"'single' quotes and {"nested string with 'single' quotes"}'" +f'"double" quotes and {"nested string with 'single' quotes"}' # syntax error pre-3.12 +t'"double" quotes and {"nested string with 'single' quotes"}' +f"'single' quotes and {'nested string with "double" quotes'}'" # syntax error pre-3.12 +t"'single' quotes and {'nested string with "double" quotes'}'" + +# Nested strings in lists and dictionaries. +f"{['1', '2']}" +t"{['1', '2']}" +f"{ {'key': [{'inner': 'value'}]} }" +t"{ {'key': [{'inner': 'value'}]} }" + +# Triple quotes and escaped quotes. +f"""{"'single'"}""" +t"""{"'single'"}""" +f"""{'"double"'}""" +t"""{'"double"'}""" +f"""'single' {"'single'"}""" +t"""'single' {"'single'"}""" +f""""double" {'"double"'}""" +t""""double" {'"double"'}""" +f"""'single' {'"double"'}""" +t"""'single' {'"double"'}""" +f""""double" {"'single'"}""" +t""""double" {"'single'"}""" + +# Triple quotes and nested f-strings. +f"{f'''{'nested'} inner'''} outer" +t"{t'''{'nested'} inner'''} outer" + +# Outer implicit concatenation. +f"{'implicit '}{'concatenation'}" +t"{'implicit '}{'concatenation'}" + +# Outer implicit concatenation with escaped quotes. +f'"double" quotes and {"implicit "}{"concatenation"} with "double" quotes' +t'"double" quotes and {"implicit "}{"concatenation"} with "double" quotes' +f"'single' quotes and {'implicit '}{'concatenation'} with \"double\" quotes" +t"'single' quotes and {'implicit '}{'concatenation'} with \"double\" quotes" +f"\"double\" quotes and {'implicit '}{'concatenation'} with 'single' quotes" +t"\"double\" quotes and {'implicit '}{'concatenation'} with 'single' quotes" +f"'single' quotes and {'implicit '}{'concatenation'} with 'single' quotes" +t"'single' quotes and {'implicit '}{'concatenation'} with 'single' quotes" + +# Inner implicit concatenation. +f"{('implicit concatenation', ['more', 'strings'])}" +t"{('implicit concatenation', ['more', 'strings'])}" + +# Inner implicit concatenation with escaped quotes. +f"{('implicit concatenation', ["'single'", '"double"'])}" +t"{('implicit concatenation', ["'single'", '"double"'])}" +``` + + +### Unsupported Syntax Errors +error[invalid-syntax]: Cannot reuse outer quote character in f-strings on Python 3.11 (syntax was added in Python 3.12) + --> nested_string_quote_style.py:30:24 + | +28 | f'"double" quotes and {'nested string with "double" quotes'}' # syntax error pre-3.12 +29 | t'"double" quotes and {'nested string with "double" quotes'}' +30 | f"'single' quotes and {"nested string with 'single' quotes"}'" # syntax error pre-3.12 + | ^ +31 | t"'single' quotes and {"nested string with 'single' quotes"}'" +32 | f'"double" quotes and {"nested string with 'single' quotes"}' # syntax error pre-3.12 + | +warning: Only accept new syntax errors if they are also present in the input. The formatter should not introduce syntax errors. + +error[invalid-syntax]: Cannot reuse outer quote character in f-strings on Python 3.11 (syntax was added in Python 3.12) + --> nested_string_quote_style.py:28:24 + | +26 | f"'single' quotes and {'nested string'}" +27 | t"'single' quotes and {'nested string'}" +28 | f'"double" quotes and {'nested string with "double" quotes'}' # syntax error pre-3.12 + | ^ +29 | t'"double" quotes and {'nested string with "double" quotes'}' +30 | f"'single' quotes and {"nested string with 'single' quotes"}'" # syntax error pre-3.12 + | +warning: Only accept new syntax errors if they are also present in the input. The formatter should not introduce syntax errors. + + +### Output 4 +``` +indent-style = space +line-width = 88 +indent-width = 4 +quote-style = Double +line-ending = LineFeed +magic-trailing-comma = Respect +docstring-code = Disabled +docstring-code-line-width = "dynamic" +preview = Disabled +target_version = 3.11 +source_type = Python +nested-string-quote-style = preferred +``` + +```python +# Nested string literals inside interpolated string expressions follow either +# alternating or preferred quote normalization depending on +# nested-string-quote-style. + +# Nested string literals. +f"{'nested'}" +t"{'nested'}" + +# Multiple levels of nested interpolated strings. +f"level 1 {f'level 2'}" +t"level 1 {f'level 2'}" +f"""level 1 {f"level 2 {f'level 3'}"}""" +t"""level 1 {f"level 2 {f'level 3'}"}""" +f"level 1 {f'level 2 {f'level 3'}'}" # syntax error pre-3.12 +f"level 1 {f'level 2 {f'level 3 {f"level 4"}'}'}" # syntax error pre-3.12 + +# Nested string literals with equal specifiers (debug expressions). +f'{ "nested" = }' +t"{ "nested" = }" +f"{10 + len('bar')=}" +t"{10 + len('bar')=}" + +# Escape minimization. +f'"double" quotes and {"nested string"}' +t'"double" quotes and {"nested string"}' +f"'single' quotes and {'nested string'}" +t"'single' quotes and {'nested string'}" +f'"double" quotes and {'nested string with "double" quotes'}' # syntax error pre-3.12 +t'"double" quotes and {'nested string with "double" quotes'}' +f"'single' quotes and {"nested string with 'single' quotes"}'" # syntax error pre-3.12 +t"'single' quotes and {"nested string with 'single' quotes"}'" +f'"double" quotes and {"nested string with 'single' quotes"}' # syntax error pre-3.12 +t'"double" quotes and {"nested string with 'single' quotes"}' +f"'single' quotes and {'nested string with "double" quotes'}'" # syntax error pre-3.12 +t"'single' quotes and {'nested string with "double" quotes'}'" + +# Nested strings in lists and dictionaries. +f"{['1', '2']}" +t"{['1', '2']}" +f"{ {'key': [{'inner': 'value'}]} }" +t"{ {'key': [{'inner': 'value'}]} }" + +# Triple quotes and escaped quotes. +f"""{"'single'"}""" +t"""{"'single'"}""" +f"""{'"double"'}""" +t"""{'"double"'}""" +f"""'single' {"'single'"}""" +t"""'single' {"'single'"}""" +f""""double" {'"double"'}""" +t""""double" {'"double"'}""" +f"""'single' {'"double"'}""" +t"""'single' {'"double"'}""" +f""""double" {"'single'"}""" +t""""double" {"'single'"}""" + +# Triple quotes and nested f-strings. +f"{f'''{'nested'} inner'''} outer" +t"{t'''{'nested'} inner'''} outer" + +# Outer implicit concatenation. +f"{'implicit '}{'concatenation'}" +t"{'implicit '}{'concatenation'}" + +# Outer implicit concatenation with escaped quotes. +f'"double" quotes and {"implicit "}{"concatenation"} with "double" quotes' +t'"double" quotes and {"implicit "}{"concatenation"} with "double" quotes' +f"'single' quotes and {'implicit '}{'concatenation'} with \"double\" quotes" +t"'single' quotes and {'implicit '}{'concatenation'} with \"double\" quotes" +f"\"double\" quotes and {'implicit '}{'concatenation'} with 'single' quotes" +t"\"double\" quotes and {'implicit '}{'concatenation'} with 'single' quotes" +f"'single' quotes and {'implicit '}{'concatenation'} with 'single' quotes" +t"'single' quotes and {'implicit '}{'concatenation'} with 'single' quotes" + +# Inner implicit concatenation. +f"{('implicit concatenation', ['more', 'strings'])}" +t"{('implicit concatenation', ['more', 'strings'])}" + +# Inner implicit concatenation with escaped quotes. +f"{('implicit concatenation', ["'single'", '"double"'])}" +t"{('implicit concatenation', ["'single'", '"double"'])}" +``` + + +### Unsupported Syntax Errors +error[invalid-syntax]: Cannot reuse outer quote character in f-strings on Python 3.11 (syntax was added in Python 3.12) + --> nested_string_quote_style.py:30:24 + | +28 | f'"double" quotes and {'nested string with "double" quotes'}' # syntax error pre-3.12 +29 | t'"double" quotes and {'nested string with "double" quotes'}' +30 | f"'single' quotes and {"nested string with 'single' quotes"}'" # syntax error pre-3.12 + | ^ +31 | t"'single' quotes and {"nested string with 'single' quotes"}'" +32 | f'"double" quotes and {"nested string with 'single' quotes"}' # syntax error pre-3.12 + | +warning: Only accept new syntax errors if they are also present in the input. The formatter should not introduce syntax errors. + +error[invalid-syntax]: Cannot reuse outer quote character in f-strings on Python 3.11 (syntax was added in Python 3.12) + --> nested_string_quote_style.py:28:24 + | +26 | f"'single' quotes and {'nested string'}" +27 | t"'single' quotes and {'nested string'}" +28 | f'"double" quotes and {'nested string with "double" quotes'}' # syntax error pre-3.12 + | ^ +29 | t'"double" quotes and {'nested string with "double" quotes'}' +30 | f"'single' quotes and {"nested string with 'single' quotes"}'" # syntax error pre-3.12 + | +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__tstring_nested_string_quote_style.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__tstring_nested_string_quote_style.py.snap deleted file mode 100644 index 0b32e2143f58d..0000000000000 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__tstring_nested_string_quote_style.py.snap +++ /dev/null @@ -1,61 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs ---- -## Input -```python -# Nested string literals inside t-string expressions follow either alternating -# or preferred quote normalization depending on nested-string-quote-style. -t'{ "nested" }' -t'{ "nested" = }' -t'{ ["1", "2"] }' -``` - -## Outputs -### Output 1 -``` -indent-style = space -line-width = 88 -indent-width = 4 -quote-style = Double -line-ending = LineFeed -magic-trailing-comma = Respect -docstring-code = Disabled -docstring-code-line-width = "dynamic" -preview = Disabled -target_version = 3.14 -source_type = Python -nested-string-quote-style = alternating -``` - -```python -# Nested string literals inside t-string expressions follow either alternating -# or preferred quote normalization depending on nested-string-quote-style. -t"{'nested'}" -t"{ "nested" = }" -t"{['1', '2']}" -``` - - -### Output 2 -``` -indent-style = space -line-width = 88 -indent-width = 4 -quote-style = Double -line-ending = LineFeed -magic-trailing-comma = Respect -docstring-code = Disabled -docstring-code-line-width = "dynamic" -preview = Disabled -target_version = 3.14 -source_type = Python -nested-string-quote-style = preferred -``` - -```python -# Nested string literals inside t-string expressions follow either alternating -# or preferred quote normalization depending on nested-string-quote-style. -t"{"nested"}" -t"{ "nested" = }" -t"{["1", "2"]}" -``` diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 419151ed730c8..310bf6f6f5c44 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -3827,23 +3827,14 @@ pub struct FormatOptions { )] pub quote_style: Option, - /// Controls the quote style for nested strings inside interpolated string expressions in - /// Python 3.12+. + /// Controls the quote style for nested strings inside interpolated string expressions. /// /// - `alternating` (default): Use alternating quotes. /// - `preferred`: Use the configured [`quote-style`](#format_quote-style). /// - /// With `nested-string-quote-style = "preferred"` and `quote-style = "double"` in Python 3.12+: - /// - /// ```python - /// f"Result: {data["key"]}" # Same quotes for outer f-string and nested string - /// ``` - /// - /// With `nested-string-quote-style = "alternating"` (default) or in Python versions before - /// 3.12: - /// /// ```python - /// f"Result: {data['key']}" # Alternating quotes for compatibility + /// f"{data['key']}" # alternating (default) + /// f"{data["key"]}" # preferred /// ``` /// /// Note: This setting has no effect when targeting Python versions below 3.12. diff --git a/ruff.schema.json b/ruff.schema.json index 69e4d04545f33..c7cfb7284971c 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1614,7 +1614,7 @@ ] }, "nested-string-quote-style": { - "description": "Controls the quote style for nested strings inside interpolated string expressions in\nPython 3.12+.\n\n- `alternating` (default): Use alternating quotes.\n- `preferred`: Use the configured [`quote-style`](#format_quote-style).\n\nWith `nested-string-quote-style = \"preferred\"` and `quote-style = \"double\"` in Python 3.12+:\n\n```python\nf\"Result: {data[\"key\"]}\" # Same quotes for outer f-string and nested string\n```\n\nWith `nested-string-quote-style = \"alternating\"` (default) or in Python versions before\n3.12:\n\n```python\nf\"Result: {data['key']}\" # Alternating quotes for compatibility\n```\n\nNote: This setting has no effect when targeting Python versions below 3.12.", + "description": "Controls the quote style for nested strings inside interpolated string expressions.\n\n- `alternating` (default): Use alternating quotes.\n- `preferred`: Use the configured [`quote-style`](#format_quote-style).\n\n```python\nf\"{data['key']}\" # alternating (default)\nf\"{data[\"key\"]}\" # preferred\n```\n\nNote: This setting has no effect when targeting Python versions below 3.12.", "anyOf": [ { "$ref": "#/definitions/NestedStringQuoteStyle"