Skip to content
Closed
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 @@ -273,6 +273,7 @@ formatter.line_ending = auto
formatter.indent_style = space
formatter.indent_width = 4
formatter.quote_style = double
formatter.f_string_consistent_quotes = disabled
formatter.magic_trailing_comma = respect
formatter.docstring_code_format = disabled
formatter.docstring_code_line_width = dynamic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ formatter.line_ending = auto
formatter.indent_style = space
formatter.indent_width = 4
formatter.quote_style = double
formatter.f_string_consistent_quotes = disabled
formatter.magic_trailing_comma = respect
formatter.docstring_code_format = disabled
formatter.docstring_code_line_width = dynamic
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.f_string_consistent_quotes = disabled
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.f_string_consistent_quotes = disabled
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 @@ -274,6 +274,7 @@ formatter.line_ending = auto
formatter.indent_style = space
formatter.indent_width = 4
formatter.quote_style = double
formatter.f_string_consistent_quotes = disabled
formatter.magic_trailing_comma = respect
formatter.docstring_code_format = disabled
formatter.docstring_code_line_width = dynamic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ formatter.line_ending = auto
formatter.indent_style = space
formatter.indent_width = 4
formatter.quote_style = double
formatter.f_string_consistent_quotes = disabled
formatter.magic_trailing_comma = respect
formatter.docstring_code_format = disabled
formatter.docstring_code_line_width = dynamic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ formatter.line_ending = auto
formatter.indent_style = space
formatter.indent_width = 4
formatter.quote_style = double
formatter.f_string_consistent_quotes = disabled
formatter.magic_trailing_comma = respect
formatter.docstring_code_format = disabled
formatter.docstring_code_line_width = dynamic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ formatter.line_ending = auto
formatter.indent_style = space
formatter.indent_width = 4
formatter.quote_style = double
formatter.f_string_consistent_quotes = disabled
formatter.magic_trailing_comma = respect
formatter.docstring_code_format = disabled
formatter.docstring_code_line_width = dynamic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ formatter.line_ending = auto
formatter.indent_style = space
formatter.indent_width = 4
formatter.quote_style = double
formatter.f_string_consistent_quotes = disabled
formatter.magic_trailing_comma = respect
formatter.docstring_code_format = disabled
formatter.docstring_code_line_width = dynamic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@ formatter.line_ending = auto
formatter.indent_style = space
formatter.indent_width = 4
formatter.quote_style = double
formatter.f_string_consistent_quotes = disabled
formatter.magic_trailing_comma = respect
formatter.docstring_code_format = disabled
formatter.docstring_code_line_width = dynamic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ formatter.line_ending = auto
formatter.indent_style = space
formatter.indent_width = 4
formatter.quote_style = double
formatter.f_string_consistent_quotes = disabled
formatter.magic_trailing_comma = respect
formatter.docstring_code_format = disabled
formatter.docstring_code_line_width = dynamic
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"target_version": "py312",
"quote_style": "double",
"f_string_consistent_quotes": "disabled"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"target_version": "py312",
"quote_style": "double",
"f_string_consistent_quotes": "enabled"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Test consistent quotes in f-strings with PEP 701 (Python 3.12+)

# Basic cases
basic_double = f"Value: {x}"
basic_single = f'Value: {x}'

# Nested quotes
nested_single_in_double = f"He said: '{greeting}'"
nested_double_in_single = f'She replied: "{response}"'

# Double quotes within expressions
expr_with_double = f"Value: {f"nested {x}"}"
expr_with_single = f"Value: {f'nested {x}'}"

# Complex nested expressions
complex_nested = f"Outer: {f'Middle: {f"Inner: {x}"}'}"

# Quotes that would need to be escaped without PEP 701
would_need_escape = f"Contains a double quote: {word} says \"{quote}\""

# String with quotes in expression
quoted_in_expr = f"Result: {get_value(key="test", default="none")}"

# Multiple f-strings in one expression
multiple = f"First: {x}" + f"Second: {y}"

