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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[
{
"quote_style": "double",
"target_version": "3.14",
"nested_string_quote_style": "alternating"
},
{
"quote_style": "double",
"target_version": "3.14",
"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"
}
]
Original file line number Diff line number Diff line change
@@ -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\""]) }'
4 changes: 2 additions & 2 deletions crates/ruff_python_formatter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
46 changes: 46 additions & 0 deletions crates/ruff_python_formatter/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 `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,
}

fn default_line_width() -> LineWidth {
Expand Down Expand Up @@ -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(),
}
}
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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"))]
Expand Down
17 changes: 15 additions & 2 deletions crates/ruff_python_formatter/src/string/normalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
Expand Down
6 changes: 4 additions & 2 deletions crates/ruff_python_formatter/tests/fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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()
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ docstring-code-line-width = "dynamic"
preview = Enabled
target_version = 3.10
source_type = Python
nested-string-quote-style = alternating
```

```python
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ docstring-code-line-width = "dynamic"
preview = Disabled
target_version = 3.10
source_type = Python
nested-string-quote-style = alternating
```

```python
Expand Down Expand Up @@ -353,6 +354,7 @@ docstring-code-line-width = "dynamic"
preview = Disabled
target_version = 3.10
source_type = Python
nested-string-quote-style = alternating
```

```python
Expand Down Expand Up @@ -529,6 +531,7 @@ docstring-code-line-width = "dynamic"
preview = Disabled
target_version = 3.10
source_type = Python
nested-string-quote-style = alternating
```

```python
Expand Down Expand Up @@ -705,6 +708,7 @@ docstring-code-line-width = "dynamic"
preview = Disabled
target_version = 3.10
source_type = Python
nested-string-quote-style = alternating
```

```python
Expand Down Expand Up @@ -881,6 +885,7 @@ docstring-code-line-width = "dynamic"
preview = Disabled
target_version = 3.10
source_type = Python
nested-string-quote-style = alternating
```

```python
Expand Down
Loading
Loading