Skip to content

Commit

Permalink
Alternate quotes for strings inside f-strings in preview (#13860)
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaReiser authored Oct 23, 2024
1 parent f335fe4 commit 2f88f84
Show file tree
Hide file tree
Showing 12 changed files with 556 additions and 118 deletions.
5 changes: 2 additions & 3 deletions crates/ruff_dev/src/format_dev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,7 @@ pub(crate) fn main(args: &Args) -> anyhow::Result<ExitCode> {
}
info!(
parent: None,
"Done: {} stability errors, {} files, similarity index {:.5}), files with differences: {} took {:.2}s, {} input files contained syntax errors ",
error_count,
"Done: {error_count} stability/syntax errors, {} files, similarity index {:.5}), files with differences: {} took {:.2}s, {} input files contained syntax errors ",
result.file_count,
result.statistics.similarity_index(),
result.statistics.files_with_differences,
Expand Down Expand Up @@ -796,7 +795,7 @@ impl CheckFileError {
| CheckFileError::PrintError(_)
| CheckFileError::Panic { .. } => false,
#[cfg(not(debug_assertions))]
CheckFileError::Slow(_) => false,
CheckFileError::Slow(_) => true,
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions crates/ruff_python_ast/src/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,10 @@ impl StringLikePart<'_> {
self.end() - kind.closer_len(),
)
}

pub const fn is_fstring(self) -> bool {
matches!(self, Self::FString(_))
}
}

impl<'a> From<&'a ast::StringLiteral> for StringLikePart<'a> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ccccccccccccccc"
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc"
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbb' = } ccccccccccccccc"

# Multiple larger expressions which exceeds the line length limit. Here, we need to decide
# whether to split at the first or second expression. This should work similarly to the
Expand Down Expand Up @@ -144,18 +146,37 @@
{'aaaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'}
}"

#############################################################################################
# Quotes
#############################################################################################
f"foo 'bar' {x}"
f"foo \"bar\" {x}"
f'foo "bar" {x}'
f'foo \'bar\' {x}'
f"foo {"bar"}"
f"foo {'\'bar\''}"

f"single quoted '{x}' double quoted \"{x}\"" # Same number of quotes => use preferred quote style
f"single quote ' {x} double quoted \"{x}\"" # More double quotes => use single quotes
f"single quoted '{x}' double quote \" {x}" # More single quotes => use double quotes

fr"single quotes ' {x}" # Keep double because `'` can't be escaped
fr'double quotes " {x}' # Keep single because `"` can't be escaped
fr'flip quotes {x}' # Use preferred quotes, because raw string contains now quotes.

# Here, the formatter will remove the escapes which is correct because they aren't allowed
# pre 3.12. This means we can assume that the f-string is used in the context of 3.12.
f"foo {'\'bar\''}"
f"foo {'\"bar\"'}"

# Quotes inside the expressions have no impact on the quote selection of the outer string.
# Required so that the following two examples result in the same formatting.
f'foo {10 + len("bar")}'
f"foo {10 + len('bar')}"

# Pre 312, preserve the outer quotes if the f-string contains quotes in the debug expression
f'foo {10 + len("bar")=}'
f'''foo {10 + len('''bar''')=}'''
f'''foo {10 + len('bar')=}''' # Fine to change the quotes because it uses triple quotes

# Triple-quoted strings
# It's ok to use the same quote char for the inner string if it's single-quoted.
Expand All @@ -164,6 +185,16 @@
# But if the inner string is also triple-quoted then we should preserve the existing quotes.
f"""test {'''inner'''}"""

# It's not okay to change the quote style if the inner string is triple quoted and contains a quote.
f'{"""other " """}'
f'{"""other " """ + "more"}'
f'{b"""other " """}'
f'{f"""other " """}'

# Not valid Pre 3.12
f"""test {f'inner {'''inner inner'''}'}"""
f"""test {f'''inner {"""inner inner"""}'''}"""