# Mix of raw and regular f-strings
raw_mix = rf"Raw: {x}\n" + f"Regular: {y}"

# Multiline f-strings
multiline = f"""
Multiple
lines
with {value}
"""

# Complex real-world example
complex_example = f"User {user['name']} logged in at {format_time(timestamp, fmt="HH:MM:SS")}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"target_version": "py311",
"quote_style": "double",
"f_string_consistent_quotes": "enabled"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"target_version": "py312",
"quote_style": "single",
"f_string_consistent_quotes": "enabled"
}
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, FStringConsistentQuotes, MagicTrailingComma,
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 f-strings in Python 3.12+. When enabled,
/// f-strings will use consistent quotes (following quote-style) even when nesting quotes,
/// leveraging Python 3.12's enhanced string handling capabilities.
/// When disabled (default), Ruff will alternate quotes inside f-strings for compatibility with
/// older Python versions.
f_string_consistent_quotes: FStringConsistentQuotes,
}

fn default_line_width() -> LineWidth {
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(),
f_string_consistent_quotes: FStringConsistentQuotes::default(),
}
}
}
Expand Down Expand Up @@ -144,6 +152,10 @@ impl PyFormatOptions {
self.preview
}

pub const fn f_string_consistent_quotes(&self) -> FStringConsistentQuotes {
self.f_string_consistent_quotes
}

#[must_use]
pub fn with_target_version(mut self, target_version: ast::PythonVersion) -> Self {
self.target_version = target_version;
Expand Down Expand Up @@ -204,6 +216,15 @@ impl PyFormatOptions {
self
}

#[must_use]
pub fn with_f_string_consistent_quotes(
mut self,
consistent_quotes: FStringConsistentQuotes,
) -> Self {
self.f_string_consistent_quotes = consistent_quotes;
self
}

#[must_use]
pub fn with_source_map_generation(mut self, source_map: SourceMapGeneration) -> Self {
self.source_map_generation = source_map;
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 FStringConsistentQuotes {
#[default]
Disabled,
Enabled,
}

impl FStringConsistentQuotes {
pub const fn is_enabled(self) -> bool {
matches!(self, FStringConsistentQuotes::Enabled)
}
}

impl fmt::Display for FStringConsistentQuotes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Disabled => write!(f, "disabled"),
Self::Enabled => write!(f, "enabled"),
}
}
}

#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, CacheKey)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
Expand Down
16 changes: 15 additions & 1 deletion crates/ruff_python_formatter/src/string/normalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,27 @@ 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. For f-strings in Python 3.12+, use consistent quotes
// if enabled.
if let InterpolatedStringState::InsideInterpolatedElement(parent_context)
| InterpolatedStringState::NestedInterpolatedElement(parent_context) =
self.context.interpolated_string_state()
{
let parent_flags = parent_context.flags();
let consistent_quotes = self
.context
.options()
.f_string_consistent_quotes()
.is_enabled();

if !parent_flags.is_triple_quoted() || string.flags().is_triple_quoted() {
// When f_string_consistent_quotes is enabled AND we're targeting Python 3.12+,
// use the preferred quote style consistently.
if supports_pep_701 && consistent_quotes && !preferred_quote_style.is_preserve() {
return preferred_quote_style;
}
// Otherwise, use alternating quotes for compatibility.
// This logic is even necessary when using preserve and the target python version doesn't support PEP701 because
// we might end up joining two f-strings that have different quote styles, in which case we need to alternate the quotes
// for inner strings to avoid a syntax error: `string = "this is my string with " f'"{params.get("mine")}"'`
Comment on lines +71 to 79
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I'd probably restructure the code here as in this patch to remove some of the repeated conditions:

Index: crates/ruff_python_formatter/src/string/normalize.rs
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/crates/ruff_python_formatter/src/string/normalize.rs b/crates/ruff_python_formatter/src/string/normalize.rs
--- a/crates/ruff_python_formatter/src/string/normalize.rs	(revision 5329113011ab9fdb93302c660f5a03052f03bbfe)
+++ b/crates/ruff_python_formatter/src/string/normalize.rs	(date 1772639527311)
@@ -10,10 +10,10 @@
 };
 use ruff_text_size::{Ranged, TextRange, TextSlice};
 
-use crate::QuoteStyle;
 use crate::context::InterpolatedStringState;
 use crate::prelude::*;
 use crate::string::{Quote, StringQuotes, TripleQuotes};
+use crate::{FStringConsistentQuotes, QuoteStyle};
 
 pub(crate) struct StringNormalizer<'a, 'src> {
     preferred_quote_style: Option<QuoteStyle>,
@@ -68,18 +68,25 @@
                 .is_enabled();
 
             if !parent_flags.is_triple_quoted() || string.flags().is_triple_quoted() {
-                // When f_string_consistent_quotes is enabled AND we're targeting Python 3.12+,
-                // use the preferred quote style consistently.
-                if supports_pep_701 && consistent_quotes && !preferred_quote_style.is_preserve() {
-                    return preferred_quote_style;
-                }
-                // Otherwise, use alternating quotes for compatibility.
                 // This logic is even necessary when using preserve and the target python version doesn't support PEP701 because
                 // we might end up joining two f-strings that have different quote styles, in which case we need to alternate the quotes
                 // for inner strings to avoid a syntax error: `string = "this is my string with " f'"{params.get("mine")}"'`
-                if !preferred_quote_style.is_preserve() || !supports_pep_701 {
+                if !supports_pep_701 {
                     return QuoteStyle::from(parent_flags.quote_style().opposite());
                 }
+
+                if !preferred_quote_style.is_preserve() {
+                    match self.context.options().f_string_consistent_quotes() {
+                        FStringConsistentQuotes::Disabled => {
+                            return QuoteStyle::from(parent_flags.quote_style().opposite());
+                        }
+                        // When f_string_consistent_quotes is enabled AND we're targeting Python 3.12+,
+                        // use the preferred quote style consistently.
+                        FStringConsistentQuotes::Enabled => {
+                            return QuoteStyle::from(parent_flags.quote_style());
+                        }
+                    }
+                }
             }
         }
 

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:?}
f-string-consistent-quotes = {f_string_consistent_quotes:?}"#,
indent_style = self.0.indent_style(),
indent_width = self.0.indent_width().value(),
line_width = self.0.line_width().value(),
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(),
f_string_consistent_quotes = self.0.f_string_consistent_quotes()
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
assertion_line: 267
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/blank_line_before_class_docstring.py
---
## Input
Expand Down Expand Up @@ -58,6 +59,7 @@ docstring-code-line-width = "dynamic"
preview = Enabled
target_version = 3.10
source_type = Python
f-string-consistent-quotes = Disabled
```

```python
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
assertion_line: 267
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring.py
---
## Input
Expand Down Expand Up @@ -177,6 +178,7 @@ docstring-code-line-width = "dynamic"
preview = Disabled
target_version = 3.10
source_type = Python
f-string-consistent-quotes = Disabled
```

```python
Expand Down Expand Up @@ -353,6 +355,7 @@ docstring-code-line-width = "dynamic"
preview = Disabled
target_version = 3.10
source_type = Python
f-string-consistent-quotes = Disabled
```

```python
Expand Down Expand Up @@ -529,6 +532,7 @@ docstring-code-line-width = "dynamic"
preview = Disabled
target_version = 3.10
source_type = Python
f-string-consistent-quotes = Disabled
```

```python
Expand Down Expand Up @@ -705,6 +709,7 @@ docstring-code-line-width = "dynamic"
preview = Disabled
target_version = 3.10
source_type = Python
f-string-consistent-quotes = Disabled
```

```python
Expand Down Expand Up @@ -881,6 +886,7 @@ docstring-code-line-width = "dynamic"
preview = Disabled
target_version = 3.10
source_type = Python
f-string-consistent-quotes = Disabled
```

```python
Expand Down
Loading