diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/quote_style.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/quote_style.options.json new file mode 100644 index 0000000000000..59431bf1c4874 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/quote_style.options.json @@ -0,0 +1,11 @@ +[ + { + "quote_style": "single" + }, + { + "quote_style": "double" + }, + { + "quote_style": "preserve" + } +] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/quote_style.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/quote_style.py new file mode 100644 index 0000000000000..8f0d159bebd4a --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/quote_style.py @@ -0,0 +1,50 @@ +'single' +"double" +r'r single' +r"r double" +f'f single' +f"f double" +fr'fr single' +fr"fr double" +rf'rf single' +rf"rf double" +b'b single' +b"b double" +rb'rb single' +rb"rb double" +br'br single' +br"br double" + +'''single triple''' +"""double triple""" +r'''r single triple''' +r"""r double triple""" +f'''f single triple''' +f"""f double triple""" +fr'''fr single triple''' +fr"""fr double triple""" +rf'''rf single triple''' +rf"""rf double triple""" +b'''b single triple''' +b"""b double triple""" +rb'''rb single triple''' +rb"""rb double triple""" +br'''br single triple''' +br"""br double triple""" + +'single1' 'single2' +'single1' "double2" +"double1" 'single2' +"double1" "double2" + +def docstring_single_triple(): + '''single triple''' + +def docstring_double_triple(): + """double triple""" + +def docstring_double(): + "double triple" + +def docstring_single(): + 'single' diff --git a/crates/ruff_python_formatter/src/context.rs b/crates/ruff_python_formatter/src/context.rs index 2e6cdc0d2ed51..ac5dea3710017 100644 --- a/crates/ruff_python_formatter/src/context.rs +++ b/crates/ruff_python_formatter/src/context.rs @@ -1,5 +1,6 @@ use crate::comments::Comments; -use crate::{PyFormatOptions, QuoteStyle}; +use crate::expression::string::QuoteChar; +use crate::PyFormatOptions; use ruff_formatter::{Buffer, FormatContext, GroupId, SourceCode}; use ruff_source_file::Locator; use std::fmt::{Debug, Formatter}; @@ -12,14 +13,14 @@ pub struct PyFormatContext<'a> { comments: Comments<'a>, node_level: NodeLevel, /// Set to a non-None value when the formatter is running on a code - /// snippet within a docstring. The value should be the quote style of the + /// snippet within a docstring. The value should be the quote character of the /// docstring containing the code snippet. /// /// Various parts of the formatter may inspect this state to change how it /// works. For example, multi-line strings will always be written with a /// quote style that is inverted from the one here in order to ensure that /// the formatted Python code will be valid. - docstring: Option, + docstring: Option, } impl<'a> PyFormatContext<'a> { @@ -57,20 +58,20 @@ impl<'a> PyFormatContext<'a> { /// Returns a non-None value only if the formatter is running on a code /// snippet within a docstring. /// - /// The quote style returned corresponds to the quoting used for the + /// The quote character returned corresponds to the quoting used for the /// docstring containing the code snippet currently being formatted. - pub(crate) fn docstring(&self) -> Option { + pub(crate) fn docstring(&self) -> Option { self.docstring } /// Return a new context suitable for formatting code snippets within a /// docstring. /// - /// The quote style given should correspond to the style of quoting used + /// The quote character given should correspond to the quote character used /// for the docstring containing the code snippets. - pub(crate) fn in_docstring(self, style: QuoteStyle) -> PyFormatContext<'a> { + pub(crate) fn in_docstring(self, quote: QuoteChar) -> PyFormatContext<'a> { PyFormatContext { - docstring: Some(style), + docstring: Some(quote), ..self } } diff --git a/crates/ruff_python_formatter/src/expression/string/docstring.rs b/crates/ruff_python_formatter/src/expression/string/docstring.rs index 41f350bd4deb2..dcbbbbc18e6e5 100644 --- a/crates/ruff_python_formatter/src/expression/string/docstring.rs +++ b/crates/ruff_python_formatter/src/expression/string/docstring.rs @@ -13,9 +13,9 @@ use { ruff_text_size::{Ranged, TextLen, TextRange, TextSize}, }; -use crate::{prelude::*, FormatModuleError, QuoteStyle}; +use crate::{prelude::*, FormatModuleError}; -use super::NormalizedString; +use super::{NormalizedString, QuoteChar}; /// Format a docstring by trimming whitespace and adjusting the indentation. /// @@ -139,7 +139,7 @@ pub(super) fn format(normalized: &NormalizedString, f: &mut PyFormatter) -> Form // Edge case: The first line is `""" "content`, so we need to insert chaperone space that keep // inner quotes and closing quotes from getting to close to avoid `""""content` - if trim_both.starts_with(normalized.quotes.style.as_char()) { + if trim_both.starts_with(normalized.quotes.quote_char.as_char()) { space().fmt(f)?; } @@ -192,7 +192,7 @@ pub(super) fn format(normalized: &NormalizedString, f: &mut PyFormatter) -> Form offset, stripped_indentation_length, already_normalized, - quote_style: normalized.quotes.style, + quote_char: normalized.quotes.quote_char, code_example: CodeExample::default(), } .add_iter(lines)?; @@ -250,8 +250,8 @@ struct DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> { /// is, the formatter can take a fast path. already_normalized: bool, - /// The quote style used by the docstring being printed. - quote_style: QuoteStyle, + /// The quote character used by the docstring being printed. + quote_char: QuoteChar, /// The current code example detected in the docstring. code_example: CodeExample<'src>, @@ -466,7 +466,7 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> { // instead of later, and as a result, get more consistent // results. .with_indent_style(IndentStyle::Space); - let printed = match docstring_format_source(options, self.quote_style, &codeblob) { + let printed = match docstring_format_source(options, self.quote_char, &codeblob) { Ok(printed) => printed, Err(FormatModuleError::FormatError(err)) => return Err(err), Err( @@ -488,9 +488,11 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> { // a docstring. As we fix corner cases over time, we can perhaps // remove this check. See the `doctest_invalid_skipped` tests in // `docstring_code_examples.py` for when this check is relevant. - let wrapped = match self.quote_style { - QuoteStyle::Single => std::format!("'''{}'''", printed.as_code()), - QuoteStyle::Double => std::format!(r#""""{}""""#, printed.as_code()), + let wrapped = match self.quote_char { + QuoteChar::Single => std::format!("'''{}'''", printed.as_code()), + QuoteChar::Double => { + std::format!(r#""""{}""""#, printed.as_code()) + } }; let result = ruff_python_parser::parse( &wrapped, @@ -1231,7 +1233,7 @@ enum CodeExampleAddAction<'src> { /// inside of a docstring. fn docstring_format_source( options: crate::PyFormatOptions, - docstring_quote_style: QuoteStyle, + docstring_quote_style: QuoteChar, source: &str, ) -> Result { use ruff_python_parser::AsMode; @@ -1258,7 +1260,7 @@ fn docstring_format_source( /// that avoids `content""""` and `content\"""`. This does only applies to un-escaped backslashes, /// so `content\\ """` doesn't need a space while `content\\\ """` does. fn needs_chaperone_space(normalized: &NormalizedString, trim_end: &str) -> bool { - trim_end.ends_with(normalized.quotes.style.as_char()) + trim_end.ends_with(normalized.quotes.quote_char.as_char()) || trim_end.chars().rev().take_while(|c| *c == '\\').count() % 2 == 1 } diff --git a/crates/ruff_python_formatter/src/expression/string/mod.rs b/crates/ruff_python_formatter/src/expression/string/mod.rs index c7e896daf2c90..6469a1ef5d212 100644 --- a/crates/ruff_python_formatter/src/expression/string/mod.rs +++ b/crates/ruff_python_formatter/src/expression/string/mod.rs @@ -322,7 +322,7 @@ impl StringPart { quoting: Quoting, locator: &'a Locator, configured_style: QuoteStyle, - parent_docstring_quote_style: Option, + parent_docstring_quote_char: Option, ) -> NormalizedString<'a> { // Per PEP 8, always prefer double quotes for triple-quoted strings. let preferred_style = if self.quotes.triple { @@ -371,8 +371,8 @@ impl StringPart { // Overall this is a bit of a corner case and just inverting the // style from what the parent ultimately decided upon works, even // if it doesn't have perfect alignment with PEP8. - if let Some(style) = parent_docstring_quote_style { - style.invert() + if let Some(quote) = parent_docstring_quote_char { + QuoteStyle::from(quote.invert()) } else { QuoteStyle::Double } @@ -385,10 +385,14 @@ impl StringPart { let quotes = match quoting { Quoting::Preserve => self.quotes, Quoting::CanChange => { - if self.prefix.is_raw_string() { - choose_quotes_raw(raw_content, self.quotes, preferred_style) + if let Some(preferred_quote) = QuoteChar::from_style(preferred_style) { + if self.prefix.is_raw_string() { + choose_quotes_raw(raw_content, self.quotes, preferred_quote) + } else { + choose_quotes(raw_content, self.quotes, preferred_quote) + } } else { - choose_quotes(raw_content, self.quotes, preferred_style) + self.quotes } } }; @@ -523,9 +527,9 @@ impl Format> for StringPrefix { fn choose_quotes_raw( input: &str, quotes: StringQuotes, - preferred_style: QuoteStyle, + preferred_quote: QuoteChar, ) -> StringQuotes { - let preferred_quote_char = preferred_style.as_char(); + let preferred_quote_char = preferred_quote.as_char(); let mut chars = input.chars().peekable(); let contains_unescaped_configured_quotes = loop { match chars.next() { @@ -563,10 +567,10 @@ fn choose_quotes_raw( StringQuotes { triple: quotes.triple, - style: if contains_unescaped_configured_quotes { - quotes.style + quote_char: if contains_unescaped_configured_quotes { + quotes.quote_char } else { - preferred_style + preferred_quote }, } } @@ -579,14 +583,14 @@ fn choose_quotes_raw( /// 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 { +fn choose_quotes(input: &str, quotes: StringQuotes, preferred_quote: QuoteChar) -> StringQuotes { + let quote = 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 preferred_quote_char = preferred_style.as_char(); + let preferred_quote_char = preferred_quote.as_char(); match c { '\\' => { if matches!(chars.peek(), Some('"' | '\\')) { @@ -634,9 +638,9 @@ fn choose_quotes(input: &str, quotes: StringQuotes, preferred_style: QuoteStyle) if uses_triple_quotes { // String contains a triple quote sequence of the configured quote style. // Keep the existing quote style. - quotes.style + quotes.quote_char } else { - preferred_style + preferred_quote } } else { let mut single_quotes = 0u32; @@ -656,19 +660,19 @@ fn choose_quotes(input: &str, quotes: StringQuotes, preferred_style: QuoteStyle) } } - match preferred_style { - QuoteStyle::Single => { + match preferred_quote { + QuoteChar::Single => { if single_quotes > double_quotes { - QuoteStyle::Double + QuoteChar::Double } else { - QuoteStyle::Single + QuoteChar::Single } } - QuoteStyle::Double => { + QuoteChar::Double => { if double_quotes > single_quotes { - QuoteStyle::Single + QuoteChar::Single } else { - QuoteStyle::Double + QuoteChar::Double } } } @@ -676,14 +680,14 @@ fn choose_quotes(input: &str, quotes: StringQuotes, preferred_style: QuoteStyle) StringQuotes { triple: quotes.triple, - style, + quote_char: quote, } } #[derive(Copy, Clone, Debug)] pub(super) struct StringQuotes { triple: bool, - style: QuoteStyle, + quote_char: QuoteChar, } impl StringQuotes { @@ -691,11 +695,14 @@ impl StringQuotes { let mut chars = input.chars(); let quote_char = chars.next()?; - let style = QuoteStyle::try_from(quote_char).ok()?; + let quote = QuoteChar::try_from(quote_char).ok()?; let triple = chars.next() == Some(quote_char) && chars.next() == Some(quote_char); - Some(Self { triple, style }) + Some(Self { + triple, + quote_char: quote, + }) } pub(super) const fn is_triple(self) -> bool { @@ -713,17 +720,74 @@ impl StringQuotes { impl Format> for StringQuotes { fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { - let quotes = match (self.style, self.triple) { - (QuoteStyle::Single, false) => "'", - (QuoteStyle::Single, true) => "'''", - (QuoteStyle::Double, false) => "\"", - (QuoteStyle::Double, true) => "\"\"\"", + let quotes = match (self.quote_char, self.triple) { + (QuoteChar::Single, false) => "'", + (QuoteChar::Single, true) => "'''", + (QuoteChar::Double, false) => "\"", + (QuoteChar::Double, true) => "\"\"\"", }; token(quotes).fmt(f) } } +/// The quotation character used to quote a string, byte, or fstring literal. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum QuoteChar { + /// A single quote: `'` + Single, + + /// A double quote: '"' + Double, +} + +impl QuoteChar { + pub const fn as_char(self) -> char { + match self { + QuoteChar::Single => '\'', + QuoteChar::Double => '"', + } + } + + #[must_use] + pub const fn invert(self) -> QuoteChar { + match self { + QuoteChar::Single => QuoteChar::Double, + QuoteChar::Double => QuoteChar::Single, + } + } + + #[must_use] + pub const fn from_style(style: QuoteStyle) -> Option { + match style { + QuoteStyle::Single => Some(QuoteChar::Single), + QuoteStyle::Double => Some(QuoteChar::Double), + QuoteStyle::Preserve => None, + } + } +} + +impl From for QuoteStyle { + fn from(value: QuoteChar) -> Self { + match value { + QuoteChar::Single => QuoteStyle::Single, + QuoteChar::Double => QuoteStyle::Double, + } + } +} + +impl TryFrom for QuoteChar { + type Error = (); + + fn try_from(value: char) -> Result { + match value { + '\'' => Ok(QuoteChar::Single), + '"' => Ok(QuoteChar::Double), + _ => Err(()), + } + } +} + /// Adds the necessary quote escapes and removes unnecessary escape sequences when quoting `input` /// with the provided [`StringQuotes`] style. /// @@ -736,9 +800,9 @@ fn normalize_string(input: &str, quotes: StringQuotes, prefix: StringPrefix) -> // If `last_index` is `0` at the end, then the input is already normalized and can be returned as is. let mut last_index = 0; - let style = quotes.style; - let preferred_quote = style.as_char(); - let opposite_quote = style.invert().as_char(); + let quote = quotes.quote_char; + let preferred_quote = quote.as_char(); + let opposite_quote = quote.invert().as_char(); let mut chars = input.char_indices().peekable(); diff --git a/crates/ruff_python_formatter/src/options.rs b/crates/ruff_python_formatter/src/options.rs index a07fabbb9a795..8ff979959cfe5 100644 --- a/crates/ruff_python_formatter/src/options.rs +++ b/crates/ruff_python_formatter/src/options.rs @@ -207,35 +207,7 @@ pub enum QuoteStyle { Single, #[default] Double, -} - -impl QuoteStyle { - pub const fn as_char(self) -> char { - match self { - QuoteStyle::Single => '\'', - QuoteStyle::Double => '"', - } - } - - #[must_use] - pub const fn invert(self) -> QuoteStyle { - match self { - QuoteStyle::Single => QuoteStyle::Double, - QuoteStyle::Double => QuoteStyle::Single, - } - } -} - -impl TryFrom for QuoteStyle { - type Error = (); - - fn try_from(value: char) -> std::result::Result { - match value { - '\'' => Ok(QuoteStyle::Single), - '"' => Ok(QuoteStyle::Double), - _ => Err(()), - } - } + Preserve, } impl FromStr for QuoteStyle { @@ -245,6 +217,7 @@ impl FromStr for QuoteStyle { match s { "\"" | "double" | "Double" => Ok(Self::Double), "'" | "single" | "Single" => Ok(Self::Single), + "preserve" | "Preserve" => Ok(Self::Preserve), // TODO: replace this error with a diagnostic _ => Err("Value not supported for QuoteStyle"), } 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 new file mode 100644 index 0000000000000..b78c7666e9636 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@quote_style.py.snap @@ -0,0 +1,270 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/quote_style.py +--- +## Input +```python +'single' +"double" +r'r single' +r"r double" +f'f single' +f"f double" +fr'fr single' +fr"fr double" +rf'rf single' +rf"rf double" +b'b single' +b"b double" +rb'rb single' +rb"rb double" +br'br single' +br"br double" + +'''single triple''' +"""double triple""" +r'''r single triple''' +r"""r double triple""" +f'''f single triple''' +f"""f double triple""" +fr'''fr single triple''' +fr"""fr double triple""" +rf'''rf single triple''' +rf"""rf double triple""" +b'''b single triple''' +b"""b double triple""" +rb'''rb single triple''' +rb"""rb double triple""" +br'''br single triple''' +br"""br double triple""" + +'single1' 'single2' +'single1' "double2" +"double1" 'single2' +"double1" "double2" + +def docstring_single_triple(): + '''single triple''' + +def docstring_double_triple(): + """double triple""" + +def docstring_double(): + "double triple" + +def docstring_single(): + 'single' +``` + +## Outputs +### Output 1 +``` +indent-style = space +line-width = 88 +indent-width = 4 +quote-style = Single +line-ending = LineFeed +magic-trailing-comma = Respect +docstring-code = Disabled +preview = Disabled +``` + +```python +'single' +'double' +r'r single' +r'r double' +f'f single' +f'f double' +rf'fr single' +rf'fr double' +rf'rf single' +rf'rf double' +b'b single' +b'b double' +rb'rb single' +rb'rb double' +rb'br single' +rb'br double' + +"""single triple""" +"""double triple""" +r"""r single triple""" +r"""r double triple""" +f"""f single triple""" +f"""f double triple""" +rf"""fr single triple""" +rf"""fr double triple""" +rf"""rf single triple""" +rf"""rf double triple""" +b"""b single triple""" +b"""b double triple""" +rb"""rb single triple""" +rb"""rb double triple""" +rb"""br single triple""" +rb"""br double triple""" + +'single1' 'single2' +'single1' 'double2' +'double1' 'single2' +'double1' 'double2' + + +def docstring_single_triple(): + """single triple""" + + +def docstring_double_triple(): + """double triple""" + + +def docstring_double(): + "double triple" + + +def docstring_single(): + "single" +``` + + +### Output 2 +``` +indent-style = space +line-width = 88 +indent-width = 4 +quote-style = Double +line-ending = LineFeed +magic-trailing-comma = Respect +docstring-code = Disabled +preview = Disabled +``` + +```python +"single" +"double" +r"r single" +r"r double" +f"f single" +f"f double" +rf"fr single" +rf"fr double" +rf"rf single" +rf"rf double" +b"b single" +b"b double" +rb"rb single" +rb"rb double" +rb"br single" +rb"br double" + +"""single triple""" +"""double triple""" +r"""r single triple""" +r"""r double triple""" +f"""f single triple""" +f"""f double triple""" +rf"""fr single triple""" +rf"""fr double triple""" +rf"""rf single triple""" +rf"""rf double triple""" +b"""b single triple""" +b"""b double triple""" +rb"""rb single triple""" +rb"""rb double triple""" +rb"""br single triple""" +rb"""br double triple""" + +"single1" "single2" +"single1" "double2" +"double1" "single2" +"double1" "double2" + + +def docstring_single_triple(): + """single triple""" + + +def docstring_double_triple(): + """double triple""" + + +def docstring_double(): + "double triple" + + +def docstring_single(): + "single" +``` + + +### Output 3 +``` +indent-style = space +line-width = 88 +indent-width = 4 +quote-style = Preserve +line-ending = LineFeed +magic-trailing-comma = Respect +docstring-code = Disabled +preview = Disabled +``` + +```python +'single' +"double" +r'r single' +r"r double" +f'f single' +f"f double" +rf'fr single' +rf"fr double" +rf'rf single' +rf"rf double" +b'b single' +b"b double" +rb'rb single' +rb"rb double" +rb'br single' +rb"br double" + +"""single triple""" +"""double triple""" +r"""r single triple""" +r"""r double triple""" +f"""f single triple""" +f"""f double triple""" +rf"""fr single triple""" +rf"""fr double triple""" +rf"""rf single triple""" +rf"""rf double triple""" +b"""b single triple""" +b"""b double triple""" +rb"""rb single triple""" +rb"""rb double triple""" +rb"""br single triple""" +rb"""br double triple""" + +'single1' 'single2' +'single1' "double2" +"double1" 'single2' +"double1" "double2" + + +def docstring_single_triple(): + """single triple""" + + +def docstring_double_triple(): + """double triple""" + + +def docstring_double(): + "double triple" + + +def docstring_single(): + "single" +``` + + + diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index 4c38249b0e5dc..6b3e2abbc45a1 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -158,12 +158,21 @@ impl Configuration { let format = self.format; let format_defaults = FormatterSettings::default(); + let quote_style = format.quote_style.unwrap_or(format_defaults.quote_style); + let format_preview = match format.preview.unwrap_or(global_preview) { + PreviewMode::Disabled => ruff_python_formatter::PreviewMode::Disabled, + PreviewMode::Enabled => ruff_python_formatter::PreviewMode::Enabled, + }; + + if quote_style == QuoteStyle::Preserve && !format_preview.is_enabled() { + return Err(anyhow!( + "'quote-style = preserve' is a preview only feature. Run with '--preview' to enable it." + )); + } + let formatter = FormatterSettings { exclude: FilePatternSet::try_from_iter(format.exclude.unwrap_or_default())?, - preview: match format.preview.unwrap_or(global_preview) { - PreviewMode::Disabled => ruff_python_formatter::PreviewMode::Disabled, - PreviewMode::Enabled => ruff_python_formatter::PreviewMode::Enabled, - }, + preview: format_preview, line_width: self .line_length .map_or(format_defaults.line_width, |length| { @@ -176,7 +185,7 @@ impl Configuration { .map_or(format_defaults.indent_width, |tab_size| { ruff_formatter::IndentWidth::from(NonZeroU8::from(tab_size)) }), - quote_style: format.quote_style.unwrap_or(format_defaults.quote_style), + quote_style, magic_trailing_comma: format .magic_trailing_comma .unwrap_or(format_defaults.magic_trailing_comma), diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 442b8d7d2e16d..065e8b77af86f 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -2812,13 +2812,18 @@ pub struct FormatOptions { )] pub indent_style: Option, - /// Whether to prefer single `'` or double `"` quotes for strings. Defaults to double quotes. + /// Configures the preferred quote character for strings. Valid options are: + /// + /// * `double` (default): Use double quotes `"` + /// * `single`: Use single quotes `'` + /// * `preserve` (preview only): Keeps the existing quote character. We don't recommend using this option except for projects + /// that already use a mixture of single and double quotes and can't migrate to using double or single 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 + /// Ruff may also deviate from using the configured quotes if doing so requires /// escaping quote characters within the string. For example, given: /// /// ```python @@ -2827,11 +2832,11 @@ pub struct FormatOptions { /// ``` /// /// Ruff will change `a` to use single quotes when using `quote-style = "single"`. However, - /// `b` 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'`. + /// `b` remains unchanged, as converting to single quotes requires escaping the inner `'`, + /// which leads to less readable code: `'It\'s monday morning'`. This does not apply when using `preserve`. #[option( default = r#"double"#, - value_type = r#""double" | "single""#, + value_type = r#""double" | "single" | "preserve""#, example = r#" # Prefer single quotes over double quotes. quote-style = "single" diff --git a/ruff.schema.json b/ruff.schema.json index 3bd21232a2551..90228a59020d0 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1281,7 +1281,7 @@ ] }, "quote-style": { - "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, `b` 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'`.", + "description": "Configures the preferred quote character for strings. Valid options are:\n\n* `double` (default): Use double quotes `\"` * `single`: Use single quotes `'` * `preserve` (preview only): Keeps the existing quote character. We don't recommend using this option except for projects that already use a mixture of single and double quotes and can't migrate to using double or single 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 using the configured quotes if doing so requires 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, `b` remains unchanged, as converting to single quotes requires escaping the inner `'`, which leads to less readable code: `'It\\'s monday morning'`. This does not apply when using `preserve`.", "anyOf": [ { "$ref": "#/definitions/QuoteStyle" @@ -2441,7 +2441,8 @@ "type": "string", "enum": [ "single", - "double" + "double", + "preserve" ] }, "RelativeImportsOrder": {