# Magic trailing comma
#
# The expression formatting will result in breaking it across multiple lines with a
Expand Down Expand Up @@ -312,6 +343,6 @@
# Implicit concatenated f-string containing quotes
_ = (
'This string should change its quotes to double quotes'
f'This string uses double quotes in an expression {"woah"}'
f'This string uses double quotes in an expression {"it's a quote"}'
f'This f-string does not use any quotes.'
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,14 @@

# Quotes reuse
f"{'a'}"

# 312+, it's okay to change the outer quotes even when there's a debug expression using the same quotes
f'foo {10 + len("bar")=}'
f'''foo {10 + len("""bar""")=}'''

# 312+, it's okay to change the quotes here without creating an invalid f-string
f'{"""other " """}'
f'{"""other " """ + "more"}'
f'{b"""other " """}'
f'{f"""other " """}'

37 changes: 12 additions & 25 deletions crates/ruff_python_formatter/src/other/f_string.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use ruff_formatter::write;
use ruff_python_ast::{AnyStringFlags, FString, StringFlags};
use ruff_source_file::Locator;

use crate::prelude::*;
use crate::preview::{
is_f_string_formatting_enabled, is_f_string_implicit_concatenated_string_literal_quotes_enabled,
};
use crate::string::{Quoting, StringNormalizer, StringQuotes};
use ruff_formatter::write;
use ruff_python_ast::{AnyStringFlags, FString, StringFlags};
use ruff_source_file::Locator;
use ruff_text_size::Ranged;

use super::f_string_element::FormatFStringElement;

Expand Down Expand Up @@ -35,7 +35,7 @@ impl Format<PyFormatContext<'_>> for FormatFString<'_> {
// f-string instead of globally for the entire f-string expression.
let quoting =
if is_f_string_implicit_concatenated_string_literal_quotes_enabled(f.context()) {
f_string_quoting(self.value, &locator)
Quoting::CanChange
} else {
self.quoting
};
Expand Down Expand Up @@ -92,17 +92,21 @@ impl Format<PyFormatContext<'_>> for FormatFString<'_> {

#[derive(Clone, Copy, Debug)]
pub(crate) struct FStringContext {
flags: AnyStringFlags,
/// The string flags of the enclosing f-string part.
enclosing_flags: AnyStringFlags,
layout: FStringLayout,
}

impl FStringContext {
const fn new(flags: AnyStringFlags, layout: FStringLayout) -> Self {
Self { flags, layout }
Self {
enclosing_flags: flags,
layout,
}
}

pub(crate) fn flags(self) -> AnyStringFlags {
self.flags
self.enclosing_flags
}

pub(crate) const fn layout(self) -> FStringLayout {
Expand Down Expand Up @@ -149,20 +153,3 @@ impl FStringLayout {
matches!(self, FStringLayout::Multiline)
}
}

fn f_string_quoting(f_string: &FString, locator: &Locator) -> Quoting {
let triple_quoted = f_string.flags.is_triple_quoted();

if f_string.elements.expressions().any(|expression| {
let string_content = locator.slice(expression.range());
if triple_quoted {
string_content.contains(r#"""""#) || string_content.contains("'''")
} else {
string_content.contains(['"', '\''])
}
}) {
Quoting::Preserve
} else {
Quoting::CanChange
}
}
18 changes: 11 additions & 7 deletions crates/ruff_python_formatter/src/other/f_string_element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use std::borrow::Cow;

use ruff_formatter::{format_args, write, Buffer, RemoveSoftLinesBuffer};
use ruff_python_ast::{
ConversionFlag, Expr, FStringElement, FStringExpressionElement, FStringLiteralElement,
StringFlags,
AnyStringFlags, ConversionFlag, Expr, FStringElement, FStringExpressionElement,
FStringLiteralElement, StringFlags,
};
use ruff_text_size::Ranged;

Expand Down Expand Up @@ -33,7 +33,7 @@ impl Format<PyFormatContext<'_>> for FormatFStringElement<'_> {
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
match self.element {
FStringElement::Literal(string_literal) => {
FormatFStringLiteralElement::new(string_literal, self.context).fmt(f)
FormatFStringLiteralElement::new(string_literal, self.context.flags()).fmt(f)
}
FStringElement::Expression(expression) => {
FormatFStringExpressionElement::new(expression, self.context).fmt(f)
Expand All @@ -45,19 +45,23 @@ impl Format<PyFormatContext<'_>> for FormatFStringElement<'_> {
/// Formats an f-string literal element.
pub(crate) struct FormatFStringLiteralElement<'a> {
element: &'a FStringLiteralElement,
context: FStringContext,
/// Flags of the enclosing F-string part
fstring_flags: AnyStringFlags,
}

impl<'a> FormatFStringLiteralElement<'a> {
pub(crate) fn new(element: &'a FStringLiteralElement, context: FStringContext) -> Self {
Self { element, context }
pub(crate) fn new(element: &'a FStringLiteralElement, fstring_flags: AnyStringFlags) -> Self {
Self {
element,
fstring_flags,
}
}
}

impl Format<PyFormatContext<'_>> for FormatFStringLiteralElement<'_> {
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
let literal_content = f.context().locator().slice(self.element.range());
let normalized = normalize_string(literal_content, 0, self.context.flags(), true);
let normalized = normalize_string(literal_content, 0, self.fstring_flags, true);
match &normalized {
Cow::Borrowed(_) => source_text_slice(self.element.range()).fmt(f),
Cow::Owned(normalized) => text(normalized).fmt(f),
Expand Down
2 changes: 2 additions & 0 deletions crates/ruff_python_formatter/src/preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ pub(crate) const fn is_hug_parens_with_braces_and_square_brackets_enabled(
}

/// Returns `true` if the [`f-string formatting`](https://github.com/astral-sh/ruff/issues/7594) preview style is enabled.
/// WARNING: This preview style depends on `is_f_string_implicit_concatenated_string_literal_quotes_enabled`.
pub(crate) fn is_f_string_formatting_enabled(context: &PyFormatContext) -> bool {
context.is_preview()
}

/// See [#13539](https://github.com/astral-sh/ruff/pull/13539)
/// Remove `Quoting` when stabalizing this preview style.
pub(crate) fn is_f_string_implicit_concatenated_string_literal_quotes_enabled(
context: &PyFormatContext,
) -> bool {
Expand Down
Loading

0 comments on commit 2f88f84

Please # to comment.