mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-26 11:59:35 +00:00
Join implicit concatenated strings when they fit on a line (#13663)
This commit is contained in:
parent
e402e27a09
commit
73ee72b665
50 changed files with 3907 additions and 363 deletions
|
@ -1454,7 +1454,7 @@ impl<Context> std::fmt::Debug for Group<'_, Context> {
|
||||||
/// layout doesn't exceed the line width too, in which case it falls back to the flat layout.
|
/// layout doesn't exceed the line width too, in which case it falls back to the flat layout.
|
||||||
///
|
///
|
||||||
/// This IR is identical to the following [`best_fitting`] layout but is implemented as custom IR for
|
/// This IR is identical to the following [`best_fitting`] layout but is implemented as custom IR for
|
||||||
/// best performance.
|
/// better performance.
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use ruff_formatter::prelude::*;
|
/// # use ruff_formatter::prelude::*;
|
||||||
|
|
|
@ -139,7 +139,7 @@ where
|
||||||
/// # Ok(())
|
/// # Ok(())
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn inspect(&mut self, f: &mut Formatter<Context>) -> FormatResult<&[FormatElement]> {
|
pub fn inspect(&self, f: &mut Formatter<Context>) -> FormatResult<&[FormatElement]> {
|
||||||
let result = self.memory.get_or_init(|| f.intern(&self.inner));
|
let result = self.memory.get_or_init(|| f.intern(&self.inner));
|
||||||
|
|
||||||
match result.as_ref() {
|
match result.as_ref() {
|
||||||
|
|
|
@ -506,7 +506,7 @@ pub enum StringLikePart<'a> {
|
||||||
FString(&'a ast::FString),
|
FString(&'a ast::FString),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StringLikePart<'_> {
|
impl<'a> StringLikePart<'a> {
|
||||||
/// Returns the [`AnyStringFlags`] for the current string-like part.
|
/// Returns the [`AnyStringFlags`] for the current string-like part.
|
||||||
pub fn flags(&self) -> AnyStringFlags {
|
pub fn flags(&self) -> AnyStringFlags {
|
||||||
match self {
|
match self {
|
||||||
|
@ -525,6 +525,17 @@ impl StringLikePart<'_> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn is_string_literal(self) -> bool {
|
||||||
|
matches!(self, Self::String(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn as_string_literal(self) -> Option<&'a ast::StringLiteral> {
|
||||||
|
match self {
|
||||||
|
StringLikePart::String(value) => Some(value),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub const fn is_fstring(self) -> bool {
|
pub const fn is_fstring(self) -> bool {
|
||||||
matches!(self, Self::FString(_))
|
matches!(self, Self::FString(_))
|
||||||
}
|
}
|
||||||
|
@ -571,6 +582,7 @@ impl Ranged for StringLikePart<'_> {
|
||||||
/// An iterator over all the [`StringLikePart`] of a string-like expression.
|
/// An iterator over all the [`StringLikePart`] of a string-like expression.
|
||||||
///
|
///
|
||||||
/// This is created by the [`StringLike::parts`] method.
|
/// This is created by the [`StringLike::parts`] method.
|
||||||
|
#[derive(Clone)]
|
||||||
pub enum StringLikePartIter<'a> {
|
pub enum StringLikePartIter<'a> {
|
||||||
String(std::slice::Iter<'a, ast::StringLiteral>),
|
String(std::slice::Iter<'a, ast::StringLiteral>),
|
||||||
Bytes(std::slice::Iter<'a, ast::BytesLiteral>),
|
Bytes(std::slice::Iter<'a, ast::BytesLiteral>),
|
||||||
|
@ -607,5 +619,25 @@ impl<'a> Iterator for StringLikePartIter<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl DoubleEndedIterator for StringLikePartIter<'_> {
|
||||||
|
fn next_back(&mut self) -> Option<Self::Item> {
|
||||||
|
let part = match self {
|
||||||
|
StringLikePartIter::String(inner) => StringLikePart::String(inner.next_back()?),
|
||||||
|
StringLikePartIter::Bytes(inner) => StringLikePart::Bytes(inner.next_back()?),
|
||||||
|
StringLikePartIter::FString(inner) => {
|
||||||
|
let part = inner.next_back()?;
|
||||||
|
match part {
|
||||||
|
ast::FStringPart::Literal(string_literal) => {
|
||||||
|
StringLikePart::String(string_literal)
|
||||||
|
}
|
||||||
|
ast::FStringPart::FString(f_string) => StringLikePart::FString(f_string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl FusedIterator for StringLikePartIter<'_> {}
|
impl FusedIterator for StringLikePartIter<'_> {}
|
||||||
impl ExactSizeIterator for StringLikePartIter<'_> {}
|
impl ExactSizeIterator for StringLikePartIter<'_> {}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"preview": "enabled"
|
||||||
|
}
|
||||||
|
]
|
|
@ -0,0 +1,321 @@
|
||||||
|
"aaaaaaaaa" "bbbbbbbbbbbbbbbbbbbb" # Join
|
||||||
|
|
||||||
|
(
|
||||||
|
"aaaaaaaaaaa" "bbbbbbbbbbbbbbbb"
|
||||||
|
) # join
|
||||||
|
|
||||||
|
|
||||||
|
(
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||||
|
) # too long to join
|
||||||
|
|
||||||
|
|
||||||
|
"different '" 'quote "are fine"' # join
|
||||||
|
|
||||||
|
# More single quotes
|
||||||
|
"one single'" "two 'single'" ' two "double"'
|
||||||
|
|
||||||
|
# More double quotes
|
||||||
|
'one double"' 'two "double"' " two 'single'"
|
||||||
|
|
||||||
|
# Equal number of single and double quotes
|
||||||
|
'two "double"' " two 'single'"
|
||||||
|
|
||||||
|
f"{'Hy \"User\"'}" 'more'
|
||||||
|
|
||||||
|
b"aaaaaaaaa" b"bbbbbbbbbbbbbbbbbbbb" # Join
|
||||||
|
|
||||||
|
(
|
||||||
|
b"aaaaaaaaaaa" b"bbbbbbbbbbbbbbbb"
|
||||||
|
) # join
|
||||||
|
|
||||||
|
|
||||||
|
(
|
||||||
|
b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" b"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||||
|
) # too long to join
|
||||||
|
|
||||||
|
|
||||||
|
# Skip joining if there is a trailing comment
|
||||||
|
(
|
||||||
|
"fffffffffffff"
|
||||||
|
"bbbbbbbbbbbbb" # comment
|
||||||
|
"cccccccccccccc"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Skip joining if there is a leading comment
|
||||||
|
(
|
||||||
|
"fffffffffffff"
|
||||||
|
# comment
|
||||||
|
"bbbbbbbbbbbbb"
|
||||||
|
"cccccccccccccc"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# F-strings
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Escape `{` and `}` when marging an f-string with a string
|
||||||
|
"a {not_a_variable}" f"b {10}" "c"
|
||||||
|
|
||||||
|
# Join, and break expressions
|
||||||
|
f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{
|
||||||
|
expression
|
||||||
|
}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" f"cccccccccccccccccccc {20999}" "more"
|
||||||
|
|
||||||
|
# Join, but don't break the expressions
|
||||||
|
f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" f"cccccccccccccccccccc {20999}" "more"
|
||||||
|
|
||||||
|
f"test{
|
||||||
|
expression
|
||||||
|
}flat" f"can be {
|
||||||
|
joined
|
||||||
|
} together"
|
||||||
|
|
||||||
|
aaaaaaaaaaa = f"test{
|
||||||
|
expression
|
||||||
|
}flat" f"cean beeeeeeee {
|
||||||
|
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
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
r"a" "normal"
|
||||||
|
R"a" "normal"
|
||||||
|
|
||||||
|
f"test" fr"test"
|
||||||
|
f"test" fR"test"
|
||||||
|
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# Don't join triple quoted strings
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"single" """triple"""
|
||||||
|
|
||||||
|
"single" f""""single"""
|
||||||
|
|
||||||
|
b"single" b"""triple"""
|
||||||
|
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# Join strings in with statements
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Fits
|
||||||
|
with "aa" "bbb" "cccccccccccccccccccccccccccccccccccccccccccccc":
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Parenthesize single-line
|
||||||
|
with "aa" "bbb" "ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc":
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Multiline
|
||||||
|
with "aa" "bbb" "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc":
|
||||||
|
pass
|
||||||
|
|
||||||
|
with f"aaaaaaa{expression}bbbb" f"ccc {20999}" "more":
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# For loops
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Flat
|
||||||
|
for a in "aaaaaaaaa" "bbbbbbbbb" "ccccccccc" "dddddddddd":
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Parenthesize single-line
|
||||||
|
for a in "aaaaaaaaa" "bbbbbbbbb" "ccccccccc" "dddddddddd" "eeeeeeeeeeeeeee" "fffffffffffff" "ggggggggggggggg" "hh":
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Multiline
|
||||||
|
for a in "aaaaaaaaa" "bbbbbbbbb" "ccccccccc" "dddddddddd" "eeeeeeeeeeeeeee" "fffffffffffff" "ggggggggggggggg" "hhhh":
|
||||||
|
pass
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# Assert statement
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Fits
|
||||||
|
assert "aaaaaaaaa" "bbbbbbbbbbbb", "cccccccccccccccc" "dddddddddddddddd"
|
||||||
|
|
||||||
|
# Wrap right
|
||||||
|
assert "aaaaaaaaa" "bbbbbbbbbbbb", "cccccccccccccccc" "dddddddddddddddd" "eeeeeeeeeeeee" "fffffffffff"
|
||||||
|
|
||||||
|
# Right multiline
|
||||||
|
assert "aaaaaaaaa" "bbbbbbbbbbbb", "cccccccccccccccc" "dddddddddddddddd" "eeeeeeeeeeeee" "fffffffffffffff" "ggggggggggggg" "hhhhhhhhhhh"
|
||||||
|
|
||||||
|
# Wrap left
|
||||||
|
assert "aaaaaaaaa" "bbbbbbbbbbbb" "cccccccccccccccc" "dddddddddddddddd" "eeeeeeeeeeeee" "fffffffffffffff", "ggggggggggggg" "hhhhhhhhhhh"
|
||||||
|
|
||||||
|
# Left multiline
|
||||||
|
assert "aaaaaaaaa" "bbbbbbbbbbbb" "cccccccccccccccc" "dddddddddddddddd" "eeeeeeeeeeeee" "fffffffffffffff" "ggggggggggggg", "hhhhhhhhhhh"
|
||||||
|
|
||||||
|
# wrap both
|
||||||
|
assert "aaaaaaaaa" "bbbbbbbbbbbb" "cccccccccccccccc" "dddddddddddddddd" "eeeeeeeeeeeee" "fffffffffffffff", "ggggggggggggg" "hhhhhhhhhhh" "iiiiiiiiiiiiiiiiii" "jjjjjjjjjjjjj" "kkkkkkkkkkkkkkkkk" "llllllllllll"
|
||||||
|
|
||||||
|
# both multiline
|
||||||
|
assert "aaaaaaaaa" "bbbbbbbbbbbb" "cccccccccccccccc" "dddddddddddddddd" "eeeeeeeeeeeee" "fffffffffffffff" "ggggggggggggg", "hhhhhhhhhhh" "iiiiiiiiiiiiiiiiii" "jjjjjjjjjjjjj" "kkkkkkkkkkkkkkkkk" "llllllllllll" "mmmmmmmmmmmmmm"
|
||||||
|
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# In clause headers (can_omit_optional_parentheses)
|
||||||
|
##############################################################################
|
||||||
|
# Use can_omit_optional_parentheses layout to avoid an instability where the formatter
|
||||||
|
# picks the can_omit_optional_parentheses layout when the strings are joined.
|
||||||
|
if (
|
||||||
|
f"implicit"
|
||||||
|
"concatenated"
|
||||||
|
"string" + f"implicit"
|
||||||
|
"concaddddddddddded"
|
||||||
|
"ring"
|
||||||
|
* len([aaaaaa, bbbbbbbbbbbbbbbb, cccccccccccccccccc, ddddddddddddddddddddddddddd])
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Keep parenthesizing multiline - implicit concatenated strings
|
||||||
|
if (
|
||||||
|
f"implicit"
|
||||||
|
"""concatenate
|
||||||
|
d"""
|
||||||
|
"string" + f"implicit"
|
||||||
|
"concaddddddddddded"
|
||||||
|
"ring"
|
||||||
|
* len([aaaaaa, bbbbbbbbbbbbbbbb, cccccccccccccccccc, ddddddddddddddddddddddddddd])
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
aaaaaa,
|
||||||
|
bbbbbbbbbbbbbbbb,
|
||||||
|
cccccccccccccccccc,
|
||||||
|
ddddddddddddddddddddddddddd,
|
||||||
|
]
|
||||||
|
+ "implicitconcat"
|
||||||
|
"enatedstriiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiing"
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# In match statements
|
||||||
|
match x:
|
||||||
|
case "implicitconcat" "enatedstring" | [
|
||||||
|
aaaaaa,
|
||||||
|
bbbbbbbbbbbbbbbb,
|
||||||
|
cccccccccccccccccc,
|
||||||
|
ddddddddddddddddddddddddddd,
|
||||||
|
]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
case [
|
||||||
|
aaaaaa,
|
||||||
|
bbbbbbbbbbbbbbbb,
|
||||||
|
cccccccccccccccccc,
|
||||||
|
ddddddddddddddddddddddddddd,
|
||||||
|
] | "implicitconcat" "enatedstring" :
|
||||||
|
pass
|
||||||
|
|
||||||
|
case "implicitconcat" "enatedstriiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiing" | [
|
||||||
|
aaaaaa,
|
||||||
|
bbbbbbbbbbbbbbbb,
|
||||||
|
cccccccccccccccccc,
|
||||||
|
ddddddddddddddddddddddddddd,
|
||||||
|
]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# In docstring positions
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
def short_docstring():
|
||||||
|
"Implicit" "concatenated" "docstring"
|
||||||
|
|
||||||
|
def long_docstring():
|
||||||
|
"Loooooooooooooooooooooong" "doooooooooooooooooooocstriiiiiiiiiiiiiiiiiiiiiiiiiiiiiiing" "exceding the line width" "but it should be concatenated anyways because it is single line"
|
||||||
|
|
||||||
|
def docstring_with_leading_whitespace():
|
||||||
|
" This is a " "implicit" "concatenated" "docstring"
|
||||||
|
|
||||||
|
def docstring_with_trailing_whitespace():
|
||||||
|
"This is a " "implicit" "concatenated" "docstring "
|
||||||
|
|
||||||
|
def docstring_with_leading_empty_parts():
|
||||||
|
" " " " "" "This is a " "implicit" "concatenated" "docstring"
|
||||||
|
|
||||||
|
def docstring_with_trailing_empty_parts():
|
||||||
|
"This is a " "implicit" "concatenated" "docstring" "" " " " "
|
||||||
|
|
||||||
|
def all_empty():
|
||||||
|
" " " " " "
|
||||||
|
|
||||||
|
def byte_string_in_docstring_position():
|
||||||
|
b" don't trim the" b"bytes literal "
|
||||||
|
|
||||||
|
def f_string_in_docstring_position():
|
||||||
|
f" don't trim the" "f-string literal "
|
||||||
|
|
||||||
|
def single_quoted():
|
||||||
|
' content\ ' ' '
|
||||||
|
return
|
||||||
|
|
||||||
|
def implicit_with_comment():
|
||||||
|
(
|
||||||
|
"a"
|
||||||
|
# leading
|
||||||
|
"the comment above"
|
||||||
|
)
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# Regressions
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
LEEEEEEEEEEEEEEEEEEEEEEFT = RRRRRRRRIIIIIIIIIIIIGGGGGHHHT | {
|
||||||
|
"entityNameeeeeeeeeeeeeeeeee", # comment must be long enough to
|
||||||
|
"some long implicit concatenated string" "that should join"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure that flipping between Multiline and BestFit layout results in stable formatting
|
||||||
|
# when using IfBreaksParenthesized layout.
|
||||||
|
assert False, "Implicit concatenated string" "uses {} layout on {} format".format(
|
||||||
|
"Multiline", "first"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert False, await "Implicit concatenated string" "uses {} layout on {} format".format(
|
||||||
|
"Multiline", "first"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert False, "Implicit concatenated stringuses {} layout on {} format"[
|
||||||
|
aaaaaaaaa, bbbbbb
|
||||||
|
]
|
||||||
|
|
||||||
|
assert False, +"Implicit concatenated string" "uses {} layout on {} format".format(
|
||||||
|
"Multiline", "first"
|
||||||
|
)
|
|
@ -0,0 +1,5 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"preview": "enabled"
|
||||||
|
}
|
||||||
|
]
|
|
@ -0,0 +1,293 @@
|
||||||
|
## Implicit concatenated strings with a trailing comment but a non splittable target.
|
||||||
|
|
||||||
|
# Don't join the string because the joined string with the inlined comment exceeds the line length limit.
|
||||||
|
____aaa = (
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvv"
|
||||||
|
) # c
|
||||||
|
|
||||||
|
# This is the same string as above and should lead to the same formatting. The only difference is that we start
|
||||||
|
# with an unparenthesized string.
|
||||||
|
____aaa = "aaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvv" # c
|
||||||
|
|
||||||
|
# Again the same string as above but this time as non-implicit concatenated string.
|
||||||
|
# It's okay if the formatting differs because it's an explicit choice to use implicit concatenation.
|
||||||
|
____aaa = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvv" # c
|
||||||
|
|
||||||
|
# Join the string because it's exactly in the line length limit when the comment is inlined.
|
||||||
|
____aaa = (
|
||||||
|
"aaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvv"
|
||||||
|
) # c
|
||||||
|
|
||||||
|
# This is the same string as above and should lead to the same formatting. The only difference is that we start
|
||||||
|
# with an unparenthesized string.
|
||||||
|
____aaa = "aaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvv" # c
|
||||||
|
|
||||||
|
# Again the same string as above but as a non-implicit concatenated string. It should result in the same formatting
|
||||||
|
# (for consistency).
|
||||||
|
____aaa = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvv" # c
|
||||||
|
|
||||||
|
# It should collapse the parentheses if the joined string and the comment fit on the same line.
|
||||||
|
# This is required for stability.
|
||||||
|
____aaa = (
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvv" # c
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# Assignments where the target or annotations are splittable
|
||||||
|
#############################################################
|
||||||
|
|
||||||
|
|
||||||
|
# The target splits because of a magic trailing comma
|
||||||
|
# The string is joined and not parenthesized because it just fits into the line length (including comment).
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b,
|
||||||
|
] = "ccccccccccccccccccccccccccccc" "cccccccccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
|
||||||
|
# Same but starting with a joined string. They should both result in the same formatting.
|
||||||
|
[
|
||||||
|
aaaaaaa,
|
||||||
|
b,
|
||||||
|
] = "ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
|
||||||
|
# The target splits because of the magic trailing comma
|
||||||
|
# The string is **not** joined because it with the inlined comment exceeds the line length limit.
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b,
|
||||||
|
] = "ccccccccccccccccccccccccccccc" "ccccccccccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
|
||||||
|
|
||||||
|
# The target should be flat
|
||||||
|
# The string should be joined because it fits into the line length
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b
|
||||||
|
] = (
|
||||||
|
"ccccccccccccccccccccccccccccccccccc" "cccccccccccccccccccccccc" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
# Same but starting with a joined string. They should both result in the same formatting.
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b
|
||||||
|
] = "ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
|
||||||
|
# The target should be flat
|
||||||
|
# The string gets parenthesized because it, with the inlined comment, exceeds the line length limit.
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b
|
||||||
|
] = "ccccccccccccccccccccccccccccc" "ccccccccccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
|
||||||
|
|
||||||
|
# Split an overlong target, but join the string if it fits
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b
|
||||||
|
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
|
||||||
|
"ccccccccccccccccccccccccccccccccccccccccc" "cccccccccccccccccccccccccccccc" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
# Split both if necessary and keep multiline
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b
|
||||||
|
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
|
||||||
|
"ccccccccccccccccccccccccccccccccccccccccc" "ccccccccccccccccccccccccccccccc" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
#########################################################
|
||||||
|
# Leading or trailing own line comments:
|
||||||
|
# Preserve the parentheses
|
||||||
|
########################################################
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b
|
||||||
|
] = (
|
||||||
|
# test
|
||||||
|
"ccccccccccccccccccccccccccccc" "ccccccccccccccccccccccccccccccccccccccccccc"
|
||||||
|
)
|
||||||
|
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b
|
||||||
|
] = (
|
||||||
|
"ccccccccccccccccccccccccccccc" "ccccccccccccccccccccccccccccccccccccccccccc"
|
||||||
|
# test
|
||||||
|
)
|
||||||
|
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b
|
||||||
|
] = (
|
||||||
|
"ccccccccccccccccccccccccccccccccccccccccc" "ccccccccccccccccccccccccccccccccccccccccccc"
|
||||||
|
# test
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# Type alias statements
|
||||||
|
#############################################################
|
||||||
|
|
||||||
|
# First break the right, join the string
|
||||||
|
type A[str, int, number] = "Literal[string, int] | None | " "CustomType" "| OtherCustomTypeExcee" # comment
|
||||||
|
|
||||||
|
# Keep multiline if overlong
|
||||||
|
type A[str, int, number] = "Literal[string, int] | None | " "CustomTypeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" # comment
|
||||||
|
|
||||||
|
# Break the left if it is over-long, join the string
|
||||||
|
type Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[stringgggggggggg, inttttttttttttttttttttttt, number] = "Literal[string, int] | None | " "CustomType" # comment
|
||||||
|
|
||||||
|
# Break both if necessary and keep multiline
|
||||||
|
type Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[stringgggggggggg, inttttttttttttttttttttttt, number] = "Literal[string, int] | None | " "CustomTypeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" # comment
|
||||||
|
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# F-Strings
|
||||||
|
#############################################################
|
||||||
|
|
||||||
|
# Flatten and join the f-string
|
||||||
|
aaaaaaaaaaa = f"test{
|
||||||
|
expression}flat" f"cean beeeeeeee {joined} eeeeeeeeeeeeeeeee" # inline
|
||||||
|
|
||||||
|
# Parenthesize the value and join it, inline the comment
|
||||||
|
aaaaaaaaaaa = f"test{
|
||||||
|
expression}flat" f"cean beeeeeeee {joined} eeeeeeeeeeeeeeeeeeeeeeeeeee" # inline
|
||||||
|
|
||||||
|
# Parenthesize the f-string and keep it multiline because it doesn't fit on a single line including the comment
|
||||||
|
aaaaaaaaaaa = f"test{
|
||||||
|
expression
|
||||||
|
}flat" f"cean beeeeeeee {
|
||||||
|
joined
|
||||||
|
} eeeeeeeeeeeeeeeeeeeeeeeeeeeee" # inline
|
||||||
|
|
||||||
|
|
||||||
|
# The target splits because of a magic trailing comma
|
||||||
|
# The string is joined and not parenthesized because it just fits into the line length (including comment).
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b,
|
||||||
|
] = f"ccccc{
|
||||||
|
expression}ccccccccccc" f"cccccccccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
|
||||||
|
|
||||||
|
# Same but starting with a joined string. They should both result in the same formatting.
|
||||||
|
[
|
||||||
|
aaaaaaa,
|
||||||
|
b,
|
||||||
|
] = f"ccccc{
|
||||||
|
expression}ccccccccccccccccccccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
|
||||||
|
# The target splits because of the magic trailing comma
|
||||||
|
# The string is **not** joined because it with the inlined comment exceeds the line length limit.
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b,
|
||||||
|
] = f"ccccc{
|
||||||
|
expression}cccccccccccccccccccc" f"cccccccccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
|
||||||
|
|
||||||
|
# The target should be flat
|
||||||
|
# The string should be joined because it fits into the line length
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b
|
||||||
|
] = (
|
||||||
|
f"ccccc{
|
||||||
|
expression}ccccccccccc" "cccccccccccccccccccccccc" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
# Same but starting with a joined string. They should both result in the same formatting.
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b
|
||||||
|
] = f"ccccc{
|
||||||
|
expression}ccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
|
||||||
|
# The target should be flat
|
||||||
|
# The string gets parenthesized because it, with the inlined comment, exceeds the line length limit.
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b
|
||||||
|
] = f"ccccc{
|
||||||
|
expression}ccccccccccc" "ccccccccccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
|
||||||
|
|
||||||
|
# Split an overlong target, but join the string if it fits
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b
|
||||||
|
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
|
||||||
|
f"ccccc{
|
||||||
|
expression}ccccccccccc" "cccccccccccccccccccccccccccccc" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
# Split both if necessary and keep multiline
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b
|
||||||
|
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
|
||||||
|
f"ccccc{
|
||||||
|
expression}cccccccccccccccccccccccccccccccc" "ccccccccccccccccccccccccccccccc" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
# Don't inline f-strings that contain expressions that are guaranteed to split, e.b. because of a magic trailing comma
|
||||||
|
aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
|
||||||
|
[a,]
|
||||||
|
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
|
||||||
|
|
||||||
|
aaaaaaaaaaaaaaaaaa = (
|
||||||
|
f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
|
||||||
|
[a,]
|
||||||
|
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
aaaaa[aaaaaaaaaaa] = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
|
||||||
|
[a,]
|
||||||
|
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
|
||||||
|
|
||||||
|
aaaaa[aaaaaaaaaaa] = (f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
|
||||||
|
[a,]
|
||||||
|
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
# Don't inline f-strings that contain commented expressions
|
||||||
|
aaaaaaaaaaaaaaaaaa = (
|
||||||
|
f"testeeeeeeeeeeeeeeeeeeeeeeeee{[
|
||||||
|
a # comment
|
||||||
|
]}" "moreeeeeeeeeeeeeeeeeetest" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
aaaaa[aaaaaaaaaaa] = (
|
||||||
|
f"testeeeeeeeeeeeeeeeeeeeeeeeee{[
|
||||||
|
a # comment
|
||||||
|
]}" "moreeeeeeeeeeeeeeeeeetest" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
# Don't inline f-strings with multiline debug expressions:
|
||||||
|
aaaaaaaaaaaaaaaaaa = (
|
||||||
|
f"testeeeeeeeeeeeeeeeeeeeeeeeee{
|
||||||
|
a=}" "moreeeeeeeeeeeeeeeeeetest" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
aaaaaaaaaaaaaaaaaa = (
|
||||||
|
f"testeeeeeeeeeeeeeeeeeeeeeeeee{a +
|
||||||
|
b=}" "moreeeeeeeeeeeeeeeeeetest" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
aaaaaaaaaaaaaaaaaa = (
|
||||||
|
f"testeeeeeeeeeeeeeeeeeeeeeeeee{a
|
||||||
|
=}" "moreeeeeeeeeeeeeeeeeetest" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
aaaaa[aaaaaaaaaaa] = (
|
||||||
|
f"testeeeeeeeeeeeeeeeeeeeeeeeee{
|
||||||
|
a=}" "moreeeeeeeeeeeeeeeeeetest" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
aaaaa[aaaaaaaaaaa] = (
|
||||||
|
f"testeeeeeeeeeeeeeeeeeeeeeeeee{a
|
||||||
|
=}" "moreeeeeeeeeeeeeeeeeetest" # comment
|
||||||
|
)
|
|
@ -0,0 +1,11 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"quote_style": "preserve",
|
||||||
|
"preview": "enabled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"quote_style": "preserve",
|
||||||
|
"preview": "enabled",
|
||||||
|
"target_version": "py312"
|
||||||
|
}
|
||||||
|
]
|
|
@ -0,0 +1,13 @@
|
||||||
|
a = "different '" 'quote "are fine"' # join
|
||||||
|
|
||||||
|
# More single quotes
|
||||||
|
"one single'" "two 'single'" ' two "double"'
|
||||||
|
|
||||||
|
# More double quotes
|
||||||
|
'one double"' 'two "double"' " two 'single'"
|
||||||
|
|
||||||
|
# Equal number of single and double quotes
|
||||||
|
'two "double"' " two 'single'"
|
||||||
|
|
||||||
|
# Already invalid Pre Python 312
|
||||||
|
f"{'Hy "User"'}" f'{"Hy 'User'"}'
|
|
@ -431,6 +431,41 @@ impl<'a> Comments<'a> {
|
||||||
pub(crate) fn debug(&'a self, source_code: SourceCode<'a>) -> DebugComments<'a> {
|
pub(crate) fn debug(&'a self, source_code: SourceCode<'a>) -> DebugComments<'a> {
|
||||||
DebugComments::new(&self.data.comments, source_code)
|
DebugComments::new(&self.data.comments, source_code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if the node itself or any of its descendants have comments.
|
||||||
|
pub(crate) fn contains_comments(&self, node: AnyNodeRef) -> bool {
|
||||||
|
use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal};
|
||||||
|
|
||||||
|
struct Visitor<'a> {
|
||||||
|
comments: &'a Comments<'a>,
|
||||||
|
has_comment: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> SourceOrderVisitor<'a> for Visitor<'a> {
|
||||||
|
fn enter_node(&mut self, node: AnyNodeRef<'a>) -> TraversalSignal {
|
||||||
|
if self.has_comment {
|
||||||
|
TraversalSignal::Skip
|
||||||
|
} else if self.comments.has(node) {
|
||||||
|
self.has_comment = true;
|
||||||
|
TraversalSignal::Skip
|
||||||
|
} else {
|
||||||
|
TraversalSignal::Traverse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.has(node) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut visitor = Visitor {
|
||||||
|
comments: self,
|
||||||
|
has_comment: false,
|
||||||
|
};
|
||||||
|
node.visit_preorder(&mut visitor);
|
||||||
|
|
||||||
|
visitor.has_comment
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) type LeadingDanglingTrailingComments<'a> = LeadingDanglingTrailing<'a, SourceComment>;
|
pub(crate) type LeadingDanglingTrailingComments<'a> = LeadingDanglingTrailing<'a, SourceComment>;
|
||||||
|
|
|
@ -8,7 +8,6 @@ use ruff_source_file::Locator;
|
||||||
use std::fmt::{Debug, Formatter};
|
use std::fmt::{Debug, Formatter};
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct PyFormatContext<'a> {
|
pub struct PyFormatContext<'a> {
|
||||||
options: PyFormatOptions,
|
options: PyFormatOptions,
|
||||||
contents: &'a str,
|
contents: &'a str,
|
||||||
|
@ -52,7 +51,6 @@ impl<'a> PyFormatContext<'a> {
|
||||||
self.contents
|
self.contents
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
pub(crate) fn locator(&self) -> Locator<'a> {
|
pub(crate) fn locator(&self) -> Locator<'a> {
|
||||||
Locator::new(self.contents)
|
Locator::new(self.contents)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ use crate::expression::parentheses::{
|
||||||
};
|
};
|
||||||
use crate::expression::OperatorPrecedence;
|
use crate::expression::OperatorPrecedence;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::string::FormatImplicitConcatenatedString;
|
use crate::string::implicit::FormatImplicitConcatenatedString;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub(super) enum BinaryLike<'a> {
|
pub(super) enum BinaryLike<'a> {
|
||||||
|
|
|
@ -5,7 +5,8 @@ use crate::expression::parentheses::{
|
||||||
in_parentheses_only_group, NeedsParentheses, OptionalParentheses,
|
in_parentheses_only_group, NeedsParentheses, OptionalParentheses,
|
||||||
};
|
};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::string::{FormatImplicitConcatenatedString, StringLikeExtensions};
|
use crate::string::implicit::FormatImplicitConcatenatedStringFlat;
|
||||||
|
use crate::string::{implicit::FormatImplicitConcatenatedString, StringLikeExtensions};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct FormatExprBytesLiteral;
|
pub struct FormatExprBytesLiteral;
|
||||||
|
@ -14,9 +15,19 @@ impl FormatNodeRule<ExprBytesLiteral> for FormatExprBytesLiteral {
|
||||||
fn fmt_fields(&self, item: &ExprBytesLiteral, f: &mut PyFormatter) -> FormatResult<()> {
|
fn fmt_fields(&self, item: &ExprBytesLiteral, f: &mut PyFormatter) -> FormatResult<()> {
|
||||||
let ExprBytesLiteral { value, .. } = item;
|
let ExprBytesLiteral { value, .. } = item;
|
||||||
|
|
||||||
match value.as_slice() {
|
if let [bytes_literal] = value.as_slice() {
|
||||||
[bytes_literal] => bytes_literal.format().fmt(f),
|
bytes_literal.format().fmt(f)
|
||||||
_ => in_parentheses_only_group(&FormatImplicitConcatenatedString::new(item)).fmt(f),
|
} else {
|
||||||
|
// Always join byte literals that aren't parenthesized and thus, always on a single line.
|
||||||
|
if !f.context().node_level().is_parenthesized() {
|
||||||
|
if let Some(format_flat) =
|
||||||
|
FormatImplicitConcatenatedStringFlat::new(item.into(), f.context())
|
||||||
|
{
|
||||||
|
return format_flat.fmt(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
in_parentheses_only_group(&FormatImplicitConcatenatedString::new(item)).fmt(f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,8 @@ use crate::expression::parentheses::{
|
||||||
};
|
};
|
||||||
use crate::other::f_string_part::FormatFStringPart;
|
use crate::other::f_string_part::FormatFStringPart;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::string::{FormatImplicitConcatenatedString, Quoting, StringLikeExtensions};
|
use crate::string::implicit::FormatImplicitConcatenatedStringFlat;
|
||||||
|
use crate::string::{implicit::FormatImplicitConcatenatedString, Quoting, StringLikeExtensions};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct FormatExprFString;
|
pub struct FormatExprFString;
|
||||||
|
@ -16,13 +17,23 @@ impl FormatNodeRule<ExprFString> for FormatExprFString {
|
||||||
fn fmt_fields(&self, item: &ExprFString, f: &mut PyFormatter) -> FormatResult<()> {
|
fn fmt_fields(&self, item: &ExprFString, f: &mut PyFormatter) -> FormatResult<()> {
|
||||||
let ExprFString { value, .. } = item;
|
let ExprFString { value, .. } = item;
|
||||||
|
|
||||||
match value.as_slice() {
|
if let [f_string_part] = value.as_slice() {
|
||||||
[f_string_part] => FormatFStringPart::new(
|
FormatFStringPart::new(
|
||||||
f_string_part,
|
f_string_part,
|
||||||
f_string_quoting(item, &f.context().locator()),
|
f_string_quoting(item, &f.context().locator()),
|
||||||
)
|
)
|
||||||
.fmt(f),
|
.fmt(f)
|
||||||
_ => in_parentheses_only_group(&FormatImplicitConcatenatedString::new(item)).fmt(f),
|
} else {
|
||||||
|
// Always join fstrings that aren't parenthesized and thus, are always on a single line.
|
||||||
|
if !f.context().node_level().is_parenthesized() {
|
||||||
|
if let Some(format_flat) =
|
||||||
|
FormatImplicitConcatenatedStringFlat::new(item.into(), f.context())
|
||||||
|
{
|
||||||
|
return format_flat.fmt(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
in_parentheses_only_group(&FormatImplicitConcatenatedString::new(item)).fmt(f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,6 +46,7 @@ impl NeedsParentheses for ExprFString {
|
||||||
) -> OptionalParentheses {
|
) -> OptionalParentheses {
|
||||||
if self.value.is_implicit_concatenated() {
|
if self.value.is_implicit_concatenated() {
|
||||||
OptionalParentheses::Multiline
|
OptionalParentheses::Multiline
|
||||||
|
}
|
||||||
// TODO(dhruvmanila): Ideally what we want here is a new variant which
|
// TODO(dhruvmanila): Ideally what we want here is a new variant which
|
||||||
// is something like:
|
// is something like:
|
||||||
// - If the expression fits by just adding the parentheses, then add them and
|
// - If the expression fits by just adding the parentheses, then add them and
|
||||||
|
@ -53,7 +65,7 @@ impl NeedsParentheses for ExprFString {
|
||||||
// ```
|
// ```
|
||||||
// This isn't decided yet, refer to the relevant discussion:
|
// This isn't decided yet, refer to the relevant discussion:
|
||||||
// https://github.com/astral-sh/ruff/discussions/9785
|
// https://github.com/astral-sh/ruff/discussions/9785
|
||||||
} else if StringLike::FString(self).is_multiline(context.source()) {
|
else if StringLike::FString(self).is_multiline(context.source()) {
|
||||||
OptionalParentheses::Never
|
OptionalParentheses::Never
|
||||||
} else {
|
} else {
|
||||||
OptionalParentheses::BestFit
|
OptionalParentheses::BestFit
|
||||||
|
|
|
@ -6,7 +6,8 @@ use crate::expression::parentheses::{
|
||||||
};
|
};
|
||||||
use crate::other::string_literal::StringLiteralKind;
|
use crate::other::string_literal::StringLiteralKind;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::string::{FormatImplicitConcatenatedString, StringLikeExtensions};
|
use crate::string::implicit::FormatImplicitConcatenatedStringFlat;
|
||||||
|
use crate::string::{implicit::FormatImplicitConcatenatedString, StringLikeExtensions};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct FormatExprStringLiteral {
|
pub struct FormatExprStringLiteral {
|
||||||
|
@ -26,16 +27,20 @@ impl FormatNodeRule<ExprStringLiteral> for FormatExprStringLiteral {
|
||||||
fn fmt_fields(&self, item: &ExprStringLiteral, f: &mut PyFormatter) -> FormatResult<()> {
|
fn fmt_fields(&self, item: &ExprStringLiteral, f: &mut PyFormatter) -> FormatResult<()> {
|
||||||
let ExprStringLiteral { value, .. } = item;
|
let ExprStringLiteral { value, .. } = item;
|
||||||
|
|
||||||
match value.as_slice() {
|
if let [string_literal] = value.as_slice() {
|
||||||
[string_literal] => string_literal.format().with_options(self.kind).fmt(f),
|
string_literal.format().with_options(self.kind).fmt(f)
|
||||||
_ => {
|
} else {
|
||||||
// This is just a sanity check because [`DocstringStmt::try_from_statement`]
|
// Always join strings that aren't parenthesized and thus, always on a single line.
|
||||||
// ensures that the docstring is a *single* string literal.
|
if !f.context().node_level().is_parenthesized() {
|
||||||
assert!(!self.kind.is_docstring());
|
if let Some(mut format_flat) =
|
||||||
|
FormatImplicitConcatenatedStringFlat::new(item.into(), f.context())
|
||||||
in_parentheses_only_group(&FormatImplicitConcatenatedString::new(item))
|
{
|
||||||
|
format_flat.set_docstring(self.kind.is_docstring());
|
||||||
|
return format_flat.fmt(f);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.fmt(f),
|
|
||||||
|
in_parentheses_only_group(&FormatImplicitConcatenatedString::new(item)).fmt(f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ use crate::expression::parentheses::{
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::preview::{
|
use crate::preview::{
|
||||||
is_empty_parameters_no_unnecessary_parentheses_around_return_value_enabled,
|
is_empty_parameters_no_unnecessary_parentheses_around_return_value_enabled,
|
||||||
|
is_f_string_implicit_concatenated_string_literal_quotes_enabled,
|
||||||
is_hug_parens_with_braces_and_square_brackets_enabled,
|
is_hug_parens_with_braces_and_square_brackets_enabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -405,38 +406,39 @@ impl Format<PyFormatContext<'_>> for MaybeParenthesizeExpression<'_> {
|
||||||
needs_parentheses => needs_parentheses,
|
needs_parentheses => needs_parentheses,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let unparenthesized = expression.format().with_options(Parentheses::Never);
|
||||||
|
|
||||||
match needs_parentheses {
|
match needs_parentheses {
|
||||||
OptionalParentheses::Multiline => match parenthesize {
|
OptionalParentheses::Multiline => match parenthesize {
|
||||||
|
|
||||||
Parenthesize::IfBreaksParenthesized | Parenthesize::IfBreaksParenthesizedNested if !is_empty_parameters_no_unnecessary_parentheses_around_return_value_enabled(f.context()) => {
|
Parenthesize::IfBreaksParenthesized | Parenthesize::IfBreaksParenthesizedNested if !is_empty_parameters_no_unnecessary_parentheses_around_return_value_enabled(f.context()) => {
|
||||||
parenthesize_if_expands(&expression.format().with_options(Parentheses::Never))
|
parenthesize_if_expands(&unparenthesized).fmt(f)
|
||||||
.fmt(f)
|
|
||||||
}
|
|
||||||
Parenthesize::IfRequired => {
|
|
||||||
expression.format().with_options(Parentheses::Never).fmt(f)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Parenthesize::IfRequired => unparenthesized.fmt(f),
|
||||||
|
|
||||||
Parenthesize::Optional | Parenthesize::IfBreaks | Parenthesize::IfBreaksParenthesized | Parenthesize::IfBreaksParenthesizedNested => {
|
Parenthesize::Optional | Parenthesize::IfBreaks | Parenthesize::IfBreaksParenthesized | Parenthesize::IfBreaksParenthesizedNested => {
|
||||||
if can_omit_optional_parentheses(expression, f.context()) {
|
if can_omit_optional_parentheses(expression, f.context()) {
|
||||||
optional_parentheses(&expression.format().with_options(Parentheses::Never))
|
optional_parentheses(&unparenthesized).fmt(f)
|
||||||
.fmt(f)
|
|
||||||
} else {
|
} else {
|
||||||
parenthesize_if_expands(
|
parenthesize_if_expands(&unparenthesized).fmt(f)
|
||||||
&expression.format().with_options(Parentheses::Never),
|
|
||||||
)
|
|
||||||
.fmt(f)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
OptionalParentheses::BestFit => match parenthesize {
|
OptionalParentheses::BestFit => match parenthesize {
|
||||||
|
Parenthesize::IfBreaksParenthesized | Parenthesize::IfBreaksParenthesizedNested if !is_empty_parameters_no_unnecessary_parentheses_around_return_value_enabled(f.context()) =>
|
||||||
|
parenthesize_if_expands(&unparenthesized).fmt(f),
|
||||||
|
|
||||||
Parenthesize::IfBreaksParenthesized | Parenthesize::IfBreaksParenthesizedNested => {
|
Parenthesize::IfBreaksParenthesized | Parenthesize::IfBreaksParenthesizedNested => {
|
||||||
parenthesize_if_expands(&expression.format().with_options(Parentheses::Never))
|
// Can-omit layout is relevant for `"abcd".call`. We don't want to add unnecessary
|
||||||
.fmt(f)
|
// parentheses in this case.
|
||||||
|
if can_omit_optional_parentheses(expression, f.context()) {
|
||||||
|
optional_parentheses(&unparenthesized).fmt(f)
|
||||||
|
} else {
|
||||||
|
parenthesize_if_expands(&unparenthesized).fmt(f)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Parenthesize::Optional | Parenthesize::IfRequired => {
|
Parenthesize::Optional | Parenthesize::IfRequired => unparenthesized.fmt(f),
|
||||||
expression.format().with_options(Parentheses::Never).fmt(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
Parenthesize::IfBreaks => {
|
Parenthesize::IfBreaks => {
|
||||||
if node_comments.has_trailing() {
|
if node_comments.has_trailing() {
|
||||||
|
@ -446,7 +448,7 @@ impl Format<PyFormatContext<'_>> for MaybeParenthesizeExpression<'_> {
|
||||||
let group_id = f.group_id("optional_parentheses");
|
let group_id = f.group_id("optional_parentheses");
|
||||||
let f = &mut WithNodeLevel::new(NodeLevel::Expression(Some(group_id)), f);
|
let f = &mut WithNodeLevel::new(NodeLevel::Expression(Some(group_id)), f);
|
||||||
|
|
||||||
best_fit_parenthesize(&expression.format().with_options(Parentheses::Never))
|
best_fit_parenthesize(&unparenthesized)
|
||||||
.with_group_id(Some(group_id))
|
.with_group_id(Some(group_id))
|
||||||
.fmt(f)
|
.fmt(f)
|
||||||
}
|
}
|
||||||
|
@ -454,13 +456,13 @@ impl Format<PyFormatContext<'_>> for MaybeParenthesizeExpression<'_> {
|
||||||
},
|
},
|
||||||
OptionalParentheses::Never => match parenthesize {
|
OptionalParentheses::Never => match parenthesize {
|
||||||
Parenthesize::IfBreaksParenthesized | Parenthesize::IfBreaksParenthesizedNested if !is_empty_parameters_no_unnecessary_parentheses_around_return_value_enabled(f.context()) => {
|
Parenthesize::IfBreaksParenthesized | Parenthesize::IfBreaksParenthesizedNested if !is_empty_parameters_no_unnecessary_parentheses_around_return_value_enabled(f.context()) => {
|
||||||
parenthesize_if_expands(&expression.format().with_options(Parentheses::Never))
|
parenthesize_if_expands(&unparenthesized)
|
||||||
.with_indent(!is_expression_huggable(expression, f.context()))
|
.with_indent(!is_expression_huggable(expression, f.context()))
|
||||||
.fmt(f)
|
.fmt(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
Parenthesize::Optional | Parenthesize::IfBreaks | Parenthesize::IfRequired | Parenthesize::IfBreaksParenthesized | Parenthesize::IfBreaksParenthesizedNested => {
|
Parenthesize::Optional | Parenthesize::IfBreaks | Parenthesize::IfRequired | Parenthesize::IfBreaksParenthesized | Parenthesize::IfBreaksParenthesizedNested => {
|
||||||
expression.format().with_options(Parentheses::Never).fmt(f)
|
unparenthesized.fmt(f)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -768,15 +770,26 @@ impl<'input> CanOmitOptionalParenthesesVisitor<'input> {
|
||||||
Expr::StringLiteral(ast::ExprStringLiteral { value, .. })
|
Expr::StringLiteral(ast::ExprStringLiteral { value, .. })
|
||||||
if value.is_implicit_concatenated() =>
|
if value.is_implicit_concatenated() =>
|
||||||
{
|
{
|
||||||
self.update_max_precedence(OperatorPrecedence::String);
|
if !is_f_string_implicit_concatenated_string_literal_quotes_enabled(self.context) {
|
||||||
|
self.update_max_precedence(OperatorPrecedence::String);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. })
|
Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. })
|
||||||
if value.is_implicit_concatenated() =>
|
if value.is_implicit_concatenated() =>
|
||||||
{
|
{
|
||||||
self.update_max_precedence(OperatorPrecedence::String);
|
if !is_f_string_implicit_concatenated_string_literal_quotes_enabled(self.context) {
|
||||||
|
self.update_max_precedence(OperatorPrecedence::String);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
Expr::FString(ast::ExprFString { value, .. }) if value.is_implicit_concatenated() => {
|
Expr::FString(ast::ExprFString { value, .. }) if value.is_implicit_concatenated() => {
|
||||||
self.update_max_precedence(OperatorPrecedence::String);
|
if !is_f_string_implicit_concatenated_string_literal_quotes_enabled(self.context) {
|
||||||
|
self.update_max_precedence(OperatorPrecedence::String);
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -76,14 +76,9 @@ impl Format<PyFormatContext<'_>> for FormatFString<'_> {
|
||||||
let quotes = StringQuotes::from(string_kind);
|
let quotes = StringQuotes::from(string_kind);
|
||||||
write!(f, [string_kind.prefix(), quotes])?;
|
write!(f, [string_kind.prefix(), quotes])?;
|
||||||
|
|
||||||
f.join()
|
for element in &self.value.elements {
|
||||||
.entries(
|
FormatFStringElement::new(element, context).fmt(f)?;
|
||||||
self.value
|
}
|
||||||
.elements
|
|
||||||
.iter()
|
|
||||||
.map(|element| FormatFStringElement::new(element, context)),
|
|
||||||
)
|
|
||||||
.finish()?;
|
|
||||||
|
|
||||||
// Ending quote
|
// Ending quote
|
||||||
quotes.fmt(f)
|
quotes.fmt(f)
|
||||||
|
@ -98,7 +93,7 @@ pub(crate) struct FStringContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FStringContext {
|
impl FStringContext {
|
||||||
const fn new(flags: AnyStringFlags, layout: FStringLayout) -> Self {
|
pub(crate) const fn new(flags: AnyStringFlags, layout: FStringLayout) -> Self {
|
||||||
Self {
|
Self {
|
||||||
enclosing_flags: flags,
|
enclosing_flags: flags,
|
||||||
layout,
|
layout,
|
||||||
|
@ -125,7 +120,7 @@ pub(crate) enum FStringLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FStringLayout {
|
impl FStringLayout {
|
||||||
fn from_f_string(f_string: &FString, locator: &Locator) -> Self {
|
pub(crate) fn from_f_string(f_string: &FString, locator: &Locator) -> Self {
|
||||||
// Heuristic: Allow breaking the f-string expressions across multiple lines
|
// Heuristic: Allow breaking the f-string expressions across multiple lines
|
||||||
// only if there already is at least one multiline expression. This puts the
|
// only if there already is at least one multiline expression. This puts the
|
||||||
// control in the hands of the user to decide if they want to break the
|
// control in the hands of the user to decide if they want to break the
|
||||||
|
|
|
@ -61,7 +61,8 @@ impl<'a> FormatFStringLiteralElement<'a> {
|
||||||
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.fstring_flags, true);
|
let normalized =
|
||||||
|
normalize_string(literal_content, 0, self.fstring_flags, false, false, 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),
|
||||||
|
@ -235,11 +236,9 @@ impl Format<PyFormatContext<'_>> for FormatFStringExpressionElement<'_> {
|
||||||
if let Some(format_spec) = format_spec.as_deref() {
|
if let Some(format_spec) = format_spec.as_deref() {
|
||||||
token(":").fmt(f)?;
|
token(":").fmt(f)?;
|
||||||
|
|
||||||
f.join()
|
for element in &format_spec.elements {
|
||||||
.entries(format_spec.elements.iter().map(|element| {
|
FormatFStringElement::new(element, self.context.f_string()).fmt(f)?;
|
||||||
FormatFStringElement::new(element, self.context.f_string())
|
}
|
||||||
}))
|
|
||||||
.finish()?;
|
|
||||||
|
|
||||||
// These trailing comments can only occur if the format specifier is
|
// These trailing comments can only occur if the format specifier is
|
||||||
// present. For example,
|
// present. For example,
|
||||||
|
|
|
@ -14,6 +14,7 @@ use crate::expression::parentheses::{
|
||||||
optional_parentheses, parenthesized, NeedsParentheses, OptionalParentheses, Parentheses,
|
optional_parentheses, parenthesized, NeedsParentheses, OptionalParentheses, Parentheses,
|
||||||
};
|
};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
use crate::preview::is_join_implicit_concatenated_string_enabled;
|
||||||
|
|
||||||
pub(crate) mod pattern_arguments;
|
pub(crate) mod pattern_arguments;
|
||||||
pub(crate) mod pattern_keyword;
|
pub(crate) mod pattern_keyword;
|
||||||
|
@ -226,7 +227,7 @@ pub(crate) fn can_pattern_omit_optional_parentheses(
|
||||||
pattern: &Pattern,
|
pattern: &Pattern,
|
||||||
context: &PyFormatContext,
|
context: &PyFormatContext,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let mut visitor = CanOmitOptionalParenthesesVisitor::default();
|
let mut visitor = CanOmitOptionalParenthesesVisitor::new(context);
|
||||||
visitor.visit_pattern(pattern, context);
|
visitor.visit_pattern(pattern, context);
|
||||||
|
|
||||||
if !visitor.any_parenthesized_expressions {
|
if !visitor.any_parenthesized_expressions {
|
||||||
|
@ -271,16 +272,32 @@ pub(crate) fn can_pattern_omit_optional_parentheses(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug)]
|
||||||
struct CanOmitOptionalParenthesesVisitor<'input> {
|
struct CanOmitOptionalParenthesesVisitor<'input> {
|
||||||
max_precedence: OperatorPrecedence,
|
max_precedence: OperatorPrecedence,
|
||||||
max_precedence_count: usize,
|
max_precedence_count: usize,
|
||||||
any_parenthesized_expressions: bool,
|
any_parenthesized_expressions: bool,
|
||||||
|
join_implicit_concatenated_strings: bool,
|
||||||
last: Option<&'input Pattern>,
|
last: Option<&'input Pattern>,
|
||||||
first: First<'input>,
|
first: First<'input>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> CanOmitOptionalParenthesesVisitor<'a> {
|
impl<'a> CanOmitOptionalParenthesesVisitor<'a> {
|
||||||
|
fn new(context: &PyFormatContext) -> Self {
|
||||||
|
Self {
|
||||||
|
max_precedence: OperatorPrecedence::default(),
|
||||||
|
max_precedence_count: 0,
|
||||||
|
any_parenthesized_expressions: false,
|
||||||
|
// TODO: Derive default for `CanOmitOptionalParenthesesVisitor` when removing the `join_implicit_concatenated_strings`
|
||||||
|
// preview style.
|
||||||
|
join_implicit_concatenated_strings: is_join_implicit_concatenated_string_enabled(
|
||||||
|
context,
|
||||||
|
),
|
||||||
|
last: None,
|
||||||
|
first: First::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn visit_pattern(&mut self, pattern: &'a Pattern, context: &PyFormatContext) {
|
fn visit_pattern(&mut self, pattern: &'a Pattern, context: &PyFormatContext) {
|
||||||
match pattern {
|
match pattern {
|
||||||
Pattern::MatchSequence(_) | Pattern::MatchMapping(_) => {
|
Pattern::MatchSequence(_) | Pattern::MatchMapping(_) => {
|
||||||
|
@ -289,18 +306,24 @@ impl<'a> CanOmitOptionalParenthesesVisitor<'a> {
|
||||||
|
|
||||||
Pattern::MatchValue(value) => match &*value.value {
|
Pattern::MatchValue(value) => match &*value.value {
|
||||||
Expr::StringLiteral(string) => {
|
Expr::StringLiteral(string) => {
|
||||||
self.update_max_precedence(OperatorPrecedence::String, string.value.len());
|
if !self.join_implicit_concatenated_strings {
|
||||||
|
self.update_max_precedence(OperatorPrecedence::String, string.value.len());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Expr::BytesLiteral(bytes) => {
|
Expr::BytesLiteral(bytes) => {
|
||||||
self.update_max_precedence(OperatorPrecedence::String, bytes.value.len());
|
if !self.join_implicit_concatenated_strings {
|
||||||
|
self.update_max_precedence(OperatorPrecedence::String, bytes.value.len());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// F-strings are allowed according to python's grammar but fail with a syntax error at runtime.
|
// F-strings are allowed according to python's grammar but fail with a syntax error at runtime.
|
||||||
// That's why we need to support them for formatting.
|
// That's why we need to support them for formatting.
|
||||||
Expr::FString(string) => {
|
Expr::FString(string) => {
|
||||||
self.update_max_precedence(
|
if !self.join_implicit_concatenated_strings {
|
||||||
OperatorPrecedence::String,
|
self.update_max_precedence(
|
||||||
string.value.as_slice().len(),
|
OperatorPrecedence::String,
|
||||||
);
|
string.value.as_slice().len(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Expr::NumberLiteral(_) | Expr::Attribute(_) | Expr::UnaryOp(_) => {
|
Expr::NumberLiteral(_) | Expr::Attribute(_) | Expr::UnaryOp(_) => {
|
||||||
|
|
|
@ -62,3 +62,11 @@ pub(crate) fn is_docstring_code_block_in_docstring_indent_enabled(
|
||||||
) -> bool {
|
) -> bool {
|
||||||
context.is_preview()
|
context.is_preview()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if implicitly concatenated strings should be joined if they all fit on a single line.
|
||||||
|
/// See [#9457](https://github.com/astral-sh/ruff/issues/9457)
|
||||||
|
/// WARNING: This preview style depends on `is_empty_parameters_no_unnecessary_parentheses_around_return_value_enabled`
|
||||||
|
/// because it relies on the new semantic of `IfBreaksParenthesized`.
|
||||||
|
pub(crate) fn is_join_implicit_concatenated_string_enabled(context: &PyFormatContext) -> bool {
|
||||||
|
context.is_preview()
|
||||||
|
}
|
||||||
|
|
|
@ -211,9 +211,9 @@ impl<'ast> SourceOrderVisitor<'ast> for FindEnclosingNode<'_, 'ast> {
|
||||||
// Don't pick potential docstrings as the closest enclosing node because `suite.rs` than fails to identify them as
|
// Don't pick potential docstrings as the closest enclosing node because `suite.rs` than fails to identify them as
|
||||||
// docstrings and docstring formatting won't kick in.
|
// docstrings and docstring formatting won't kick in.
|
||||||
// Format the enclosing node instead and slice the formatted docstring from the result.
|
// Format the enclosing node instead and slice the formatted docstring from the result.
|
||||||
let is_maybe_docstring = node.as_stmt_expr().is_some_and(|stmt| {
|
let is_maybe_docstring = node
|
||||||
DocstringStmt::is_docstring_statement(stmt, self.context.options().source_type())
|
.as_stmt_expr()
|
||||||
});
|
.is_some_and(|stmt| DocstringStmt::is_docstring_statement(stmt, self.context));
|
||||||
|
|
||||||
if is_maybe_docstring {
|
if is_maybe_docstring {
|
||||||
return TraversalSignal::Skip;
|
return TraversalSignal::Skip;
|
||||||
|
|
|
@ -6,6 +6,7 @@ use crate::comments::SourceComment;
|
||||||
|
|
||||||
use crate::expression::maybe_parenthesize_expression;
|
use crate::expression::maybe_parenthesize_expression;
|
||||||
use crate::expression::parentheses::Parenthesize;
|
use crate::expression::parentheses::Parenthesize;
|
||||||
|
use crate::preview::is_join_implicit_concatenated_string_enabled;
|
||||||
use crate::{has_skip_comment, prelude::*};
|
use crate::{has_skip_comment, prelude::*};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -29,12 +30,18 @@ impl FormatNodeRule<StmtAssert> for FormatStmtAssert {
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
if let Some(msg) = msg {
|
if let Some(msg) = msg {
|
||||||
|
let parenthesize = if is_join_implicit_concatenated_string_enabled(f.context()) {
|
||||||
|
Parenthesize::IfBreaksParenthesized
|
||||||
|
} else {
|
||||||
|
Parenthesize::IfBreaks
|
||||||
|
};
|
||||||
|
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
[
|
[
|
||||||
token(","),
|
token(","),
|
||||||
space(),
|
space(),
|
||||||
maybe_parenthesize_expression(msg, item, Parenthesize::IfBreaks),
|
maybe_parenthesize_expression(msg, item, parenthesize),
|
||||||
]
|
]
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use ruff_formatter::{format_args, write, FormatError};
|
use ruff_formatter::{format_args, write, FormatError, RemoveSoftLinesBuffer};
|
||||||
use ruff_python_ast::{
|
use ruff_python_ast::{
|
||||||
AnyNodeRef, Expr, ExprAttribute, ExprCall, Operator, StmtAssign, TypeParams,
|
AnyNodeRef, Expr, ExprAttribute, ExprCall, Operator, StmtAssign, StringLike, TypeParams,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::builders::parenthesize_if_expands;
|
use crate::builders::parenthesize_if_expands;
|
||||||
|
@ -16,7 +16,11 @@ use crate::expression::{
|
||||||
can_omit_optional_parentheses, has_own_parentheses, has_parentheses,
|
can_omit_optional_parentheses, has_own_parentheses, has_parentheses,
|
||||||
maybe_parenthesize_expression,
|
maybe_parenthesize_expression,
|
||||||
};
|
};
|
||||||
|
use crate::preview::is_join_implicit_concatenated_string_enabled;
|
||||||
use crate::statement::trailing_semicolon;
|
use crate::statement::trailing_semicolon;
|
||||||
|
use crate::string::implicit::{
|
||||||
|
FormatImplicitConcatenatedStringExpanded, FormatImplicitConcatenatedStringFlat,
|
||||||
|
};
|
||||||
use crate::{has_skip_comment, prelude::*};
|
use crate::{has_skip_comment, prelude::*};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -281,8 +285,11 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
|
||||||
match self {
|
match self {
|
||||||
FormatStatementsLastExpression::LeftToRight { value, statement } => {
|
FormatStatementsLastExpression::LeftToRight { value, statement } => {
|
||||||
let can_inline_comment = should_inline_comments(value, *statement, f.context());
|
let can_inline_comment = should_inline_comments(value, *statement, f.context());
|
||||||
|
let format_implicit_flat = StringLike::try_from(*value).ok().and_then(|string| {
|
||||||
|
FormatImplicitConcatenatedStringFlat::new(string, f.context())
|
||||||
|
});
|
||||||
|
|
||||||
if !can_inline_comment {
|
if !can_inline_comment && format_implicit_flat.is_none() {
|
||||||
return maybe_parenthesize_expression(
|
return maybe_parenthesize_expression(
|
||||||
value,
|
value,
|
||||||
*statement,
|
*statement,
|
||||||
|
@ -301,28 +308,149 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
|
||||||
) {
|
) {
|
||||||
let group_id = f.group_id("optional_parentheses");
|
let group_id = f.group_id("optional_parentheses");
|
||||||
|
|
||||||
let f = &mut WithNodeLevel::new(NodeLevel::Expression(Some(group_id)), f);
|
// Special case for implicit concatenated strings in assignment value positions.
|
||||||
|
// The special handling is necessary to prevent an instability where an assignment has
|
||||||
best_fit_parenthesize(&format_with(|f| {
|
// a trailing own line comment and the implicit concatenated string fits on the line,
|
||||||
|
// but only if the comment doesn't get inlined.
|
||||||
|
//
|
||||||
|
// ```python
|
||||||
|
// ____aaa = (
|
||||||
|
// "aaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvv"
|
||||||
|
// ) # c
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Without the special handling, this would get formatted to:
|
||||||
|
// ```python
|
||||||
|
// ____aaa = (
|
||||||
|
// "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvv"
|
||||||
|
// ) # c
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// However, this now gets reformatted again because Ruff now takes the `BestFit` layout for the string
|
||||||
|
// because the value is no longer an implicit concatenated string.
|
||||||
|
// ```python
|
||||||
|
// ____aaa = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvv" # c
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// The special handling here ensures that the implicit concatenated string only gets
|
||||||
|
// joined **if** it fits with the trailing comment inlined. Otherwise, keep the multiline
|
||||||
|
// formatting.
|
||||||
|
if let Some(flat) = format_implicit_flat {
|
||||||
inline_comments.mark_formatted();
|
inline_comments.mark_formatted();
|
||||||
|
let string = flat.string();
|
||||||
|
|
||||||
value.format().with_options(Parentheses::Never).fmt(f)?;
|
let flat = format_with(|f| {
|
||||||
|
if string.is_fstring() {
|
||||||
|
let mut buffer = RemoveSoftLinesBuffer::new(&mut *f);
|
||||||
|
|
||||||
if !inline_comments.is_empty() {
|
write!(buffer, [flat])
|
||||||
// If the expressions exceeds the line width, format the comments in the parentheses
|
} else {
|
||||||
if_group_breaks(&inline_comments).fmt(f)?;
|
flat.fmt(f)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.memoized();
|
||||||
|
|
||||||
|
// F-String containing an expression with a magic trailing comma, a comment, or a
|
||||||
|
// multiline debug expression should never be joined. Use the default layout.
|
||||||
|
// ```python
|
||||||
|
// aaaa = f"abcd{[
|
||||||
|
// 1,
|
||||||
|
// 2,
|
||||||
|
// ]}" "more"
|
||||||
|
// ```
|
||||||
|
if string.is_fstring() && flat.inspect(f)?.will_break() {
|
||||||
|
inline_comments.mark_unformatted();
|
||||||
|
|
||||||
|
return write!(
|
||||||
|
f,
|
||||||
|
[maybe_parenthesize_expression(
|
||||||
|
value,
|
||||||
|
*statement,
|
||||||
|
Parenthesize::IfBreaks,
|
||||||
|
)]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
let expanded = format_with(|f| {
|
||||||
}))
|
let f =
|
||||||
.with_group_id(Some(group_id))
|
&mut WithNodeLevel::new(NodeLevel::Expression(Some(group_id)), f);
|
||||||
.fmt(f)?;
|
|
||||||
|
|
||||||
if !inline_comments.is_empty() {
|
write!(f, [FormatImplicitConcatenatedStringExpanded::new(string)])
|
||||||
// If the line fits into the line width, format the comments after the parenthesized expression
|
});
|
||||||
if_group_fits_on_line(&inline_comments)
|
|
||||||
|
// Join the implicit concatenated string if it fits on a single line
|
||||||
|
// ```python
|
||||||
|
// a = "testmorelong" # comment
|
||||||
|
// ```
|
||||||
|
let single_line = format_with(|f| write!(f, [flat, inline_comments]));
|
||||||
|
|
||||||
|
// Parenthesize the string but join the implicit concatenated string and inline the comment.
|
||||||
|
// ```python
|
||||||
|
// a = (
|
||||||
|
// "testmorelong" # comment
|
||||||
|
// )
|
||||||
|
// ```
|
||||||
|
let joined_parenthesized = format_with(|f| {
|
||||||
|
group(&format_args![
|
||||||
|
token("("),
|
||||||
|
soft_block_indent(&format_args![flat, inline_comments]),
|
||||||
|
token(")"),
|
||||||
|
])
|
||||||
.with_group_id(Some(group_id))
|
.with_group_id(Some(group_id))
|
||||||
|
.should_expand(true)
|
||||||
|
.fmt(f)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep the implicit concatenated string multiline and don't inline the comment.
|
||||||
|
// ```python
|
||||||
|
// a = (
|
||||||
|
// "test"
|
||||||
|
// "more"
|
||||||
|
// "long"
|
||||||
|
// ) # comment
|
||||||
|
// ```
|
||||||
|
let implicit_expanded = format_with(|f| {
|
||||||
|
group(&format_args![
|
||||||
|
token("("),
|
||||||
|
block_indent(&expanded),
|
||||||
|
token(")"),
|
||||||
|
inline_comments,
|
||||||
|
])
|
||||||
|
.with_group_id(Some(group_id))
|
||||||
|
.should_expand(true)
|
||||||
|
.fmt(f)
|
||||||
|
});
|
||||||
|
|
||||||
|
// We can't use `optional_parentheses` here because the `inline_comments` contains
|
||||||
|
// a `expand_parent` which results in an instability because the next format
|
||||||
|
// collapses the parentheses.
|
||||||
|
// We can't use `parenthesize_if_expands` because it defaults to
|
||||||
|
// the *flat* layout when the expanded layout doesn't fit.
|
||||||
|
best_fitting![single_line, joined_parenthesized, implicit_expanded]
|
||||||
|
.with_mode(BestFittingMode::AllLines)
|
||||||
.fmt(f)?;
|
.fmt(f)?;
|
||||||
|
} else {
|
||||||
|
best_fit_parenthesize(&format_once(|f| {
|
||||||
|
inline_comments.mark_formatted();
|
||||||
|
|
||||||
|
value.format().with_options(Parentheses::Never).fmt(f)?;
|
||||||
|
|
||||||
|
if !inline_comments.is_empty() {
|
||||||
|
// If the expressions exceeds the line width, format the comments in the parentheses
|
||||||
|
if_group_breaks(&inline_comments).fmt(f)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}))
|
||||||
|
.with_group_id(Some(group_id))
|
||||||
|
.fmt(f)?;
|
||||||
|
|
||||||
|
if !inline_comments.is_empty() {
|
||||||
|
// If the line fits into the line width, format the comments after the parenthesized expression
|
||||||
|
if_group_fits_on_line(&inline_comments)
|
||||||
|
.with_group_id(Some(group_id))
|
||||||
|
.fmt(f)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -339,10 +467,14 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
|
||||||
statement,
|
statement,
|
||||||
} => {
|
} => {
|
||||||
let should_inline_comments = should_inline_comments(value, *statement, f.context());
|
let should_inline_comments = should_inline_comments(value, *statement, f.context());
|
||||||
|
let format_implicit_flat = StringLike::try_from(*value).ok().and_then(|string| {
|
||||||
|
FormatImplicitConcatenatedStringFlat::new(string, f.context())
|
||||||
|
});
|
||||||
|
|
||||||
// Use the normal `maybe_parenthesize_layout` for splittable `value`s.
|
// Use the normal `maybe_parenthesize_layout` for splittable `value`s.
|
||||||
if !should_inline_comments
|
if !should_inline_comments
|
||||||
&& !should_non_inlineable_use_best_fit(value, *statement, f.context())
|
&& !should_non_inlineable_use_best_fit(value, *statement, f.context())
|
||||||
|
&& format_implicit_flat.is_none()
|
||||||
{
|
{
|
||||||
return write!(
|
return write!(
|
||||||
f,
|
f,
|
||||||
|
@ -364,7 +496,7 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
|
||||||
let expression_comments = comments.leading_dangling_trailing(*value);
|
let expression_comments = comments.leading_dangling_trailing(*value);
|
||||||
|
|
||||||
// Don't inline comments for attribute and call expressions for black compatibility
|
// Don't inline comments for attribute and call expressions for black compatibility
|
||||||
let inline_comments = if should_inline_comments {
|
let inline_comments = if should_inline_comments || format_implicit_flat.is_some() {
|
||||||
OptionalParenthesesInlinedComments::new(
|
OptionalParenthesesInlinedComments::new(
|
||||||
&expression_comments,
|
&expression_comments,
|
||||||
*statement,
|
*statement,
|
||||||
|
@ -396,13 +528,14 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
|
||||||
// Prevent inline comments to be formatted as part of the expression.
|
// Prevent inline comments to be formatted as part of the expression.
|
||||||
inline_comments.mark_formatted();
|
inline_comments.mark_formatted();
|
||||||
|
|
||||||
let mut last_target = before_operator.memoized();
|
let last_target = before_operator.memoized();
|
||||||
|
let last_target_breaks = last_target.inspect(f)?.will_break();
|
||||||
|
|
||||||
// Don't parenthesize the `value` if it is known that the target will break.
|
// Don't parenthesize the `value` if it is known that the target will break.
|
||||||
// This is mainly a performance optimisation that avoids unnecessary memoization
|
// This is mainly a performance optimisation that avoids unnecessary memoization
|
||||||
// and using the costly `BestFitting` layout if it is already known that only the last variant
|
// and using the costly `BestFitting` layout if it is already known that only the last variant
|
||||||
// can ever fit because the left breaks.
|
// can ever fit because the left breaks.
|
||||||
if last_target.inspect(f)?.will_break() {
|
if format_implicit_flat.is_none() && last_target_breaks {
|
||||||
return write!(
|
return write!(
|
||||||
f,
|
f,
|
||||||
[
|
[
|
||||||
|
@ -416,13 +549,29 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let format_value = value.format().with_options(Parentheses::Never).memoized();
|
let format_value = format_with(|f| {
|
||||||
|
if let Some(format_implicit_flat) = format_implicit_flat.as_ref() {
|
||||||
|
if format_implicit_flat.string().is_fstring() {
|
||||||
|
// Remove any soft line breaks emitted by the f-string formatting.
|
||||||
|
// This is important when formatting f-strings as part of an assignment right side
|
||||||
|
// because `best_fit_parenthesize` will otherwise still try to break inner
|
||||||
|
// groups if wrapped in a `group(..).should_expand(true)`
|
||||||
|
let mut buffer = RemoveSoftLinesBuffer::new(&mut *f);
|
||||||
|
write!(buffer, [format_implicit_flat])
|
||||||
|
} else {
|
||||||
|
format_implicit_flat.fmt(f)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value.format().with_options(Parentheses::Never).fmt(f)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.memoized();
|
||||||
|
|
||||||
// Tries to fit the `left` and the `value` on a single line:
|
// Tries to fit the `left` and the `value` on a single line:
|
||||||
// ```python
|
// ```python
|
||||||
// a = b = c
|
// a = b = c
|
||||||
// ```
|
// ```
|
||||||
let format_flat = format_with(|f| {
|
let single_line = format_with(|f| {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
[
|
[
|
||||||
|
@ -443,19 +592,21 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
|
||||||
// c
|
// c
|
||||||
// )
|
// )
|
||||||
// ```
|
// ```
|
||||||
let format_parenthesize_value = format_with(|f| {
|
let flat_target_parenthesize_value = format_with(|f| {
|
||||||
write!(
|
write!(f, [last_target, space(), operator, space(), token("("),])?;
|
||||||
f,
|
|
||||||
[
|
if is_join_implicit_concatenated_string_enabled(f.context()) {
|
||||||
last_target,
|
group(&soft_block_indent(&format_args![
|
||||||
space(),
|
format_value,
|
||||||
operator,
|
inline_comments
|
||||||
space(),
|
]))
|
||||||
token("("),
|
.should_expand(true)
|
||||||
block_indent(&format_args![format_value, inline_comments]),
|
.fmt(f)?;
|
||||||
token(")")
|
} else {
|
||||||
]
|
block_indent(&format_args![format_value, inline_comments]).fmt(f)?;
|
||||||
)
|
}
|
||||||
|
|
||||||
|
token(")").fmt(f)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fall back to parenthesizing (or splitting) the last target part if we can't make the value
|
// Fall back to parenthesizing (or splitting) the last target part if we can't make the value
|
||||||
|
@ -466,17 +617,16 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
|
||||||
// "bbbbb"
|
// "bbbbb"
|
||||||
// ] = c
|
// ] = c
|
||||||
// ```
|
// ```
|
||||||
let format_split_left = format_with(|f| {
|
let split_target_flat_value = format_with(|f| {
|
||||||
|
if is_join_implicit_concatenated_string_enabled(f.context()) {
|
||||||
|
group(&last_target).should_expand(true).fmt(f)?;
|
||||||
|
} else {
|
||||||
|
last_target.fmt(f)?;
|
||||||
|
}
|
||||||
|
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
[
|
[space(), operator, space(), format_value, inline_comments]
|
||||||
last_target,
|
|
||||||
space(),
|
|
||||||
operator,
|
|
||||||
space(),
|
|
||||||
format_value,
|
|
||||||
inline_comments
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -486,7 +636,7 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
|
||||||
// For attribute chains that contain any parenthesized value: Try expanding the parenthesized value first.
|
// For attribute chains that contain any parenthesized value: Try expanding the parenthesized value first.
|
||||||
if value.is_call_expr() || value.is_subscript_expr() || value.is_attribute_expr() {
|
if value.is_call_expr() || value.is_subscript_expr() || value.is_attribute_expr() {
|
||||||
best_fitting![
|
best_fitting![
|
||||||
format_flat,
|
single_line,
|
||||||
// Avoid parenthesizing the call expression if the `(` fit on the line
|
// Avoid parenthesizing the call expression if the `(` fit on the line
|
||||||
format_args![
|
format_args![
|
||||||
last_target,
|
last_target,
|
||||||
|
@ -495,12 +645,165 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
|
||||||
space(),
|
space(),
|
||||||
group(&format_value).should_expand(true),
|
group(&format_value).should_expand(true),
|
||||||
],
|
],
|
||||||
format_parenthesize_value,
|
flat_target_parenthesize_value,
|
||||||
format_split_left
|
split_target_flat_value
|
||||||
]
|
]
|
||||||
.fmt(f)
|
.fmt(f)
|
||||||
|
} else if let Some(format_implicit_flat) = &format_implicit_flat {
|
||||||
|
// F-String containing an expression with a magic trailing comma, a comment, or a
|
||||||
|
// multiline debug expression should never be joined. Use the default layout.
|
||||||
|
//
|
||||||
|
// ```python
|
||||||
|
// aaaa = f"abcd{[
|
||||||
|
// 1,
|
||||||
|
// 2,
|
||||||
|
// ]}" "more"
|
||||||
|
// ```
|
||||||
|
if format_implicit_flat.string().is_fstring()
|
||||||
|
&& format_value.inspect(f)?.will_break()
|
||||||
|
{
|
||||||
|
inline_comments.mark_unformatted();
|
||||||
|
|
||||||
|
return write!(
|
||||||
|
f,
|
||||||
|
[
|
||||||
|
before_operator,
|
||||||
|
space(),
|
||||||
|
operator,
|
||||||
|
space(),
|
||||||
|
maybe_parenthesize_expression(
|
||||||
|
value,
|
||||||
|
*statement,
|
||||||
|
Parenthesize::IfBreaks
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let group_id = f.group_id("optional_parentheses");
|
||||||
|
let format_expanded = format_with(|f| {
|
||||||
|
let f = &mut WithNodeLevel::new(NodeLevel::Expression(Some(group_id)), f);
|
||||||
|
|
||||||
|
FormatImplicitConcatenatedStringExpanded::new(
|
||||||
|
StringLike::try_from(*value).unwrap(),
|
||||||
|
)
|
||||||
|
.fmt(f)
|
||||||
|
})
|
||||||
|
.memoized();
|
||||||
|
|
||||||
|
// Keep the target flat, parenthesize the value, and keep it multiline.
|
||||||
|
//
|
||||||
|
// ```python
|
||||||
|
// Literal[ "a", "b"] = (
|
||||||
|
// "looooooooooooooooooooooooooooooong"
|
||||||
|
// "string"
|
||||||
|
// ) # comment
|
||||||
|
// ```
|
||||||
|
let flat_target_value_parenthesized_multiline = format_with(|f| {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
[
|
||||||
|
last_target,
|
||||||
|
space(),
|
||||||
|
operator,
|
||||||
|
space(),
|
||||||
|
token("("),
|
||||||
|
group(&soft_block_indent(&format_expanded))
|
||||||
|
.with_group_id(Some(group_id))
|
||||||
|
.should_expand(true),
|
||||||
|
token(")"),
|
||||||
|
inline_comments
|
||||||
|
]
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expand the parent and parenthesize the joined string with the inlined comment.
|
||||||
|
//
|
||||||
|
// ```python
|
||||||
|
// Literal[
|
||||||
|
// "a",
|
||||||
|
// "b",
|
||||||
|
// ] = (
|
||||||
|
// "not that long string" # comment
|
||||||
|
// )
|
||||||
|
// ```
|
||||||
|
let split_target_value_parenthesized_flat = format_with(|f| {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
[
|
||||||
|
group(&last_target).should_expand(true),
|
||||||
|
space(),
|
||||||
|
operator,
|
||||||
|
space(),
|
||||||
|
token("("),
|
||||||
|
group(&soft_block_indent(&format_args![
|
||||||
|
format_value,
|
||||||
|
inline_comments
|
||||||
|
]))
|
||||||
|
.should_expand(true),
|
||||||
|
token(")")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
// The most expanded variant: Expand both the target and the string.
|
||||||
|
//
|
||||||
|
// ```python
|
||||||
|
// Literal[
|
||||||
|
// "a",
|
||||||
|
// "b",
|
||||||
|
// ] = (
|
||||||
|
// "looooooooooooooooooooooooooooooong"
|
||||||
|
// "string"
|
||||||
|
// ) # comment
|
||||||
|
// ```
|
||||||
|
let split_target_value_parenthesized_multiline = format_with(|f| {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
[
|
||||||
|
group(&last_target).should_expand(true),
|
||||||
|
space(),
|
||||||
|
operator,
|
||||||
|
space(),
|
||||||
|
token("("),
|
||||||
|
group(&soft_block_indent(&format_expanded))
|
||||||
|
.with_group_id(Some(group_id))
|
||||||
|
.should_expand(true),
|
||||||
|
token(")"),
|
||||||
|
inline_comments
|
||||||
|
]
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
// This is only a perf optimisation. No point in trying all the "flat-target"
|
||||||
|
// variants if we know that the last target must break.
|
||||||
|
if last_target_breaks {
|
||||||
|
best_fitting![
|
||||||
|
split_target_flat_value,
|
||||||
|
split_target_value_parenthesized_flat,
|
||||||
|
split_target_value_parenthesized_multiline,
|
||||||
|
]
|
||||||
|
.with_mode(BestFittingMode::AllLines)
|
||||||
|
.fmt(f)
|
||||||
|
} else {
|
||||||
|
best_fitting![
|
||||||
|
single_line,
|
||||||
|
flat_target_parenthesize_value,
|
||||||
|
flat_target_value_parenthesized_multiline,
|
||||||
|
split_target_flat_value,
|
||||||
|
split_target_value_parenthesized_flat,
|
||||||
|
split_target_value_parenthesized_multiline,
|
||||||
|
]
|
||||||
|
.with_mode(BestFittingMode::AllLines)
|
||||||
|
.fmt(f)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
best_fitting![format_flat, format_parenthesize_value, format_split_left].fmt(f)
|
best_fitting![
|
||||||
|
single_line,
|
||||||
|
flat_target_parenthesize_value,
|
||||||
|
split_target_flat_value
|
||||||
|
]
|
||||||
|
.fmt(f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -556,6 +859,12 @@ impl<'a> OptionalParenthesesInlinedComments<'a> {
|
||||||
comment.mark_formatted();
|
comment.mark_formatted();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn mark_unformatted(&self) {
|
||||||
|
for comment in self.expression {
|
||||||
|
comment.mark_unformatted();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Format<PyFormatContext<'_>> for OptionalParenthesesInlinedComments<'_> {
|
impl Format<PyFormatContext<'_>> for OptionalParenthesesInlinedComments<'_> {
|
||||||
|
|
|
@ -138,7 +138,7 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
|
||||||
|
|
||||||
SuiteKind::Function | SuiteKind::Class | SuiteKind::TopLevel => {
|
SuiteKind::Function | SuiteKind::Class | SuiteKind::TopLevel => {
|
||||||
if let Some(docstring) =
|
if let Some(docstring) =
|
||||||
DocstringStmt::try_from_statement(first, self.kind, source_type)
|
DocstringStmt::try_from_statement(first, self.kind, f.context())
|
||||||
{
|
{
|
||||||
SuiteChildStatement::Docstring(docstring)
|
SuiteChildStatement::Docstring(docstring)
|
||||||
} else {
|
} else {
|
||||||
|
@ -179,7 +179,7 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
|
||||||
// Insert a newline after a module level docstring, but treat
|
// Insert a newline after a module level docstring, but treat
|
||||||
// it as a docstring otherwise. See: https://github.com/psf/black/pull/3932.
|
// it as a docstring otherwise. See: https://github.com/psf/black/pull/3932.
|
||||||
self.kind == SuiteKind::TopLevel
|
self.kind == SuiteKind::TopLevel
|
||||||
&& DocstringStmt::try_from_statement(first.statement(), self.kind, source_type)
|
&& DocstringStmt::try_from_statement(first.statement(), self.kind, f.context())
|
||||||
.is_some()
|
.is_some()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -785,37 +785,23 @@ impl<'a> DocstringStmt<'a> {
|
||||||
fn try_from_statement(
|
fn try_from_statement(
|
||||||
stmt: &'a Stmt,
|
stmt: &'a Stmt,
|
||||||
suite_kind: SuiteKind,
|
suite_kind: SuiteKind,
|
||||||
source_type: PySourceType,
|
context: &PyFormatContext,
|
||||||
) -> Option<DocstringStmt<'a>> {
|
) -> Option<DocstringStmt<'a>> {
|
||||||
// Notebooks don't have a concept of modules, therefore, don't recognise the first string as the module docstring.
|
// Notebooks don't have a concept of modules, therefore, don't recognise the first string as the module docstring.
|
||||||
if source_type.is_ipynb() && suite_kind == SuiteKind::TopLevel {
|
if context.options().source_type().is_ipynb() && suite_kind == SuiteKind::TopLevel {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Stmt::Expr(ast::StmtExpr { value, .. }) = stmt else {
|
Self::is_docstring_statement(stmt.as_expr_stmt()?, context).then_some(DocstringStmt {
|
||||||
return None;
|
docstring: stmt,
|
||||||
};
|
suite_kind,
|
||||||
|
})
|
||||||
match value.as_ref() {
|
|
||||||
Expr::StringLiteral(ast::ExprStringLiteral { value, .. })
|
|
||||||
if !value.is_implicit_concatenated() =>
|
|
||||||
{
|
|
||||||
Some(DocstringStmt {
|
|
||||||
docstring: stmt,
|
|
||||||
suite_kind,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_docstring_statement(stmt: &StmtExpr, source_type: PySourceType) -> bool {
|
pub(crate) fn is_docstring_statement(stmt: &StmtExpr, context: &PyFormatContext) -> bool {
|
||||||
if source_type.is_ipynb() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = stmt.value.as_ref() {
|
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = stmt.value.as_ref() {
|
||||||
!value.is_implicit_concatenated()
|
!value.is_implicit_concatenated()
|
||||||
|
|| !value.iter().any(|literal| context.comments().has(literal))
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ use std::{borrow::Cow, collections::VecDeque};
|
||||||
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use ruff_formatter::printer::SourceMapGeneration;
|
use ruff_formatter::printer::SourceMapGeneration;
|
||||||
use ruff_python_ast::{str::Quote, StringFlags};
|
use ruff_python_ast::{str::Quote, AnyStringFlags, StringFlags};
|
||||||
use ruff_python_trivia::CommentRanges;
|
use ruff_python_trivia::CommentRanges;
|
||||||
use {
|
use {
|
||||||
ruff_formatter::{write, FormatOptions, IndentStyle, LineWidth, Printed},
|
ruff_formatter::{write, FormatOptions, IndentStyle, LineWidth, Printed},
|
||||||
|
@ -19,7 +19,10 @@ use {
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::NormalizedString;
|
use super::NormalizedString;
|
||||||
use crate::preview::is_docstring_code_block_in_docstring_indent_enabled;
|
use crate::preview::{
|
||||||
|
is_docstring_code_block_in_docstring_indent_enabled,
|
||||||
|
is_join_implicit_concatenated_string_enabled,
|
||||||
|
};
|
||||||
use crate::string::StringQuotes;
|
use crate::string::StringQuotes;
|
||||||
use crate::{prelude::*, DocstringCodeLineWidth, FormatModuleError};
|
use crate::{prelude::*, DocstringCodeLineWidth, FormatModuleError};
|
||||||
|
|
||||||
|
@ -167,7 +170,7 @@ pub(crate) fn format(normalized: &NormalizedString, f: &mut PyFormatter) -> Form
|
||||||
if docstring[first.len()..].trim().is_empty() {
|
if docstring[first.len()..].trim().is_empty() {
|
||||||
// For `"""\n"""` or other whitespace between the quotes, black keeps a single whitespace,
|
// For `"""\n"""` or other whitespace between the quotes, black keeps a single whitespace,
|
||||||
// but `""""""` doesn't get one inserted.
|
// but `""""""` doesn't get one inserted.
|
||||||
if needs_chaperone_space(normalized, trim_end)
|
if needs_chaperone_space(normalized.flags(), trim_end, f.context())
|
||||||
|| (trim_end.is_empty() && !docstring.is_empty())
|
|| (trim_end.is_empty() && !docstring.is_empty())
|
||||||
{
|
{
|
||||||
space().fmt(f)?;
|
space().fmt(f)?;
|
||||||
|
@ -207,7 +210,7 @@ pub(crate) fn format(normalized: &NormalizedString, f: &mut PyFormatter) -> Form
|
||||||
let trim_end = docstring
|
let trim_end = docstring
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.trim_end_matches(|c: char| c.is_whitespace() && c != '\n');
|
.trim_end_matches(|c: char| c.is_whitespace() && c != '\n');
|
||||||
if needs_chaperone_space(normalized, trim_end) {
|
if needs_chaperone_space(normalized.flags(), trim_end, f.context()) {
|
||||||
space().fmt(f)?;
|
space().fmt(f)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1604,9 +1607,18 @@ fn docstring_format_source(
|
||||||
/// If the last line of the docstring is `content" """` or `content\ """`, we need a chaperone space
|
/// If the last line of the docstring is `content" """` or `content\ """`, we need a chaperone space
|
||||||
/// that avoids `content""""` and `content\"""`. This does only applies to un-escaped backslashes,
|
/// that avoids `content""""` and `content\"""`. This does only applies to un-escaped backslashes,
|
||||||
/// so `content\\ """` doesn't need a space while `content\\\ """` does.
|
/// so `content\\ """` doesn't need a space while `content\\\ """` does.
|
||||||
fn needs_chaperone_space(normalized: &NormalizedString, trim_end: &str) -> bool {
|
pub(super) fn needs_chaperone_space(
|
||||||
trim_end.ends_with(normalized.flags().quote_style().as_char())
|
flags: AnyStringFlags,
|
||||||
|| trim_end.chars().rev().take_while(|c| *c == '\\').count() % 2 == 1
|
trim_end: &str,
|
||||||
|
context: &PyFormatContext,
|
||||||
|
) -> bool {
|
||||||
|
if trim_end.chars().rev().take_while(|c| *c == '\\').count() % 2 == 1 {
|
||||||
|
true
|
||||||
|
} else if is_join_implicit_concatenated_string_enabled(context) {
|
||||||
|
flags.is_triple_quoted() && trim_end.ends_with(flags.quote_style().as_char())
|
||||||
|
} else {
|
||||||
|
trim_end.ends_with(flags.quote_style().as_char())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
|
406
crates/ruff_python_formatter/src/string/implicit.rs
Normal file
406
crates/ruff_python_formatter/src/string/implicit.rs
Normal file
|
@ -0,0 +1,406 @@
|
||||||
|
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 crate::comments::{leading_comments, trailing_comments};
|
||||||
|
use crate::expression::parentheses::in_parentheses_only_soft_line_break_or_space;
|
||||||
|
use crate::other::f_string::{FStringContext, FStringLayout, FormatFString};
|
||||||
|
use crate::other::f_string_element::FormatFStringExpressionElement;
|
||||||
|
use crate::other::string_literal::StringLiteralKind;
|
||||||
|
use crate::prelude::*;
|
||||||
|
use crate::preview::{
|
||||||
|
is_f_string_formatting_enabled, is_join_implicit_concatenated_string_enabled,
|
||||||
|
};
|
||||||
|
use crate::string::docstring::needs_chaperone_space;
|
||||||
|
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
|
||||||
|
/// of string, bytes or f-string literals.
|
||||||
|
pub(crate) struct FormatImplicitConcatenatedString<'a> {
|
||||||
|
string: StringLike<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> FormatImplicitConcatenatedString<'a> {
|
||||||
|
pub(crate) fn new(string: impl Into<StringLike<'a>>) -> Self {
|
||||||
|
Self {
|
||||||
|
string: string.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Format<PyFormatContext<'_>> for FormatImplicitConcatenatedString<'_> {
|
||||||
|
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
||||||
|
let expanded = FormatImplicitConcatenatedStringExpanded::new(self.string);
|
||||||
|
|
||||||
|
// If the string can be joined, try joining the implicit concatenated string into a single string
|
||||||
|
// if it fits on the line. Otherwise, parenthesize the string parts and format each part on its
|
||||||
|
// own line.
|
||||||
|
if let Some(flat) = FormatImplicitConcatenatedStringFlat::new(self.string, f.context()) {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
[if_group_fits_on_line(&flat), if_group_breaks(&expanded)]
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
expanded.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats an implicit concatenated string where parts are separated by a space or line break.
|
||||||
|
pub(crate) struct FormatImplicitConcatenatedStringExpanded<'a> {
|
||||||
|
string: StringLike<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> FormatImplicitConcatenatedStringExpanded<'a> {
|
||||||
|
pub(crate) fn new(string: StringLike<'a>) -> Self {
|
||||||
|
assert!(string.is_implicit_concatenated());
|
||||||
|
|
||||||
|
Self { string }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Format<PyFormatContext<'_>> for FormatImplicitConcatenatedStringExpanded<'_> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
|
||||||
|
let comments = f.context().comments().clone();
|
||||||
|
let quoting = self.string.quoting(&f.context().locator());
|
||||||
|
|
||||||
|
let join_implicit_concatenated_string_enabled =
|
||||||
|
is_join_implicit_concatenated_string_enabled(f.context());
|
||||||
|
let mut joiner = f.join_with(in_parentheses_only_soft_line_break_or_space());
|
||||||
|
|
||||||
|
for part in self.string.parts() {
|
||||||
|
let format_part = format_with(|f: &mut PyFormatter| match part {
|
||||||
|
StringLikePart::String(part) => {
|
||||||
|
let kind = if self.string.is_fstring() {
|
||||||
|
#[allow(deprecated)]
|
||||||
|
StringLiteralKind::InImplicitlyConcatenatedFString(quoting)
|
||||||
|
} else {
|
||||||
|
StringLiteralKind::String
|
||||||
|
};
|
||||||
|
|
||||||
|
part.format().with_options(kind).fmt(f)
|
||||||
|
}
|
||||||
|
StringLikePart::Bytes(bytes_literal) => bytes_literal.format().fmt(f),
|
||||||
|
StringLikePart::FString(part) => FormatFString::new(part, quoting).fmt(f),
|
||||||
|
});
|
||||||
|
|
||||||
|
let part_comments = comments.leading_dangling_trailing(&part);
|
||||||
|
joiner.entry(&format_args![
|
||||||
|
(!join_implicit_concatenated_string_enabled).then_some(line_suffix_boundary()),
|
||||||
|
leading_comments(part_comments.leading),
|
||||||
|
format_part,
|
||||||
|
trailing_comments(part_comments.trailing)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
joiner.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats an implicit concatenated string where parts are joined into a single string if possible.
|
||||||
|
pub(crate) struct FormatImplicitConcatenatedStringFlat<'a> {
|
||||||
|
string: StringLike<'a>,
|
||||||
|
flags: AnyStringFlags,
|
||||||
|
docstring: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> FormatImplicitConcatenatedStringFlat<'a> {
|
||||||
|
/// Creates a new formatter. Returns `None` if the string can't be merged into a single string.
|
||||||
|
pub(crate) fn new(string: StringLike<'a>, context: &PyFormatContext) -> Option<Self> {
|
||||||
|
fn merge_flags(string: StringLike, context: &PyFormatContext) -> Option<AnyStringFlags> {
|
||||||
|
if !is_join_implicit_concatenated_string_enabled(context) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiline strings can never fit on a single line.
|
||||||
|
if !string.is_fstring() && string.is_multiline(context.source()) {
|
||||||
|
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.
|
||||||
|
// We could technically join strings that are raw-strings and use the same quotes but lets not do this for now.
|
||||||
|
// Joining triple quoted strings is more complicated because an
|
||||||
|
// implicit concatenated string could become a docstring (if it's the first string in a block).
|
||||||
|
// That means the joined string formatting would have to call into
|
||||||
|
// the docstring formatting or otherwise guarantee that the output
|
||||||
|
// won't change on a second run.
|
||||||
|
if part.flags().is_triple_quoted() || part.flags().is_raw_string() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, preserve comments documenting a specific part over possibly
|
||||||
|
// collapsing onto a single line. Collapsing could result in pragma comments
|
||||||
|
// now covering more code.
|
||||||
|
if context.comments().leading_trailing(&part).next().is_some() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let StringLikePart::FString(fstring) = part {
|
||||||
|
if fstring.elements.iter().any(|element| match element {
|
||||||
|
// Same as for other literals. Multiline literals can't fit on a single line.
|
||||||
|
FStringElement::Literal(literal) => context
|
||||||
|
.locator()
|
||||||
|
.slice(literal.range())
|
||||||
|
.contains(['\n', '\r']),
|
||||||
|
FStringElement::Expression(expression) => {
|
||||||
|
if is_f_string_formatting_enabled(context) {
|
||||||
|
// Expressions containing comments can't be joined.
|
||||||
|
context.comments().contains_comments(expression.into())
|
||||||
|
} else {
|
||||||
|
// Multiline f-string expressions can't be joined if the f-string formatting is disabled because
|
||||||
|
// the string gets inserted in verbatim preserving the newlines.
|
||||||
|
context.locator().slice(expression).contains(['\n', '\r'])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
preserve_quotes_requirement = Some(part.flags().quote_style());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The string is either a regular string, f-string, or bytes string.
|
||||||
|
let mut merged_quotes: Option<QuoteMetadata> = None;
|
||||||
|
|
||||||
|
// Only preserve the string type but disregard the `u` and `r` prefixes.
|
||||||
|
// * It's not necessary to preserve the `r` prefix because Ruff doesn't support joining raw strings (we shouldn't get here).
|
||||||
|
// * It's not necessary to preserve the `u` prefix because Ruff discards the `u` prefix (it's meaningless in Python 3+)
|
||||||
|
let prefix = match string {
|
||||||
|
StringLike::String(_) => AnyStringPrefix::Regular(StringLiteralPrefix::Empty),
|
||||||
|
StringLike::Bytes(_) => AnyStringPrefix::Bytes(ByteStringPrefix::Regular),
|
||||||
|
StringLike::FString(_) => AnyStringPrefix::Format(FStringPrefix::Regular),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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) =
|
||||||
|
Quote::try_from(normalizer.preferred_quote_style(first_part))
|
||||||
|
{
|
||||||
|
for part in string.parts() {
|
||||||
|
let part_quote_metadata =
|
||||||
|
QuoteMetadata::from_part(part, context, preferred_quote);
|
||||||
|
|
||||||
|
if let Some(merged) = merged_quotes.as_mut() {
|
||||||
|
*merged = part_quote_metadata.merge(merged)?;
|
||||||
|
} else {
|
||||||
|
merged_quotes = Some(part_quote_metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
merged_quotes?.choose(preferred_quote)
|
||||||
|
} else {
|
||||||
|
// Use the quotes of the first part if the quotes should be preserved.
|
||||||
|
first_part.flags().quote_style()
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(AnyStringFlags::new(prefix, quote, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !string.is_implicit_concatenated() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Self {
|
||||||
|
flags: merge_flags(string, context)?,
|
||||||
|
string,
|
||||||
|
docstring: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_docstring(&mut self, is_docstring: bool) {
|
||||||
|
self.docstring = is_docstring;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn string(&self) -> StringLike<'a> {
|
||||||
|
self.string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Format<PyFormatContext<'_>> for FormatImplicitConcatenatedStringFlat<'_> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
|
||||||
|
// Merges all string parts into a single string.
|
||||||
|
let quotes = StringQuotes::from(self.flags);
|
||||||
|
|
||||||
|
write!(f, [self.flags.prefix(), quotes])?;
|
||||||
|
|
||||||
|
let mut parts = self.string.parts().peekable();
|
||||||
|
|
||||||
|
// Trim implicit concatenated strings in docstring positions.
|
||||||
|
// Skip over any trailing parts that are all whitespace.
|
||||||
|
// Leading parts are handled as part of the formatting loop below.
|
||||||
|
if self.docstring {
|
||||||
|
for part in self.string.parts().rev() {
|
||||||
|
assert!(part.is_string_literal());
|
||||||
|
|
||||||
|
if f.context()
|
||||||
|
.locator()
|
||||||
|
.slice(part.content_range())
|
||||||
|
.trim()
|
||||||
|
.is_empty()
|
||||||
|
{
|
||||||
|
// Don't format the part.
|
||||||
|
parts.next_back();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut first_non_empty = self.docstring;
|
||||||
|
|
||||||
|
while let Some(part) = parts.next() {
|
||||||
|
match part {
|
||||||
|
StringLikePart::String(_) | StringLikePart::Bytes(_) => {
|
||||||
|
FormatLiteralContent {
|
||||||
|
range: part.content_range(),
|
||||||
|
flags: self.flags,
|
||||||
|
is_fstring: false,
|
||||||
|
trim_start: first_non_empty && self.docstring,
|
||||||
|
trim_end: self.docstring && parts.peek().is_none(),
|
||||||
|
}
|
||||||
|
.fmt(f)?;
|
||||||
|
|
||||||
|
if first_non_empty {
|
||||||
|
first_non_empty = f
|
||||||
|
.context()
|
||||||
|
.locator()
|
||||||
|
.slice(part.content_range())
|
||||||
|
.trim_start()
|
||||||
|
.is_empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StringLikePart::FString(f_string) => {
|
||||||
|
if is_f_string_formatting_enabled(f.context()) {
|
||||||
|
for element in &f_string.elements {
|
||||||
|
match element {
|
||||||
|
FStringElement::Literal(literal) => {
|
||||||
|
FormatLiteralContent {
|
||||||
|
range: literal.range(),
|
||||||
|
flags: self.flags,
|
||||||
|
is_fstring: true,
|
||||||
|
trim_end: false,
|
||||||
|
trim_start: false,
|
||||||
|
}
|
||||||
|
.fmt(f)?;
|
||||||
|
}
|
||||||
|
// Formatting the expression here and in the expanded version is safe **only**
|
||||||
|
// because we assert that the f-string never contains any comments.
|
||||||
|
FStringElement::Expression(expression) => {
|
||||||
|
let context = FStringContext::new(
|
||||||
|
self.flags,
|
||||||
|
FStringLayout::from_f_string(
|
||||||
|
f_string,
|
||||||
|
&f.context().locator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
FormatFStringExpressionElement::new(expression, context)
|
||||||
|
.fmt(f)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
FormatLiteralContent {
|
||||||
|
range: part.content_range(),
|
||||||
|
flags: self.flags,
|
||||||
|
is_fstring: true,
|
||||||
|
trim_end: false,
|
||||||
|
trim_start: false,
|
||||||
|
}
|
||||||
|
.fmt(f)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
quotes.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FormatLiteralContent {
|
||||||
|
range: TextRange,
|
||||||
|
flags: AnyStringFlags,
|
||||||
|
is_fstring: bool,
|
||||||
|
trim_start: bool,
|
||||||
|
trim_end: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Format<PyFormatContext<'_>> for FormatLiteralContent {
|
||||||
|
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
||||||
|
let content = f.context().locator().slice(self.range);
|
||||||
|
let mut normalized = normalize_string(
|
||||||
|
content,
|
||||||
|
0,
|
||||||
|
self.flags,
|
||||||
|
self.flags.is_f_string() && !self.is_fstring,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Trim the start and end of the string if it's the first or last part of a docstring.
|
||||||
|
// This is rare, so don't bother with optimizing to use `Cow`.
|
||||||
|
if self.trim_start {
|
||||||
|
let trimmed = normalized.trim_start();
|
||||||
|
if trimmed.len() < normalized.len() {
|
||||||
|
normalized = trimmed.to_string().into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.trim_end {
|
||||||
|
let trimmed = normalized.trim_end();
|
||||||
|
if trimmed.len() < normalized.len() {
|
||||||
|
normalized = trimmed.to_string().into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !normalized.is_empty() {
|
||||||
|
match &normalized {
|
||||||
|
Cow::Borrowed(_) => source_text_slice(self.range).fmt(f)?,
|
||||||
|
Cow::Owned(normalized) => text(normalized).fmt(f)?,
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.trim_end && needs_chaperone_space(self.flags, &normalized, f.context()) {
|
||||||
|
space().fmt(f)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,25 +1,21 @@
|
||||||
use memchr::memchr2;
|
use memchr::memchr2;
|
||||||
|
|
||||||
pub(crate) use normalize::{normalize_string, NormalizedString, StringNormalizer};
|
pub(crate) use normalize::{normalize_string, NormalizedString, StringNormalizer};
|
||||||
use ruff_formatter::format_args;
|
|
||||||
use ruff_python_ast::str::Quote;
|
use ruff_python_ast::str::Quote;
|
||||||
use ruff_python_ast::{
|
use ruff_python_ast::{
|
||||||
self as ast,
|
self as ast,
|
||||||
str_prefix::{AnyStringPrefix, StringLiteralPrefix},
|
str_prefix::{AnyStringPrefix, StringLiteralPrefix},
|
||||||
AnyStringFlags, StringFlags, StringLike, StringLikePart,
|
AnyStringFlags, StringFlags,
|
||||||
};
|
};
|
||||||
use ruff_source_file::Locator;
|
use ruff_source_file::Locator;
|
||||||
use ruff_text_size::Ranged;
|
use ruff_text_size::Ranged;
|
||||||
|
|
||||||
use crate::comments::{leading_comments, trailing_comments};
|
|
||||||
use crate::expression::expr_f_string::f_string_quoting;
|
use crate::expression::expr_f_string::f_string_quoting;
|
||||||
use crate::expression::parentheses::in_parentheses_only_soft_line_break_or_space;
|
|
||||||
use crate::other::f_string::FormatFString;
|
|
||||||
use crate::other::string_literal::StringLiteralKind;
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::QuoteStyle;
|
use crate::QuoteStyle;
|
||||||
|
|
||||||
pub(crate) mod docstring;
|
pub(crate) mod docstring;
|
||||||
|
pub(crate) mod implicit;
|
||||||
mod normalize;
|
mod normalize;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Default)]
|
#[derive(Copy, Clone, Debug, Default)]
|
||||||
|
@ -29,57 +25,6 @@ pub(crate) enum Quoting {
|
||||||
Preserve,
|
Preserve,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Formats any implicitly concatenated string. This could be any valid combination
|
|
||||||
/// of string, bytes or f-string literals.
|
|
||||||
pub(crate) struct FormatImplicitConcatenatedString<'a> {
|
|
||||||
string: StringLike<'a>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> FormatImplicitConcatenatedString<'a> {
|
|
||||||
pub(crate) fn new(string: impl Into<StringLike<'a>>) -> Self {
|
|
||||||
Self {
|
|
||||||
string: string.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Format<PyFormatContext<'_>> for FormatImplicitConcatenatedString<'_> {
|
|
||||||
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
|
||||||
let comments = f.context().comments().clone();
|
|
||||||
let quoting = self.string.quoting(&f.context().locator());
|
|
||||||
|
|
||||||
let mut joiner = f.join_with(in_parentheses_only_soft_line_break_or_space());
|
|
||||||
|
|
||||||
for part in self.string.parts() {
|
|
||||||
let part_comments = comments.leading_dangling_trailing(&part);
|
|
||||||
|
|
||||||
let format_part = format_with(|f: &mut PyFormatter| match part {
|
|
||||||
StringLikePart::String(part) => {
|
|
||||||
let kind = if self.string.is_fstring() {
|
|
||||||
#[allow(deprecated)]
|
|
||||||
StringLiteralKind::InImplicitlyConcatenatedFString(quoting)
|
|
||||||
} else {
|
|
||||||
StringLiteralKind::String
|
|
||||||
};
|
|
||||||
|
|
||||||
part.format().with_options(kind).fmt(f)
|
|
||||||
}
|
|
||||||
StringLikePart::Bytes(bytes_literal) => bytes_literal.format().fmt(f),
|
|
||||||
StringLikePart::FString(part) => FormatFString::new(part, quoting).fmt(f),
|
|
||||||
});
|
|
||||||
|
|
||||||
joiner.entry(&format_args![
|
|
||||||
line_suffix_boundary(),
|
|
||||||
leading_comments(part_comments.leading),
|
|
||||||
format_part,
|
|
||||||
trailing_comments(part_comments.trailing)
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
joiner.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Format<PyFormatContext<'_>> for AnyStringPrefix {
|
impl Format<PyFormatContext<'_>> for AnyStringPrefix {
|
||||||
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
||||||
// Remove the unicode prefix `u` if any because it is meaningless in Python 3+.
|
// Remove the unicode prefix `u` if any because it is meaningless in Python 3+.
|
||||||
|
@ -159,12 +104,10 @@ impl StringLikeExtensions for ast::StringLike<'_> {
|
||||||
|
|
||||||
fn is_multiline(&self, source: &str) -> bool {
|
fn is_multiline(&self, source: &str) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::String(_) | Self::Bytes(_) => {
|
Self::String(_) | Self::Bytes(_) => self.parts().any(|part| {
|
||||||
self.parts()
|
part.flags().is_triple_quoted()
|
||||||
.next()
|
|
||||||
.is_some_and(|part| part.flags().is_triple_quoted())
|
|
||||||
&& memchr2(b'\n', b'\r', source[self.range()].as_bytes()).is_some()
|
&& memchr2(b'\n', b'\r', source[self.range()].as_bytes()).is_some()
|
||||||
}
|
}),
|
||||||
Self::FString(fstring) => {
|
Self::FString(fstring) => {
|
||||||
memchr2(b'\n', b'\r', source[fstring.range].as_bytes()).is_some()
|
memchr2(b'\n', b'\r', source[fstring.range].as_bytes()).is_some()
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,9 @@ impl<'a, 'src> StringNormalizer<'a, 'src> {
|
||||||
/// 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.
|
||||||
|
///
|
||||||
|
/// 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 {
|
pub(super) fn preferred_quote_style(&self, string: StringLikePart) -> QuoteStyle {
|
||||||
match self.quoting {
|
match self.quoting {
|
||||||
Quoting::Preserve => QuoteStyle::Preserve,
|
Quoting::Preserve => QuoteStyle::Preserve,
|
||||||
|
@ -205,6 +208,8 @@ impl<'a, 'src> StringNormalizer<'a, 'src> {
|
||||||
quote_selection.flags,
|
quote_selection.flags,
|
||||||
// TODO: Remove the `b'{'` in `choose_quotes` when promoting the
|
// TODO: Remove the `b'{'` in `choose_quotes` when promoting the
|
||||||
// `format_fstring` preview style
|
// `format_fstring` preview style
|
||||||
|
false,
|
||||||
|
false,
|
||||||
is_f_string_formatting_enabled(self.context),
|
is_f_string_formatting_enabled(self.context),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -598,6 +603,8 @@ pub(crate) fn normalize_string(
|
||||||
input: &str,
|
input: &str,
|
||||||
start_offset: usize,
|
start_offset: usize,
|
||||||
new_flags: AnyStringFlags,
|
new_flags: AnyStringFlags,
|
||||||
|
escape_braces: bool,
|
||||||
|
flip_nested_fstring_quotes: bool,
|
||||||
format_f_string: bool,
|
format_f_string: bool,
|
||||||
) -> Cow<str> {
|
) -> Cow<str> {
|
||||||
// The normalized string if `input` is not yet normalized.
|
// The normalized string if `input` is not yet normalized.
|
||||||
|
@ -620,16 +627,24 @@ pub(crate) fn normalize_string(
|
||||||
|
|
||||||
while let Some((index, c)) = chars.next() {
|
while let Some((index, c)) = chars.next() {
|
||||||
if matches!(c, '{' | '}') && is_fstring {
|
if matches!(c, '{' | '}') && is_fstring {
|
||||||
if chars.peek().copied().is_some_and(|(_, next)| next == c) {
|
if escape_braces {
|
||||||
// Skip over the second character of the double braces
|
// Escape `{` and `}` when converting a regular string literal to an f-string literal.
|
||||||
chars.next();
|
output.push_str(&input[last_index..=index]);
|
||||||
} else if c == '{' {
|
output.push(c);
|
||||||
formatted_value_nesting += 1;
|
last_index = index + c.len_utf8();
|
||||||
} else {
|
continue;
|
||||||
// Safe to assume that `c == '}'` here because of the matched pattern above
|
} else if is_fstring {
|
||||||
formatted_value_nesting = formatted_value_nesting.saturating_sub(1);
|
if chars.peek().copied().is_some_and(|(_, next)| next == c) {
|
||||||
|
// Skip over the second character of the double braces
|
||||||
|
chars.next();
|
||||||
|
} else if c == '{' {
|
||||||
|
formatted_value_nesting += 1;
|
||||||
|
} else {
|
||||||
|
// Safe to assume that `c == '}'` here because of the matched pattern above
|
||||||
|
formatted_value_nesting = formatted_value_nesting.saturating_sub(1);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if c == '\r' {
|
if c == '\r' {
|
||||||
|
@ -697,6 +712,14 @@ pub(crate) fn normalize_string(
|
||||||
output.push('\\');
|
output.push('\\');
|
||||||
output.push(c);
|
output.push(c);
|
||||||
last_index = index + preferred_quote.len_utf8();
|
last_index = index + preferred_quote.len_utf8();
|
||||||
|
} else if c == preferred_quote
|
||||||
|
&& flip_nested_fstring_quotes
|
||||||
|
&& formatted_value_nesting > 0
|
||||||
|
{
|
||||||
|
// Flip the quotes
|
||||||
|
output.push_str(&input[last_index..index]);
|
||||||
|
output.push(opposite_quote);
|
||||||
|
last_index = index + preferred_quote.len_utf8();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -981,6 +1004,7 @@ pub(super) fn is_fstring_with_triple_quoted_literal_expression_containing_quotes
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use ruff_python_ast::str_prefix::FStringPrefix;
|
||||||
use ruff_python_ast::{
|
use ruff_python_ast::{
|
||||||
str::Quote,
|
str::Quote,
|
||||||
str_prefix::{AnyStringPrefix, ByteStringPrefix},
|
str_prefix::{AnyStringPrefix, ByteStringPrefix},
|
||||||
|
@ -1013,9 +1037,35 @@ mod tests {
|
||||||
Quote::Double,
|
Quote::Double,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
false,
|
||||||
|
false,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(r"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a", &normalized);
|
assert_eq!(r"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a", &normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalize_nested_fstring() {
|
||||||
|
let input =
|
||||||
|
r#"With single quote: ' {my_dict['foo']} With double quote: " {my_dict["bar"]}"#;
|
||||||
|
|
||||||
|
let normalized = normalize_string(
|
||||||
|
input,
|
||||||
|
0,
|
||||||
|
AnyStringFlags::new(
|
||||||
|
AnyStringPrefix::Format(FStringPrefix::Regular),
|
||||||
|
Quote::Double,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
"With single quote: ' {my_dict['foo']} With double quote: \\\" {my_dict['bar']}",
|
||||||
|
&normalized
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,11 @@ use {
|
||||||
|
|
||||||
use ruff_python_ast::visitor::transformer;
|
use ruff_python_ast::visitor::transformer;
|
||||||
use ruff_python_ast::visitor::transformer::Transformer;
|
use ruff_python_ast::visitor::transformer::Transformer;
|
||||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
use ruff_python_ast::{
|
||||||
|
self as ast, BytesLiteralFlags, Expr, FStringElement, FStringFlags, FStringLiteralElement,
|
||||||
|
FStringPart, Stmt, StringFlags, StringLiteralFlags,
|
||||||
|
};
|
||||||
|
use ruff_text_size::{Ranged, TextRange};
|
||||||
|
|
||||||
/// A struct to normalize AST nodes for the purpose of comparing formatted representations for
|
/// A struct to normalize AST nodes for the purpose of comparing formatted representations for
|
||||||
/// semantic equivalence.
|
/// semantic equivalence.
|
||||||
|
@ -59,6 +63,135 @@ impl Transformer for Normalizer {
|
||||||
transformer::walk_stmt(self, stmt);
|
transformer::walk_stmt(self, stmt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn visit_expr(&self, expr: &mut Expr) {
|
||||||
|
// Ruff supports joining implicitly concatenated strings. The code below implements this
|
||||||
|
// at an AST level by joining the string literals in the AST if they can be joined (it doesn't mean that
|
||||||
|
// they'll be joined in the formatted output but they could).
|
||||||
|
// Comparable expression handles some of this by comparing the concatenated string
|
||||||
|
// but not joining here doesn't play nicely with other string normalizations done in the
|
||||||
|
// Normalizer.
|
||||||
|
match expr {
|
||||||
|
Expr::StringLiteral(string) => {
|
||||||
|
if string.value.is_implicit_concatenated() {
|
||||||
|
let can_join = string.value.iter().all(|literal| {
|
||||||
|
!literal.flags.is_triple_quoted() && !literal.flags.prefix().is_raw()
|
||||||
|
});
|
||||||
|
|
||||||
|
if can_join {
|
||||||
|
string.value = ast::StringLiteralValue::single(ast::StringLiteral {
|
||||||
|
value: string.value.to_str().to_string().into_boxed_str(),
|
||||||
|
range: string.range,
|
||||||
|
flags: StringLiteralFlags::default(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Expr::BytesLiteral(bytes) => {
|
||||||
|
if bytes.value.is_implicit_concatenated() {
|
||||||
|
let can_join = bytes.value.iter().all(|literal| {
|
||||||
|
!literal.flags.is_triple_quoted() && !literal.flags.prefix().is_raw()
|
||||||
|
});
|
||||||
|
|
||||||
|
if can_join {
|
||||||
|
bytes.value = ast::BytesLiteralValue::single(ast::BytesLiteral {
|
||||||
|
value: bytes.value.bytes().collect(),
|
||||||
|
range: bytes.range,
|
||||||
|
flags: BytesLiteralFlags::default(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Expr::FString(fstring) => {
|
||||||
|
if fstring.value.is_implicit_concatenated() {
|
||||||
|
let can_join = fstring.value.iter().all(|part| match part {
|
||||||
|
FStringPart::Literal(literal) => {
|
||||||
|
!literal.flags.is_triple_quoted() && !literal.flags.prefix().is_raw()
|
||||||
|
}
|
||||||
|
FStringPart::FString(string) => {
|
||||||
|
!string.flags.is_triple_quoted() && !string.flags.prefix().is_raw()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if can_join {
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Collector {
|
||||||
|
elements: Vec<FStringElement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Collector {
|
||||||
|
// The logic for concatenating adjacent string literals
|
||||||
|
// occurs here, implicitly: when we encounter a sequence
|
||||||
|
// of string literals, the first gets pushed to the
|
||||||
|
// `elements` vector, while subsequent strings
|
||||||
|
// are concatenated onto this top string.
|
||||||
|
fn push_literal(&mut self, literal: &str, range: TextRange) {
|
||||||
|
if let Some(FStringElement::Literal(existing_literal)) =
|
||||||
|
self.elements.last_mut()
|
||||||
|
{
|
||||||
|
let value = std::mem::take(&mut existing_literal.value);
|
||||||
|
let mut value = value.into_string();
|
||||||
|
value.push_str(literal);
|
||||||
|
existing_literal.value = value.into_boxed_str();
|
||||||
|
existing_literal.range =
|
||||||
|
TextRange::new(existing_literal.start(), range.end());
|
||||||
|
} else {
|
||||||
|
self.elements.push(FStringElement::Literal(
|
||||||
|
FStringLiteralElement {
|
||||||
|
range,
|
||||||
|
value: literal.into(),
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_expression(
|
||||||
|
&mut self,
|
||||||
|
expression: ast::FStringExpressionElement,
|
||||||
|
) {
|
||||||
|
self.elements.push(FStringElement::Expression(expression));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut collector = Collector::default();
|
||||||
|
|
||||||
|
for part in &fstring.value {
|
||||||
|
match part {
|
||||||
|
ast::FStringPart::Literal(string_literal) => {
|
||||||
|
collector
|
||||||
|
.push_literal(&string_literal.value, string_literal.range);
|
||||||
|
}
|
||||||
|
ast::FStringPart::FString(fstring) => {
|
||||||
|
for element in &fstring.elements {
|
||||||
|
match element {
|
||||||
|
ast::FStringElement::Literal(literal) => {
|
||||||
|
collector
|
||||||
|
.push_literal(&literal.value, literal.range);
|
||||||
|
}
|
||||||
|
ast::FStringElement::Expression(expression) => {
|
||||||
|
collector.push_expression(expression.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fstring.value = ast::FStringValue::single(ast::FString {
|
||||||
|
elements: collector.elements.into(),
|
||||||
|
range: fstring.range,
|
||||||
|
flags: FStringFlags::default(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
transformer::walk_expr(self, expr);
|
||||||
|
}
|
||||||
|
|
||||||
fn visit_string_literal(&self, string_literal: &mut ast::StringLiteral) {
|
fn visit_string_literal(&self, string_literal: &mut ast::StringLiteral) {
|
||||||
static STRIP_DOC_TESTS: LazyLock<Regex> = LazyLock::new(|| {
|
static STRIP_DOC_TESTS: LazyLock<Regex> = LazyLock::new(|| {
|
||||||
Regex::new(
|
Regex::new(
|
||||||
|
|
|
@ -610,5 +610,3 @@ class C:
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -667,21 +667,21 @@ log.info(f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share
|
||||||
"Arg #2",
|
"Arg #2",
|
||||||
"Arg #3",
|
"Arg #3",
|
||||||
"Arg #4",
|
"Arg #4",
|
||||||
@@ -315,80 +232,72 @@
|
@@ -316,79 +233,75 @@
|
||||||
|
|
||||||
triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched."""
|
triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched."""
|
||||||
|
|
||||||
-assert some_type_of_boolean_expression, (
|
assert some_type_of_boolean_expression, (
|
||||||
- "Followed by a really really really long string that is used to provide context to"
|
- "Followed by a really really really long string that is used to provide context to"
|
||||||
- " the AssertionError exception."
|
- " the AssertionError exception."
|
||||||
-)
|
+ "Followed by a really really really long string that is used to provide context to the AssertionError exception."
|
||||||
+assert some_type_of_boolean_expression, "Followed by a really really really long string that is used to provide context to the AssertionError exception."
|
)
|
||||||
|
|
||||||
-assert some_type_of_boolean_expression, (
|
assert some_type_of_boolean_expression, (
|
||||||
- "Followed by a really really really long string that is used to provide context to"
|
- "Followed by a really really really long string that is used to provide context to"
|
||||||
- " the AssertionError exception, which uses dynamic string {}.".format("formatting")
|
- " the AssertionError exception, which uses dynamic string {}.".format("formatting")
|
||||||
+assert some_type_of_boolean_expression, "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string {}.".format(
|
+ "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string {}.".format(
|
||||||
+ "formatting"
|
+ "formatting"
|
||||||
|
+ )
|
||||||
)
|
)
|
||||||
|
|
||||||
assert some_type_of_boolean_expression, (
|
assert some_type_of_boolean_expression, (
|
||||||
|
@ -772,7 +772,7 @@ log.info(f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
z,
|
z,
|
||||||
@@ -397,7 +306,7 @@
|
@@ -397,7 +310,7 @@
|
||||||
func_with_bad_parens(
|
func_with_bad_parens(
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
|
@ -781,7 +781,7 @@ log.info(f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share
|
||||||
z,
|
z,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -408,50 +317,27 @@
|
@@ -408,50 +321,27 @@
|
||||||
+ CONCATENATED
|
+ CONCATENATED
|
||||||
+ "using the '+' operator."
|
+ "using the '+' operator."
|
||||||
)
|
)
|
||||||
|
@ -813,11 +813,10 @@ log.info(f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share
|
||||||
+backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\\\"
|
+backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\\\"
|
||||||
+backslashes = "This is a really 'long' string with \"embedded double quotes\" and 'single' quotes that also handles checking for an odd number of backslashes \\\", like this...\\\\\\"
|
+backslashes = "This is a really 'long' string with \"embedded double quotes\" and 'single' quotes that also handles checking for an odd number of backslashes \\\", like this...\\\\\\"
|
||||||
|
|
||||||
-short_string = "Hi there."
|
short_string = "Hi there."
|
||||||
+short_string = "Hi" " there."
|
|
||||||
|
|
||||||
-func_call(short_string="Hi there.")
|
-func_call(short_string="Hi there.")
|
||||||
+func_call(short_string=("Hi" " there."))
|
+func_call(short_string=("Hi there."))
|
||||||
|
|
||||||
raw_strings = r"Don't" " get" r" merged" " unless they are all raw."
|
raw_strings = r"Don't" " get" r" merged" " unless they are all raw."
|
||||||
|
|
||||||
|
@ -841,7 +840,7 @@ log.info(f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share
|
||||||
|
|
||||||
long_unmergable_string_with_pragma = (
|
long_unmergable_string_with_pragma = (
|
||||||
"This is a really long string that can't be merged because it has a likely pragma at the end" # type: ignore
|
"This is a really long string that can't be merged because it has a likely pragma at the end" # type: ignore
|
||||||
@@ -468,51 +354,24 @@
|
@@ -468,51 +358,24 @@
|
||||||
" of it."
|
" of it."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -902,7 +901,7 @@ log.info(f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share
|
||||||
)
|
)
|
||||||
|
|
||||||
dict_with_lambda_values = {
|
dict_with_lambda_values = {
|
||||||
@@ -524,65 +383,58 @@
|
@@ -524,65 +387,58 @@
|
||||||
|
|
||||||
# Complex string concatenations with a method call in the middle.
|
# Complex string concatenations with a method call in the middle.
|
||||||
code = (
|
code = (
|
||||||
|
@ -986,7 +985,7 @@ log.info(f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share
|
||||||
)
|
)
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
@@ -590,5 +442,5 @@
|
@@ -590,5 +446,5 @@
|
||||||
)
|
)
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
|
@ -1232,10 +1231,14 @@ pragma_comment_string2 = "Lines which end with an inline pragma comment of the f
|
||||||
|
|
||||||
triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched."""
|
triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched."""
|
||||||
|
|
||||||
assert some_type_of_boolean_expression, "Followed by a really really really long string that is used to provide context to the AssertionError exception."
|
assert some_type_of_boolean_expression, (
|
||||||
|
"Followed by a really really really long string that is used to provide context to the AssertionError exception."
|
||||||
|
)
|
||||||
|
|
||||||
assert some_type_of_boolean_expression, "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string {}.".format(
|
assert some_type_of_boolean_expression, (
|
||||||
"formatting"
|
"Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string {}.".format(
|
||||||
|
"formatting"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
assert some_type_of_boolean_expression, (
|
assert some_type_of_boolean_expression, (
|
||||||
|
@ -1326,9 +1329,9 @@ backslashes = "This is a really long string with \"embedded\" double quotes and
|
||||||
backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\\\"
|
backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\\\"
|
||||||
backslashes = "This is a really 'long' string with \"embedded double quotes\" and 'single' quotes that also handles checking for an odd number of backslashes \\\", like this...\\\\\\"
|
backslashes = "This is a really 'long' string with \"embedded double quotes\" and 'single' quotes that also handles checking for an odd number of backslashes \\\", like this...\\\\\\"
|
||||||
|
|
||||||
short_string = "Hi" " there."
|
short_string = "Hi there."
|
||||||
|
|
||||||
func_call(short_string=("Hi" " there."))
|
func_call(short_string=("Hi there."))
|
||||||
|
|
||||||
raw_strings = r"Don't" " get" r" merged" " unless they are all raw."
|
raw_strings = r"Don't" " get" r" merged" " unless they are all raw."
|
||||||
|
|
||||||
|
|
|
@ -614,26 +614,20 @@ s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry:
|
||||||
),
|
),
|
||||||
varX,
|
varX,
|
||||||
varY,
|
varY,
|
||||||
@@ -70,9 +69,10 @@
|
@@ -71,8 +70,9 @@
|
||||||
def foo(xxxx):
|
|
||||||
for xxx_xxxx, _xxx_xxx, _xxx_xxxxx, xxx_xxxx in xxxx:
|
for xxx_xxxx, _xxx_xxx, _xxx_xxxxx, xxx_xxxx in xxxx:
|
||||||
for xxx in xxx_xxxx:
|
for xxx in xxx_xxxx:
|
||||||
- assert ("x" in xxx) or (xxx in xxx_xxx_xxxxx), (
|
assert ("x" in xxx) or (xxx in xxx_xxx_xxxxx), (
|
||||||
- "{0} xxxxxxx xx {1}, xxx {1} xx xxx xx xxxx xx xxx xxxx: xxx xxxx {2}"
|
- "{0} xxxxxxx xx {1}, xxx {1} xx xxx xx xxxx xx xxx xxxx: xxx xxxx {2}"
|
||||||
- .format(xxx_xxxx, xxx, xxxxxx.xxxxxxx(xxx_xxx_xxxxx))
|
- .format(xxx_xxxx, xxx, xxxxxx.xxxxxxx(xxx_xxx_xxxxx))
|
||||||
+ assert (
|
+ "{0} xxxxxxx xx {1}, xxx {1} xx xxx xx xxxx xx xxx xxxx: xxx xxxx {2}".format(
|
||||||
+ ("x" in xxx) or (xxx in xxx_xxx_xxxxx)
|
+ xxx_xxxx, xxx, xxxxxx.xxxxxxx(xxx_xxx_xxxxx)
|
||||||
+ ), "{0} xxxxxxx xx {1}, xxx {1} xx xxx xx xxxx xx xxx xxxx: xxx xxxx {2}".format(
|
+ )
|
||||||
+ xxx_xxxx, xxx, xxxxxx.xxxxxxx(xxx_xxx_xxxxx)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -80,10 +80,11 @@
|
@@ -83,7 +83,8 @@
|
||||||
def disappearing_comment():
|
"{{xxx_xxxxxxxxxx_xxxxxxxx}} xxx xxxx {} {{xxxx}} >&2".format(
|
||||||
return (
|
|
||||||
( # xx -x xxxxxxx xx xxx xxxxxxx.
|
|
||||||
- "{{xxx_xxxxxxxxxx_xxxxxxxx}} xxx xxxx {} {{xxxx}} >&2".format(
|
|
||||||
+ "{{xxx_xxxxxxxxxx_xxxxxxxx}} xxx xxxx" " {} {{xxxx}} >&2".format(
|
|
||||||
"{xxxx} {xxxxxx}"
|
"{xxxx} {xxxxxx}"
|
||||||
if xxxxx.xx_xxxxxxxxxx
|
if xxxxx.xx_xxxxxxxxxx
|
||||||
- else ( # Disappearing Comment
|
- else ( # Disappearing Comment
|
||||||
|
@ -689,7 +683,7 @@ s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry:
|
||||||
+ (
|
+ (
|
||||||
+ "xxxxxxxxxx xxxx xx xxxxxx(%x) xx %x xxxx xx xxx %x.xx"
|
+ "xxxxxxxxxx xxxx xx xxxxxx(%x) xx %x xxxx xx xxx %x.xx"
|
||||||
+ % (len(self) + 1, xxxx.xxxxxxxxxx, xxxx.xxxxxxxxxx)
|
+ % (len(self) + 1, xxxx.xxxxxxxxxx, xxxx.xxxxxxxxxx)
|
||||||
+ )
|
)
|
||||||
+ + (
|
+ + (
|
||||||
+ " %.3f (%s) to %.3f (%s).\n"
|
+ " %.3f (%s) to %.3f (%s).\n"
|
||||||
+ % (
|
+ % (
|
||||||
|
@ -698,7 +692,7 @@ s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry:
|
||||||
+ x,
|
+ x,
|
||||||
+ xxxx.xxxxxxxxxxxxxx(xx),
|
+ xxxx.xxxxxxxxxxxxxx(xx),
|
||||||
+ )
|
+ )
|
||||||
)
|
+ )
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -783,7 +777,7 @@ s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -232,39 +248,24 @@
|
@@ -232,36 +248,21 @@
|
||||||
|
|
||||||
some_dictionary = {
|
some_dictionary = {
|
||||||
"xxxxx006": [
|
"xxxxx006": [
|
||||||
|
@ -827,12 +821,8 @@ s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry:
|
||||||
+ ) # xxxx xxxxxxxxxx xxxx xx xxxx xx xxx xxxxxxxx xxxxxx xxxxx.
|
+ ) # xxxx xxxxxxxxxx xxxx xx xxxx xx xxx xxxxxxxx xxxxxx xxxxx.
|
||||||
|
|
||||||
|
|
||||||
-some_tuple = ("some string", "some string which should be joined")
|
some_tuple = ("some string", "some string which should be joined")
|
||||||
+some_tuple = ("some string", "some string" " which should be joined")
|
@@ -279,34 +280,21 @@
|
||||||
|
|
||||||
some_commented_string = ( # This comment stays at the top.
|
|
||||||
"This string is long but not so long that it needs hahahah toooooo be so greatttt"
|
|
||||||
@@ -279,37 +280,26 @@
|
|
||||||
)
|
)
|
||||||
|
|
||||||
lpar_and_rpar_have_comments = func_call( # LPAR Comment
|
lpar_and_rpar_have_comments = func_call( # LPAR Comment
|
||||||
|
@ -852,33 +842,28 @@ s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry:
|
||||||
- f" {'' if ID is None else ID} | perl -nE 'print if /^{field}:/'"
|
- f" {'' if ID is None else ID} | perl -nE 'print if /^{field}:/'"
|
||||||
-)
|
-)
|
||||||
+cmd_fstring = f"sudo -E deluge-console info --detailed --sort-reverse=time_added {'' if ID is None else ID} | perl -nE 'print if /^{field}:/'"
|
+cmd_fstring = f"sudo -E deluge-console info --detailed --sort-reverse=time_added {'' if ID is None else ID} | perl -nE 'print if /^{field}:/'"
|
||||||
+
|
|
||||||
+cmd_fstring = f"sudo -E deluge-console info --detailed --sort-reverse=time_added {'{{}}' if ID is None else ID} | perl -nE 'print if /^{field}:/'"
|
|
||||||
|
|
||||||
-cmd_fstring = (
|
-cmd_fstring = (
|
||||||
- "sudo -E deluge-console info --detailed --sort-reverse=time_added"
|
- "sudo -E deluge-console info --detailed --sort-reverse=time_added"
|
||||||
- f" {'{{}}' if ID is None else ID} | perl -nE 'print if /^{field}:/'"
|
- f" {'{{}}' if ID is None else ID} | perl -nE 'print if /^{field}:/'"
|
||||||
-)
|
-)
|
||||||
+cmd_fstring = f"sudo -E deluge-console info --detailed --sort-reverse=time_added {{'' if ID is None else ID}} | perl -nE 'print if /^{field}:/'"
|
+cmd_fstring = f"sudo -E deluge-console info --detailed --sort-reverse=time_added {'{{}}' if ID is None else ID} | perl -nE 'print if /^{field}:/'"
|
||||||
|
|
||||||
-cmd_fstring = (
|
-cmd_fstring = (
|
||||||
- "sudo -E deluge-console info --detailed --sort-reverse=time_added {'' if ID is"
|
- "sudo -E deluge-console info --detailed --sort-reverse=time_added {'' if ID is"
|
||||||
- f" None else ID}} | perl -nE 'print if /^{field}:/'"
|
- f" None else ID}} | perl -nE 'print if /^{field}:/'"
|
||||||
-)
|
-)
|
||||||
+fstring = f"This string really doesn't need to be an {{{{fstring}}}}, but this one most certainly, absolutely {does}."
|
+cmd_fstring = f"sudo -E deluge-console info --detailed --sort-reverse=time_added {{'' if ID is None else ID}} | perl -nE 'print if /^{field}:/'"
|
||||||
|
|
||||||
fstring = (
|
-fstring = (
|
||||||
- "This string really doesn't need to be an {{fstring}}, but this one most"
|
- "This string really doesn't need to be an {{fstring}}, but this one most"
|
||||||
- f" certainly, absolutely {does}."
|
- f" certainly, absolutely {does}."
|
||||||
+ f"We have to remember to escape {braces}." " Like {these}." f" But not {this}."
|
-)
|
||||||
)
|
+fstring = f"This string really doesn't need to be an {{{{fstring}}}}, but this one most certainly, absolutely {does}."
|
||||||
|
|
||||||
-fstring = f"We have to remember to escape {braces}. Like {{these}}. But not {this}."
|
fstring = f"We have to remember to escape {braces}. Like {{these}}. But not {this}."
|
||||||
-
|
|
||||||
|
|
||||||
class A:
|
@@ -364,10 +352,7 @@
|
||||||
class B:
|
|
||||||
@@ -364,10 +354,7 @@
|
|
||||||
def foo():
|
def foo():
|
||||||
if not hasattr(module, name):
|
if not hasattr(module, name):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
@ -890,7 +875,7 @@ s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry:
|
||||||
% (name, module_name, get_docs_version())
|
% (name, module_name, get_docs_version())
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -382,23 +369,19 @@
|
@@ -382,23 +367,19 @@
|
||||||
|
|
||||||
class Step(StepBase):
|
class Step(StepBase):
|
||||||
def who(self):
|
def who(self):
|
||||||
|
@ -921,7 +906,7 @@ s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry:
|
||||||
# xxxxx xxxxxxxxxxxx xxxx xxx (xxxxxxxxxxxxxxxx) xx x xxxxxxxxx xx xxxxxx.
|
# xxxxx xxxxxxxxxxxx xxxx xxx (xxxxxxxxxxxxxxxx) xx x xxxxxxxxx xx xxxxxx.
|
||||||
"(x.bbbbbbbbbbbb.xxx != "
|
"(x.bbbbbbbbbbbb.xxx != "
|
||||||
'"xxx:xxx:xxx::cccccccccccc:xxxxxxx-xxxx/xxxxxxxxxxx/xxxxxxxxxxxxxxxxx") && '
|
'"xxx:xxx:xxx::cccccccccccc:xxxxxxx-xxxx/xxxxxxxxxxx/xxxxxxxxxxxxxxxxx") && '
|
||||||
@@ -409,8 +392,8 @@
|
@@ -409,8 +390,8 @@
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
for i in range(4, 8):
|
for i in range(4, 8):
|
||||||
cmd = (
|
cmd = (
|
||||||
|
@ -932,7 +917,7 @@ s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -432,9 +415,7 @@
|
@@ -432,9 +413,7 @@
|
||||||
assert xxxxxxx_xxxx in [
|
assert xxxxxxx_xxxx in [
|
||||||
x.xxxxx.xxxxxx.xxxxx.xxxxxx,
|
x.xxxxx.xxxxxx.xxxxx.xxxxxx,
|
||||||
x.xxxxx.xxxxxx.xxxxx.xxxx,
|
x.xxxxx.xxxxxx.xxxxx.xxxx,
|
||||||
|
@ -943,7 +928,7 @@ s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry:
|
||||||
|
|
||||||
|
|
||||||
value.__dict__[key] = (
|
value.__dict__[key] = (
|
||||||
@@ -449,8 +430,7 @@
|
@@ -449,8 +428,7 @@
|
||||||
|
|
||||||
RE_TWO_BACKSLASHES = {
|
RE_TWO_BACKSLASHES = {
|
||||||
"asdf_hjkl_jkl": re.compile(
|
"asdf_hjkl_jkl": re.compile(
|
||||||
|
@ -953,23 +938,23 @@ s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry:
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,13 +442,9 @@
|
@@ -462,13 +440,9 @@
|
||||||
|
|
||||||
# We do NOT split on f-string expressions.
|
# We do NOT split on f-string expressions.
|
||||||
print(
|
print(
|
||||||
- "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam."
|
- "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam."
|
||||||
- f" {[f'{i}' for i in range(10)]}"
|
- f" {[f'{i}' for i in range(10)]}"
|
||||||
+ f"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam. {[f'{i}' for i in range(10)]}"
|
-)
|
||||||
)
|
|
||||||
-x = (
|
-x = (
|
||||||
- "This is a long string which contains an f-expr that should not split"
|
- "This is a long string which contains an f-expr that should not split"
|
||||||
- f" {{{[i for i in range(5)]}}}."
|
- f" {{{[i for i in range(5)]}}}."
|
||||||
-)
|
+ f"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam. {[f'{i}' for i in range(10)]}"
|
||||||
|
)
|
||||||
+x = f"This is a long string which contains an f-expr that should not split {{{[i for i in range(5)]}}}."
|
+x = f"This is a long string which contains an f-expr that should not split {{{[i for i in range(5)]}}}."
|
||||||
|
|
||||||
# The parens should NOT be removed in this case.
|
# The parens should NOT be removed in this case.
|
||||||
(
|
(
|
||||||
@@ -478,8 +454,8 @@
|
@@ -478,8 +452,8 @@
|
||||||
|
|
||||||
# The parens should NOT be removed in this case.
|
# The parens should NOT be removed in this case.
|
||||||
(
|
(
|
||||||
|
@ -980,7 +965,7 @@ s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry:
|
||||||
)
|
)
|
||||||
|
|
||||||
# The parens should NOT be removed in this case.
|
# The parens should NOT be removed in this case.
|
||||||
@@ -513,93 +489,83 @@
|
@@ -513,93 +487,83 @@
|
||||||
|
|
||||||
|
|
||||||
temp_msg = (
|
temp_msg = (
|
||||||
|
@ -1110,7 +1095,7 @@ s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry:
|
||||||
"6. Click on Create Credential at the top."
|
"6. Click on Create Credential at the top."
|
||||||
'7. At the top click the link for "API key".'
|
'7. At the top click the link for "API key".'
|
||||||
"8. No application restrictions are needed. Click Create at the bottom."
|
"8. No application restrictions are needed. Click Create at the bottom."
|
||||||
@@ -608,60 +574,45 @@
|
@@ -608,7 +572,7 @@
|
||||||
|
|
||||||
# It shouldn't matter if the string prefixes are capitalized.
|
# It shouldn't matter if the string prefixes are capitalized.
|
||||||
temp_msg = (
|
temp_msg = (
|
||||||
|
@ -1119,11 +1104,7 @@ s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry:
|
||||||
f"{balance: <{bal_len + 5}} "
|
f"{balance: <{bal_len + 5}} "
|
||||||
f"<<{author.display_name}>>\n"
|
f"<<{author.display_name}>>\n"
|
||||||
)
|
)
|
||||||
|
@@ -617,51 +581,34 @@
|
||||||
-fstring = f"We have to remember to escape {braces}. Like {{these}}. But not {this}."
|
|
||||||
+fstring = (
|
|
||||||
+ f"We have to remember to escape {braces}." " Like {these}." f" But not {this}."
|
|
||||||
+)
|
|
||||||
|
|
||||||
welcome_to_programming = R"hello," R" world!"
|
welcome_to_programming = R"hello," R" world!"
|
||||||
|
|
||||||
|
@ -1189,21 +1170,14 @@ s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry:
|
||||||
)
|
)
|
||||||
|
|
||||||
# Regression test for https://github.com/psf/black/issues/3455.
|
# Regression test for https://github.com/psf/black/issues/3455.
|
||||||
@@ -672,9 +623,11 @@
|
@@ -674,7 +621,4 @@
|
||||||
}
|
|
||||||
|
|
||||||
# Regression test for https://github.com/psf/black/issues/3506.
|
# Regression test for https://github.com/psf/black/issues/3506.
|
||||||
-s = f"With single quote: ' {my_dict['foo']} With double quote: \" {my_dict['bar']}"
|
s = f"With single quote: ' {my_dict['foo']} With double quote: \" {my_dict['bar']}"
|
||||||
-
|
|
||||||
s = (
|
-s = (
|
||||||
- "Lorem Ipsum is simply dummy text of the printing and typesetting"
|
- "Lorem Ipsum is simply dummy text of the printing and typesetting"
|
||||||
- f" industry:'{my_dict['foo']}'"
|
- f" industry:'{my_dict['foo']}'"
|
||||||
+ "With single quote: ' "
|
-)
|
||||||
+ f" {my_dict['foo']}"
|
|
||||||
+ ' With double quote: " '
|
|
||||||
+ 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']}'"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -1281,10 +1255,10 @@ class A:
|
||||||
def foo(xxxx):
|
def foo(xxxx):
|
||||||
for xxx_xxxx, _xxx_xxx, _xxx_xxxxx, xxx_xxxx in xxxx:
|
for xxx_xxxx, _xxx_xxx, _xxx_xxxxx, xxx_xxxx in xxxx:
|
||||||
for xxx in xxx_xxxx:
|
for xxx in xxx_xxxx:
|
||||||
assert (
|
assert ("x" in xxx) or (xxx in xxx_xxx_xxxxx), (
|
||||||
("x" in xxx) or (xxx in xxx_xxx_xxxxx)
|
"{0} xxxxxxx xx {1}, xxx {1} xx xxx xx xxxx xx xxx xxxx: xxx xxxx {2}".format(
|
||||||
), "{0} xxxxxxx xx {1}, xxx {1} xx xxx xx xxxx xx xxx xxxx: xxx xxxx {2}".format(
|
xxx_xxxx, xxx, xxxxxx.xxxxxxx(xxx_xxx_xxxxx)
|
||||||
xxx_xxxx, xxx, xxxxxx.xxxxxxx(xxx_xxx_xxxxx)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1292,7 +1266,7 @@ class A:
|
||||||
def disappearing_comment():
|
def disappearing_comment():
|
||||||
return (
|
return (
|
||||||
( # xx -x xxxxxxx xx xxx xxxxxxx.
|
( # xx -x xxxxxxx xx xxx xxxxxxx.
|
||||||
"{{xxx_xxxxxxxxxx_xxxxxxxx}} xxx xxxx" " {} {{xxxx}} >&2".format(
|
"{{xxx_xxxxxxxxxx_xxxxxxxx}} xxx xxxx {} {{xxxx}} >&2".format(
|
||||||
"{xxxx} {xxxxxx}"
|
"{xxxx} {xxxxxx}"
|
||||||
if xxxxx.xx_xxxxxxxxxx
|
if xxxxx.xx_xxxxxxxxxx
|
||||||
# Disappearing Comment
|
# Disappearing Comment
|
||||||
|
@ -1477,7 +1451,7 @@ def foo():
|
||||||
) # xxxx xxxxxxxxxx xxxx xx xxxx xx xxx xxxxxxxx xxxxxx xxxxx.
|
) # xxxx xxxxxxxxxx xxxx xx xxxx xx xxx xxxxxxxx xxxxxx xxxxx.
|
||||||
|
|
||||||
|
|
||||||
some_tuple = ("some string", "some string" " which should be joined")
|
some_tuple = ("some string", "some string which should be joined")
|
||||||
|
|
||||||
some_commented_string = ( # This comment stays at the top.
|
some_commented_string = ( # This comment stays at the top.
|
||||||
"This string is long but not so long that it needs hahahah toooooo be so greatttt"
|
"This string is long but not so long that it needs hahahah toooooo be so greatttt"
|
||||||
|
@ -1508,9 +1482,7 @@ cmd_fstring = f"sudo -E deluge-console info --detailed --sort-reverse=time_added
|
||||||
|
|
||||||
fstring = f"This string really doesn't need to be an {{{{fstring}}}}, but this one most certainly, absolutely {does}."
|
fstring = f"This string really doesn't need to be an {{{{fstring}}}}, but this one most certainly, absolutely {does}."
|
||||||
|
|
||||||
fstring = (
|
fstring = f"We have to remember to escape {braces}. Like {{these}}. But not {this}."
|
||||||
f"We have to remember to escape {braces}." " Like {these}." f" But not {this}."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class A:
|
class A:
|
||||||
|
@ -1791,9 +1763,7 @@ temp_msg = (
|
||||||
f"<<{author.display_name}>>\n"
|
f"<<{author.display_name}>>\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
fstring = (
|
fstring = f"We have to remember to escape {braces}. Like {{these}}. But not {this}."
|
||||||
f"We have to remember to escape {braces}." " Like {these}." f" But not {this}."
|
|
||||||
)
|
|
||||||
|
|
||||||
welcome_to_programming = R"hello," R" world!"
|
welcome_to_programming = R"hello," R" world!"
|
||||||
|
|
||||||
|
@ -1835,12 +1805,7 @@ a_dict = {
|
||||||
}
|
}
|
||||||
|
|
||||||
# Regression test for https://github.com/psf/black/issues/3506.
|
# Regression test for https://github.com/psf/black/issues/3506.
|
||||||
s = (
|
s = f"With single quote: ' {my_dict['foo']} With double quote: \" {my_dict['bar']}"
|
||||||
"With single quote: ' "
|
|
||||||
f" {my_dict['foo']}"
|
|
||||||
' With double quote: " '
|
|
||||||
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']}'"
|
||||||
```
|
```
|
||||||
|
|
|
@ -45,8 +45,9 @@ def func(
|
||||||
|
|
||||||
def func(
|
def func(
|
||||||
- argument: "int |" "str",
|
- argument: "int |" "str",
|
||||||
+ argument: ("int |" "str"),
|
-) -> Set["int |" " str"]:
|
||||||
) -> Set["int |" " str"]:
|
+ argument: ("int |str"),
|
||||||
|
+) -> Set["int | str"]:
|
||||||
pass
|
pass
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -76,8 +77,8 @@ def func(
|
||||||
|
|
||||||
|
|
||||||
def func(
|
def func(
|
||||||
argument: ("int |" "str"),
|
argument: ("int |str"),
|
||||||
) -> Set["int |" " str"]:
|
) -> Set["int | str"]:
|
||||||
pass
|
pass
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -111,5 +112,3 @@ def func(
|
||||||
) -> Set["int |" " str"]:
|
) -> Set["int |" " str"]:
|
||||||
pass
|
pass
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -366,22 +366,13 @@ actual: {some_var}"""
|
||||||
[
|
[
|
||||||
"""cow
|
"""cow
|
||||||
moos""",
|
moos""",
|
||||||
@@ -198,7 +239,7 @@
|
|
||||||
`--global-option` is reserved to flags like `--verbose` or `--quiet`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
-this_will_become_one_line = "abc"
|
|
||||||
+this_will_become_one_line = "a" "b" "c"
|
|
||||||
|
|
||||||
this_will_stay_on_three_lines = (
|
|
||||||
"a" # comment
|
|
||||||
@@ -206,7 +247,9 @@
|
@@ -206,7 +247,9 @@
|
||||||
"c"
|
"c"
|
||||||
)
|
)
|
||||||
|
|
||||||
-this_will_also_become_one_line = "abc" # comment
|
-this_will_also_become_one_line = "abc" # comment
|
||||||
+this_will_also_become_one_line = ( # comment
|
+this_will_also_become_one_line = ( # comment
|
||||||
+ "a" "b" "c"
|
+ "abc"
|
||||||
+)
|
+)
|
||||||
|
|
||||||
assert some_var == expected_result, """
|
assert some_var == expected_result, """
|
||||||
|
@ -632,7 +623,7 @@ Please use `--build-option` instead,
|
||||||
`--global-option` is reserved to flags like `--verbose` or `--quiet`.
|
`--global-option` is reserved to flags like `--verbose` or `--quiet`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
this_will_become_one_line = "a" "b" "c"
|
this_will_become_one_line = "abc"
|
||||||
|
|
||||||
this_will_stay_on_three_lines = (
|
this_will_stay_on_three_lines = (
|
||||||
"a" # comment
|
"a" # comment
|
||||||
|
@ -641,7 +632,7 @@ this_will_stay_on_three_lines = (
|
||||||
)
|
)
|
||||||
|
|
||||||
this_will_also_become_one_line = ( # comment
|
this_will_also_become_one_line = ( # comment
|
||||||
"a" "b" "c"
|
"abc"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert some_var == expected_result, """
|
assert some_var == expected_result, """
|
||||||
|
|
|
@ -175,5 +175,3 @@ assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx(
|
||||||
"xxx {xxxxxxxxx} xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
"xxx {xxxxxxxxx} xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
50
crates/ruff_python_formatter/tests/snapshots/format.snap
Normal file
50
crates/ruff_python_formatter/tests/snapshots/format.snap
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||||
|
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string_preserve.py
|
||||||
|
---
|
||||||
|
## Input
|
||||||
|
```python
|
||||||
|
"diffent '" 'quote "are fine"' # join
|
||||||
|
|
||||||
|
# More single quotes
|
||||||
|
"one single'" "two 'single'" ' two "double"'
|
||||||
|
|
||||||
|
# More double quotes
|
||||||
|
'one double"' 'two "double"' " two 'single'"
|
||||||
|
|
||||||
|
# Equal number of single and double quotes
|
||||||
|
'two "double"' " two 'single'"
|
||||||
|
|
||||||
|
f"{'Hy \"User\"'}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
### Output 1
|
||||||
|
```
|
||||||
|
indent-style = space
|
||||||
|
line-width = 88
|
||||||
|
indent-width = 4
|
||||||
|
quote-style = Preserve
|
||||||
|
line-ending = LineFeed
|
||||||
|
magic-trailing-comma = Respect
|
||||||
|
docstring-code = Disabled
|
||||||
|
docstring-code-line-width = "dynamic"
|
||||||
|
preview = Enabled
|
||||||
|
target_version = Py38
|
||||||
|
source_type = Python
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
"diffent 'quote \"are fine\"" # join
|
||||||
|
|
||||||
|
# More single quotes
|
||||||
|
"one single'two 'single' two \"double\""
|
||||||
|
|
||||||
|
# More double quotes
|
||||||
|
'one double"two "double" two \'single\''
|
||||||
|
|
||||||
|
# Equal number of single and double quotes
|
||||||
|
'two "double" two \'single\''
|
||||||
|
|
||||||
|
f"{'Hy "User"'}"
|
||||||
|
```
|
|
@ -406,4 +406,19 @@ class EC2REPATH:
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Preview changes
|
||||||
|
```diff
|
||||||
|
--- Stable
|
||||||
|
+++ Preview
|
||||||
|
@@ -197,8 +197,8 @@
|
||||||
|
"dddddddddddddddddddddddddd" % aaaaaaaaaaaa + x
|
||||||
|
)
|
||||||
|
|
||||||
|
-"a" "b" "c" + "d" "e" + "f" "g" + "h" "i" "j"
|
||||||
|
+"abc" + "de" + "fg" + "hij"
|
||||||
|
|
||||||
|
|
||||||
|
class EC2REPATH:
|
||||||
|
- f.write("Pathway name" + "\t" "Database Identifier" + "\t" "Source database" + "\n")
|
||||||
|
+ f.write("Pathway name" + "\tDatabase Identifier" + "\tSource database" + "\n")
|
||||||
|
```
|
||||||
|
|
|
@ -285,6 +285,33 @@ b"Unicode Escape sequence don't apply to bytes: \N{0x} \u{ABCD} \U{ABCDEFGH}"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Preview changes
|
||||||
|
```diff
|
||||||
|
--- Stable
|
||||||
|
+++ Preview
|
||||||
|
@@ -63,9 +63,9 @@
|
||||||
|
|
||||||
|
# String continuation
|
||||||
|
|
||||||
|
-b"Let's" b"start" b"with" b"a" b"simple" b"example"
|
||||||
|
+b"Let'sstartwithasimpleexample"
|
||||||
|
|
||||||
|
-b"Let's" b"start" b"with" b"a" b"simple" b"example" b"now repeat after me:" b"I am confident" b"I am confident" b"I am confident" b"I am confident" b"I am confident"
|
||||||
|
+b"Let'sstartwithasimpleexamplenow repeat after me:I am confidentI am confidentI am confidentI am confidentI am confident"
|
||||||
|
|
||||||
|
(
|
||||||
|
b"Let's"
|
||||||
|
@@ -132,6 +132,6 @@
|
||||||
|
]
|
||||||
|
|
||||||
|
# Parenthesized string continuation with messed up indentation
|
||||||
|
-{"key": ([], b"a" b"b" b"c")}
|
||||||
|
+{"key": ([], b"abc")}
|
||||||
|
|
||||||
|
b"Unicode Escape sequence don't apply to bytes: \N{0x} \u{ABCD} \U{ABCDEFGH}"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Output 2
|
### Output 2
|
||||||
```
|
```
|
||||||
indent-style = space
|
indent-style = space
|
||||||
|
@ -441,4 +468,28 @@ b"Unicode Escape sequence don't apply to bytes: \N{0x} \u{ABCD} \U{ABCDEFGH}"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Preview changes
|
||||||
|
```diff
|
||||||
|
--- Stable
|
||||||
|
+++ Preview
|
||||||
|
@@ -63,9 +63,9 @@
|
||||||
|
|
||||||
|
# String continuation
|
||||||
|
|
||||||
|
-b"Let's" b'start' b'with' b'a' b'simple' b'example'
|
||||||
|
+b"Let'sstartwithasimpleexample"
|
||||||
|
|
||||||
|
-b"Let's" b'start' b'with' b'a' b'simple' b'example' b'now repeat after me:' b'I am confident' b'I am confident' b'I am confident' b'I am confident' b'I am confident'
|
||||||
|
+b"Let'sstartwithasimpleexamplenow repeat after me:I am confidentI am confidentI am confidentI am confidentI am confident"
|
||||||
|
|
||||||
|
(
|
||||||
|
b"Let's"
|
||||||
|
@@ -132,6 +132,6 @@
|
||||||
|
]
|
||||||
|
|
||||||
|
# Parenthesized string continuation with messed up indentation
|
||||||
|
-{'key': ([], b'a' b'b' b'c')}
|
||||||
|
+{'key': ([], b'abc')}
|
||||||
|
|
||||||
|
b"Unicode Escape sequence don't apply to bytes: \N{0x} \u{ABCD} \U{ABCDEFGH}"
|
||||||
|
```
|
||||||
|
|
|
@ -371,7 +371,7 @@ source_type = Python
|
||||||
```
|
```
|
||||||
|
|
||||||
```python
|
```python
|
||||||
(f"{one}" f"{two}")
|
(f"{one}{two}")
|
||||||
|
|
||||||
|
|
||||||
rf"Not-so-tricky \"quote"
|
rf"Not-so-tricky \"quote"
|
||||||
|
@ -411,7 +411,7 @@ result_f = (
|
||||||
)
|
)
|
||||||
|
|
||||||
(
|
(
|
||||||
f"{1}" f"{2}" # comment 3
|
f"{1}{2}" # comment 3
|
||||||
)
|
)
|
||||||
|
|
||||||
(
|
(
|
||||||
|
@ -1097,6 +1097,12 @@ _ = (
|
||||||
```diff
|
```diff
|
||||||
--- Stable
|
--- Stable
|
||||||
+++ Preview
|
+++ Preview
|
||||||
|
@@ -1,4 +1,4 @@
|
||||||
|
-(f"{one}" f"{two}")
|
||||||
|
+(f"{one}{two}")
|
||||||
|
|
||||||
|
|
||||||
|
rf"Not-so-tricky \"quote"
|
||||||
@@ -6,13 +6,13 @@
|
@@ -6,13 +6,13 @@
|
||||||
# Regression test for fstrings dropping comments
|
# Regression test for fstrings dropping comments
|
||||||
result_f = (
|
result_f = (
|
||||||
|
@ -1115,6 +1121,15 @@ _ = (
|
||||||
" 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
|
||||||
|
@@ -38,7 +38,7 @@
|
||||||
|
)
|
||||||
|
|
||||||
|
(
|
||||||
|
- f"{1}" f"{2}" # comment 3
|
||||||
|
+ f"{1}{2}" # comment 3
|
||||||
|
)
|
||||||
|
|
||||||
|
(
|
||||||
@@ -67,29 +67,31 @@
|
@@ -67,29 +67,31 @@
|
||||||
x = f"{a}"
|
x = f"{a}"
|
||||||
x = f"{
|
x = f"{
|
||||||
|
|
|
@ -0,0 +1,735 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||||
|
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string.py
|
||||||
|
---
|
||||||
|
## Input
|
||||||
|
```python
|
||||||
|
"aaaaaaaaa" "bbbbbbbbbbbbbbbbbbbb" # Join
|
||||||
|
|
||||||
|
(
|
||||||
|
"aaaaaaaaaaa" "bbbbbbbbbbbbbbbb"
|
||||||
|
) # join
|
||||||
|
|
||||||
|
|
||||||
|
(
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||||
|
) # too long to join
|
||||||
|
|
||||||
|
|
||||||
|
"different '" 'quote "are fine"' # join
|
||||||
|
|
||||||
|
# More single quotes
|
||||||
|
"one single'" "two 'single'" ' two "double"'
|
||||||
|
|
||||||
|
# More double quotes
|
||||||
|
'one double"' 'two "double"' " two 'single'"
|
||||||
|
|
||||||
|
# Equal number of single and double quotes
|
||||||
|
'two "double"' " two 'single'"
|
||||||
|
|
||||||
|
f"{'Hy \"User\"'}" 'more'
|
||||||
|
|
||||||
|
b"aaaaaaaaa" b"bbbbbbbbbbbbbbbbbbbb" # Join
|
||||||
|
|
||||||
|
(
|
||||||
|
b"aaaaaaaaaaa" b"bbbbbbbbbbbbbbbb"
|
||||||
|
) # join
|
||||||
|
|
||||||
|
|
||||||
|
(
|
||||||
|
b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" b"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||||
|
) # too long to join
|
||||||
|
|
||||||
|
|
||||||
|
# Skip joining if there is a trailing comment
|
||||||
|
(
|
||||||
|
"fffffffffffff"
|
||||||
|
"bbbbbbbbbbbbb" # comment
|
||||||
|
"cccccccccccccc"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Skip joining if there is a leading comment
|
||||||
|
(
|
||||||
|
"fffffffffffff"
|
||||||
|
# comment
|
||||||
|
"bbbbbbbbbbbbb"
|
||||||
|
"cccccccccccccc"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# F-strings
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Escape `{` and `}` when marging an f-string with a string
|
||||||
|
"a {not_a_variable}" f"b {10}" "c"
|
||||||
|
|
||||||
|
# Join, and break expressions
|
||||||
|
f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{
|
||||||
|
expression
|
||||||
|
}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" f"cccccccccccccccccccc {20999}" "more"
|
||||||
|
|
||||||
|
# Join, but don't break the expressions
|
||||||
|
f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" f"cccccccccccccccccccc {20999}" "more"
|
||||||
|
|
||||||
|
f"test{
|
||||||
|
expression
|
||||||
|
}flat" f"can be {
|
||||||
|
joined
|
||||||
|
} together"
|
||||||
|
|
||||||
|
aaaaaaaaaaa = f"test{
|
||||||
|
expression
|
||||||
|
}flat" f"cean beeeeeeee {
|
||||||
|
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
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
r"a" "normal"
|
||||||
|
R"a" "normal"
|
||||||
|
|
||||||
|
f"test" fr"test"
|
||||||
|
f"test" fR"test"
|
||||||
|
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# Don't join triple quoted strings
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"single" """triple"""
|
||||||
|
|
||||||
|
"single" f""""single"""
|
||||||
|
|
||||||
|
b"single" b"""triple"""
|
||||||
|
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# Join strings in with statements
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Fits
|
||||||
|
with "aa" "bbb" "cccccccccccccccccccccccccccccccccccccccccccccc":
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Parenthesize single-line
|
||||||
|
with "aa" "bbb" "ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc":
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Multiline
|
||||||
|
with "aa" "bbb" "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc":
|
||||||
|
pass
|
||||||
|
|
||||||
|
with f"aaaaaaa{expression}bbbb" f"ccc {20999}" "more":
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# For loops
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Flat
|
||||||
|
for a in "aaaaaaaaa" "bbbbbbbbb" "ccccccccc" "dddddddddd":
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Parenthesize single-line
|
||||||
|
for a in "aaaaaaaaa" "bbbbbbbbb" "ccccccccc" "dddddddddd" "eeeeeeeeeeeeeee" "fffffffffffff" "ggggggggggggggg" "hh":
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Multiline
|
||||||
|
for a in "aaaaaaaaa" "bbbbbbbbb" "ccccccccc" "dddddddddd" "eeeeeeeeeeeeeee" "fffffffffffff" "ggggggggggggggg" "hhhh":
|
||||||
|
pass
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# Assert statement
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Fits
|
||||||
|
assert "aaaaaaaaa" "bbbbbbbbbbbb", "cccccccccccccccc" "dddddddddddddddd"
|
||||||
|
|
||||||
|
# Wrap right
|
||||||
|
assert "aaaaaaaaa" "bbbbbbbbbbbb", "cccccccccccccccc" "dddddddddddddddd" "eeeeeeeeeeeee" "fffffffffff"
|
||||||
|
|
||||||
|
# Right multiline
|
||||||
|
assert "aaaaaaaaa" "bbbbbbbbbbbb", "cccccccccccccccc" "dddddddddddddddd" "eeeeeeeeeeeee" "fffffffffffffff" "ggggggggggggg" "hhhhhhhhhhh"
|
||||||
|
|
||||||
|
# Wrap left
|
||||||
|
assert "aaaaaaaaa" "bbbbbbbbbbbb" "cccccccccccccccc" "dddddddddddddddd" "eeeeeeeeeeeee" "fffffffffffffff", "ggggggggggggg" "hhhhhhhhhhh"
|
||||||
|
|
||||||
|
# Left multiline
|
||||||
|
assert "aaaaaaaaa" "bbbbbbbbbbbb" "cccccccccccccccc" "dddddddddddddddd" "eeeeeeeeeeeee" "fffffffffffffff" "ggggggggggggg", "hhhhhhhhhhh"
|
||||||
|
|
||||||
|
# wrap both
|
||||||
|
assert "aaaaaaaaa" "bbbbbbbbbbbb" "cccccccccccccccc" "dddddddddddddddd" "eeeeeeeeeeeee" "fffffffffffffff", "ggggggggggggg" "hhhhhhhhhhh" "iiiiiiiiiiiiiiiiii" "jjjjjjjjjjjjj" "kkkkkkkkkkkkkkkkk" "llllllllllll"
|
||||||
|
|
||||||
|
# both multiline
|
||||||
|
assert "aaaaaaaaa" "bbbbbbbbbbbb" "cccccccccccccccc" "dddddddddddddddd" "eeeeeeeeeeeee" "fffffffffffffff" "ggggggggggggg", "hhhhhhhhhhh" "iiiiiiiiiiiiiiiiii" "jjjjjjjjjjjjj" "kkkkkkkkkkkkkkkkk" "llllllllllll" "mmmmmmmmmmmmmm"
|
||||||
|
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# In clause headers (can_omit_optional_parentheses)
|
||||||
|
##############################################################################
|
||||||
|
# Use can_omit_optional_parentheses layout to avoid an instability where the formatter
|
||||||
|
# picks the can_omit_optional_parentheses layout when the strings are joined.
|
||||||
|
if (
|
||||||
|
f"implicit"
|
||||||
|
"concatenated"
|
||||||
|
"string" + f"implicit"
|
||||||
|
"concaddddddddddded"
|
||||||
|
"ring"
|
||||||
|
* len([aaaaaa, bbbbbbbbbbbbbbbb, cccccccccccccccccc, ddddddddddddddddddddddddddd])
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Keep parenthesizing multiline - implicit concatenated strings
|
||||||
|
if (
|
||||||
|
f"implicit"
|
||||||
|
"""concatenate
|
||||||
|
d"""
|
||||||
|
"string" + f"implicit"
|
||||||
|
"concaddddddddddded"
|
||||||
|
"ring"
|
||||||
|
* len([aaaaaa, bbbbbbbbbbbbbbbb, cccccccccccccccccc, ddddddddddddddddddddddddddd])
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
aaaaaa,
|
||||||
|
bbbbbbbbbbbbbbbb,
|
||||||
|
cccccccccccccccccc,
|
||||||
|
ddddddddddddddddddddddddddd,
|
||||||
|
]
|
||||||
|
+ "implicitconcat"
|
||||||
|
"enatedstriiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiing"
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# In match statements
|
||||||
|
match x:
|
||||||
|
case "implicitconcat" "enatedstring" | [
|
||||||
|
aaaaaa,
|
||||||
|
bbbbbbbbbbbbbbbb,
|
||||||
|
cccccccccccccccccc,
|
||||||
|
ddddddddddddddddddddddddddd,
|
||||||
|
]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
case [
|
||||||
|
aaaaaa,
|
||||||
|
bbbbbbbbbbbbbbbb,
|
||||||
|
cccccccccccccccccc,
|
||||||
|
ddddddddddddddddddddddddddd,
|
||||||
|
] | "implicitconcat" "enatedstring" :
|
||||||
|
pass
|
||||||
|
|
||||||
|
case "implicitconcat" "enatedstriiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiing" | [
|
||||||
|
aaaaaa,
|
||||||
|
bbbbbbbbbbbbbbbb,
|
||||||
|
cccccccccccccccccc,
|
||||||
|
ddddddddddddddddddddddddddd,
|
||||||
|
]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# In docstring positions
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
def short_docstring():
|
||||||
|
"Implicit" "concatenated" "docstring"
|
||||||
|
|
||||||
|
def long_docstring():
|
||||||
|
"Loooooooooooooooooooooong" "doooooooooooooooooooocstriiiiiiiiiiiiiiiiiiiiiiiiiiiiiiing" "exceding the line width" "but it should be concatenated anyways because it is single line"
|
||||||
|
|
||||||
|
def docstring_with_leading_whitespace():
|
||||||
|
" This is a " "implicit" "concatenated" "docstring"
|
||||||
|
|
||||||
|
def docstring_with_trailing_whitespace():
|
||||||
|
"This is a " "implicit" "concatenated" "docstring "
|
||||||
|
|
||||||
|
def docstring_with_leading_empty_parts():
|
||||||
|
" " " " "" "This is a " "implicit" "concatenated" "docstring"
|
||||||
|
|
||||||
|
def docstring_with_trailing_empty_parts():
|
||||||
|
"This is a " "implicit" "concatenated" "docstring" "" " " " "
|
||||||
|
|
||||||
|
def all_empty():
|
||||||
|
" " " " " "
|
||||||
|
|
||||||
|
def byte_string_in_docstring_position():
|
||||||
|
b" don't trim the" b"bytes literal "
|
||||||
|
|
||||||
|
def f_string_in_docstring_position():
|
||||||
|
f" don't trim the" "f-string literal "
|
||||||
|
|
||||||
|
def single_quoted():
|
||||||
|
' content\ ' ' '
|
||||||
|
return
|
||||||
|
|
||||||
|
def implicit_with_comment():
|
||||||
|
(
|
||||||
|
"a"
|
||||||
|
# leading
|
||||||
|
"the comment above"
|
||||||
|
)
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# Regressions
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
LEEEEEEEEEEEEEEEEEEEEEEFT = RRRRRRRRIIIIIIIIIIIIGGGGGHHHT | {
|
||||||
|
"entityNameeeeeeeeeeeeeeeeee", # comment must be long enough to
|
||||||
|
"some long implicit concatenated string" "that should join"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure that flipping between Multiline and BestFit layout results in stable formatting
|
||||||
|
# when using IfBreaksParenthesized layout.
|
||||||
|
assert False, "Implicit concatenated string" "uses {} layout on {} format".format(
|
||||||
|
"Multiline", "first"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert False, await "Implicit concatenated string" "uses {} layout on {} format".format(
|
||||||
|
"Multiline", "first"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert False, "Implicit concatenated stringuses {} layout on {} format"[
|
||||||
|
aaaaaaaaa, bbbbbb
|
||||||
|
]
|
||||||
|
|
||||||
|
assert False, +"Implicit concatenated string" "uses {} layout on {} format".format(
|
||||||
|
"Multiline", "first"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
### Output 1
|
||||||
|
```
|
||||||
|
indent-style = space
|
||||||
|
line-width = 88
|
||||||
|
indent-width = 4
|
||||||
|
quote-style = Double
|
||||||
|
line-ending = LineFeed
|
||||||
|
magic-trailing-comma = Respect
|
||||||
|
docstring-code = Disabled
|
||||||
|
docstring-code-line-width = "dynamic"
|
||||||
|
preview = Enabled
|
||||||
|
target_version = Py38
|
||||||
|
source_type = Python
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
"aaaaaaaaabbbbbbbbbbbbbbbbbbbb" # Join
|
||||||
|
|
||||||
|
("aaaaaaaaaaabbbbbbbbbbbbbbbb") # join
|
||||||
|
|
||||||
|
|
||||||
|
(
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||||
|
) # too long to join
|
||||||
|
|
||||||
|
|
||||||
|
'different \'quote "are fine"' # join
|
||||||
|
|
||||||
|
# More single quotes
|
||||||
|
"one single'two 'single' two \"double\""
|
||||||
|
|
||||||
|
# More double quotes
|
||||||
|
'one double"two "double" two \'single\''
|
||||||
|
|
||||||
|
# Equal number of single and double quotes
|
||||||
|
"two \"double\" two 'single'"
|
||||||
|
|
||||||
|
f"{'Hy "User"'}more"
|
||||||
|
|
||||||
|
b"aaaaaaaaabbbbbbbbbbbbbbbbbbbb" # Join
|
||||||
|
|
||||||
|
(b"aaaaaaaaaaabbbbbbbbbbbbbbbb") # join
|
||||||
|
|
||||||
|
|
||||||
|
(
|
||||||
|
b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
b"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||||
|
) # too long to join
|
||||||
|
|
||||||
|
|
||||||
|
# Skip joining if there is a trailing comment
|
||||||
|
(
|
||||||
|
"fffffffffffff"
|
||||||
|
"bbbbbbbbbbbbb" # comment
|
||||||
|
"cccccccccccccc"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Skip joining if there is a leading comment
|
||||||
|
(
|
||||||
|
"fffffffffffff"
|
||||||
|
# comment
|
||||||
|
"bbbbbbbbbbbbb"
|
||||||
|
"cccccccccccccc"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# F-strings
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Escape `{` and `}` when marging an f-string with a string
|
||||||
|
f"a {{not_a_variable}}b {10}c"
|
||||||
|
|
||||||
|
# Join, and break expressions
|
||||||
|
f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{
|
||||||
|
expression
|
||||||
|
}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccc {20999}more"
|
||||||
|
|
||||||
|
# Join, but don't break the expressions
|
||||||
|
f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccc {20999}more"
|
||||||
|
|
||||||
|
f"test{expression}flatcan be {joined} together"
|
||||||
|
|
||||||
|
aaaaaaaaaaa = (
|
||||||
|
f"test{expression}flat"
|
||||||
|
f"cean beeeeeeee {joined} eeeeeeeeeeeeeeeeeeeeeeeeeeeee"
|
||||||
|
) # 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
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
r"a" "normal"
|
||||||
|
R"a" "normal"
|
||||||
|
|
||||||
|
f"test" rf"test"
|
||||||
|
f"test" Rf"test"
|
||||||
|
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# Don't join triple quoted strings
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"single" """triple"""
|
||||||
|
|
||||||
|
"single" f""""single"""
|
||||||
|
|
||||||
|
b"single" b"""triple"""
|
||||||
|
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# Join strings in with statements
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Fits
|
||||||
|
with "aabbbcccccccccccccccccccccccccccccccccccccccccccccc":
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Parenthesize single-line
|
||||||
|
with (
|
||||||
|
"aabbbccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Multiline
|
||||||
|
with (
|
||||||
|
"aa"
|
||||||
|
"bbb"
|
||||||
|
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
with f"aaaaaaa{expression}bbbbccc {20999}more":
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# For loops
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Flat
|
||||||
|
for a in "aaaaaaaaabbbbbbbbbcccccccccdddddddddd":
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Parenthesize single-line
|
||||||
|
for a in (
|
||||||
|
"aaaaaaaaabbbbbbbbbcccccccccddddddddddeeeeeeeeeeeeeeefffffffffffffggggggggggggggghh"
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Multiline
|
||||||
|
for a in (
|
||||||
|
"aaaaaaaaa"
|
||||||
|
"bbbbbbbbb"
|
||||||
|
"ccccccccc"
|
||||||
|
"dddddddddd"
|
||||||
|
"eeeeeeeeeeeeeee"
|
||||||
|
"fffffffffffff"
|
||||||
|
"ggggggggggggggg"
|
||||||
|
"hhhh"
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# Assert statement
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Fits
|
||||||
|
assert "aaaaaaaaabbbbbbbbbbbb", "ccccccccccccccccdddddddddddddddd"
|
||||||
|
|
||||||
|
# Wrap right
|
||||||
|
assert "aaaaaaaaabbbbbbbbbbbb", (
|
||||||
|
"ccccccccccccccccddddddddddddddddeeeeeeeeeeeeefffffffffff"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Right multiline
|
||||||
|
assert "aaaaaaaaabbbbbbbbbbbb", (
|
||||||
|
"cccccccccccccccc"
|
||||||
|
"dddddddddddddddd"
|
||||||
|
"eeeeeeeeeeeee"
|
||||||
|
"fffffffffffffff"
|
||||||
|
"ggggggggggggg"
|
||||||
|
"hhhhhhhhhhh"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wrap left
|
||||||
|
assert (
|
||||||
|
"aaaaaaaaabbbbbbbbbbbbccccccccccccccccddddddddddddddddeeeeeeeeeeeeefffffffffffffff"
|
||||||
|
), "ggggggggggggghhhhhhhhhhh"
|
||||||
|
|
||||||
|
# Left multiline
|
||||||
|
assert (
|
||||||
|
"aaaaaaaaa"
|
||||||
|
"bbbbbbbbbbbb"
|
||||||
|
"cccccccccccccccc"
|
||||||
|
"dddddddddddddddd"
|
||||||
|
"eeeeeeeeeeeee"
|
||||||
|
"fffffffffffffff"
|
||||||
|
"ggggggggggggg"
|
||||||
|
), "hhhhhhhhhhh"
|
||||||
|
|
||||||
|
# wrap both
|
||||||
|
assert (
|
||||||
|
"aaaaaaaaabbbbbbbbbbbbccccccccccccccccddddddddddddddddeeeeeeeeeeeeefffffffffffffff"
|
||||||
|
), (
|
||||||
|
"ggggggggggggg"
|
||||||
|
"hhhhhhhhhhh"
|
||||||
|
"iiiiiiiiiiiiiiiiii"
|
||||||
|
"jjjjjjjjjjjjj"
|
||||||
|
"kkkkkkkkkkkkkkkkk"
|
||||||
|
"llllllllllll"
|
||||||
|
)
|
||||||
|
|
||||||
|
# both multiline
|
||||||
|
assert (
|
||||||
|
"aaaaaaaaa"
|
||||||
|
"bbbbbbbbbbbb"
|
||||||
|
"cccccccccccccccc"
|
||||||
|
"dddddddddddddddd"
|
||||||
|
"eeeeeeeeeeeee"
|
||||||
|
"fffffffffffffff"
|
||||||
|
"ggggggggggggg"
|
||||||
|
), (
|
||||||
|
"hhhhhhhhhhh"
|
||||||
|
"iiiiiiiiiiiiiiiiii"
|
||||||
|
"jjjjjjjjjjjjj"
|
||||||
|
"kkkkkkkkkkkkkkkkk"
|
||||||
|
"llllllllllll"
|
||||||
|
"mmmmmmmmmmmmmm"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# In clause headers (can_omit_optional_parentheses)
|
||||||
|
##############################################################################
|
||||||
|
# Use can_omit_optional_parentheses layout to avoid an instability where the formatter
|
||||||
|
# picks the can_omit_optional_parentheses layout when the strings are joined.
|
||||||
|
if f"implicitconcatenatedstring" + f"implicitconcadddddddddddedring" * len([
|
||||||
|
aaaaaa,
|
||||||
|
bbbbbbbbbbbbbbbb,
|
||||||
|
cccccccccccccccccc,
|
||||||
|
ddddddddddddddddddddddddddd,
|
||||||
|
]):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Keep parenthesizing multiline - implicit concatenated strings
|
||||||
|
if (
|
||||||
|
f"implicit"
|
||||||
|
"""concatenate
|
||||||
|
d"""
|
||||||
|
"string" + f"implicit"
|
||||||
|
"concaddddddddddded"
|
||||||
|
"ring"
|
||||||
|
* len([aaaaaa, bbbbbbbbbbbbbbbb, cccccccccccccccccc, ddddddddddddddddddddddddddd])
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
aaaaaa,
|
||||||
|
bbbbbbbbbbbbbbbb,
|
||||||
|
cccccccccccccccccc,
|
||||||
|
ddddddddddddddddddddddddddd,
|
||||||
|
]
|
||||||
|
+ "implicitconcat"
|
||||||
|
"enatedstriiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiing"
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# In match statements
|
||||||
|
match x:
|
||||||
|
case "implicitconcatenatedstring" | [
|
||||||
|
aaaaaa,
|
||||||
|
bbbbbbbbbbbbbbbb,
|
||||||
|
cccccccccccccccccc,
|
||||||
|
ddddddddddddddddddddddddddd,
|
||||||
|
]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
case [
|
||||||
|
aaaaaa,
|
||||||
|
bbbbbbbbbbbbbbbb,
|
||||||
|
cccccccccccccccccc,
|
||||||
|
ddddddddddddddddddddddddddd,
|
||||||
|
] | "implicitconcatenatedstring":
|
||||||
|
pass
|
||||||
|
|
||||||
|
case (
|
||||||
|
"implicitconcat"
|
||||||
|
"enatedstriiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiing"
|
||||||
|
| [
|
||||||
|
aaaaaa,
|
||||||
|
bbbbbbbbbbbbbbbb,
|
||||||
|
cccccccccccccccccc,
|
||||||
|
ddddddddddddddddddddddddddd,
|
||||||
|
]
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# In docstring positions
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
def short_docstring():
|
||||||
|
"Implicitconcatenateddocstring"
|
||||||
|
|
||||||
|
|
||||||
|
def long_docstring():
|
||||||
|
"Loooooooooooooooooooooongdoooooooooooooooooooocstriiiiiiiiiiiiiiiiiiiiiiiiiiiiiiingexceding the line widthbut it should be concatenated anyways because it is single line"
|
||||||
|
|
||||||
|
|
||||||
|
def docstring_with_leading_whitespace():
|
||||||
|
"This is a implicitconcatenateddocstring"
|
||||||
|
|
||||||
|
|
||||||
|
def docstring_with_trailing_whitespace():
|
||||||
|
"This is a implicitconcatenateddocstring"
|
||||||
|
|
||||||
|
|
||||||
|
def docstring_with_leading_empty_parts():
|
||||||
|
"This is a implicitconcatenateddocstring"
|
||||||
|
|
||||||
|
|
||||||
|
def docstring_with_trailing_empty_parts():
|
||||||
|
"This is a implicitconcatenateddocstring"
|
||||||
|
|
||||||
|
|
||||||
|
def all_empty():
|
||||||
|
""
|
||||||
|
|
||||||
|
|
||||||
|
def byte_string_in_docstring_position():
|
||||||
|
b" don't trim thebytes literal "
|
||||||
|
|
||||||
|
|
||||||
|
def f_string_in_docstring_position():
|
||||||
|
f" don't trim thef-string literal "
|
||||||
|
|
||||||
|
|
||||||
|
def single_quoted():
|
||||||
|
"content\ "
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def implicit_with_comment():
|
||||||
|
(
|
||||||
|
"a"
|
||||||
|
# leading
|
||||||
|
"the comment above"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# Regressions
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
LEEEEEEEEEEEEEEEEEEEEEEFT = RRRRRRRRIIIIIIIIIIIIGGGGGHHHT | {
|
||||||
|
"entityNameeeeeeeeeeeeeeeeee", # comment must be long enough to
|
||||||
|
"some long implicit concatenated stringthat should join",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure that flipping between Multiline and BestFit layout results in stable formatting
|
||||||
|
# when using IfBreaksParenthesized layout.
|
||||||
|
assert False, "Implicit concatenated stringuses {} layout on {} format".format(
|
||||||
|
"Multiline", "first"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert False, await "Implicit concatenated stringuses {} layout on {} format".format(
|
||||||
|
"Multiline", "first"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert False, "Implicit concatenated stringuses {} layout on {} format"[
|
||||||
|
aaaaaaaaa, bbbbbb
|
||||||
|
]
|
||||||
|
|
||||||
|
assert False, +"Implicit concatenated stringuses {} layout on {} format".format(
|
||||||
|
"Multiline", "first"
|
||||||
|
)
|
||||||
|
```
|
|
@ -0,0 +1,637 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||||
|
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string_assignment.py
|
||||||
|
---
|
||||||
|
## Input
|
||||||
|
```python
|
||||||
|
## Implicit concatenated strings with a trailing comment but a non splittable target.
|
||||||
|
|
||||||
|
# Don't join the string because the joined string with the inlined comment exceeds the line length limit.
|
||||||
|
____aaa = (
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvv"
|
||||||
|
) # c
|
||||||
|
|
||||||
|
# This is the same string as above and should lead to the same formatting. The only difference is that we start
|
||||||
|
# with an unparenthesized string.
|
||||||
|
____aaa = "aaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvv" # c
|
||||||
|
|
||||||
|
# Again the same string as above but this time as non-implicit concatenated string.
|
||||||
|
# It's okay if the formatting differs because it's an explicit choice to use implicit concatenation.
|
||||||
|
____aaa = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvv" # c
|
||||||
|
|
||||||
|
# Join the string because it's exactly in the line length limit when the comment is inlined.
|
||||||
|
____aaa = (
|
||||||
|
"aaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvv"
|
||||||
|
) # c
|
||||||
|
|
||||||
|
# This is the same string as above and should lead to the same formatting. The only difference is that we start
|
||||||
|
# with an unparenthesized string.
|
||||||
|
____aaa = "aaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvv" # c
|
||||||
|
|
||||||
|
# Again the same string as above but as a non-implicit concatenated string. It should result in the same formatting
|
||||||
|
# (for consistency).
|
||||||
|
____aaa = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvv" # c
|
||||||
|
|
||||||
|
# It should collapse the parentheses if the joined string and the comment fit on the same line.
|
||||||
|
# This is required for stability.
|
||||||
|
____aaa = (
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvv" # c
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# Assignments where the target or annotations are splittable
|
||||||
|
#############################################################
|
||||||
|
|
||||||
|
|
||||||
|
# The target splits because of a magic trailing comma
|
||||||
|
# The string is joined and not parenthesized because it just fits into the line length (including comment).
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b,
|
||||||
|
] = "ccccccccccccccccccccccccccccc" "cccccccccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
|
||||||
|
# Same but starting with a joined string. They should both result in the same formatting.
|
||||||
|
[
|
||||||
|
aaaaaaa,
|
||||||
|
b,
|
||||||
|
] = "ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
|
||||||
|
# The target splits because of the magic trailing comma
|
||||||
|
# The string is **not** joined because it with the inlined comment exceeds the line length limit.
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b,
|
||||||
|
] = "ccccccccccccccccccccccccccccc" "ccccccccccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
|
||||||
|
|
||||||
|
# The target should be flat
|
||||||
|
# The string should be joined because it fits into the line length
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b
|
||||||
|
] = (
|
||||||
|
"ccccccccccccccccccccccccccccccccccc" "cccccccccccccccccccccccc" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
# Same but starting with a joined string. They should both result in the same formatting.
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b
|
||||||
|
] = "ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
|
||||||
|
# The target should be flat
|
||||||
|
# The string gets parenthesized because it, with the inlined comment, exceeds the line length limit.
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b
|
||||||
|
] = "ccccccccccccccccccccccccccccc" "ccccccccccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
|
||||||
|
|
||||||
|
# Split an overlong target, but join the string if it fits
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b
|
||||||
|
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
|
||||||
|
"ccccccccccccccccccccccccccccccccccccccccc" "cccccccccccccccccccccccccccccc" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
# Split both if necessary and keep multiline
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b
|
||||||
|
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
|
||||||
|
"ccccccccccccccccccccccccccccccccccccccccc" "ccccccccccccccccccccccccccccccc" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
#########################################################
|
||||||
|
# Leading or trailing own line comments:
|
||||||
|
# Preserve the parentheses
|
||||||
|
########################################################
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b
|
||||||
|
] = (
|
||||||
|
# test
|
||||||
|
"ccccccccccccccccccccccccccccc" "ccccccccccccccccccccccccccccccccccccccccccc"
|
||||||
|
)
|
||||||
|
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b
|
||||||
|
] = (
|
||||||
|
"ccccccccccccccccccccccccccccc" "ccccccccccccccccccccccccccccccccccccccccccc"
|
||||||
|
# test
|
||||||
|
)
|
||||||
|
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b
|
||||||
|
] = (
|
||||||
|
"ccccccccccccccccccccccccccccccccccccccccc" "ccccccccccccccccccccccccccccccccccccccccccc"
|
||||||
|
# test
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# Type alias statements
|
||||||
|
#############################################################
|
||||||
|
|
||||||
|
# First break the right, join the string
|
||||||
|
type A[str, int, number] = "Literal[string, int] | None | " "CustomType" "| OtherCustomTypeExcee" # comment
|
||||||
|
|
||||||
|
# Keep multiline if overlong
|
||||||
|
type A[str, int, number] = "Literal[string, int] | None | " "CustomTypeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" # comment
|
||||||
|
|
||||||
|
# Break the left if it is over-long, join the string
|
||||||
|
type Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[stringgggggggggg, inttttttttttttttttttttttt, number] = "Literal[string, int] | None | " "CustomType" # comment
|
||||||
|
|
||||||
|
# Break both if necessary and keep multiline
|
||||||
|
type Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[stringgggggggggg, inttttttttttttttttttttttt, number] = "Literal[string, int] | None | " "CustomTypeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" # comment
|
||||||
|
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# F-Strings
|
||||||
|
#############################################################
|
||||||
|
|
||||||
|
# Flatten and join the f-string
|
||||||
|
aaaaaaaaaaa = f"test{
|
||||||
|
expression}flat" f"cean beeeeeeee {joined} eeeeeeeeeeeeeeeee" # inline
|
||||||
|
|
||||||
|
# Parenthesize the value and join it, inline the comment
|
||||||
|
aaaaaaaaaaa = f"test{
|
||||||
|
expression}flat" f"cean beeeeeeee {joined} eeeeeeeeeeeeeeeeeeeeeeeeeee" # inline
|
||||||
|
|
||||||
|
# Parenthesize the f-string and keep it multiline because it doesn't fit on a single line including the comment
|
||||||
|
aaaaaaaaaaa = f"test{
|
||||||
|
expression
|
||||||
|
}flat" f"cean beeeeeeee {
|
||||||
|
joined
|
||||||
|
} eeeeeeeeeeeeeeeeeeeeeeeeeeeee" # inline
|
||||||
|
|
||||||
|
|
||||||
|
# The target splits because of a magic trailing comma
|
||||||
|
# The string is joined and not parenthesized because it just fits into the line length (including comment).
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b,
|
||||||
|
] = f"ccccc{
|
||||||
|
expression}ccccccccccc" f"cccccccccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
|
||||||
|
|
||||||
|
# Same but starting with a joined string. They should both result in the same formatting.
|
||||||
|
[
|
||||||
|
aaaaaaa,
|
||||||
|
b,
|
||||||
|
] = f"ccccc{
|
||||||
|
expression}ccccccccccccccccccccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
|
||||||
|
# The target splits because of the magic trailing comma
|
||||||
|
# The string is **not** joined because it with the inlined comment exceeds the line length limit.
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b,
|
||||||
|
] = f"ccccc{
|
||||||
|
expression}cccccccccccccccccccc" f"cccccccccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
|
||||||
|
|
||||||
|
# The target should be flat
|
||||||
|
# The string should be joined because it fits into the line length
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b
|
||||||
|
] = (
|
||||||
|
f"ccccc{
|
||||||
|
expression}ccccccccccc" "cccccccccccccccccccccccc" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
# Same but starting with a joined string. They should both result in the same formatting.
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b
|
||||||
|
] = f"ccccc{
|
||||||
|
expression}ccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
|
||||||
|
# The target should be flat
|
||||||
|
# The string gets parenthesized because it, with the inlined comment, exceeds the line length limit.
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b
|
||||||
|
] = f"ccccc{
|
||||||
|
expression}ccccccccccc" "ccccccccccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
|
||||||
|
|
||||||
|
# Split an overlong target, but join the string if it fits
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b
|
||||||
|
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
|
||||||
|
f"ccccc{
|
||||||
|
expression}ccccccccccc" "cccccccccccccccccccccccccccccc" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
# Split both if necessary and keep multiline
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b
|
||||||
|
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
|
||||||
|
f"ccccc{
|
||||||
|
expression}cccccccccccccccccccccccccccccccc" "ccccccccccccccccccccccccccccccc" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
# Don't inline f-strings that contain expressions that are guaranteed to split, e.b. because of a magic trailing comma
|
||||||
|
aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
|
||||||
|
[a,]
|
||||||
|
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
|
||||||
|
|
||||||
|
aaaaaaaaaaaaaaaaaa = (
|
||||||
|
f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
|
||||||
|
[a,]
|
||||||
|
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
aaaaa[aaaaaaaaaaa] = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
|
||||||
|
[a,]
|
||||||
|
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
|
||||||
|
|
||||||
|
aaaaa[aaaaaaaaaaa] = (f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
|
||||||
|
[a,]
|
||||||
|
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
# Don't inline f-strings that contain commented expressions
|
||||||
|
aaaaaaaaaaaaaaaaaa = (
|
||||||
|
f"testeeeeeeeeeeeeeeeeeeeeeeeee{[
|
||||||
|
a # comment
|
||||||
|
]}" "moreeeeeeeeeeeeeeeeeetest" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
aaaaa[aaaaaaaaaaa] = (
|
||||||
|
f"testeeeeeeeeeeeeeeeeeeeeeeeee{[
|
||||||
|
a # comment
|
||||||
|
]}" "moreeeeeeeeeeeeeeeeeetest" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
# Don't inline f-strings with multiline debug expressions:
|
||||||
|
aaaaaaaaaaaaaaaaaa = (
|
||||||
|
f"testeeeeeeeeeeeeeeeeeeeeeeeee{
|
||||||
|
a=}" "moreeeeeeeeeeeeeeeeeetest" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
aaaaaaaaaaaaaaaaaa = (
|
||||||
|
f"testeeeeeeeeeeeeeeeeeeeeeeeee{a +
|
||||||
|
b=}" "moreeeeeeeeeeeeeeeeeetest" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
aaaaaaaaaaaaaaaaaa = (
|
||||||
|
f"testeeeeeeeeeeeeeeeeeeeeeeeee{a
|
||||||
|
=}" "moreeeeeeeeeeeeeeeeeetest" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
aaaaa[aaaaaaaaaaa] = (
|
||||||
|
f"testeeeeeeeeeeeeeeeeeeeeeeeee{
|
||||||
|
a=}" "moreeeeeeeeeeeeeeeeeetest" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
aaaaa[aaaaaaaaaaa] = (
|
||||||
|
f"testeeeeeeeeeeeeeeeeeeeeeeeee{a
|
||||||
|
=}" "moreeeeeeeeeeeeeeeeeetest" # comment
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
### Output 1
|
||||||
|
```
|
||||||
|
indent-style = space
|
||||||
|
line-width = 88
|
||||||
|
indent-width = 4
|
||||||
|
quote-style = Double
|
||||||
|
line-ending = LineFeed
|
||||||
|
magic-trailing-comma = Respect
|
||||||
|
docstring-code = Disabled
|
||||||
|
docstring-code-line-width = "dynamic"
|
||||||
|
preview = Enabled
|
||||||
|
target_version = Py38
|
||||||
|
source_type = Python
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
## Implicit concatenated strings with a trailing comment but a non splittable target.
|
||||||
|
|
||||||
|
# Don't join the string because the joined string with the inlined comment exceeds the line length limit.
|
||||||
|
____aaa = (
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
"aaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvv"
|
||||||
|
) # c
|
||||||
|
|
||||||
|
# This is the same string as above and should lead to the same formatting. The only difference is that we start
|
||||||
|
# with an unparenthesized string.
|
||||||
|
____aaa = (
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
"aaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvv"
|
||||||
|
) # c
|
||||||
|
|
||||||
|
# Again the same string as above but this time as non-implicit concatenated string.
|
||||||
|
# It's okay if the formatting differs because it's an explicit choice to use implicit concatenation.
|
||||||
|
____aaa = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvv" # c
|
||||||
|
|
||||||
|
# Join the string because it's exactly in the line length limit when the comment is inlined.
|
||||||
|
____aaa = (
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvv" # c
|
||||||
|
)
|
||||||
|
|
||||||
|
# This is the same string as above and should lead to the same formatting. The only difference is that we start
|
||||||
|
# with an unparenthesized string.
|
||||||
|
____aaa = (
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvv" # c
|
||||||
|
)
|
||||||
|
|
||||||
|
# Again the same string as above but as a non-implicit concatenated string. It should result in the same formatting
|
||||||
|
# (for consistency).
|
||||||
|
____aaa = (
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvv" # c
|
||||||
|
)
|
||||||
|
|
||||||
|
# It should collapse the parentheses if the joined string and the comment fit on the same line.
|
||||||
|
# This is required for stability.
|
||||||
|
____aaa = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvv" # c
|
||||||
|
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# Assignments where the target or annotations are splittable
|
||||||
|
#############################################################
|
||||||
|
|
||||||
|
|
||||||
|
# The target splits because of a magic trailing comma
|
||||||
|
# The string is joined and not parenthesized because it just fits into the line length (including comment).
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b,
|
||||||
|
] = "ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
|
||||||
|
# Same but starting with a joined string. They should both result in the same formatting.
|
||||||
|
[
|
||||||
|
aaaaaaa,
|
||||||
|
b,
|
||||||
|
] = "ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
|
||||||
|
# The target splits because of the magic trailing comma
|
||||||
|
# The string is **not** joined because it with the inlined comment exceeds the line length limit.
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b,
|
||||||
|
] = (
|
||||||
|
"ccccccccccccccccccccccccccccc"
|
||||||
|
"ccccccccccccccccccccccccccccccccccccccccccc"
|
||||||
|
) # comment
|
||||||
|
|
||||||
|
|
||||||
|
# The target should be flat
|
||||||
|
# The string should be joined because it fits into the line length
|
||||||
|
a[aaaaaaa, b] = "ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
|
||||||
|
# Same but starting with a joined string. They should both result in the same formatting.
|
||||||
|
a[aaaaaaa, b] = "ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
|
||||||
|
# The target should be flat
|
||||||
|
# The string gets parenthesized because it, with the inlined comment, exceeds the line length limit.
|
||||||
|
a[aaaaaaa, b] = (
|
||||||
|
"ccccccccccccccccccccccccccccc"
|
||||||
|
"ccccccccccccccccccccccccccccccccccccccccccc"
|
||||||
|
) # comment
|
||||||
|
|
||||||
|
|
||||||
|
# Split an overlong target, but join the string if it fits
|
||||||
|
a[
|
||||||
|
aaaaaaa, b
|
||||||
|
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
|
||||||
|
"ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
# Split both if necessary and keep multiline
|
||||||
|
a[
|
||||||
|
aaaaaaa, b
|
||||||
|
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
|
||||||
|
"ccccccccccccccccccccccccccccccccccccccccc"
|
||||||
|
"ccccccccccccccccccccccccccccccc"
|
||||||
|
) # comment
|
||||||
|
|
||||||
|
#########################################################
|
||||||
|
# Leading or trailing own line comments:
|
||||||
|
# Preserve the parentheses
|
||||||
|
########################################################
|
||||||
|
a[aaaaaaa, b] = (
|
||||||
|
# test
|
||||||
|
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
|
||||||
|
)
|
||||||
|
|
||||||
|
a[aaaaaaa, b] = (
|
||||||
|
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
|
||||||
|
# test
|
||||||
|
)
|
||||||
|
|
||||||
|
a[aaaaaaa, b] = (
|
||||||
|
"ccccccccccccccccccccccccccccccccccccccccc"
|
||||||
|
"ccccccccccccccccccccccccccccccccccccccccccc"
|
||||||
|
# test
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# Type alias statements
|
||||||
|
#############################################################
|
||||||
|
|
||||||
|
# First break the right, join the string
|
||||||
|
type A[str, int, number] = (
|
||||||
|
"Literal[string, int] | None | CustomType| OtherCustomTypeExcee" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
# Keep multiline if overlong
|
||||||
|
type A[str, int, number] = (
|
||||||
|
"Literal[string, int] | None | "
|
||||||
|
"CustomTypeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
|
||||||
|
) # comment
|
||||||
|
|
||||||
|
# Break the left if it is over-long, join the string
|
||||||
|
type Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[
|
||||||
|
stringgggggggggg,
|
||||||
|
inttttttttttttttttttttttt,
|
||||||
|
number,
|
||||||
|
] = "Literal[string, int] | None | CustomType" # comment
|
||||||
|
|
||||||
|
# Break both if necessary and keep multiline
|
||||||
|
type Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[
|
||||||
|
stringgggggggggg,
|
||||||
|
inttttttttttttttttttttttt,
|
||||||
|
number,
|
||||||
|
] = (
|
||||||
|
"Literal[string, int] | None | "
|
||||||
|
"CustomTypeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
|
||||||
|
) # comment
|
||||||
|
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# F-Strings
|
||||||
|
#############################################################
|
||||||
|
|
||||||
|
# Flatten and join the f-string
|
||||||
|
aaaaaaaaaaa = f"test{expression}flatcean beeeeeeee {joined} eeeeeeeeeeeeeeeee" # inline
|
||||||
|
|
||||||
|
# Parenthesize the value and join it, inline the comment
|
||||||
|
aaaaaaaaaaa = (
|
||||||
|
f"test{expression}flatcean beeeeeeee {joined} eeeeeeeeeeeeeeeeeeeeeeeeeee" # inline
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parenthesize the f-string and keep it multiline because it doesn't fit on a single line including the comment
|
||||||
|
aaaaaaaaaaa = (
|
||||||
|
f"test{expression}flat"
|
||||||
|
f"cean beeeeeeee {joined} eeeeeeeeeeeeeeeeeeeeeeeeeeeee"
|
||||||
|
) # inline
|
||||||
|
|
||||||
|
|
||||||
|
# The target splits because of a magic trailing comma
|
||||||
|
# The string is joined and not parenthesized because it just fits into the line length (including comment).
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b,
|
||||||
|
] = f"ccccc{expression}ccccccccccccccccccccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
|
||||||
|
|
||||||
|
# Same but starting with a joined string. They should both result in the same formatting.
|
||||||
|
[
|
||||||
|
aaaaaaa,
|
||||||
|
b,
|
||||||
|
] = f"ccccc{expression}ccccccccccccccccccccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
|
||||||
|
# The target splits because of the magic trailing comma
|
||||||
|
# The string is **not** joined because it with the inlined comment exceeds the line length limit.
|
||||||
|
a[
|
||||||
|
aaaaaaa,
|
||||||
|
b,
|
||||||
|
] = (
|
||||||
|
f"ccccc{expression}cccccccccccccccccccc"
|
||||||
|
f"cccccccccccccccccccccccccccccccccccccccccc"
|
||||||
|
) # comment
|
||||||
|
|
||||||
|
|
||||||
|
# The target should be flat
|
||||||
|
# The string should be joined because it fits into the line length
|
||||||
|
a[aaaaaaa, b] = f"ccccc{expression}ccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
|
||||||
|
# Same but starting with a joined string. They should both result in the same formatting.
|
||||||
|
a[aaaaaaa, b] = f"ccccc{expression}ccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
|
||||||
|
# The target should be flat
|
||||||
|
# The string gets parenthesized because it, with the inlined comment, exceeds the line length limit.
|
||||||
|
a[aaaaaaa, b] = (
|
||||||
|
f"ccccc{expression}ccccccccccc"
|
||||||
|
"ccccccccccccccccccccccccccccccccccccccccccc"
|
||||||
|
) # comment
|
||||||
|
|
||||||
|
|
||||||
|
# Split an overlong target, but join the string if it fits
|
||||||
|
a[
|
||||||
|
aaaaaaa, b
|
||||||
|
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
|
||||||
|
f"ccccc{expression}ccccccccccccccccccccccccccccccccccccccccc" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
# Split both if necessary and keep multiline
|
||||||
|
a[
|
||||||
|
aaaaaaa, b
|
||||||
|
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
|
||||||
|
f"ccccc{expression}cccccccccccccccccccccccccccccccc"
|
||||||
|
"ccccccccccccccccccccccccccccccc"
|
||||||
|
) # comment
|
||||||
|
|
||||||
|
# Don't inline f-strings that contain expressions that are guaranteed to split, e.b. because of a magic trailing comma
|
||||||
|
aaaaaaaaaaaaaaaaaa = (
|
||||||
|
f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
|
||||||
|
[
|
||||||
|
a,
|
||||||
|
]
|
||||||
|
}"
|
||||||
|
"moreeeeeeeeeeeeeeeeeeee"
|
||||||
|
"test"
|
||||||
|
) # comment
|
||||||
|
|
||||||
|
aaaaaaaaaaaaaaaaaa = (
|
||||||
|
f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
|
||||||
|
[
|
||||||
|
a,
|
||||||
|
]
|
||||||
|
}"
|
||||||
|
"moreeeeeeeeeeeeeeeeeeee"
|
||||||
|
"test" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
aaaaa[aaaaaaaaaaa] = (
|
||||||
|
f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
|
||||||
|
[
|
||||||
|
a,
|
||||||
|
]
|
||||||
|
}"
|
||||||
|
"moreeeeeeeeeeeeeeeeeeee"
|
||||||
|
"test"
|
||||||
|
) # comment
|
||||||
|
|
||||||
|
aaaaa[aaaaaaaaaaa] = (
|
||||||
|
f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
|
||||||
|
[
|
||||||
|
a,
|
||||||
|
]
|
||||||
|
}"
|
||||||
|
"moreeeeeeeeeeeeeeeeeeee"
|
||||||
|
"test" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
# Don't inline f-strings that contain commented expressions
|
||||||
|
aaaaaaaaaaaaaaaaaa = (
|
||||||
|
f"testeeeeeeeeeeeeeeeeeeeeeeeee{
|
||||||
|
[
|
||||||
|
a # comment
|
||||||
|
]
|
||||||
|
}"
|
||||||
|
"moreeeeeeeeeeeeeeeeeetest" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
aaaaa[aaaaaaaaaaa] = (
|
||||||
|
f"testeeeeeeeeeeeeeeeeeeeeeeeee{
|
||||||
|
[
|
||||||
|
a # comment
|
||||||
|
]
|
||||||
|
}"
|
||||||
|
"moreeeeeeeeeeeeeeeeeetest" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
# Don't inline f-strings with multiline debug expressions:
|
||||||
|
aaaaaaaaaaaaaaaaaa = (
|
||||||
|
f"testeeeeeeeeeeeeeeeeeeeeeeeee{
|
||||||
|
a=}"
|
||||||
|
"moreeeeeeeeeeeeeeeeeetest" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
aaaaaaaaaaaaaaaaaa = (
|
||||||
|
f"testeeeeeeeeeeeeeeeeeeeeeeeee{a +
|
||||||
|
b=}"
|
||||||
|
"moreeeeeeeeeeeeeeeeeetest" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
aaaaaaaaaaaaaaaaaa = (
|
||||||
|
f"testeeeeeeeeeeeeeeeeeeeeeeeee{a
|
||||||
|
=}"
|
||||||
|
"moreeeeeeeeeeeeeeeeeetest" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
aaaaa[aaaaaaaaaaa] = (
|
||||||
|
f"testeeeeeeeeeeeeeeeeeeeeeeeee{
|
||||||
|
a=}"
|
||||||
|
"moreeeeeeeeeeeeeeeeeetest" # comment
|
||||||
|
)
|
||||||
|
|
||||||
|
aaaaa[aaaaaaaaaaa] = (
|
||||||
|
f"testeeeeeeeeeeeeeeeeeeeeeeeee{a
|
||||||
|
=}"
|
||||||
|
"moreeeeeeeeeeeeeeeeeetest" # comment
|
||||||
|
)
|
||||||
|
```
|
|
@ -0,0 +1,84 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||||
|
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string_preserve.py
|
||||||
|
---
|
||||||
|
## Input
|
||||||
|
```python
|
||||||
|
a = "different '" 'quote "are fine"' # join
|
||||||
|
|
||||||
|
# More single quotes
|
||||||
|
"one single'" "two 'single'" ' two "double"'
|
||||||
|
|
||||||
|
# More double quotes
|
||||||
|
'one double"' 'two "double"' " two 'single'"
|
||||||
|
|
||||||
|
# Equal number of single and double quotes
|
||||||
|
'two "double"' " two 'single'"
|
||||||
|
|
||||||
|
# Already invalid Pre Python 312
|
||||||
|
f"{'Hy "User"'}" f'{"Hy 'User'"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
### Output 1
|
||||||
|
```
|
||||||
|
indent-style = space
|
||||||
|
line-width = 88
|
||||||
|
indent-width = 4
|
||||||
|
quote-style = Preserve
|
||||||
|
line-ending = LineFeed
|
||||||
|
magic-trailing-comma = Respect
|
||||||
|
docstring-code = Disabled
|
||||||
|
docstring-code-line-width = "dynamic"
|
||||||
|
preview = Enabled
|
||||||
|
target_version = Py38
|
||||||
|
source_type = Python
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
a = "different 'quote \"are fine\"" # join
|
||||||
|
|
||||||
|
# More single quotes
|
||||||
|
"one single'two 'single' two \"double\""
|
||||||
|
|
||||||
|
# More double quotes
|
||||||
|
'one double"two "double" two \'single\''
|
||||||
|
|
||||||
|
# Equal number of single and double quotes
|
||||||
|
'two "double" two \'single\''
|
||||||
|
|
||||||
|
# Already invalid Pre Python 312
|
||||||
|
f"{'Hy "User"'}{"Hy 'User'"}"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Output 2
|
||||||
|
```
|
||||||
|
indent-style = space
|
||||||
|
line-width = 88
|
||||||
|
indent-width = 4
|
||||||
|
quote-style = Preserve
|
||||||
|
line-ending = LineFeed
|
||||||
|
magic-trailing-comma = Respect
|
||||||
|
docstring-code = Disabled
|
||||||
|
docstring-code-line-width = "dynamic"
|
||||||
|
preview = Enabled
|
||||||
|
target_version = Py312
|
||||||
|
source_type = Python
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
a = "different 'quote \"are fine\"" # join
|
||||||
|
|
||||||
|
# More single quotes
|
||||||
|
"one single'two 'single' two \"double\""
|
||||||
|
|
||||||
|
# More double quotes
|
||||||
|
'one double"two "double" two \'single\''
|
||||||
|
|
||||||
|
# Equal number of single and double quotes
|
||||||
|
'two "double" two \'single\''
|
||||||
|
|
||||||
|
# Already invalid Pre Python 312
|
||||||
|
f"{'Hy "User"'}{"Hy 'User'"}"
|
||||||
|
```
|
|
@ -331,6 +331,34 @@ a = """\\\x1f"""
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Preview changes
|
||||||
|
```diff
|
||||||
|
--- Stable
|
||||||
|
+++ Preview
|
||||||
|
@@ -70,9 +70,9 @@
|
||||||
|
|
||||||
|
# String continuation
|
||||||
|
|
||||||
|
-"Let's" "start" "with" "a" "simple" "example"
|
||||||
|
+"Let'sstartwithasimpleexample"
|
||||||
|
|
||||||
|
-"Let's" "start" "with" "a" "simple" "example" "now repeat after me:" "I am confident" "I am confident" "I am confident" "I am confident" "I am confident"
|
||||||
|
+"Let'sstartwithasimpleexamplenow repeat after me:I am confidentI am confidentI am confidentI am confidentI am confident"
|
||||||
|
|
||||||
|
(
|
||||||
|
"Let's"
|
||||||
|
@@ -139,7 +139,7 @@
|
||||||
|
]
|
||||||
|
|
||||||
|
# Parenthesized string continuation with messed up indentation
|
||||||
|
-{"key": ([], "a" "b" "c")}
|
||||||
|
+{"key": ([], "abc")}
|
||||||
|
|
||||||
|
|
||||||
|
# Regression test for https://github.com/astral-sh/ruff/issues/5893
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Output 2
|
### Output 2
|
||||||
```
|
```
|
||||||
indent-style = space
|
indent-style = space
|
||||||
|
@ -515,4 +543,29 @@ a = """\\\x1f"""
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Preview changes
|
||||||
|
```diff
|
||||||
|
--- Stable
|
||||||
|
+++ Preview
|
||||||
|
@@ -70,9 +70,9 @@
|
||||||
|
|
||||||
|
# String continuation
|
||||||
|
|
||||||
|
-"Let's" 'start' 'with' 'a' 'simple' 'example'
|
||||||
|
+"Let'sstartwithasimpleexample"
|
||||||
|
|
||||||
|
-"Let's" 'start' 'with' 'a' 'simple' 'example' 'now repeat after me:' 'I am confident' 'I am confident' 'I am confident' 'I am confident' 'I am confident'
|
||||||
|
+"Let'sstartwithasimpleexamplenow repeat after me:I am confidentI am confidentI am confidentI am confidentI am confident"
|
||||||
|
|
||||||
|
(
|
||||||
|
"Let's"
|
||||||
|
@@ -139,7 +139,7 @@
|
||||||
|
]
|
||||||
|
|
||||||
|
# Parenthesized string continuation with messed up indentation
|
||||||
|
-{'key': ([], 'a' 'b' 'c')}
|
||||||
|
+{'key': ([], 'abc')}
|
||||||
|
|
||||||
|
|
||||||
|
# Regression test for https://github.com/astral-sh/ruff/issues/5893
|
||||||
|
```
|
||||||
|
|
|
@ -279,4 +279,37 @@ print((yield x))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Preview changes
|
||||||
|
```diff
|
||||||
|
--- Stable
|
||||||
|
+++ Preview
|
||||||
|
@@ -78,7 +78,7 @@
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
-yield "Cache key will cause errors if used with memcached: %r " "(longer than %s)" % (
|
||||||
|
+yield "Cache key will cause errors if used with memcached: %r (longer than %s)" % (
|
||||||
|
key,
|
||||||
|
MEMCACHE_MAX_KEY_LENGTH,
|
||||||
|
)
|
||||||
|
@@ -96,8 +96,7 @@
|
||||||
|
"Django to create, modify, and delete the table"
|
||||||
|
)
|
||||||
|
yield (
|
||||||
|
- "# Feel free to rename the models, but don't rename db_table values or "
|
||||||
|
- "field names."
|
||||||
|
+ "# Feel free to rename the models, but don't rename db_table values or field names."
|
||||||
|
)
|
||||||
|
|
||||||
|
yield (
|
||||||
|
@@ -109,8 +108,7 @@
|
||||||
|
"Django to create, modify, and delete the table"
|
||||||
|
)
|
||||||
|
yield (
|
||||||
|
- "# Feel free to rename the models, but don't rename db_table values or "
|
||||||
|
- "field names."
|
||||||
|
+ "# Feel free to rename the models, but don't rename db_table values or field names."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Regression test for: https://github.com/astral-sh/ruff/issues/7420
|
||||||
|
```
|
||||||
|
|
|
@ -188,6 +188,3 @@ f3 = { # f3
|
||||||
{ # f 4
|
{ # f 4
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -831,7 +831,51 @@ match x:
|
||||||
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb,
|
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb,
|
||||||
ccccccccccccccccccccccccccccccccc,
|
ccccccccccccccccccccccccccccccccc,
|
||||||
):
|
):
|
||||||
@@ -246,63 +238,48 @@
|
@@ -220,89 +212,80 @@
|
||||||
|
|
||||||
|
## Always use parentheses for implicitly concatenated strings
|
||||||
|
match x:
|
||||||
|
- case (
|
||||||
|
- "implicit" "concatenated" "string"
|
||||||
|
- | [aaaaaa, bbbbbbbbbbbbbbbb, cccccccccccccccccc, ddddddddddddddddddddddddddd]
|
||||||
|
- ):
|
||||||
|
+ case "implicitconcatenatedstring" | [
|
||||||
|
+ aaaaaa,
|
||||||
|
+ bbbbbbbbbbbbbbbb,
|
||||||
|
+ cccccccccccccccccc,
|
||||||
|
+ ddddddddddddddddddddddddddd,
|
||||||
|
+ ]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
match x:
|
||||||
|
- case (
|
||||||
|
- b"implicit" b"concatenated" b"string"
|
||||||
|
- | [aaaaaa, bbbbbbbbbbbbbbbb, cccccccccccccccccc, ddddddddddddddddddddddddddd]
|
||||||
|
- ):
|
||||||
|
+ case b"implicitconcatenatedstring" | [
|
||||||
|
+ aaaaaa,
|
||||||
|
+ bbbbbbbbbbbbbbbb,
|
||||||
|
+ cccccccccccccccccc,
|
||||||
|
+ ddddddddddddddddddddddddddd,
|
||||||
|
+ ]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
match x:
|
||||||
|
- case (
|
||||||
|
- f"implicit" "concatenated" "string"
|
||||||
|
- | [aaaaaa, bbbbbbbbbbbbbbbb, cccccccccccccccccc, ddddddddddddddddddddddddddd]
|
||||||
|
- ):
|
||||||
|
+ case f"implicitconcatenatedstring" | [
|
||||||
|
+ aaaaaa,
|
||||||
|
+ bbbbbbbbbbbbbbbb,
|
||||||
|
+ cccccccccccccccccc,
|
||||||
|
+ ddddddddddddddddddddddddddd,
|
||||||
|
+ ]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
## Complex number expressions and unary expressions
|
## Complex number expressions and unary expressions
|
||||||
|
|
||||||
match x:
|
match x:
|
||||||
|
|
|
@ -131,6 +131,28 @@ def docstring_single():
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Preview changes
|
||||||
|
```diff
|
||||||
|
--- Stable
|
||||||
|
+++ Preview
|
||||||
|
@@ -33,10 +33,10 @@
|
||||||
|
rb"""br single triple"""
|
||||||
|
rb"""br double triple"""
|
||||||
|
|
||||||
|
-'single1' 'single2'
|
||||||
|
-'single1' 'double2'
|
||||||
|
-'double1' 'single2'
|
||||||
|
-'double1' 'double2'
|
||||||
|
+'single1single2'
|
||||||
|
+'single1double2'
|
||||||
|
+'double1single2'
|
||||||
|
+'double1double2'
|
||||||
|
|
||||||
|
|
||||||
|
def docstring_single_triple():
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Output 2
|
### Output 2
|
||||||
```
|
```
|
||||||
indent-style = space
|
indent-style = space
|
||||||
|
@ -205,6 +227,28 @@ def docstring_single():
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Preview changes
|
||||||
|
```diff
|
||||||
|
--- Stable
|
||||||
|
+++ Preview
|
||||||
|
@@ -33,10 +33,10 @@
|
||||||
|
rb"""br single triple"""
|
||||||
|
rb"""br double triple"""
|
||||||
|
|
||||||
|
-"single1" "single2"
|
||||||
|
-"single1" "double2"
|
||||||
|
-"double1" "single2"
|
||||||
|
-"double1" "double2"
|
||||||
|
+"single1single2"
|
||||||
|
+"single1double2"
|
||||||
|
+"double1single2"
|
||||||
|
+"double1double2"
|
||||||
|
|
||||||
|
|
||||||
|
def docstring_single_triple():
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Output 3
|
### Output 3
|
||||||
```
|
```
|
||||||
indent-style = space
|
indent-style = space
|
||||||
|
@ -279,4 +323,23 @@ def docstring_single():
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Preview changes
|
||||||
|
```diff
|
||||||
|
--- Stable
|
||||||
|
+++ Preview
|
||||||
|
@@ -33,10 +33,10 @@
|
||||||
|
rb'''br single triple'''
|
||||||
|
rb"""br double triple"""
|
||||||
|
|
||||||
|
-'single1' 'single2'
|
||||||
|
-'single1' "double2"
|
||||||
|
-"double1" 'single2'
|
||||||
|
-"double1" "double2"
|
||||||
|
+'single1single2'
|
||||||
|
+'single1double2'
|
||||||
|
+"double1single2"
|
||||||
|
+"double1double2"
|
||||||
|
|
||||||
|
|
||||||
|
def docstring_single_triple():
|
||||||
|
```
|
||||||
|
|
|
@ -362,4 +362,154 @@ assert package.files == [
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Preview changes
|
||||||
|
```diff
|
||||||
|
--- Stable
|
||||||
|
+++ Preview
|
||||||
|
@@ -30,50 +30,47 @@
|
||||||
|
|
||||||
|
|
||||||
|
def test():
|
||||||
|
- assert (
|
||||||
|
- {
|
||||||
|
- key1: value1,
|
||||||
|
- key2: value2,
|
||||||
|
- key3: value3,
|
||||||
|
- key4: value4,
|
||||||
|
- key5: value5,
|
||||||
|
- key6: value6,
|
||||||
|
- key7: value7,
|
||||||
|
- key8: value8,
|
||||||
|
- key9: value9,
|
||||||
|
- }
|
||||||
|
- == expected
|
||||||
|
- ), "Not what we expected and the message is too long to fit ineeeeee one line"
|
||||||
|
+ assert {
|
||||||
|
+ key1: value1,
|
||||||
|
+ key2: value2,
|
||||||
|
+ key3: value3,
|
||||||
|
+ key4: value4,
|
||||||
|
+ key5: value5,
|
||||||
|
+ key6: value6,
|
||||||
|
+ key7: value7,
|
||||||
|
+ key8: value8,
|
||||||
|
+ key9: value9,
|
||||||
|
+ } == expected, (
|
||||||
|
+ "Not what we expected and the message is too long to fit ineeeeee one line"
|
||||||
|
+ )
|
||||||
|
|
||||||
|
- assert (
|
||||||
|
- {
|
||||||
|
- key1: value1,
|
||||||
|
- key2: value2,
|
||||||
|
- key3: value3,
|
||||||
|
- key4: value4,
|
||||||
|
- key5: value5,
|
||||||
|
- key6: value6,
|
||||||
|
- key7: value7,
|
||||||
|
- key8: value8,
|
||||||
|
- key9: value9,
|
||||||
|
- }
|
||||||
|
- == expected
|
||||||
|
- ), "Not what we expected and the message is too long to fit in one lineeeee"
|
||||||
|
+ assert {
|
||||||
|
+ key1: value1,
|
||||||
|
+ key2: value2,
|
||||||
|
+ key3: value3,
|
||||||
|
+ key4: value4,
|
||||||
|
+ key5: value5,
|
||||||
|
+ key6: value6,
|
||||||
|
+ key7: value7,
|
||||||
|
+ key8: value8,
|
||||||
|
+ key9: value9,
|
||||||
|
+ } == expected, (
|
||||||
|
+ "Not what we expected and the message is too long to fit in one lineeeee"
|
||||||
|
+ )
|
||||||
|
|
||||||
|
- assert (
|
||||||
|
- {
|
||||||
|
- key1: value1,
|
||||||
|
- key2: value2,
|
||||||
|
- key3: value3,
|
||||||
|
- key4: value4,
|
||||||
|
- key5: value5,
|
||||||
|
- key6: value6,
|
||||||
|
- key7: value7,
|
||||||
|
- key8: value8,
|
||||||
|
- key9: value9,
|
||||||
|
- }
|
||||||
|
- == expected
|
||||||
|
- ), "Not what we expected and the message is too long to fit in one lineeeeeeeeeeeee"
|
||||||
|
+ assert {
|
||||||
|
+ key1: value1,
|
||||||
|
+ key2: value2,
|
||||||
|
+ key3: value3,
|
||||||
|
+ key4: value4,
|
||||||
|
+ key5: value5,
|
||||||
|
+ key6: value6,
|
||||||
|
+ key7: value7,
|
||||||
|
+ key8: value8,
|
||||||
|
+ key9: value9,
|
||||||
|
+ } == expected, (
|
||||||
|
+ "Not what we expected and the message is too long to fit in one lineeeeeeeeeeeee"
|
||||||
|
+ )
|
||||||
|
|
||||||
|
assert (
|
||||||
|
{
|
||||||
|
@@ -103,7 +100,9 @@
|
||||||
|
key9: value9,
|
||||||
|
}
|
||||||
|
== expectedeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
|
||||||
|
- ), "Not what we expected and the message is too long to fit in one lineeeeeeeeeeeeeeeee"
|
||||||
|
+ ), (
|
||||||
|
+ "Not what we expected and the message is too long to fit in one lineeeeeeeeeeeeeeeee"
|
||||||
|
+ )
|
||||||
|
|
||||||
|
assert expected == {
|
||||||
|
key1: value1,
|
||||||
|
@@ -117,20 +116,19 @@
|
||||||
|
key9: value9,
|
||||||
|
}, "Not what we expected and the message is too long to fit ineeeeee one line"
|
||||||
|
|
||||||
|
- assert (
|
||||||
|
- expected
|
||||||
|
- == {
|
||||||
|
- key1: value1,
|
||||||
|
- key2: value2,
|
||||||
|
- key3: value3,
|
||||||
|
- key4: value4,
|
||||||
|
- key5: value5,
|
||||||
|
- key6: value6,
|
||||||
|
- key7: value7,
|
||||||
|
- key8: value8,
|
||||||
|
- key9: value9,
|
||||||
|
- }
|
||||||
|
- ), "Not what we expected and the message is too long to fit in one lineeeeeeeeeeeeeeeeeeee"
|
||||||
|
+ assert expected == {
|
||||||
|
+ key1: value1,
|
||||||
|
+ key2: value2,
|
||||||
|
+ key3: value3,
|
||||||
|
+ key4: value4,
|
||||||
|
+ key5: value5,
|
||||||
|
+ key6: value6,
|
||||||
|
+ key7: value7,
|
||||||
|
+ key8: value8,
|
||||||
|
+ key9: value9,
|
||||||
|
+ }, (
|
||||||
|
+ "Not what we expected and the message is too long to fit in one lineeeeeeeeeeeeeeeeeeee"
|
||||||
|
+ )
|
||||||
|
|
||||||
|
assert (
|
||||||
|
expectedeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
|
||||||
|
@@ -160,7 +158,9 @@
|
||||||
|
key8: value8,
|
||||||
|
key9: value9,
|
||||||
|
}
|
||||||
|
- ), "Not what we expected and the message is too long to fit in one lineeeeeeeeeeeeeee"
|
||||||
|
+ ), (
|
||||||
|
+ "Not what we expected and the message is too long to fit in one lineeeeeeeeeeeeeee"
|
||||||
|
+ )
|
||||||
|
|
||||||
|
|
||||||
|
# Test for https://github.com/astral-sh/ruff/issues/7246
|
||||||
|
```
|
||||||
|
|
|
@ -397,6 +397,21 @@ def f() -> (
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@@ -80,12 +78,12 @@
|
||||||
|
#########################################################################################
|
||||||
|
|
||||||
|
|
||||||
|
-def test_implicit_concatenated_string_return_type() -> "str" "bbbbbbbbbbbbbbbb":
|
||||||
|
+def test_implicit_concatenated_string_return_type() -> "strbbbbbbbbbbbbbbbb":
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_overlong_implicit_concatenated_string_return_type() -> (
|
||||||
|
- "liiiiiiiiiiiisssssst[str]" "bbbbbbbbbbbbbbbb"
|
||||||
|
+ "liiiiiiiiiiiisssssst[str]bbbbbbbbbbbbbbbb"
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
@@ -108,9 +106,9 @@
|
@@ -108,9 +106,9 @@
|
||||||
# 1. Black tries to keep the list flat by parenthesizing the list as shown below even when the `list` identifier
|
# 1. Black tries to keep the list flat by parenthesizing the list as shown below even when the `list` identifier
|
||||||
# fits on the header line. IMO, this adds unnecessary parentheses that can be avoided
|
# fits on the header line. IMO, this adds unnecessary parentheses that can be avoided
|
||||||
|
|
|
@ -412,3 +412,26 @@ def test_return_multiline_string_binary_expression_return_type_annotation(
|
||||||
]:
|
]:
|
||||||
pass
|
pass
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Preview changes
|
||||||
|
```diff
|
||||||
|
--- Stable
|
||||||
|
+++ Preview
|
||||||
|
@@ -82,13 +82,13 @@
|
||||||
|
#########################################################################################
|
||||||
|
|
||||||
|
|
||||||
|
-def test_implicit_concatenated_string_return_type(a) -> "str" "bbbbbbbbbbbbbbbb":
|
||||||
|
+def test_implicit_concatenated_string_return_type(a) -> "strbbbbbbbbbbbbbbbb":
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_overlong_implicit_concatenated_string_return_type(
|
||||||
|
a,
|
||||||
|
-) -> "liiiiiiiiiiiisssssst[str]" "bbbbbbbbbbbbbbbb":
|
||||||
|
+) -> "liiiiiiiiiiiisssssst[str]bbbbbbbbbbbbbbbb":
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue