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 @@ -382,6 +382,7 @@ formatter.line_ending = auto
formatter.indent_style = space
formatter.indent_width = 4
formatter.quote_style = double
formatter.f_string_consistent_quotes = disabled
formatter.magic_trailing_comma = respect
formatter.docstring_code_format = disabled
formatter.docstring_code_line_width = dynamic
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 @@ -14,8 +14,8 @@ use crate::comments::{
};
pub use crate::context::PyFormatContext;
pub use crate::options::{
DocstringCode, DocstringCodeLineWidth, MagicTrailingComma, PreviewMode, PyFormatOptions,
QuoteStyle,
DocstringCode, DocstringCodeLineWidth, FStringConsistentQuotes, MagicTrailingComma,
PreviewMode, PyFormatOptions, QuoteStyle,
};
use crate::range::is_logical_line;
pub use crate::shared_traits::{AsFormat, FormattedIter, FormattedIterExt, IntoFormat};
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 @@ -347,6 +368,31 @@ impl fmt::Display for PreviewMode {
}
}

#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, CacheKey)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum FStringConsistentQuotes {
#[default]
Disabled,
Enabled,
}

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

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

#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, CacheKey)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
Expand Down
16 changes: 14 additions & 2 deletions crates/ruff_python_formatter/src/string/normalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,28 @@ impl<'a, 'src> StringNormalizer<'a, 'src> {
.unwrap_or(self.context.options().quote_style());
let supports_pep_701 = self.context.options().target_version().supports_pep_701();

// For f-strings prefer alternating the quotes unless The outer string is triple quoted and the inner isn't.
// For f-strings in Python 3.12+, use consistent quotes if enabled.
// Otherwise, alternate quotes for compatibility with older Python versions.
if let FStringState::InsideExpressionElement(parent_context) = self.context.f_string_state()
{
let parent_flags = parent_context.f_string().flags();
let consistent_quotes = self
.context
.options()
.f_string_consistent_quotes()
.is_enabled();

if !parent_flags.is_triple_quoted() || string.flags().is_triple_quoted() {
// When f_string_consistent_quotes is enabled AND we're targeting Python 3.12+,
// use the preferred quote style consistently
if supports_pep_701 && consistent_quotes && !preferred_quote_style.is_preserve() {
return preferred_quote_style;
}
// Otherwise, use alternating quotes for compatibility
// This logic is even necessary when using preserve and the target python version doesn't support PEP701 because
// we might end up joining two f-strings that have different quote styles, in which case we need to alternate the quotes
// for inner strings to avoid a syntax error: `string = "this is my string with " f'"{params.get("mine")}"'`
if !preferred_quote_style.is_preserve() || !supports_pep_701 {
else if !preferred_quote_style.is_preserve() || !supports_pep_701 {
return QuoteStyle::from(parent_flags.quote_style().opposite());
}
}
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 @@ -477,7 +477,8 @@ docstring-code = {docstring_code:?}
docstring-code-line-width = {docstring_code_line_width:?}
preview = {preview:?}
target_version = {target_version}
source_type = {source_type:?}"#,
source_type = {source_type:?}
f-string-consistent-quotes = {f_string_consistent_quotes:?}"#,
indent_style = self.0.indent_style(),
indent_width = self.0.indent_width().value(),
line_width = self.0.line_width().value(),
Expand All @@ -488,7 +489,8 @@ source_type = {source_type:?}"#,
docstring_code_line_width = self.0.docstring_code_line_width(),
preview = self.0.preview(),
target_version = self.0.target_version(),
source_type = self.0.source_type()
source_type = self.0.source_type(),
f_string_consistent_quotes = self.0.f_string_consistent_quotes()
)
}
}
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.9
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.9
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.9
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.9
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.9
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.9
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_code_examples.py
---
## Input
Expand Down Expand Up @@ -1370,6 +1371,7 @@ docstring-code-line-width = "dynamic"
preview = Disabled
target_version = 3.9
source_type = Python
f-string-consistent-quotes = Disabled
```

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

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

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

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

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

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

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

```python
Expand Down Expand Up @@ -12327,6 +12336,7 @@ docstring-code-line-width = 60
preview = Disabled
target_version = 3.9
source_type = Python
f-string-consistent-quotes = Disabled
```

```python
Expand Down Expand Up @@ -13701,6 +13711,7 @@ docstring-code-line-width = "dynamic"
preview = Disabled
target_version = 3.9
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_code_examples_crlf.py
---
## Input
Expand Down Expand Up @@ -29,6 +30,7 @@ docstring-code-line-width = "dynamic"
preview = Disabled
target_version = 3.9
source_type = Python
f-string-consistent-quotes = Disabled
```

```python
Expand Down
Loading
Loading