diff --git a/crates/ruff_python_formatter/src/expression/string.rs b/crates/ruff_python_formatter/src/expression/string.rs index 3bd7bdb12982b..ae220f0c4014b 100644 --- a/crates/ruff_python_formatter/src/expression/string.rs +++ b/crates/ruff_python_formatter/src/expression/string.rs @@ -323,24 +323,32 @@ impl StringPart { self, quoting: Quoting, locator: &'a Locator, - quote_style: QuoteStyle, + configured_style: QuoteStyle, ) -> NormalizedString<'a> { + // Per PEP 8 and PEP 257, always prefer double quotes for docstrings and triple-quoted + // strings. (We assume docstrings are always triple-quoted.) + let preferred_style = if self.quotes.triple { + QuoteStyle::Double + } else { + configured_style + }; + let raw_content = locator.slice(self.content_range); - let preferred_quotes = match quoting { + let quotes = match quoting { Quoting::Preserve => self.quotes, Quoting::CanChange => { if self.prefix.is_raw_string() { - preferred_quotes_raw(raw_content, self.quotes, quote_style) + choose_quotes_raw(raw_content, self.quotes, preferred_style) } else { - preferred_quotes(raw_content, self.quotes, quote_style) + choose_quotes(raw_content, self.quotes, preferred_style) } } }; let normalized = normalize_string( locator.slice(self.content_range), - preferred_quotes, + quotes, self.prefix.is_raw_string(), ); @@ -348,7 +356,7 @@ impl StringPart { prefix: self.prefix, content_range: self.content_range, text: normalized, - quotes: preferred_quotes, + quotes, } } } @@ -460,16 +468,17 @@ impl Format> for StringPrefix { } } -/// Detects the preferred quotes for raw string `input`. -/// The configured quote style is preferred unless `input` contains unescaped quotes of the -/// configured style. For example, `r"foo"` is preferred over `r'foo'` if the configured -/// quote style is double quotes. -fn preferred_quotes_raw( +/// Choose the appropriate quote style for a raw string. +/// +/// The preferred quote style is chosen unless the string contains unescaped quotes of the +/// preferred style. For example, `r"foo"` is chosen over `r'foo'` if the preferred quote +/// style is double quotes. +fn choose_quotes_raw( input: &str, quotes: StringQuotes, - configured_style: QuoteStyle, + preferred_style: QuoteStyle, ) -> StringQuotes { - let configured_quote_char = configured_style.as_char(); + let preferred_quote_char = preferred_style.as_char(); let mut chars = input.chars().peekable(); let contains_unescaped_configured_quotes = loop { match chars.next() { @@ -478,7 +487,7 @@ fn preferred_quotes_raw( chars.next(); } // `"` or `'` - Some(c) if c == configured_quote_char => { + Some(c) if c == preferred_quote_char => { if !quotes.triple { break true; } @@ -487,13 +496,13 @@ fn preferred_quotes_raw( // We can't turn `r'''\""'''` into `r"""\"""""`, this would confuse the parser // about where the closing triple quotes start None => break true, - Some(next) if *next == configured_quote_char => { + Some(next) if *next == preferred_quote_char => { // `""` or `''` chars.next(); // We can't turn `r'''""'''` into `r""""""""`, nor can we have // `"""` or `'''` respectively inside the string - if chars.peek().is_none() || chars.peek() == Some(&configured_quote_char) { + if chars.peek().is_none() || chars.peek() == Some(&preferred_quote_char) { break true; } } @@ -510,26 +519,27 @@ fn preferred_quotes_raw( style: if contains_unescaped_configured_quotes { quotes.style } else { - configured_style + preferred_style }, } } -/// Detects the preferred quotes for `input`. -/// * single quoted strings: The preferred quote style is the one that requires less escape sequences. -/// * triple quoted strings: Use double quotes except the string contains a sequence of `"""`. -fn preferred_quotes( - input: &str, - quotes: StringQuotes, - configured_style: QuoteStyle, -) -> StringQuotes { - let preferred_style = if quotes.triple { +/// Choose the appropriate quote style for a string. +/// +/// For single quoted strings, the preferred quote style is used, unless the alternative quote style +/// would require fewer escapes. +/// +/// For triple quoted strings, the preferred quote style is always used, unless the string contains +/// a triplet of the quote character (e.g., if double quotes are preferred, double quotes will be +/// used unless the string contains `"""`). +fn choose_quotes(input: &str, quotes: StringQuotes, preferred_style: QuoteStyle) -> StringQuotes { + let style = if quotes.triple { // True if the string contains a triple quote sequence of the configured quote style. let mut uses_triple_quotes = false; let mut chars = input.chars().peekable(); while let Some(c) = chars.next() { - let configured_quote_char = configured_style.as_char(); + let preferred_quote_char = preferred_style.as_char(); match c { '\\' => { if matches!(chars.peek(), Some('"' | '\\')) { @@ -537,14 +547,14 @@ fn preferred_quotes( } } // `"` or `'` - c if c == configured_quote_char => { + c if c == preferred_quote_char => { match chars.peek().copied() { - Some(c) if c == configured_quote_char => { + Some(c) if c == preferred_quote_char => { // `""` or `''` chars.next(); match chars.peek().copied() { - Some(c) if c == configured_quote_char => { + Some(c) if c == preferred_quote_char => { // `"""` or `'''` chars.next(); uses_triple_quotes = true; @@ -579,7 +589,7 @@ fn preferred_quotes( // Keep the existing quote style. quotes.style } else { - configured_style + preferred_style } } else { let mut single_quotes = 0u32; @@ -599,7 +609,7 @@ fn preferred_quotes( } } - match configured_style { + match preferred_style { QuoteStyle::Single => { if single_quotes > double_quotes { QuoteStyle::Double @@ -619,7 +629,7 @@ fn preferred_quotes( StringQuotes { triple: quotes.triple, - style: preferred_style, + style, } } @@ -668,7 +678,7 @@ impl Format> for StringQuotes { } /// Adds the necessary quote escapes and removes unnecessary escape sequences when quoting `input` -/// with the provided `style`. +/// with the provided [`StringQuotes`] style. /// /// Returns the normalized string and whether it contains new lines. fn normalize_string(input: &str, quotes: StringQuotes, is_raw: bool) -> Cow { 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 3b07b0c15d0df..158e4f65c6d13 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 @@ -320,17 +320,17 @@ if True: b'This string will not include \ backslashes or newline characters.' -b'''Multiline +b"""Multiline String \" -''' +""" -b'''Multiline +b"""Multiline String \' -''' +""" -b'''Multiline +b"""Multiline String "" -''' +""" b'''Multiline String """ @@ -346,9 +346,9 @@ String ''' b"""Multiline String '""" -b'''Multiline +b"""Multiline String \"\"\" -''' +""" # String continuation 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 9c7a8ba79dab6..28ebcfd8502dc 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 @@ -365,17 +365,17 @@ if True: 'This string will not include \ backslashes or newline characters.' -'''Multiline +"""Multiline String \" -''' +""" -'''Multiline +"""Multiline String \' -''' +""" -'''Multiline +"""Multiline String "" -''' +""" '''Multiline String """ @@ -391,9 +391,9 @@ String ''' """Multiline String '""" -'''Multiline +"""Multiline String \"\"\" -''' +""" # String continuation @@ -471,16 +471,16 @@ test_particular = [ # Regression test for https://github.com/astral-sh/ruff/issues/5893 x = ( - '''aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa''' - '''bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb''' + """aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa""" + """bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb""" ) x = ( - f'''aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa''' - f'''bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb''' + f"""aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa""" + f"""bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb""" ) x = ( - b'''aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa''' - b'''bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb''' + b"""aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa""" + b"""bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb""" ) # https://github.com/astral-sh/ruff/issues/7460 diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 6c2a38e9ca210..823095a429749 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -2461,7 +2461,7 @@ pub struct FormatOptions { default = "false", value_type = "bool", example = r#" - # Enable preview style formatting + # Enable preview style formatting. preview = true "# )] @@ -2469,34 +2469,40 @@ pub struct FormatOptions { /// Whether to use 4 spaces or hard tabs for indenting code. /// - /// Defaults to 4 spaces. We care about accessibility; if you do not need tabs for accessibility, we do not recommend you use them. + /// Defaults to 4 spaces. We care about accessibility; if you do not need tabs for + /// accessibility, we do not recommend you use them. #[option( default = "space", value_type = r#""space" | "tab""#, example = r#" - # Use tabs instead of 4 space indentation + # Use tabs instead of 4 space indentation. indent-style = "tab" "# )] pub indent_style: Option, - /// Whether to prefer single `'` or double `"` quotes for strings and docstrings. + /// Whether to prefer single `'` or double `"` quotes for strings. Defaults to double quotes. /// - /// Ruff may deviate from this option if using the configured quotes would require more escaped quotes: + /// In compliance with [PEP 8](https://peps.python.org/pep-0008/) and [PEP 257](https://peps.python.org/pep-0257/), + /// Ruff prefers double quotes for multiline strings and docstrings, regardless of the + /// configured quote style. + /// + /// Ruff may also deviate from this option if using the configured quotes would require + /// escaping quote characters within the string. For example, given: /// /// ```python - /// a = "It's monday morning" - /// b = "a string without any quotes" + /// a = "a string without any quotes" + /// b = "It's monday morning" /// ``` /// - /// Ruff leaves `a` unchanged when using `quote-style = "single"` because it is otherwise - /// necessary to escape the `'` which leads to less readable code: `'It\'s monday morning'`. - /// Ruff changes the quotes of `b` to use single quotes. + /// Ruff will change `a` to use single quotes when using `quote-style = "single"`. However, + /// `a` will be unchanged, as converting to single quotes would require the inner `'` to be + /// escaped, which leads to less readable code: `'It\'s monday morning'`. #[option( default = r#"double"#, value_type = r#""double" | "single""#, example = r#" - # Prefer single quotes over double quotes + # Prefer single quotes over double quotes. quote-style = "single" "# )] diff --git a/ruff.schema.json b/ruff.schema.json index 9aa7c51714389..a6c425c7fa333 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1207,7 +1207,7 @@ ] }, "quote-style": { - "description": "Whether to prefer single `'` or double `\"` quotes for strings and docstrings.\n\nRuff may deviate from this option if using the configured quotes would require more escaped quotes:\n\n```python a = \"It's monday morning\" b = \"a string without any quotes\" ```\n\nRuff leaves `a` unchanged when using `quote-style = \"single\"` because it is otherwise necessary to escape the `'` which leads to less readable code: `'It\\'s monday morning'`. Ruff changes the quotes of `b` to use single quotes.", + "description": "Whether to prefer single `'` or double `\"` quotes for strings. Defaults to double quotes.\n\nIn compliance with [PEP 8](https://peps.python.org/pep-0008/) and [PEP 257](https://peps.python.org/pep-0257/), Ruff prefers double quotes for multiline strings and docstrings, regardless of the configured quote style.\n\nRuff may also deviate from this option if using the configured quotes would require escaping quote characters within the string. For example, given:\n\n```python a = \"a string without any quotes\" b = \"It's monday morning\" ```\n\nRuff will change `a` to use single quotes when using `quote-style = \"single\"`. However, `a` will be unchanged, as converting to single quotes would require the inner `'` to be escaped, which leads to less readable code: `'It\\'s monday morning'`.", "anyOf": [ { "$ref": "#/definitions/QuoteStyle"