mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-03 05:03:33 +00:00
Always prefer double quotes for docstrings and triple-quoted srings (#7680)
## Summary At present, `quote-style` is used universally. However, [PEP 8](https://peps.python.org/pep-0008/) and [PEP 257](https://peps.python.org/pep-0257/) suggest that while either single or double quotes are acceptable in general (as long as they're consistent), docstrings and triple-quoted strings should always use double quotes. In our research, the vast majority of Ruff users that enable the `flake8-quotes` rules only enable them for inline strings (i.e., non-triple-quoted strings). Additionally, many Black forks (like Blue and Pyink) use double quotes for docstrings and triple-quoted strings. Our decision for now is to always prefer double quotes for triple-quoted strings (which should include docstrings). Based on feedback, we may consider adding additional options (e.g., a `"preserve"` mode, to avoid changing quotes; or a `"multiline-quote-style"` to override this). Closes https://github.com/astral-sh/ruff/issues/7615. ## Test Plan `cargo test`
This commit is contained in:
parent
f62b4c801f
commit
695dbbc539
5 changed files with 84 additions and 68 deletions
|
|
@ -323,24 +323,32 @@ impl StringPart {
|
||||||
self,
|
self,
|
||||||
quoting: Quoting,
|
quoting: Quoting,
|
||||||
locator: &'a Locator,
|
locator: &'a Locator,
|
||||||
quote_style: QuoteStyle,
|
configured_style: QuoteStyle,
|
||||||
) -> NormalizedString<'a> {
|
) -> 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 raw_content = locator.slice(self.content_range);
|
||||||
|
|
||||||
let preferred_quotes = match quoting {
|
let quotes = match quoting {
|
||||||
Quoting::Preserve => self.quotes,
|
Quoting::Preserve => self.quotes,
|
||||||
Quoting::CanChange => {
|
Quoting::CanChange => {
|
||||||
if self.prefix.is_raw_string() {
|
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 {
|
} else {
|
||||||
preferred_quotes(raw_content, self.quotes, quote_style)
|
choose_quotes(raw_content, self.quotes, preferred_style)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let normalized = normalize_string(
|
let normalized = normalize_string(
|
||||||
locator.slice(self.content_range),
|
locator.slice(self.content_range),
|
||||||
preferred_quotes,
|
quotes,
|
||||||
self.prefix.is_raw_string(),
|
self.prefix.is_raw_string(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -348,7 +356,7 @@ impl StringPart {
|
||||||
prefix: self.prefix,
|
prefix: self.prefix,
|
||||||
content_range: self.content_range,
|
content_range: self.content_range,
|
||||||
text: normalized,
|
text: normalized,
|
||||||
quotes: preferred_quotes,
|
quotes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -460,16 +468,17 @@ impl Format<PyFormatContext<'_>> for StringPrefix {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Detects the preferred quotes for raw string `input`.
|
/// Choose the appropriate quote style for a raw string.
|
||||||
/// 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
|
/// The preferred quote style is chosen unless the string contains unescaped quotes of the
|
||||||
/// quote style is double quotes.
|
/// preferred style. For example, `r"foo"` is chosen over `r'foo'` if the preferred quote
|
||||||
fn preferred_quotes_raw(
|
/// style is double quotes.
|
||||||
|
fn choose_quotes_raw(
|
||||||
input: &str,
|
input: &str,
|
||||||
quotes: StringQuotes,
|
quotes: StringQuotes,
|
||||||
configured_style: QuoteStyle,
|
preferred_style: QuoteStyle,
|
||||||
) -> StringQuotes {
|
) -> StringQuotes {
|
||||||
let configured_quote_char = configured_style.as_char();
|
let preferred_quote_char = preferred_style.as_char();
|
||||||
let mut chars = input.chars().peekable();
|
let mut chars = input.chars().peekable();
|
||||||
let contains_unescaped_configured_quotes = loop {
|
let contains_unescaped_configured_quotes = loop {
|
||||||
match chars.next() {
|
match chars.next() {
|
||||||
|
|
@ -478,7 +487,7 @@ fn preferred_quotes_raw(
|
||||||
chars.next();
|
chars.next();
|
||||||
}
|
}
|
||||||
// `"` or `'`
|
// `"` or `'`
|
||||||
Some(c) if c == configured_quote_char => {
|
Some(c) if c == preferred_quote_char => {
|
||||||
if !quotes.triple {
|
if !quotes.triple {
|
||||||
break true;
|
break true;
|
||||||
}
|
}
|
||||||
|
|
@ -487,13 +496,13 @@ fn preferred_quotes_raw(
|
||||||
// We can't turn `r'''\""'''` into `r"""\"""""`, this would confuse the parser
|
// We can't turn `r'''\""'''` into `r"""\"""""`, this would confuse the parser
|
||||||
// about where the closing triple quotes start
|
// about where the closing triple quotes start
|
||||||
None => break true,
|
None => break true,
|
||||||
Some(next) if *next == configured_quote_char => {
|
Some(next) if *next == preferred_quote_char => {
|
||||||
// `""` or `''`
|
// `""` or `''`
|
||||||
chars.next();
|
chars.next();
|
||||||
|
|
||||||
// We can't turn `r'''""'''` into `r""""""""`, nor can we have
|
// We can't turn `r'''""'''` into `r""""""""`, nor can we have
|
||||||
// `"""` or `'''` respectively inside the string
|
// `"""` 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;
|
break true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -510,26 +519,27 @@ fn preferred_quotes_raw(
|
||||||
style: if contains_unescaped_configured_quotes {
|
style: if contains_unescaped_configured_quotes {
|
||||||
quotes.style
|
quotes.style
|
||||||
} else {
|
} else {
|
||||||
configured_style
|
preferred_style
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Detects the preferred quotes for `input`.
|
/// Choose the appropriate quote style for a string.
|
||||||
/// * 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 `"""`.
|
/// For single quoted strings, the preferred quote style is used, unless the alternative quote style
|
||||||
fn preferred_quotes(
|
/// would require fewer escapes.
|
||||||
input: &str,
|
///
|
||||||
quotes: StringQuotes,
|
/// For triple quoted strings, the preferred quote style is always used, unless the string contains
|
||||||
configured_style: QuoteStyle,
|
/// a triplet of the quote character (e.g., if double quotes are preferred, double quotes will be
|
||||||
) -> StringQuotes {
|
/// used unless the string contains `"""`).
|
||||||
let preferred_style = if quotes.triple {
|
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.
|
// True if the string contains a triple quote sequence of the configured quote style.
|
||||||
let mut uses_triple_quotes = false;
|
let mut uses_triple_quotes = false;
|
||||||
let mut chars = input.chars().peekable();
|
let mut chars = input.chars().peekable();
|
||||||
|
|
||||||
while let Some(c) = chars.next() {
|
while let Some(c) = chars.next() {
|
||||||
let configured_quote_char = configured_style.as_char();
|
let preferred_quote_char = preferred_style.as_char();
|
||||||
match c {
|
match c {
|
||||||
'\\' => {
|
'\\' => {
|
||||||
if matches!(chars.peek(), Some('"' | '\\')) {
|
if matches!(chars.peek(), Some('"' | '\\')) {
|
||||||
|
|
@ -537,14 +547,14 @@ fn preferred_quotes(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// `"` or `'`
|
// `"` or `'`
|
||||||
c if c == configured_quote_char => {
|
c if c == preferred_quote_char => {
|
||||||
match chars.peek().copied() {
|
match chars.peek().copied() {
|
||||||
Some(c) if c == configured_quote_char => {
|
Some(c) if c == preferred_quote_char => {
|
||||||
// `""` or `''`
|
// `""` or `''`
|
||||||
chars.next();
|
chars.next();
|
||||||
|
|
||||||
match chars.peek().copied() {
|
match chars.peek().copied() {
|
||||||
Some(c) if c == configured_quote_char => {
|
Some(c) if c == preferred_quote_char => {
|
||||||
// `"""` or `'''`
|
// `"""` or `'''`
|
||||||
chars.next();
|
chars.next();
|
||||||
uses_triple_quotes = true;
|
uses_triple_quotes = true;
|
||||||
|
|
@ -579,7 +589,7 @@ fn preferred_quotes(
|
||||||
// Keep the existing quote style.
|
// Keep the existing quote style.
|
||||||
quotes.style
|
quotes.style
|
||||||
} else {
|
} else {
|
||||||
configured_style
|
preferred_style
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let mut single_quotes = 0u32;
|
let mut single_quotes = 0u32;
|
||||||
|
|
@ -599,7 +609,7 @@ fn preferred_quotes(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match configured_style {
|
match preferred_style {
|
||||||
QuoteStyle::Single => {
|
QuoteStyle::Single => {
|
||||||
if single_quotes > double_quotes {
|
if single_quotes > double_quotes {
|
||||||
QuoteStyle::Double
|
QuoteStyle::Double
|
||||||
|
|
@ -619,7 +629,7 @@ fn preferred_quotes(
|
||||||
|
|
||||||
StringQuotes {
|
StringQuotes {
|
||||||
triple: quotes.triple,
|
triple: quotes.triple,
|
||||||
style: preferred_style,
|
style,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -668,7 +678,7 @@ impl Format<PyFormatContext<'_>> for StringQuotes {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds the necessary quote escapes and removes unnecessary escape sequences when quoting `input`
|
/// 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.
|
/// Returns the normalized string and whether it contains new lines.
|
||||||
fn normalize_string(input: &str, quotes: StringQuotes, is_raw: bool) -> Cow<str> {
|
fn normalize_string(input: &str, quotes: StringQuotes, is_raw: bool) -> Cow<str> {
|
||||||
|
|
|
||||||
|
|
@ -320,17 +320,17 @@ if True:
|
||||||
b'This string will not include \
|
b'This string will not include \
|
||||||
backslashes or newline characters.'
|
backslashes or newline characters.'
|
||||||
|
|
||||||
b'''Multiline
|
b"""Multiline
|
||||||
String \"
|
String \"
|
||||||
'''
|
"""
|
||||||
|
|
||||||
b'''Multiline
|
b"""Multiline
|
||||||
String \'
|
String \'
|
||||||
'''
|
"""
|
||||||
|
|
||||||
b'''Multiline
|
b"""Multiline
|
||||||
String ""
|
String ""
|
||||||
'''
|
"""
|
||||||
|
|
||||||
b'''Multiline
|
b'''Multiline
|
||||||
String """
|
String """
|
||||||
|
|
@ -346,9 +346,9 @@ String '''
|
||||||
b"""Multiline
|
b"""Multiline
|
||||||
String '"""
|
String '"""
|
||||||
|
|
||||||
b'''Multiline
|
b"""Multiline
|
||||||
String \"\"\"
|
String \"\"\"
|
||||||
'''
|
"""
|
||||||
|
|
||||||
# String continuation
|
# String continuation
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -365,17 +365,17 @@ if True:
|
||||||
'This string will not include \
|
'This string will not include \
|
||||||
backslashes or newline characters.'
|
backslashes or newline characters.'
|
||||||
|
|
||||||
'''Multiline
|
"""Multiline
|
||||||
String \"
|
String \"
|
||||||
'''
|
"""
|
||||||
|
|
||||||
'''Multiline
|
"""Multiline
|
||||||
String \'
|
String \'
|
||||||
'''
|
"""
|
||||||
|
|
||||||
'''Multiline
|
"""Multiline
|
||||||
String ""
|
String ""
|
||||||
'''
|
"""
|
||||||
|
|
||||||
'''Multiline
|
'''Multiline
|
||||||
String """
|
String """
|
||||||
|
|
@ -391,9 +391,9 @@ String '''
|
||||||
"""Multiline
|
"""Multiline
|
||||||
String '"""
|
String '"""
|
||||||
|
|
||||||
'''Multiline
|
"""Multiline
|
||||||
String \"\"\"
|
String \"\"\"
|
||||||
'''
|
"""
|
||||||
|
|
||||||
# String continuation
|
# String continuation
|
||||||
|
|
||||||
|
|
@ -471,16 +471,16 @@ test_particular = [
|
||||||
|
|
||||||
# Regression test for https://github.com/astral-sh/ruff/issues/5893
|
# Regression test for https://github.com/astral-sh/ruff/issues/5893
|
||||||
x = (
|
x = (
|
||||||
'''aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'''
|
"""aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"""
|
||||||
'''bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'''
|
"""bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"""
|
||||||
)
|
)
|
||||||
x = (
|
x = (
|
||||||
f'''aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'''
|
f"""aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"""
|
||||||
f'''bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'''
|
f"""bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"""
|
||||||
)
|
)
|
||||||
x = (
|
x = (
|
||||||
b'''aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'''
|
b"""aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"""
|
||||||
b'''bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'''
|
b"""bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"""
|
||||||
)
|
)
|
||||||
|
|
||||||
# https://github.com/astral-sh/ruff/issues/7460
|
# https://github.com/astral-sh/ruff/issues/7460
|
||||||
|
|
|
||||||
|
|
@ -2461,7 +2461,7 @@ pub struct FormatOptions {
|
||||||
default = "false",
|
default = "false",
|
||||||
value_type = "bool",
|
value_type = "bool",
|
||||||
example = r#"
|
example = r#"
|
||||||
# Enable preview style formatting
|
# Enable preview style formatting.
|
||||||
preview = true
|
preview = true
|
||||||
"#
|
"#
|
||||||
)]
|
)]
|
||||||
|
|
@ -2469,34 +2469,40 @@ pub struct FormatOptions {
|
||||||
|
|
||||||
/// Whether to use 4 spaces or hard tabs for indenting code.
|
/// 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(
|
#[option(
|
||||||
default = "space",
|
default = "space",
|
||||||
value_type = r#""space" | "tab""#,
|
value_type = r#""space" | "tab""#,
|
||||||
example = r#"
|
example = r#"
|
||||||
# Use tabs instead of 4 space indentation
|
# Use tabs instead of 4 space indentation.
|
||||||
indent-style = "tab"
|
indent-style = "tab"
|
||||||
"#
|
"#
|
||||||
)]
|
)]
|
||||||
pub indent_style: Option<IndentStyle>,
|
pub indent_style: Option<IndentStyle>,
|
||||||
|
|
||||||
/// 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
|
/// ```python
|
||||||
/// a = "It's monday morning"
|
/// a = "a string without any quotes"
|
||||||
/// b = "a string without any quotes"
|
/// b = "It's monday morning"
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// Ruff leaves `a` unchanged when using `quote-style = "single"` because it is otherwise
|
/// Ruff will change `a` to use single quotes when using `quote-style = "single"`. However,
|
||||||
/// necessary to escape the `'` which leads to less readable code: `'It\'s monday morning'`.
|
/// `a` will be unchanged, as converting to single quotes would require the inner `'` to be
|
||||||
/// Ruff changes the quotes of `b` to use single quotes.
|
/// escaped, which leads to less readable code: `'It\'s monday morning'`.
|
||||||
#[option(
|
#[option(
|
||||||
default = r#"double"#,
|
default = r#"double"#,
|
||||||
value_type = r#""double" | "single""#,
|
value_type = r#""double" | "single""#,
|
||||||
example = r#"
|
example = r#"
|
||||||
# Prefer single quotes over double quotes
|
# Prefer single quotes over double quotes.
|
||||||
quote-style = "single"
|
quote-style = "single"
|
||||||
"#
|
"#
|
||||||
)]
|
)]
|
||||||
|
|
|
||||||
2
ruff.schema.json
generated
2
ruff.schema.json
generated
|
|
@ -1207,7 +1207,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"quote-style": {
|
"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": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/QuoteStyle"
|
"$ref": "#/definitions/QuoteStyle"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue