mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-03 07:04:37 +00:00
Alternate quotes for strings inside f-strings in preview (#13860)
This commit is contained in:
parent
f335fe4d4a
commit
2f88f84972
12 changed files with 556 additions and 118 deletions
|
@ -251,8 +251,7 @@ pub(crate) fn main(args: &Args) -> anyhow::Result<ExitCode> {
|
||||||
}
|
}
|
||||||
info!(
|
info!(
|
||||||
parent: None,
|
parent: None,
|
||||||
"Done: {} stability errors, {} files, similarity index {:.5}), files with differences: {} took {:.2}s, {} input files contained syntax errors ",
|
"Done: {error_count} stability/syntax errors, {} files, similarity index {:.5}), files with differences: {} took {:.2}s, {} input files contained syntax errors ",
|
||||||
error_count,
|
|
||||||
result.file_count,
|
result.file_count,
|
||||||
result.statistics.similarity_index(),
|
result.statistics.similarity_index(),
|
||||||
result.statistics.files_with_differences,
|
result.statistics.files_with_differences,
|
||||||
|
@ -796,7 +795,7 @@ impl CheckFileError {
|
||||||
| CheckFileError::PrintError(_)
|
| CheckFileError::PrintError(_)
|
||||||
| CheckFileError::Panic { .. } => false,
|
| CheckFileError::Panic { .. } => false,
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
CheckFileError::Slow(_) => false,
|
CheckFileError::Slow(_) => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -524,6 +524,10 @@ impl StringLikePart<'_> {
|
||||||
self.end() - kind.closer_len(),
|
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> {
|
impl<'a> From<&'a ast::StringLiteral> for StringLikePart<'a> {
|
||||||
|
|
|
@ -94,6 +94,8 @@ x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 8
|
||||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ccccccccccccccc"
|
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ccccccccccccccc"
|
||||||
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9
|
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9
|
||||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc"
|
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc"
|
||||||
|
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9
|
||||||
|
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbb' = } ccccccccccccccc"
|
||||||
|
|
||||||
# Multiple larger expressions which exceeds the line length limit. Here, we need to decide
|
# 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
|
# whether to split at the first or second expression. This should work similarly to the
|
||||||
|
@ -144,18 +146,37 @@ xxxxxxx = f"{
|
||||||
{'aaaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'}
|
{'aaaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'}
|
||||||
}"
|
}"
|
||||||
|
|
||||||
|
#############################################################################################
|
||||||
# Quotes
|
# Quotes
|
||||||
|
#############################################################################################
|
||||||
f"foo 'bar' {x}"
|
f"foo 'bar' {x}"
|
||||||
f"foo \"bar\" {x}"
|
f"foo \"bar\" {x}"
|
||||||
f'foo "bar" {x}'
|
f'foo "bar" {x}'
|
||||||
f'foo \'bar\' {x}'
|
f'foo \'bar\' {x}'
|
||||||
f"foo {"bar"}"
|
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
|
# 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.
|
# 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\"'}"
|
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
|
# Triple-quoted strings
|
||||||
# It's ok to use the same quote char for the inner string if it's single-quoted.
|
# It's ok to use the same quote char for the inner string if it's single-quoted.
|
||||||
|
@ -164,6 +185,16 @@ f"""test {"inner"}"""
|
||||||
# But if the inner string is also triple-quoted then we should preserve the existing quotes.
|
# But if the inner string is also triple-quoted then we should preserve the existing quotes.
|
||||||
f"""test {'''inner'''}"""
|
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
|
# Magic trailing comma
|
||||||
#
|
#
|
||||||
# The expression formatting will result in breaking it across multiple lines with a
|
# The expression formatting will result in breaking it across multiple lines with a
|
||||||
|
@ -312,6 +343,6 @@ hello {
|
||||||
# Implicit concatenated f-string containing quotes
|
# Implicit concatenated f-string containing quotes
|
||||||
_ = (
|
_ = (
|
||||||
'This string should change its quotes to double 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.'
|
f'This f-string does not use any quotes.'
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,3 +4,14 @@
|
||||||
|
|
||||||
# Quotes reuse
|
# Quotes reuse
|
||||||
f"{'a'}"
|
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 " """}'
|
||||||
|
|
||||||
|
|
|
@ -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::prelude::*;
|
||||||
use crate::preview::{
|
use crate::preview::{
|
||||||
is_f_string_formatting_enabled, is_f_string_implicit_concatenated_string_literal_quotes_enabled,
|
is_f_string_formatting_enabled, is_f_string_implicit_concatenated_string_literal_quotes_enabled,
|
||||||
};
|
};
|
||||||
use crate::string::{Quoting, StringNormalizer, StringQuotes};
|
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;
|
use super::f_string_element::FormatFStringElement;
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ impl Format<PyFormatContext<'_>> for FormatFString<'_> {
|
||||||
// f-string instead of globally for the entire f-string expression.
|
// f-string instead of globally for the entire f-string expression.
|
||||||
let quoting =
|
let quoting =
|
||||||
if is_f_string_implicit_concatenated_string_literal_quotes_enabled(f.context()) {
|
if is_f_string_implicit_concatenated_string_literal_quotes_enabled(f.context()) {
|
||||||
f_string_quoting(self.value, &locator)
|
Quoting::CanChange
|
||||||
} else {
|
} else {
|
||||||
self.quoting
|
self.quoting
|
||||||
};
|
};
|
||||||
|
@ -92,17 +92,21 @@ impl Format<PyFormatContext<'_>> for FormatFString<'_> {
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub(crate) struct FStringContext {
|
pub(crate) struct FStringContext {
|
||||||
flags: AnyStringFlags,
|
/// The string flags of the enclosing f-string part.
|
||||||
|
enclosing_flags: AnyStringFlags,
|
||||||
layout: FStringLayout,
|
layout: FStringLayout,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FStringContext {
|
impl FStringContext {
|
||||||
const fn new(flags: AnyStringFlags, layout: FStringLayout) -> Self {
|
const fn new(flags: AnyStringFlags, layout: FStringLayout) -> Self {
|
||||||
Self { flags, layout }
|
Self {
|
||||||
|
enclosing_flags: flags,
|
||||||
|
layout,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn flags(self) -> AnyStringFlags {
|
pub(crate) fn flags(self) -> AnyStringFlags {
|
||||||
self.flags
|
self.enclosing_flags
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) const fn layout(self) -> FStringLayout {
|
pub(crate) const fn layout(self) -> FStringLayout {
|
||||||
|
@ -149,20 +153,3 @@ impl FStringLayout {
|
||||||
matches!(self, FStringLayout::Multiline)
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,8 +2,8 @@ use std::borrow::Cow;
|
||||||
|
|
||||||
use ruff_formatter::{format_args, write, Buffer, RemoveSoftLinesBuffer};
|
use ruff_formatter::{format_args, write, Buffer, RemoveSoftLinesBuffer};
|
||||||
use ruff_python_ast::{
|
use ruff_python_ast::{
|
||||||
ConversionFlag, Expr, FStringElement, FStringExpressionElement, FStringLiteralElement,
|
AnyStringFlags, ConversionFlag, Expr, FStringElement, FStringExpressionElement,
|
||||||
StringFlags,
|
FStringLiteralElement, StringFlags,
|
||||||
};
|
};
|
||||||
use ruff_text_size::Ranged;
|
use ruff_text_size::Ranged;
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ impl Format<PyFormatContext<'_>> for FormatFStringElement<'_> {
|
||||||
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
||||||
match self.element {
|
match self.element {
|
||||||
FStringElement::Literal(string_literal) => {
|
FStringElement::Literal(string_literal) => {
|
||||||
FormatFStringLiteralElement::new(string_literal, self.context).fmt(f)
|
FormatFStringLiteralElement::new(string_literal, self.context.flags()).fmt(f)
|
||||||
}
|
}
|
||||||
FStringElement::Expression(expression) => {
|
FStringElement::Expression(expression) => {
|
||||||
FormatFStringExpressionElement::new(expression, self.context).fmt(f)
|
FormatFStringExpressionElement::new(expression, self.context).fmt(f)
|
||||||
|
@ -45,19 +45,23 @@ impl Format<PyFormatContext<'_>> for FormatFStringElement<'_> {
|
||||||
/// Formats an f-string literal element.
|
/// Formats an f-string literal element.
|
||||||
pub(crate) struct FormatFStringLiteralElement<'a> {
|
pub(crate) struct FormatFStringLiteralElement<'a> {
|
||||||
element: &'a FStringLiteralElement,
|
element: &'a FStringLiteralElement,
|
||||||
context: FStringContext,
|
/// Flags of the enclosing F-string part
|
||||||
|
fstring_flags: AnyStringFlags,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> FormatFStringLiteralElement<'a> {
|
impl<'a> FormatFStringLiteralElement<'a> {
|
||||||
pub(crate) fn new(element: &'a FStringLiteralElement, context: FStringContext) -> Self {
|
pub(crate) fn new(element: &'a FStringLiteralElement, fstring_flags: AnyStringFlags) -> Self {
|
||||||
Self { element, context }
|
Self {
|
||||||
|
element,
|
||||||
|
fstring_flags,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Format<PyFormatContext<'_>> for FormatFStringLiteralElement<'_> {
|
impl Format<PyFormatContext<'_>> for FormatFStringLiteralElement<'_> {
|
||||||
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
||||||
let literal_content = f.context().locator().slice(self.element.range());
|
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 {
|
match &normalized {
|
||||||
Cow::Borrowed(_) => source_text_slice(self.element.range()).fmt(f),
|
Cow::Borrowed(_) => source_text_slice(self.element.range()).fmt(f),
|
||||||
Cow::Owned(normalized) => text(normalized).fmt(f),
|
Cow::Owned(normalized) => text(normalized).fmt(f),
|
||||||
|
|
|
@ -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.
|
/// 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 {
|
pub(crate) fn is_f_string_formatting_enabled(context: &PyFormatContext) -> bool {
|
||||||
context.is_preview()
|
context.is_preview()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// See [#13539](https://github.com/astral-sh/ruff/pull/13539)
|
/// 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(
|
pub(crate) fn is_f_string_implicit_concatenated_string_literal_quotes_enabled(
|
||||||
context: &PyFormatContext,
|
context: &PyFormatContext,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
|
|
|
@ -3,7 +3,10 @@ use std::cmp::Ordering;
|
||||||
use std::iter::FusedIterator;
|
use std::iter::FusedIterator;
|
||||||
|
|
||||||
use ruff_formatter::FormatContext;
|
use ruff_formatter::FormatContext;
|
||||||
use ruff_python_ast::{str::Quote, AnyStringFlags, StringFlags, StringLikePart};
|
use ruff_python_ast::visitor::source_order::SourceOrderVisitor;
|
||||||
|
use ruff_python_ast::{
|
||||||
|
str::Quote, AnyStringFlags, BytesLiteral, FString, StringFlags, StringLikePart, StringLiteral,
|
||||||
|
};
|
||||||
use ruff_text_size::{Ranged, TextRange};
|
use ruff_text_size::{Ranged, TextRange};
|
||||||
|
|
||||||
use crate::context::FStringState;
|
use crate::context::FStringState;
|
||||||
|
@ -37,53 +40,55 @@ impl<'a, 'src> StringNormalizer<'a, 'src> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn quoting(&self, string: StringLikePart) -> Quoting {
|
|
||||||
match (self.quoting, self.context.f_string_state()) {
|
|
||||||
(Quoting::Preserve, _) => Quoting::Preserve,
|
|
||||||
|
|
||||||
// If we're inside an f-string, we need to make sure to preserve the
|
|
||||||
// existing quotes unless we're inside a triple-quoted f-string and
|
|
||||||
// the inner string itself isn't triple-quoted. For example:
|
|
||||||
//
|
|
||||||
// ```python
|
|
||||||
// f"""outer {"inner"}""" # Valid
|
|
||||||
// f"""outer {"""inner"""}""" # Invalid
|
|
||||||
// ```
|
|
||||||
//
|
|
||||||
// Or, if the target version supports PEP 701.
|
|
||||||
//
|
|
||||||
// The reason to preserve the quotes is based on the assumption that
|
|
||||||
// the original f-string is valid in terms of quoting, and we don't
|
|
||||||
// want to change that to make it invalid.
|
|
||||||
(Quoting::CanChange, FStringState::InsideExpressionElement(context)) => {
|
|
||||||
if (context.f_string().flags().is_triple_quoted()
|
|
||||||
&& !string.flags().is_triple_quoted())
|
|
||||||
|| self.context.options().target_version().supports_pep_701()
|
|
||||||
{
|
|
||||||
Quoting::CanChange
|
|
||||||
} else {
|
|
||||||
Quoting::Preserve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(Quoting::CanChange, _) => Quoting::CanChange,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Determines the preferred quote style for `string`.
|
/// Determines the preferred quote style for `string`.
|
||||||
/// The formatter should use the preferred quote style unless
|
/// The formatter should use the preferred quote style unless
|
||||||
/// it can't because the string contains the preferred quotes OR
|
/// it can't because the string contains the preferred quotes OR
|
||||||
/// it leads to more escaping.
|
/// it leads to more escaping.
|
||||||
pub(super) fn preferred_quote_style(&self, string: StringLikePart) -> QuoteStyle {
|
pub(super) fn preferred_quote_style(&self, string: StringLikePart) -> QuoteStyle {
|
||||||
match self.quoting(string) {
|
match self.quoting {
|
||||||
Quoting::Preserve => QuoteStyle::Preserve,
|
Quoting::Preserve => QuoteStyle::Preserve,
|
||||||
Quoting::CanChange => {
|
Quoting::CanChange => {
|
||||||
let preferred_quote_style = self
|
let preferred_quote_style = self
|
||||||
.preferred_quote_style
|
.preferred_quote_style
|
||||||
.unwrap_or(self.context.options().quote_style());
|
.unwrap_or(self.context.options().quote_style());
|
||||||
|
|
||||||
|
if preferred_quote_style.is_preserve() {
|
||||||
|
return QuoteStyle::Preserve;
|
||||||
|
}
|
||||||
|
|
||||||
|
// There are two cases where it's necessary to preserve the quotes
|
||||||
|
// if the target version is pre 3.12 and the part is an f-string.
|
||||||
|
if !self.context.options().target_version().supports_pep_701() {
|
||||||
|
if let StringLikePart::FString(fstring) = string {
|
||||||
|
// An f-string expression contains a debug text with a quote character
|
||||||
|
// because the formatter will emit the debug expression **exactly** the same as in the source text.
|
||||||
|
if is_fstring_with_quoted_debug_expression(fstring, self.context) {
|
||||||
|
return QuoteStyle::Preserve;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An f-string expression that contains a triple quoted string literal expression
|
||||||
|
// that contains a quote.
|
||||||
|
if is_fstring_with_triple_quoted_literal_expression_containing_quotes(
|
||||||
|
fstring,
|
||||||
|
self.context,
|
||||||
|
) {
|
||||||
|
return QuoteStyle::Preserve;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For f-strings prefer alternating the quotes unless The outer string is triple quoted and the inner isn't.
|
||||||
|
if let FStringState::InsideExpressionElement(parent_context) =
|
||||||
|
self.context.f_string_state()
|
||||||
|
{
|
||||||
|
let parent_flags = parent_context.f_string().flags();
|
||||||
|
|
||||||
|
if !parent_flags.is_triple_quoted() || string.flags().is_triple_quoted() {
|
||||||
|
return QuoteStyle::from(parent_flags.quote_style().opposite());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Per PEP 8, always prefer double quotes for triple-quoted strings.
|
// Per PEP 8, always prefer double quotes for triple-quoted strings.
|
||||||
// Except when using quote-style-preserve.
|
|
||||||
if string.flags().is_triple_quoted() {
|
if string.flags().is_triple_quoted() {
|
||||||
// ... unless we're formatting a code snippet inside a docstring,
|
// ... unless we're formatting a code snippet inside a docstring,
|
||||||
// then we specifically want to invert our quote style to avoid
|
// then we specifically want to invert our quote style to avoid
|
||||||
|
@ -132,8 +137,6 @@ impl<'a, 'src> StringNormalizer<'a, 'src> {
|
||||||
// if it doesn't have perfect alignment with PEP8.
|
// if it doesn't have perfect alignment with PEP8.
|
||||||
if let Some(quote) = self.context.docstring() {
|
if let Some(quote) = self.context.docstring() {
|
||||||
QuoteStyle::from(quote.opposite())
|
QuoteStyle::from(quote.opposite())
|
||||||
} else if preferred_quote_style.is_preserve() {
|
|
||||||
QuoteStyle::Preserve
|
|
||||||
} else {
|
} else {
|
||||||
QuoteStyle::Double
|
QuoteStyle::Double
|
||||||
}
|
}
|
||||||
|
@ -163,12 +166,18 @@ impl<'a, 'src> StringNormalizer<'a, 'src> {
|
||||||
// The preferred quote style is single or double quotes, and the string contains a quote or
|
// The preferred quote style is single or double quotes, and the string contains a quote or
|
||||||
// another character that may require escaping
|
// another character that may require escaping
|
||||||
(Ok(preferred_quote), Some(first_quote_or_normalized_char_offset)) => {
|
(Ok(preferred_quote), Some(first_quote_or_normalized_char_offset)) => {
|
||||||
let quote = QuoteMetadata::from_str(
|
let metadata = if string.is_fstring() {
|
||||||
|
QuoteMetadata::from_part(string, self.context, preferred_quote)
|
||||||
|
} else {
|
||||||
|
QuoteMetadata::from_str(
|
||||||
&raw_content[first_quote_or_normalized_char_offset..],
|
&raw_content[first_quote_or_normalized_char_offset..],
|
||||||
string.flags(),
|
string.flags(),
|
||||||
preferred_quote,
|
preferred_quote,
|
||||||
)
|
)
|
||||||
.choose(preferred_quote);
|
};
|
||||||
|
|
||||||
|
let quote = metadata.choose(preferred_quote);
|
||||||
|
|
||||||
string_flags.with_quote_style(quote)
|
string_flags.with_quote_style(quote)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,6 +244,59 @@ pub(crate) struct QuoteMetadata {
|
||||||
/// Tracks information about the used quotes in a string which is used
|
/// Tracks information about the used quotes in a string which is used
|
||||||
/// to choose the quotes for a part.
|
/// to choose the quotes for a part.
|
||||||
impl QuoteMetadata {
|
impl QuoteMetadata {
|
||||||
|
pub(crate) fn from_part(
|
||||||
|
part: StringLikePart,
|
||||||
|
context: &PyFormatContext,
|
||||||
|
preferred_quote: Quote,
|
||||||
|
) -> Self {
|
||||||
|
match part {
|
||||||
|
StringLikePart::String(_) | StringLikePart::Bytes(_) => {
|
||||||
|
let text = context.locator().slice(part.content_range());
|
||||||
|
|
||||||
|
Self::from_str(text, part.flags(), preferred_quote)
|
||||||
|
}
|
||||||
|
StringLikePart::FString(fstring) => {
|
||||||
|
if is_f_string_formatting_enabled(context) {
|
||||||
|
// For f-strings, only consider the quotes inside string-literals but ignore
|
||||||
|
// quotes inside expressions. This allows both the outer and the nested literals
|
||||||
|
// to make the optimal local-choice to reduce the total number of quotes necessary.
|
||||||
|
// This doesn't require any pre 312 special handling because an expression
|
||||||
|
// can never contain the outer quote character, not even escaped:
|
||||||
|
// ```python
|
||||||
|
// f"{'escaping a quote like this \" is a syntax error pre 312'}"
|
||||||
|
// ```
|
||||||
|
let mut literals = fstring.elements.literals();
|
||||||
|
|
||||||
|
let Some(first) = literals.next() else {
|
||||||
|
return QuoteMetadata::from_str("", part.flags(), preferred_quote);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut metadata = QuoteMetadata::from_str(
|
||||||
|
context.locator().slice(first.range()),
|
||||||
|
fstring.flags.into(),
|
||||||
|
preferred_quote,
|
||||||
|
);
|
||||||
|
|
||||||
|
for literal in literals {
|
||||||
|
metadata = metadata
|
||||||
|
.merge(&QuoteMetadata::from_str(
|
||||||
|
context.locator().slice(literal.range()),
|
||||||
|
fstring.flags.into(),
|
||||||
|
preferred_quote,
|
||||||
|
))
|
||||||
|
.expect("Merge to succeed because all parts have the same flags");
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata
|
||||||
|
} else {
|
||||||
|
let text = context.locator().slice(part.content_range());
|
||||||
|
|
||||||
|
Self::from_str(text, part.flags(), preferred_quote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn from_str(text: &str, flags: AnyStringFlags, preferred_quote: Quote) -> Self {
|
pub(crate) fn from_str(text: &str, flags: AnyStringFlags, preferred_quote: Quote) -> Self {
|
||||||
let kind = if flags.is_raw_string() {
|
let kind = if flags.is_raw_string() {
|
||||||
QuoteMetadataKind::raw(text, preferred_quote, flags.is_triple_quoted())
|
QuoteMetadataKind::raw(text, preferred_quote, flags.is_triple_quoted())
|
||||||
|
@ -276,6 +338,61 @@ impl QuoteMetadata {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Merges the quotes metadata of different literals.
|
||||||
|
///
|
||||||
|
/// ## Raw and triple quoted strings
|
||||||
|
/// Merging raw and triple quoted strings is only correct if all literals are from the same part.
|
||||||
|
/// E.g. it's okay to merge triple and raw strings from a single `FString` part's literals
|
||||||
|
/// but it isn't safe to merge raw and triple quoted strings from different parts of an implicit
|
||||||
|
/// concatenated string. Where safe means, it may lead to incorrect results.
|
||||||
|
pub(super) fn merge(self, other: &QuoteMetadata) -> Option<QuoteMetadata> {
|
||||||
|
let kind = match (self.kind, other.kind) {
|
||||||
|
(
|
||||||
|
QuoteMetadataKind::Regular {
|
||||||
|
single_quotes: self_single,
|
||||||
|
double_quotes: self_double,
|
||||||
|
},
|
||||||
|
QuoteMetadataKind::Regular {
|
||||||
|
single_quotes: other_single,
|
||||||
|
double_quotes: other_double,
|
||||||
|
},
|
||||||
|
) => QuoteMetadataKind::Regular {
|
||||||
|
single_quotes: self_single + other_single,
|
||||||
|
double_quotes: self_double + other_double,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Can't merge quotes from raw strings (even when both strings are raw)
|
||||||
|
(
|
||||||
|
QuoteMetadataKind::Raw {
|
||||||
|
contains_preferred: self_contains_preferred,
|
||||||
|
},
|
||||||
|
QuoteMetadataKind::Raw {
|
||||||
|
contains_preferred: other_contains_preferred,
|
||||||
|
},
|
||||||
|
) => QuoteMetadataKind::Raw {
|
||||||
|
contains_preferred: self_contains_preferred || other_contains_preferred,
|
||||||
|
},
|
||||||
|
|
||||||
|
(
|
||||||
|
QuoteMetadataKind::Triple {
|
||||||
|
contains_preferred: self_contains_preferred,
|
||||||
|
},
|
||||||
|
QuoteMetadataKind::Triple {
|
||||||
|
contains_preferred: other_contains_preferred,
|
||||||
|
},
|
||||||
|
) => QuoteMetadataKind::Triple {
|
||||||
|
contains_preferred: self_contains_preferred || other_contains_preferred,
|
||||||
|
},
|
||||||
|
|
||||||
|
(_, _) => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(Self {
|
||||||
|
kind,
|
||||||
|
source_style: self.source_style,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
@ -738,18 +855,142 @@ impl UnicodeEscape {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if `string` is an f-string part that contains a debug expression that uses quotes
|
||||||
|
/// and the format target is pre Python 312
|
||||||
|
/// We can't join f-strings where:
|
||||||
|
///
|
||||||
|
/// ```python
|
||||||
|
/// f"{10 + len('bar')=}"
|
||||||
|
/// f'{10 + len("bar")=}'
|
||||||
|
/// f""""{10 + len('''bar''')=}"""
|
||||||
|
/// ```
|
||||||
|
pub(super) fn is_fstring_with_quoted_debug_expression(
|
||||||
|
fstring: &FString,
|
||||||
|
context: &PyFormatContext,
|
||||||
|
) -> bool {
|
||||||
|
if fstring.elements.expressions().any(|expression| {
|
||||||
|
if expression.debug_text.is_some() {
|
||||||
|
let content = context.locator().slice(expression.range());
|
||||||
|
match fstring.flags.quote_style() {
|
||||||
|
Quote::Single => {
|
||||||
|
if fstring.flags.is_triple_quoted() {
|
||||||
|
content.contains(r#"""""#)
|
||||||
|
} else {
|
||||||
|
content.contains('"')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Quote::Double => {
|
||||||
|
if fstring.flags.is_triple_quoted() {
|
||||||
|
content.contains("'''")
|
||||||
|
} else {
|
||||||
|
content.contains('\'')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests if the `fstring` contains any triple quoted string, byte, or f-string literal that
|
||||||
|
/// contains a quote character opposite to its own quote character.
|
||||||
|
///
|
||||||
|
/// ```python
|
||||||
|
/// f'{"""other " """}'
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// We can't flip the quote of the outer f-string because it would result in invalid syntax:
|
||||||
|
/// ```python
|
||||||
|
/// f"{'''other " '''}'
|
||||||
|
/// ```
|
||||||
|
pub(super) fn is_fstring_with_triple_quoted_literal_expression_containing_quotes(
|
||||||
|
fstring: &FString,
|
||||||
|
context: &PyFormatContext,
|
||||||
|
) -> bool {
|
||||||
|
struct Visitor<'a> {
|
||||||
|
context: &'a PyFormatContext<'a>,
|
||||||
|
found: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Visitor<'_> {
|
||||||
|
fn visit_string_like_part(&mut self, part: StringLikePart) {
|
||||||
|
if !part.flags().is_triple_quoted() || self.found {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let contains_quotes = match part {
|
||||||
|
StringLikePart::String(_) | StringLikePart::Bytes(_) => {
|
||||||
|
self.contains_quote(part.content_range(), part.flags())
|
||||||
|
}
|
||||||
|
StringLikePart::FString(fstring) => {
|
||||||
|
let mut contains_quotes = false;
|
||||||
|
for literal in fstring.elements.literals() {
|
||||||
|
if self.contains_quote(literal.range(), fstring.flags.into()) {
|
||||||
|
contains_quotes = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contains_quotes
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if contains_quotes {
|
||||||
|
self.found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contains_quote(&self, range: TextRange, flags: AnyStringFlags) -> bool {
|
||||||
|
self.context
|
||||||
|
.locator()
|
||||||
|
.slice(range)
|
||||||
|
.contains(flags.quote_style().as_char())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SourceOrderVisitor<'_> for Visitor<'_> {
|
||||||
|
fn visit_f_string(&mut self, f_string: &FString) {
|
||||||
|
self.visit_string_like_part(StringLikePart::FString(f_string));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_string_literal(&mut self, string_literal: &StringLiteral) {
|
||||||
|
self.visit_string_like_part(StringLikePart::String(string_literal));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_bytes_literal(&mut self, bytes_literal: &BytesLiteral) {
|
||||||
|
self.visit_string_like_part(StringLikePart::Bytes(bytes_literal));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut visitor = Visitor {
|
||||||
|
context,
|
||||||
|
found: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
ruff_python_ast::visitor::source_order::walk_f_string(&mut visitor, fstring);
|
||||||
|
|
||||||
|
visitor.found
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use super::UnicodeEscape;
|
|
||||||
use crate::string::normalize_string;
|
|
||||||
use ruff_python_ast::{
|
use ruff_python_ast::{
|
||||||
str::Quote,
|
str::Quote,
|
||||||
str_prefix::{AnyStringPrefix, ByteStringPrefix},
|
str_prefix::{AnyStringPrefix, ByteStringPrefix},
|
||||||
AnyStringFlags,
|
AnyStringFlags,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::string::normalize_string;
|
||||||
|
|
||||||
|
use super::UnicodeEscape;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn normalize_32_escape() {
|
fn normalize_32_escape() {
|
||||||
let escape_sequence = UnicodeEscape::new('U', true).unwrap();
|
let escape_sequence = UnicodeEscape::new('U', true).unwrap();
|
||||||
|
|
|
@ -941,7 +941,7 @@ log.info(f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share
|
||||||
log.info(
|
log.info(
|
||||||
- "Skipping:"
|
- "Skipping:"
|
||||||
- f" {desc['db_id']} {foo('bar',x=123)} {'foo' != 'bar'} {(x := 'abc=')} {pos_share=} {desc['status']} {desc['exposure_max']}"
|
- f" {desc['db_id']} {foo('bar',x=123)} {'foo' != 'bar'} {(x := 'abc=')} {pos_share=} {desc['status']} {desc['exposure_max']}"
|
||||||
+ f'Skipping: {desc["db_id"]} {foo("bar", x=123)} {"foo" != "bar"} {(x := "abc=")} {pos_share=} {desc["status"]} {desc["exposure_max"]}'
|
+ f"Skipping: {desc['db_id']} {foo('bar', x=123)} {'foo' != 'bar'} {(x := 'abc=')} {pos_share=} {desc['status']} {desc['exposure_max']}"
|
||||||
)
|
)
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
|
@ -965,7 +965,7 @@ log.info(f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share
|
||||||
log.info(
|
log.info(
|
||||||
- "Skipping:"
|
- "Skipping:"
|
||||||
- f" {'a' == 'b' == 'c' == 'd'} {desc['ms_name']} {money=} {dte=} {pos_share=} {desc['status']} {desc['exposure_max']}"
|
- f" {'a' == 'b' == 'c' == 'd'} {desc['ms_name']} {money=} {dte=} {pos_share=} {desc['status']} {desc['exposure_max']}"
|
||||||
+ f'Skipping: {"a" == "b" == "c" == "d"} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}'
|
+ f"Skipping: {'a' == 'b' == 'c' == 'd'} {desc['ms_name']} {money=} {dte=} {pos_share=} {desc['status']} {desc['exposure_max']}"
|
||||||
)
|
)
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
|
@ -1406,7 +1406,7 @@ log.info(
|
||||||
)
|
)
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
f'Skipping: {desc["db_id"]} {foo("bar", x=123)} {"foo" != "bar"} {(x := "abc=")} {pos_share=} {desc["status"]} {desc["exposure_max"]}'
|
f"Skipping: {desc['db_id']} {foo('bar', x=123)} {'foo' != 'bar'} {(x := 'abc=')} {pos_share=} {desc['status']} {desc['exposure_max']}"
|
||||||
)
|
)
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
|
@ -1422,7 +1422,7 @@ log.info(
|
||||||
)
|
)
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
f'Skipping: {"a" == "b" == "c" == "d"} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}'
|
f"Skipping: {'a' == 'b' == 'c' == 'd'} {desc['ms_name']} {money=} {dte=} {pos_share=} {desc['status']} {desc['exposure_max']}"
|
||||||
)
|
)
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
|
|
|
@ -1201,10 +1201,10 @@ s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry:
|
||||||
+ "With single quote: ' "
|
+ "With single quote: ' "
|
||||||
+ f" {my_dict['foo']}"
|
+ f" {my_dict['foo']}"
|
||||||
+ ' With double quote: " '
|
+ ' With double quote: " '
|
||||||
+ f' {my_dict["bar"]}'
|
+ f" {my_dict['bar']}"
|
||||||
)
|
)
|
||||||
+
|
+
|
||||||
+s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry:\'{my_dict["foo"]}\''
|
+s = f"Lorem Ipsum is simply dummy text of the printing and typesetting industry:'{my_dict['foo']}'"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Ruff Output
|
## Ruff Output
|
||||||
|
@ -1839,10 +1839,10 @@ s = (
|
||||||
"With single quote: ' "
|
"With single quote: ' "
|
||||||
f" {my_dict['foo']}"
|
f" {my_dict['foo']}"
|
||||||
' With double quote: " '
|
' With double quote: " '
|
||||||
f' {my_dict["bar"]}'
|
f" {my_dict['bar']}"
|
||||||
)
|
)
|
||||||
|
|
||||||
s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry:\'{my_dict["foo"]}\''
|
s = f"Lorem Ipsum is simply dummy text of the printing and typesetting industry:'{my_dict['foo']}'"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Black Output
|
## Black Output
|
||||||
|
|
|
@ -100,6 +100,8 @@ x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 8
|
||||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ccccccccccccccc"
|
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ccccccccccccccc"
|
||||||
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9
|
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9
|
||||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc"
|
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc"
|
||||||
|
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9
|
||||||
|
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbb' = } ccccccccccccccc"
|
||||||
|
|
||||||
# Multiple larger expressions which exceeds the line length limit. Here, we need to decide
|
# 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
|
# whether to split at the first or second expression. This should work similarly to the
|
||||||
|
@ -150,18 +152,37 @@ xxxxxxx = f"{
|
||||||
{'aaaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'}
|
{'aaaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'}
|
||||||
}"
|
}"
|
||||||
|
|
||||||
|
#############################################################################################
|
||||||
# Quotes
|
# Quotes
|
||||||
|
#############################################################################################
|
||||||
f"foo 'bar' {x}"
|
f"foo 'bar' {x}"
|
||||||
f"foo \"bar\" {x}"
|
f"foo \"bar\" {x}"
|
||||||
f'foo "bar" {x}'
|
f'foo "bar" {x}'
|
||||||
f'foo \'bar\' {x}'
|
f'foo \'bar\' {x}'
|
||||||
f"foo {"bar"}"
|
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
|
# 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.
|
# 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\"'}"
|
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
|
# Triple-quoted strings
|
||||||
# It's ok to use the same quote char for the inner string if it's single-quoted.
|
# It's ok to use the same quote char for the inner string if it's single-quoted.
|
||||||
|
@ -170,6 +191,16 @@ f"""test {"inner"}"""
|
||||||
# But if the inner string is also triple-quoted then we should preserve the existing quotes.
|
# But if the inner string is also triple-quoted then we should preserve the existing quotes.
|
||||||
f"""test {'''inner'''}"""
|
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
|
# Magic trailing comma
|
||||||
#
|
#
|
||||||
# The expression formatting will result in breaking it across multiple lines with a
|
# The expression formatting will result in breaking it across multiple lines with a
|
||||||
|
@ -318,7 +349,7 @@ hello {
|
||||||
# Implicit concatenated f-string containing quotes
|
# Implicit concatenated f-string containing quotes
|
||||||
_ = (
|
_ = (
|
||||||
'This string should change its quotes to double 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.'
|
f'This f-string does not use any quotes.'
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
@ -429,13 +460,15 @@ aaaaaaaaaaa = (
|
||||||
|
|
||||||
# This should never add the optional parentheses because even after adding them, the
|
# This should never add the optional parentheses because even after adding them, the
|
||||||
# f-string exceeds the line length limit.
|
# f-string exceeds the line length limit.
|
||||||
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa {"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb"} ccccccccccccccc"
|
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa {'bbbbbbbbbbbbbbbbbbbbbbbbbbbbb'} ccccccccccccccc"
|
||||||
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc"
|
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc"
|
||||||
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 8
|
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 8
|
||||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
|
||||||
} ccccccccccccccc"
|
} ccccccccccccccc"
|
||||||
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9
|
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9
|
||||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc"
|
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc"
|
||||||
|
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9
|
||||||
|
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbb' = } ccccccccccccccc"
|
||||||
|
|
||||||
# Multiple larger expressions which exceeds the line length limit. Here, we need to decide
|
# 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
|
# whether to split at the first or second expression. This should work similarly to the
|
||||||
|
@ -496,18 +529,37 @@ xxxxxxx = f"{
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
|
|
||||||
|
#############################################################################################
|
||||||
# Quotes
|
# Quotes
|
||||||
|
#############################################################################################
|
||||||
f"foo 'bar' {x}"
|
f"foo 'bar' {x}"
|
||||||
f'foo "bar" {x}'
|
f'foo "bar" {x}'
|
||||||
f'foo "bar" {x}'
|
f'foo "bar" {x}'
|
||||||
f"foo 'bar' {x}"
|
f"foo 'bar' {x}"
|
||||||
f"foo {"bar"}"
|
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
|
||||||
|
|
||||||
|
rf"single quotes ' {x}" # Keep double because `'` can't be escaped
|
||||||
|
rf'double quotes " {x}' # Keep single because `"` can't be escaped
|
||||||
|
rf"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
|
# 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.
|
# 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"'}"
|
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
|
# Triple-quoted strings
|
||||||
# It's ok to use the same quote char for the inner string if it's single-quoted.
|
# It's ok to use the same quote char for the inner string if it's single-quoted.
|
||||||
|
@ -516,6 +568,16 @@ f"""test {"inner"}"""
|
||||||
# But if the inner string is also triple-quoted then we should preserve the existing quotes.
|
# But if the inner string is also triple-quoted then we should preserve the existing quotes.
|
||||||
f"""test {'''inner'''}"""
|
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
|
# Magic trailing comma
|
||||||
#
|
#
|
||||||
# The expression formatting will result in breaking it across multiple lines with a
|
# The expression formatting will result in breaking it across multiple lines with a
|
||||||
|
@ -662,7 +724,7 @@ hello {
|
||||||
# Implicit concatenated f-string containing quotes
|
# Implicit concatenated f-string containing quotes
|
||||||
_ = (
|
_ = (
|
||||||
"This string should change its quotes to double 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."
|
f"This f-string does not use any quotes."
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
@ -778,6 +840,8 @@ x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 8
|
||||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ccccccccccccccc"
|
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ccccccccccccccc"
|
||||||
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9
|
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9
|
||||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc"
|
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc"
|
||||||
|
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9
|
||||||
|
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbb' = } ccccccccccccccc"
|
||||||
|
|
||||||
# Multiple larger expressions which exceeds the line length limit. Here, we need to decide
|
# 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
|
# whether to split at the first or second expression. This should work similarly to the
|
||||||
|
@ -828,18 +892,37 @@ xxxxxxx = f"{
|
||||||
{'aaaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'}
|
{'aaaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'}
|
||||||
}"
|
}"
|
||||||
|
|
||||||
|
#############################################################################################
|
||||||
# Quotes
|
# Quotes
|
||||||
|
#############################################################################################
|
||||||
f"foo 'bar' {x}"
|
f"foo 'bar' {x}"
|
||||||
f'foo "bar" {x}'
|
f'foo "bar" {x}'
|
||||||
f'foo "bar" {x}'
|
f'foo "bar" {x}'
|
||||||
f"foo 'bar' {x}"
|
f"foo 'bar' {x}"
|
||||||
f"foo {"bar"}"
|
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
|
||||||
|
|
||||||
|
rf"single quotes ' {x}" # Keep double because `'` can't be escaped
|
||||||
|
rf'double quotes " {x}' # Keep single because `"` can't be escaped
|
||||||
|
rf"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
|
# 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.
|
# 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\"'}"
|
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
|
# Triple-quoted strings
|
||||||
# It's ok to use the same quote char for the inner string if it's single-quoted.
|
# It's ok to use the same quote char for the inner string if it's single-quoted.
|
||||||
|
@ -848,6 +931,16 @@ f"""test {"inner"}"""
|
||||||
# But if the inner string is also triple-quoted then we should preserve the existing quotes.
|
# But if the inner string is also triple-quoted then we should preserve the existing quotes.
|
||||||
f"""test {'''inner'''}"""
|
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
|
# Magic trailing comma
|
||||||
#
|
#
|
||||||
# The expression formatting will result in breaking it across multiple lines with a
|
# The expression formatting will result in breaking it across multiple lines with a
|
||||||
|
@ -994,7 +1087,7 @@ hello {
|
||||||
# Implicit concatenated f-string containing quotes
|
# Implicit concatenated f-string containing quotes
|
||||||
_ = (
|
_ = (
|
||||||
'This string should change its quotes to double 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.'
|
f'This f-string does not use any quotes.'
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
@ -1022,7 +1115,7 @@ _ = (
|
||||||
" f()\n"
|
" f()\n"
|
||||||
# XXX: The following line changes depending on whether the tests
|
# XXX: The following line changes depending on whether the tests
|
||||||
# are run through the interactive interpreter or with -m
|
# are run through the interactive interpreter or with -m
|
||||||
@@ -67,64 +67,72 @@
|
@@ -67,29 +67,31 @@
|
||||||
x = f"{a}"
|
x = f"{a}"
|
||||||
x = f"{
|
x = f"{
|
||||||
a = }"
|
a = }"
|
||||||
|
@ -1052,16 +1145,17 @@ _ = (
|
||||||
# This should never add the optional parentheses because even after adding them, the
|
# This should never add the optional parentheses because even after adding them, the
|
||||||
# f-string exceeds the line length limit.
|
# f-string exceeds the line length limit.
|
||||||
-x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ccccccccccccccc"
|
-x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ccccccccccccccc"
|
||||||
+x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa {"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb"} ccccccccccccccc"
|
+x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa {'bbbbbbbbbbbbbbbbbbbbbbbbbbbbb'} ccccccccccccccc"
|
||||||
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc"
|
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc"
|
||||||
-x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 8
|
-x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 8
|
||||||
- "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ccccccccccccccc"
|
- "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ccccccccccccccc"
|
||||||
+x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 8
|
+x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 8
|
||||||
+ "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
+ 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
|
||||||
+} ccccccccccccccc"
|
+} ccccccccccccccc"
|
||||||
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9
|
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9
|
||||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc"
|
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc"
|
||||||
|
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9
|
||||||
|
@@ -98,35 +100,41 @@
|
||||||
# Multiple larger expressions which exceeds the line length limit. Here, we need to decide
|
# 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
|
# whether to split at the first or second expression. This should work similarly to the
|
||||||
# assignment statement formatting where we split from right to left in preview mode.
|
# assignment statement formatting where we split from right to left in preview mode.
|
||||||
|
@ -1119,7 +1213,7 @@ _ = (
|
||||||
x = f"{ # comment 13
|
x = f"{ # comment 13
|
||||||
{'x': 1, 'y': 2} = }"
|
{'x': 1, 'y': 2} = }"
|
||||||
# But, if there's a format specifier or a conversion flag then we don't need to add
|
# But, if there's a format specifier or a conversion flag then we don't need to add
|
||||||
@@ -139,7 +147,11 @@
|
@@ -141,7 +149,11 @@
|
||||||
}"
|
}"
|
||||||
# And, split the expression itself because it exceeds the line length.
|
# And, split the expression itself because it exceeds the line length.
|
||||||
xxxxxxx = f"{
|
xxxxxxx = f"{
|
||||||
|
@ -1131,14 +1225,36 @@ _ = (
|
||||||
+ }
|
+ }
|
||||||
}"
|
}"
|
||||||
|
|
||||||
# Quotes
|
#############################################################################################
|
||||||
@@ -152,13 +164,13 @@
|
@@ -151,7 +163,7 @@
|
||||||
|
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
|
||||||
|
@@ -163,23 +175,23 @@
|
||||||
|
|
||||||
# Here, the formatter will remove the escapes which is correct because they aren't allowed
|
# 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.
|
# 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\"'}"
|
-f"foo {'\"bar\"'}"
|
||||||
|
+f"foo {"'bar'"}"
|
||||||
+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')}"
|
||||||
|
+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''')=}"""
|
||||||
|
f"""foo {10 + len('bar')=}""" # Fine to change the quotes because it uses triple quotes
|
||||||
|
|
||||||
# Triple-quoted strings
|
# Triple-quoted strings
|
||||||
# It's ok to use the same quote char for the inner string if it's single-quoted.
|
# It's ok to use the same quote char for the inner string if it's single-quoted.
|
||||||
|
@ -1148,7 +1264,16 @@ _ = (
|
||||||
# But if the inner string is also triple-quoted then we should preserve the existing quotes.
|
# But if the inner string is also triple-quoted then we should preserve the existing quotes.
|
||||||
f"""test {'''inner'''}"""
|
f"""test {'''inner'''}"""
|
||||||
|
|
||||||
@@ -171,63 +183,66 @@
|
@@ -190,7 +202,7 @@
|
||||||
|
f'{f"""other " """}'
|
||||||
|
|
||||||
|
# Not valid Pre 3.12
|
||||||
|
-f"""test {f'inner {'''inner inner'''}'}"""
|
||||||
|
+f"""test {f"inner {'''inner inner'''}"}"""
|
||||||
|
f"""test {f'''inner {"""inner inner"""}'''}"""
|
||||||
|
|
||||||
|
# Magic trailing comma
|
||||||
|
@@ -202,63 +214,66 @@
|
||||||
f"aaaaaaa {['aaaaaaaaaaaaaaa', 'bbbbbbbbbbbbb', 'ccccccccccccccccc', 'ddddddddddddddd', 'eeeeeeeeeeeeee']} aaaaaaa"
|
f"aaaaaaa {['aaaaaaaaaaaaaaa', 'bbbbbbbbbbbbb', 'ccccccccccccccccc', 'ddddddddddddddd', 'eeeeeeeeeeeeee']} aaaaaaa"
|
||||||
|
|
||||||
# And, if the trailing comma is already present, we still need to remove it.
|
# And, if the trailing comma is already present, we still need to remove it.
|
||||||
|
@ -1243,7 +1368,7 @@ _ = (
|
||||||
# comment} cccccccccc"""
|
# comment} cccccccccc"""
|
||||||
|
|
||||||
# Conversion flags
|
# Conversion flags
|
||||||
@@ -235,24 +250,21 @@
|
@@ -266,24 +281,21 @@
|
||||||
# This is not a valid Python code because of the additional whitespace between the `!`
|
# This is not a valid Python code because of the additional whitespace between the `!`
|
||||||
# and conversion type. But, our parser isn't strict about this. This should probably be
|
# and conversion type. But, our parser isn't strict about this. This should probably be
|
||||||
# removed once we have a strict parser.
|
# removed once we have a strict parser.
|
||||||
|
@ -1275,7 +1400,7 @@ _ = (
|
||||||
|
|
||||||
x = f"""
|
x = f"""
|
||||||
{ # comment 22
|
{ # comment 22
|
||||||
@@ -261,19 +273,19 @@
|
@@ -292,19 +304,19 @@
|
||||||
|
|
||||||
# Here, the debug expression is in a nested f-string so we should start preserving
|
# Here, the debug expression is in a nested f-string so we should start preserving
|
||||||
# whitespaces from that point onwards. This means we should format the outer f-string.
|
# whitespaces from that point onwards. This means we should format the outer f-string.
|
||||||
|
@ -1303,7 +1428,7 @@ _ = (
|
||||||
# comment 27
|
# comment 27
|
||||||
# comment 28
|
# comment 28
|
||||||
} woah {x}"
|
} woah {x}"
|
||||||
@@ -287,27 +299,27 @@
|
@@ -318,27 +330,27 @@
|
||||||
if indent2:
|
if indent2:
|
||||||
foo = f"""hello world
|
foo = f"""hello world
|
||||||
hello {
|
hello {
|
||||||
|
@ -1343,9 +1468,10 @@ _ = (
|
||||||
# Implicit concatenated f-string containing quotes
|
# Implicit concatenated f-string containing quotes
|
||||||
_ = (
|
_ = (
|
||||||
- 'This string should change its quotes to double quotes'
|
- 'This string should change its quotes to double quotes'
|
||||||
+ "This string should change its quotes to double quotes"
|
- f'This string uses double quotes in an expression {"it's a quote"}'
|
||||||
f'This string uses double quotes in an expression {"woah"}'
|
|
||||||
- f'This f-string does not use any quotes.'
|
- f'This f-string does not use any quotes.'
|
||||||
|
+ "This string should change its quotes to double quotes"
|
||||||
|
+ f"This string uses double quotes in an expression {"it's a quote"}"
|
||||||
+ f"This f-string does not use any quotes."
|
+ f"This f-string does not use any quotes."
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
|
@ -10,6 +10,17 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression
|
||||||
|
|
||||||
# Quotes reuse
|
# Quotes reuse
|
||||||
f"{'a'}"
|
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 " """}'
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Outputs
|
## Outputs
|
||||||
|
@ -35,6 +46,16 @@ source_type = Python
|
||||||
|
|
||||||
# Quotes reuse
|
# Quotes reuse
|
||||||
f"{'a'}"
|
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 " """}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,10 +63,22 @@ f"{'a'}"
|
||||||
```diff
|
```diff
|
||||||
--- Stable
|
--- Stable
|
||||||
+++ Preview
|
+++ Preview
|
||||||
@@ -3,4 +3,4 @@
|
@@ -6,11 +6,11 @@
|
||||||
# version isn't set.
|
f"{'a'}"
|
||||||
|
|
||||||
# Quotes reuse
|
# 312+, it's okay to change the outer quotes even when there's a debug expression using the same quotes
|
||||||
-f"{'a'}"
|
-f'foo {10 + len("bar")=}'
|
||||||
+f"{"a"}"
|
-f'''foo {10 + len("""bar""")=}'''
|
||||||
|
+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 " """}'
|
||||||
|
+f"{'''other " '''}"
|
||||||
|
+f"{'''other " ''' + 'more'}"
|
||||||
|
+f"{b'''other " '''}"
|
||||||
|
+f"{f'''other " '''}"
|
||||||
```
|
```
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue