mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 18:58:26 +00:00
Add "preserve" quote-style to mimic Black's skip-string-normalization (#8822)
Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
parent
6bbabceead
commit
2414298289
10 changed files with 482 additions and 96 deletions
11
crates/ruff_python_formatter/resources/test/fixtures/ruff/quote_style.options.json
vendored
Normal file
11
crates/ruff_python_formatter/resources/test/fixtures/ruff/quote_style.options.json
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
[
|
||||
{
|
||||
"quote_style": "single"
|
||||
},
|
||||
{
|
||||
"quote_style": "double"
|
||||
},
|
||||
{
|
||||
"quote_style": "preserve"
|
||||
}
|
||||
]
|
50
crates/ruff_python_formatter/resources/test/fixtures/ruff/quote_style.py
vendored
Normal file
50
crates/ruff_python_formatter/resources/test/fixtures/ruff/quote_style.py
vendored
Normal file
|
@ -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'
|
|
@ -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<QuoteStyle>,
|
||||
docstring: Option<QuoteChar>,
|
||||
}
|
||||
|
||||
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<QuoteStyle> {
|
||||
pub(crate) fn docstring(&self) -> Option<QuoteChar> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
|
@ -476,7 +476,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(
|
||||
|
@ -498,9 +498,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,
|
||||
|
@ -1483,7 +1485,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<Printed, FormatModuleError> {
|
||||
use ruff_python_parser::AsMode;
|
||||
|
@ -1510,7 +1512,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
|
||||
}
|
||||
|
||||
|
|
|
@ -325,7 +325,7 @@ impl StringPart {
|
|||
quoting: Quoting,
|
||||
locator: &'a Locator,
|
||||
configured_style: QuoteStyle,
|
||||
parent_docstring_quote_style: Option<QuoteStyle>,
|
||||
parent_docstring_quote_char: Option<QuoteChar>,
|
||||
) -> NormalizedString<'a> {
|
||||
// Per PEP 8, always prefer double quotes for triple-quoted strings.
|
||||
let preferred_style = if self.quotes.triple {
|
||||
|
@ -374,8 +374,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
|
||||
}
|
||||
|
@ -388,10 +388,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
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -526,9 +530,9 @@ impl Format<PyFormatContext<'_>> 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() {
|
||||
|
@ -566,10 +570,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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -582,14 +586,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('"' | '\\')) {
|
||||
|
@ -637,9 +641,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;
|
||||
|
@ -659,19 +663,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -679,14 +683,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 {
|
||||
|
@ -694,11 +698,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 {
|
||||
|
@ -716,17 +723,74 @@ impl StringQuotes {
|
|||
|
||||
impl Format<PyFormatContext<'_>> 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<QuoteChar> {
|
||||
match style {
|
||||
QuoteStyle::Single => Some(QuoteChar::Single),
|
||||
QuoteStyle::Double => Some(QuoteChar::Double),
|
||||
QuoteStyle::Preserve => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<QuoteChar> for QuoteStyle {
|
||||
fn from(value: QuoteChar) -> Self {
|
||||
match value {
|
||||
QuoteChar::Single => QuoteStyle::Single,
|
||||
QuoteChar::Double => QuoteStyle::Double,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<char> for QuoteChar {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: char) -> Result<Self, Self::Error> {
|
||||
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.
|
||||
///
|
||||
|
@ -739,9 +803,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();
|
||||
|
||||
|
|
|
@ -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<char> for QuoteStyle {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: char) -> std::result::Result<Self, Self::Error> {
|
||||
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"),
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -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),
|
||||
|
|
|
@ -2819,13 +2819,18 @@ pub struct FormatOptions {
|
|||
)]
|
||||
pub indent_style: Option<IndentStyle>,
|
||||
|
||||
/// 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
|
||||
|
@ -2834,11 +2839,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"
|
||||
|
|
5
ruff.schema.json
generated
5
ruff.schema.json
generated
|
@ -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": {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue