Skip to content

Commit

Permalink
Handle f-strings with quoted debug expressions and triple quoted lite…
Browse files Browse the repository at this point in the history
…rals containing quotes
  • Loading branch information
MichaReiser committed Oct 23, 2024
1 parent c4bf7f6 commit 17642b4
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,27 @@
} eeeeeeeeeeeeeeeeeeeeeeeeeeeee" # inline


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

# Different triple quoted strings
f"{'''test'''}" f'{"""other"""}'

# Now with inner quotes
f"{'''test ' '''}" f'{"""other " """}'
f"{some_where_nested('''test ' ''')}" f'{"""other " """ + "more"}'
f"{b'''test ' '''}" f'{b"""other " """}'
f"{f'''test ' '''}" f'{f"""other " """}'

# debug expressions containing quotes
f"{10 + len('bar')=}" f"{10 + len('bar')=}"
f"{10 + len('bar')=}" f'no debug{10}' f"{10 + len('bar')=}"

# We can't savely merge this pre Python 3.12 without altering the debug expression.
f"{10 + len('bar')=}" f'{10 + len("bar")=}'


##############################################################################
# Don't join raw strings
##############################################################################
Expand Down
46 changes: 35 additions & 11 deletions crates/ruff_python_formatter/src/string/implicit.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use ruff_formatter::{format_args, write};
use std::borrow::Cow;

use ruff_formatter::{format_args, write, FormatContext};
use ruff_python_ast::str::Quote;
use ruff_python_ast::str_prefix::{
AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix,
};
use ruff_python_ast::{AnyStringFlags, FStringElement, StringFlags, StringLike, StringLikePart};
use ruff_text_size::{Ranged, TextRange};
use std::borrow::Cow;

use crate::comments::{leading_comments, trailing_comments};
use crate::expression::parentheses::in_parentheses_only_soft_line_break_or_space;
Expand All @@ -16,7 +17,10 @@ use crate::prelude::*;
use crate::preview::{
is_f_string_formatting_enabled, is_join_implicit_concatenated_string_enabled,
};
use crate::string::normalize::QuoteMetadata;
use crate::string::normalize::{
is_fstring_with_quoted_debug_expression,
is_fstring_with_triple_quoted_literal_expression_containing_quotes, QuoteMetadata,
};
use crate::string::{normalize_string, StringLikeExtensions, StringNormalizer, StringQuotes};

/// Formats any implicitly concatenated string. This could be any valid combination
Expand Down Expand Up @@ -117,6 +121,14 @@ impl<'a> FormatImplicitConcatenatedStringFlat<'a> {
return None;
}

let first_part = string.parts().next()?;

// The string is either a regular string, f-string, or bytes string.
let normalizer = StringNormalizer::from_context(context);

// Some if a part requires preserving its quotes.
let mut preserve_quotes_requirement: Option<Quote> = None;

// Early exit if it's known that this string can't be joined
for part in string.parts() {
// Similar to Black, don't collapse triple quoted and raw strings.
Expand Down Expand Up @@ -170,13 +182,29 @@ impl<'a> FormatImplicitConcatenatedStringFlat<'a> {
}) {
return None;
}

// Avoid invalid syntax for pre Python 312:
// * When joining parts that have debug expressions with quotes: `f"{10 + len('bar')=}" f'{10 + len("bar")=}'
// * When joining parts that contain triple quoted strings with quotes: `f"{'''test ' '''}" f'{"""other " """}'`
if !context.options().target_version().supports_pep_701() {
if is_fstring_with_quoted_debug_expression(fstring, context)
|| is_fstring_with_triple_quoted_literal_expression_containing_quotes(
fstring, context,
)
{
if preserve_quotes_requirement
.is_some_and(|quote| quote != part.flags().quote_style())
{
return None;
} else {
preserve_quotes_requirement = Some(part.flags().quote_style());
}
}
}
}
}

// The string is either a regular string, f-string, or bytes string.
let normalizer = StringNormalizer::from_context(context);

// TODO: Do we need to respect the quoting from an enclosing f-string?
let mut merged_quotes: Option<QuoteMetadata> = None;

// Only preserve the string type but disregard the `u` and `r` prefixes.
Expand All @@ -188,8 +216,6 @@ impl<'a> FormatImplicitConcatenatedStringFlat<'a> {
StringLike::FString(_) => AnyStringPrefix::Format(FStringPrefix::Regular),
};

let first_part = string.parts().next()?;

// Only determining the preferred quote for the first string is sufficient
// because we don't support joining triple quoted strings with non triple quoted strings.
let quote = if let Ok(preferred_quote) =
Expand All @@ -208,7 +234,7 @@ impl<'a> FormatImplicitConcatenatedStringFlat<'a> {

merged_quotes?.choose(preferred_quote)
} else {
// If the options is to preserve quotes, pick the quotes of the first string part.
// Use the quotes of the first part if the quotes should be preserved.
first_part.flags().quote_style()
};

Expand Down Expand Up @@ -242,8 +268,6 @@ impl Format<PyFormatContext<'_>> for FormatImplicitConcatenatedStringFlat<'_> {

write!(f, [self.flags.prefix(), quotes])?;

// TODO: FStrings when the f-string preview style is enabled???

let mut parts = self.string.parts().peekable();

// Trim implicit concatenated strings in docstring positions.
Expand Down
5 changes: 5 additions & 0 deletions crates/ruff_python_formatter/src/string/normalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ impl<'a, 'src> StringNormalizer<'a, 'src> {
/// The formatter should use the preferred quote style unless
/// it can't because the string contains the preferred quotes OR
/// it leads to more escaping.
///
/// Note: If you add more cases here where we return `QuoteStyle::Preserve`,
/// make sure to also add them to [`FormatImplicitConcatenatedStringFlat::new`].
pub(super) fn preferred_quote_style(&self, string: StringLikePart) -> QuoteStyle {
match self.quoting {
Quoting::Preserve => QuoteStyle::Preserve,
Expand Down Expand Up @@ -1008,8 +1011,10 @@ mod tests {
};
use ruff_python_ast::str_prefix::FStringPrefix;

use crate::string::normalize_string;
use crate::string::normalize_string;

use super::UnicodeEscape;
use super::UnicodeEscape;

#[test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,27 @@ joined
} eeeeeeeeeeeeeeeeeeeeeeeeeeeee" # inline
f"single quoted '{x}'" f'double quoted "{x}"' # Same number of quotes => use preferred quote style
f"single quote ' {x}" f'double quoted "{x}"' # More double quotes => use single quotes
f"single quoted '{x}'" f'double quote " {x}"' # More single quotes => use double quotes
# Different triple quoted strings
f"{'''test'''}" f'{"""other"""}'
# Now with inner quotes
f"{'''test ' '''}" f'{"""other " """}'
f"{some_where_nested('''test ' ''')}" f'{"""other " """ + "more"}'
f"{b'''test ' '''}" f'{b"""other " """}'
f"{f'''test ' '''}" f'{f"""other " """}'
# debug expressions containing quotes
f"{10 + len('bar')=}" f"{10 + len('bar')=}"
f"{10 + len('bar')=}" f'no debug{10}' f"{10 + len('bar')=}"
# We can't savely merge this pre Python 3.12 without altering the debug expression.
f"{10 + len('bar')=}" f'{10 + len("bar")=}'
##############################################################################
# Don't join raw strings
##############################################################################
Expand Down Expand Up @@ -357,6 +378,27 @@ aaaaaaaaaaa = (
) # inline
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
# Different triple quoted strings
f"{'''test'''}{'''other'''}"
# Now with inner quotes
f"{'''test ' '''}" f'{"""other " """}'
f"{some_where_nested('''test ' ''')}" f'{"""other " """ + "more"}'
f"{b'''test ' '''}" f'{b"""other " """}'
f"{f'''test ' '''}" f'{f"""other " """}'
# debug expressions containing quotes
f"{10 + len('bar')=}{10 + len('bar')=}"
f"{10 + len('bar')=}no debug{10}{10 + len('bar')=}"
# We can't savely merge this pre Python 3.12 without altering the debug expression.
f"{10 + len('bar')=}" f'{10 + len("bar")=}'
##############################################################################
# Don't join raw strings
##############################################################################
Expand Down

0 comments on commit 17642b4

Please sign in to comment.