Implement template strings (#17851)

This PR implements template strings (t-strings) in the parser and
formatter for Ruff.

Minimal changes necessary to compile were made in other parts of the code (e.g. ty, the linter, etc.). These will be covered properly in follow-up PRs.
This commit is contained in:
Dylan 2025-05-30 15:00:56 -05:00 committed by GitHub
parent ad024f9a09
commit 9bbf4987e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
261 changed files with 18023 additions and 1802 deletions

View file

@ -38,9 +38,9 @@ for node_line in node_lines:
# `FStringLiteralElement`, `FStringFormatSpec` and `FStringExpressionElement` are
# handled by the `FString` implementation.
if node in (
"FStringLiteralElement",
"FStringExpressionElement",
"FStringFormatSpec",
"InterpolatedStringLiteralElement",
"InterpolatedElement",
"InterpolatedStringFormatSpec",
"Identifier",
):
continue

View file

@ -431,3 +431,16 @@ f"hellooooooooooooooooooooooo \
worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb)
aaaaaaaaaaa = f"hellooooooooooooooooooooooo \
worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb)
# This t-string should be flattened
xxxxxxxxxxxxxxxx = t"aaaaaaaaaaaaaaaaaaaaa {
expression } bbbbbbbbbbbbbbbbbbbbbbbb" + (
yyyyyyyyyyyyyy + zzzzzzzzzzz
)
# This is not a multiline t-string, but the expression is too long so it should be
# wrapped in parentheses.
t"hellooooooooooooooooooooooo \
worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb)
aaaaaaaaaaa = t"hellooooooooooooooooooooooo \
worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb)

View file

@ -100,6 +100,55 @@ f"{10 + len('bar')=}" f'no debug{10}' f"{10 + len('bar')=}"
f"{10 + len('bar')=}" f'{10 + len("bar")=}'
##############################################################################
# T-strings
##############################################################################
# Escape `{` and `}` when merging a t-string with a string
"a {not_a_variable}" t"b {10}" "c"
# Join, and break expressions
t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{
expression
}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" t"cccccccccccccccccccc {20999}" "more"
# Join, but don't break the expressions
t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" t"cccccccccccccccccccc {20999}" "more"
t"test{
expression
}flat" t"can be {
joined
} together"
aaaaaaaaaaa = t"test{
expression
}flat" t"cean beeeeeeee {
joined
} eeeeeeeeeeeeeeeeeeeeeeeeeeeee" # inline
t"single quoted '{x}'" t'double quoted "{x}"' # Same number of quotes => use preferred quote style
t"single quote ' {x}" t'double quoted "{x}"' # More double quotes => use single quotes
t"single quoted '{x}'" t'double quote " {x}"' # More single quotes => use double quotes
# Different triple quoted strings
t"{'''test'''}" t'{"""other"""}'
# Now with inner quotes
t"{'''test ' '''}" t'{"""other " """}'
t"{some_where_nested('''test ' ''')}" t'{"""other " """ + "more"}'
t"{b'''test ' '''}" t'{b"""other " """}'
t"{t'''test ' '''}" t'{t"""other " """}'
# debug expressions containing quotes
t"{10 + len('bar')=}" t"{10 + len('bar')=}"
t"{10 + len('bar')=}" t'no debug{10}' t"{10 + len('bar')=}"
# We can't safely merge this pre Python 3.12 without altering the debug expression.
t"{10 + len('bar')=}" t'{10 + len("bar")=}'
##############################################################################
# Don't join raw strings
##############################################################################
@ -110,6 +159,9 @@ R"a" "normal"
f"test" fr"test"
f"test" fR"test"
t"test" tr"test"
t"test" tR"test"
##############################################################################
# Don't join triple quoted strings
@ -119,9 +171,22 @@ f"test" fR"test"
"single" f""""single"""
"single" t""""single"""
b"single" b"""triple"""
##############################################################################
# Don't join t-strings and f-strings
##############################################################################
t"{interp}" f"{expr}"
f"{expr}" t"{interp}"
f"{expr}" "string" t"{interp}"
##############################################################################
# Join strings in with statements
##############################################################################

View file

@ -293,6 +293,155 @@ aaaaa[aaaaaaaaaaa] = (
)
#############################################################
# T-Strings
#############################################################
# Flatten and join the t-string
aaaaaaaaaaa = t"test{
expression}flat" t"cean beeeeeeee {joined} eeeeeeeeeeeeeeeee" # inline
# Parenthesize the value and join it, inline the comment
aaaaaaaaaaa = t"test{
expression}flat" t"cean beeeeeeee {joined} eeeeeeeeeeeeeeeeeeeeeeeeeee" # inline
# Parenthesize the t-string and keep it multiline because it doesn't fit on a single line including the comment
aaaaaaaaaaa = t"test{
expression
}flat" t"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,
] = t"ccccc{
expression}ccccccccccc" t"cccccccccccccccccccccccccccccccccccccccccc" # comment
# Same but starting with a joined string. They should both result in the same formatting.
[
aaaaaaa,
b,
] = t"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,
] = t"ccccc{
expression}cccccccccccccccccccc" t"cccccccccccccccccccccccccccccccccccccccccc" # comment
# The target should be flat
# The string should be joined because it fits into the line length
a[
aaaaaaa,
b
] = (
t"ccccc{
expression}ccccccccccc" "cccccccccccccccccccccccc" # comment
)
# Same but starting with a joined string. They should both result in the same formatting.
a[
aaaaaaa,
b
] = t"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
] = t"ccccc{
expression}ccccccccccc" "ccccccccccccccccccccccccccccccccccccccccccc" # comment
# Split an overlong target, but join the string if it fits
a[
aaaaaaa,
b
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
t"ccccc{
expression}ccccccccccc" "cccccccccccccccccccccccccccccc" # comment
)
# Split both if necessary and keep multiline
a[
aaaaaaa,
b
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
t"ccccc{
expression}cccccccccccccccccccccccccccccccc" "ccccccccccccccccccccccccccccccc" # comment
)
# Don't inline t-strings that contain expressions that are guaranteed to split, e.b. because of a magic trailing comma
aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
)
aaaaa[aaaaaaaaaaa] = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
aaaaa[aaaaaaaaaaa] = (t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
)
# Don't inline t-strings that contain commented expressions
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{[
a # comment
]}" "moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{[
a # comment
]}" "moreeeeeeeeeeeeeeeeeetest" # comment
)
# Don't inline t-strings with multiline debug expressions:
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
a=}" "moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a +
b=}" "moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a
=}" "moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
a=}" "moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a
=}" "moreeeeeeeeeeeeeeeeeetest" # comment
)
# Trailing last-part comments
a = (
@ -374,4 +523,4 @@ self._attr_unique_id = (
return (
f"Exception in {call_back_name} when handling msg on "
f"'{msg.topic}': '{msg.payload}'" # type: ignore[str-bytes-safe]
)
)

View file

@ -0,0 +1 @@
[{"target_version": "3.14"}]

View file

@ -0,0 +1,731 @@
(
t'{one}'
t'{two}'
)
rt"Not-so-tricky \"quote"
# Regression test for tstrings dropping comments
result_f = (
'Traceback (most recent call last):\n'
t' File "{__file__}", line {lineno_f+5}, in _check_recursive_traceback_display\n'
' f()\n'
t' File "{__file__}", line {lineno_f+1}, in f\n'
' f()\n'
t' File "{__file__}", line {lineno_f+1}, in f\n'
' f()\n'
t' File "{__file__}", line {lineno_f+1}, in f\n'
' f()\n'
# XXX: The following line changes depending on whether the tests
# are run through the interactive interpreter or with -m
# It also varies depending on the platform (stack size)
# Fortunately, we don't care about exactness here, so we use regex
r' \[Previous line repeated (\d+) more times\]' '\n'
'RecursionError: maximum recursion depth exceeded\n'
)
# Regression for tstring dropping comments that were accidentally attached to
# an expression inside a formatted value
(
t'{1}'
# comment 1
''
)
(
t'{1}' # comment 2
t'{2}'
)
(
t'{1}'
t'{2}' # comment 3
)
(
1, ( # comment 4
t'{2}'
)
)
(
(
t'{1}'
# comment 5
),
2
)
# https://github.com/astral-sh/ruff/issues/6841
x = t'''a{""}b'''
y = t'''c{1}d"""e'''
z = t'''a{""}b''' t'''c{1}d"""e'''
# T-String formatting test cases (Preview)
# Simple expression with a mix of debug expression and comments.
x = t"{a}"
x = t"{
a = }"
x = t"{ # comment 6
a }"
x = t"{ # comment 7
a = }"
# Remove the parentheses as adding them doesn't make then fit within the line length limit.
# This is similar to how we format it before t-string formatting.
aaaaaaaaaaa = (
t"asaaaaaaaaaaaaaaaa { aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd } cccccccccc"
)
# Here, we would use the best fit layout to put the t-string indented on the next line
# similar to the next example.
aaaaaaaaaaa = t"asaaaaaaaaaaaaaaaa { aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc } cccccccccc"
aaaaaaaaaaa = (
t"asaaaaaaaaaaaaaaaa { aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc } cccccccccc"
)
# This should never add the optional parentheses because even after adding them, the
# t-string exceeds the line length limit.
x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ccccccccccccccc"
x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc"
x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 8
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ccccccccccccccc"
x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc"
x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbb' = } ccccccccccccccc"
# Multiple larger expressions which exceeds the line length limit. Here, we need to decide
# whether to split at the first or second expression. This should work similarly to the
# assignment statement formatting where we split from right to left in preview mode.
x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb } cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee"
# The above example won't split but when we start introducing line breaks:
x = t"aaaaaaaaaaaa {
bbbbbbbbbbbbbb } cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee"
x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb
} cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee"
x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb } cccccccccccccccccccc {
ddddddddddddddd } eeeeeeeeeeeeee"
x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb } cccccccccccccccccccc { ddddddddddddddd
} eeeeeeeeeeeeee"
# But, in case comments are present, we would split at the expression containing the
# comments:
x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb # comment 10
} cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee"
x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb
} cccccccccccccccccccc { # comment 11
ddddddddddddddd } eeeeeeeeeeeeee"
# Here, the expression part itself starts with a curly brace so we need to add an extra
# space between the opening curly brace and the expression.
x = t"{ {'x': 1, 'y': 2} }"
# Although the extra space isn't required before the ending curly brace, we add it for
# consistency.
x = t"{ {'x': 1, 'y': 2}}"
x = t"{ {'x': 1, 'y': 2} = }"
x = t"{ # comment 12
{'x': 1, 'y': 2} }"
x = t"{ # comment 13
{'x': 1, 'y': 2} = }"
# But, if there's a format specifier or a conversion flag then we don't need to add
# any whitespace at the end
x = t"aaaaa { {'aaaaaa', 'bbbbbbb', 'ccccccccc'}!s} bbbbbb"
x = t"aaaaa { {'aaaaaa', 'bbbbbbb', 'ccccccccc'}:.3f} bbbbbb"
# But, in this case, we would split the expression itself because it exceeds the line
# length limit so we need not add the extra space.
xxxxxxx = t"{
{'aaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbb', 'ccccccccccccccccccccc'}
}"
# And, split the expression itself because it exceeds the line length.
xxxxxxx = t"{
{'aaaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'}
}"
#############################################################################################
# Quotes
#############################################################################################
t"foo 'bar' {x}"
t"foo \"bar\" {x}"
t'foo "bar" {x}'
t'foo \'bar\' {x}'
t"foo {"bar"}"
t"single quoted '{x}' double quoted \"{x}\"" # Same number of quotes => use preferred quote style
t"single quote ' {x} double quoted \"{x}\"" # More double quotes => use single quotes
t"single quoted '{x}' double quote \" {x}" # More single quotes => use double quotes
fr"single quotes ' {x}" # Keep double because `'` can't be escaped
fr'double quotes " {x}' # Keep single because `"` can't be escaped
fr'flip quotes {x}' # Use preferred quotes, because raw string contains now quotes.
# Here, the formatter will remove the escapes
t"foo {'\'bar\''}"
t"foo {'\"bar\"'}"
# Quotes inside the expressions have no impact on the quote selection of the outer string.
# Required so that the following two examples result in the same formatting.
t'foo {10 + len("bar")}'
t"foo {10 + len('bar')}"
# Pre 312, preserve the outer quotes if the t-string contains quotes in the debug expression
t'foo {10 + len("bar")=}'
t'''foo {10 + len('''bar''')=}'''
t'''foo {10 + len('bar')=}''' # Fine to change the quotes because it uses triple quotes
# Triple-quoted strings
# It's ok to use the same quote char for the inner string if it's single-quoted.
t"""test {'inner'}"""
t"""test {"inner"}"""
# But if the inner string is also triple-quoted then we should preserve the existing quotes.
t"""test {'''inner'''}"""
# It's not okay to change the quote style if the inner string is triple quoted and contains a quote.
t'{"""other " """}'
t'{"""other " """ + "more"}'
t'{b"""other " """}'
t'{t"""other " """}'
t"""test {t'inner {'''inner inner'''}'}"""
t"""test {t'''inner {"""inner inner"""}'''}"""
# Magic trailing comma
#
# The expression formatting will result in breaking it across multiple lines with a
# trailing comma but as the expression isn't already broken, we will remove all the line
# breaks which results in the trailing comma being present. This test case makes sure
# that the trailing comma is removed as well.
t"aaaaaaa {['aaaaaaaaaaaaaaa', 'bbbbbbbbbbbbb', 'ccccccccccccccccc', 'ddddddddddddddd', 'eeeeeeeeeeeeee']} aaaaaaa"
# And, if the trailing comma is already present, we still need to remove it.
t"aaaaaaa {['aaaaaaaaaaaaaaa', 'bbbbbbbbbbbbb', 'ccccccccccccccccc', 'ddddddddddddddd', 'eeeeeeeeeeeeee',]} aaaaaaa"
# Keep this Multiline by breaking it at the square brackets.
t"""aaaaaa {[
xxxxxxxx,
yyyyyyyy,
]} ccc"""
# Add the magic trailing comma because the elements don't fit within the line length limit
# when collapsed.
t"aaaaaa {[
xxxxxxxxxxxx,
xxxxxxxxxxxx,
xxxxxxxxxxxx,
xxxxxxxxxxxx,
xxxxxxxxxxxx,
xxxxxxxxxxxx,
yyyyyyyyyyyy
]} ccccccc"
# Remove the parentheses because they aren't required
xxxxxxxxxxxxxxx = (
t"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbb {
xxxxxxxxxxx # comment 14
+ yyyyyyyyyy
} dddddddddd"
)
# Comments
# No comments should be dropped!
t"{ # comment 15
# comment 16
foo # comment 17
# comment 18
}" # comment 19
# comment 20
# Single-quoted t-strings with a format specificer can be multiline
t"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {
variable:.3f} ddddddddddddddd eeeeeeee"
# But, if it's triple-quoted then we can't or the format specificer will have a
# trailing newline
t"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {
variable:.3f} ddddddddddddddd eeeeeeee"""
# But, we can break the ones which don't have a format specifier
t"""fooooooooooooooooooo barrrrrrrrrrrrrrrrrrr {
xxxxxxxxxxxxxxx:.3f} aaaaaaaaaaaaaaaaa { xxxxxxxxxxxxxxxxxxxx } bbbbbbbbbbbb"""
# Throw in a random comment in it but surprise, this is not a comment but just a text
# which is part of the format specifier
aaaaaaaaaaa = t"""asaaaaaaaaaaaaaaaa {
aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f
# comment
} cccccccccc"""
aaaaaaaaaaa = t"""asaaaaaaaaaaaaaaaa {
aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f
# comment} cccccccccc"""
# Conversion flags
#
# This is not a valid Python code because of the additional whitespace between the `!`
# and conversion type. But, our parser isn't strict about this. This should probably be
# removed once we have a strict parser.
x = t"aaaaaaaaa { x ! r }"
# Even in the case of debug expressions, we only need to preserve the whitespace within
# the expression part of the replacement field.
x = t"aaaaaaaaa { x = ! r }"
# Combine conversion flags with format specifiers
x = t"{x = ! s
:>0
}"
# This is interesting. There can be a comment after the format specifier but only if it's
# on it's own line. Refer to https://github.com/astral-sh/ruff/pull/7787 for more details.
# We'll format is as trailing comments.
x = t"{x !s
:>0
# comment 21
}"
x = t"""
{ # comment 22
x = :.0{y # comment 23
}f}"""
# Here, the debug expression is in a nested t-string so we should start preserving
# whitespaces from that point onwards. This means we should format the outer t-string.
x = t"""{"foo " + # comment 24
t"{ x =
}" # comment 25
}
"""
# Mix of various features.
t"{ # comment 26
foo # after foo
:>{
x # after x
}
# comment 27
# comment 28
} woah {x}"
# Assignment statement
# Even though this t-string has multiline expression, thus allowing us to break it at the
# curly braces, the t-string fits on a single line if it's moved inside the parentheses.
# We should prefer doing that instead.
aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
expression}moreeeeeeeeeeeeeeeee"
# Same as above
xxxxxxx = t"{
{'aaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'}
}"
# Similar to the previous example, but the t-string will exceed the line length limit,
# we shouldn't add any parentheses here.
xxxxxxx = t"{
{'aaaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'}
}"
# Same as above but with an inline comment. The t-string should be formatted inside the
# parentheses and the comment should be part of the line inside the parentheses.
aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
expression}moreeeeeeeeeeeeeeeee" # comment
# Similar to the previous example but this time parenthesizing doesn't work because it
# exceeds the line length. So, avoid parenthesizing this t-string.
aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
expression}moreeeeeeeeeeeeeeeee" # comment loooooooong
# Similar to the previous example but we start with the parenthesized layout. This should
# remove the parentheses and format the t-string on a single line. This shows that the
# final layout for the formatter is same for this and the previous case. The only
# difference is that in the previous case the expression is already mulitline which means
# the formatter can break it further at the curly braces.
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" # comment loooooooong
)
# The following t-strings are going to break because of the trailing comma so we should
# avoid using the best fit layout and instead use the default layout.
# left-to-right
aaaa = t"aaaa {[
1, 2,
]} bbbb"
# right-to-left
aaaa, bbbb = t"aaaa {[
1, 2,
]} bbbb"
# Using the right-to-left assignment statement variant.
aaaaaaaaaaaaaaaaaa, bbbbbbbbbbb = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
expression}moreeeeeeeeeeeeeeeee" # comment
# Here, the t-string layout is flat but it exceeds the line length limit. This shouldn't
# try the custom best fit layout because the t-string doesn't have any split points.
aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = (
t"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd"
)
# Same as above but without the parentheses to test that it gets formatted to the same
# layout as the previous example.
aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = t"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd"
# But, the following t-string does have a split point because of the multiline expression.
aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = (
t"aaaaaaaaaaaaaaaaaaa {
aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd"
)
aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = (
t"aaaaaaaaaaaaaaaaaaa {
aaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbb + cccccccccccccccccccccc + dddddddddddddddddddddddddddd} ddddddddddddddddddd"
)
# This is an implicitly concatenated t-string but it cannot be joined because otherwise
# it'll exceed the line length limit. So, the two t-strings will be inside parentheses
# instead and the inline comment should be outside the parentheses.
a = t"test{
expression
}flat" t"can be {
joined
} togethereeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" # inline
# Similar to the above example but this fits within the line length limit.
a = t"test{
expression
}flat" t"can be {
joined
} togethereeeeeeeeeeeeeeeeeeeeeeeeeee" # inline
# The following test cases are adopted from implicit string concatenation but for a
# single t-string instead.
# Don't inline t-strings that contain expressions that are guaranteed to split, e.g. because of a magic trailing comma
aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}moreeeeeeeeeeeeeeeeeeee" # comment
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}moreeeeeeeeeeeeeeeeeeee" # comment
)
aaaaa[aaaaaaaaaaa] = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}moreeeeeeeeeeeeeeeeeeee" # comment
aaaaa[aaaaaaaaaaa] = (t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}moreeeeeeeeeeeeeeeeeeee" # comment
)
# Don't inline t-strings that contain commented expressions
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{[
a # comment
]}moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{[
a # comment
]}moreeeeeeeeeeeeeeeeeetest" # comment
)
# Don't inline t-strings with multiline debug expressions or format specifiers
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
a=}moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a +
b=}moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a
=}moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
a=}moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a
=}moreeeeeeeeeeeeeeeeeetest" # comment
)
# This is not a multiline t-string even though it has a newline after the format specifier.
aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeee{
a:.3f
}moreeeeeeeeeeeeeeeeeetest" # comment
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
a:.3f
}moreeeeeeeeeeeeeeeeeetest" # comment
)
# The newline is only considered when it's a tripled-quoted t-string.
aaaaaaaaaaaaaaaaaa = t"""testeeeeeeeeeeeeeeeeeeeeeeeee{
a:.3f
}moreeeeeeeeeeeeeeeeeetest""" # comment
aaaaaaaaaaaaaaaaaa = (
t"""testeeeeeeeeeeeeeeeeeeeeeeeee{
a:.3f
}moreeeeeeeeeeeeeeeeeetest""" # comment
)
# Remove the parentheses here
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{[a, b,
# comment
]}moee" # comment
)
# ... but not here because of the ownline comment
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{[a, b,
]}moee"
# comment
)
# t-strings in other positions
if t"aaaaaaaaaaa {ttttteeeeeeeeest} more {
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
}": pass
if (
t"aaaaaaaaaaa {ttttteeeeeeeeest} more {
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
}"
): pass
if t"aaaaaaaaaaa {ttttteeeeeeeeest} more {
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
}": pass
if t"aaaaaaaaaaa {ttttteeeeeeeeest} more { # comment
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
}": pass
if t"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more {
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
}":
pass
if (
t"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more {
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
}"
):
pass
if t"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more {
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
}":
pass
# For loops
for a in t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
expression}moreeeeeeeeeeeeeeeeeeee":
pass
for a in t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee":
pass
for a in t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee":
pass
for a in (
t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeee"
):
pass
# With statements
with t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
expression}moreeeeeeeeeeeeeeeeeeee":
pass
with t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee":
pass
with t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee":
pass
with (
t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeee"
):
pass
# Assert statements
assert t"aaaaaaaaa{
expression}bbbbbbbbbbbb", t"cccccccccc{
expression}dddddddddd"
assert t"aaaaaaaaa{expression}bbbbbbbbbbbb", t"cccccccccccccccc{
expression}dddddddddddddddd"
assert t"aaaaaaaaa{expression}bbbbbbbbbbbb", t"cccccccccccccccc{expression}dddddddddddddddd"
assert t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{
expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", t"ccccccc{expression}dddddddddd"
assert t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", t"ccccccc{expression}dddddddddd"
assert t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{
expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", t"ccccccccccccccccccccc {
expression} dddddddddddddddddddddddddd"
assert t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", t"cccccccccccccccccccccccccccccccc {expression} ddddddddddddddddddddddddddddddddddddd"
# t-strings as a single argument to a call expression to test whether it's huggable or not.
call(t"{
testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
}")
call(t"{
testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
}")
call(t"{ # comment
testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
}")
call(t"""aaaaaaaaaaaaaaaa bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee}""")
call(t"""aaaaaaaaaaaaaaaa bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
}""")
call(t"""aaaaaaaaaaaaaaaa
bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
}""")
call(t"""aaaaaaaaaaaaaaaa
bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # comment
}""")
call(
t"""aaaaaaaaaaaaaaaa
bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # comment
}"""
)
call(t"{
aaaaaa
+ '''test
more'''
}")
# Indentation
# What should be the indentation?
# https://github.com/astral-sh/ruff/discussions/9785#discussioncomment-8470590
if indent0:
if indent1:
if indent2:
foo = t"""hello world
hello {
t"aaaaaaa {
[
'aaaaaaaaaaaaaaaaaaaaa',
'bbbbbbbbbbbbbbbbbbbbb',
'ccccccccccccccccccccc',
'ddddddddddddddddddddd'
]
} bbbbbbbb" +
[
'aaaaaaaaaaaaaaaaaaaaa',
'bbbbbbbbbbbbbbbbbbbbb',
'ccccccccccccccccccccc',
'ddddddddddddddddddddd'
]
} --------
"""
# Implicit concatenated t-string containing quotes
_ = (
'This string should change its quotes to double quotes'
t'This string uses double quotes in an expression {"it's a quote"}'
t'This t-string does not use any quotes.'
)
# Regression test for https://github.com/astral-sh/ruff/issues/14487
t"aaaaaaaaaaaaaaaaaaaaaaaaaa {10**27} bbbbbbbbbbbbbbbbbbbbbbbbbb ccccccccccccccccccccccccc"
# Regression test for https://github.com/astral-sh/ruff/issues/14778
t"{'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 'a' if True else ""}"
t"{'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 'a' if True else ""}"
# Quotes reuse
t"{'a'}"
# 312+, it's okay to change the outer quotes even when there's a debug expression using the same quotes
t'foo {10 + len("bar")=}'
t'''foo {10 + len("""bar""")=}'''
# 312+, it's okay to change the quotes here without creating an invalid t-string
t'{"""other " """}'
t'{"""other " """ + "more"}'
t'{b"""other " """}'
t'{t"""other " """}'
# Regression tests for https://github.com/astral-sh/ruff/issues/13935
t'{1: hy "user"}'
t'{1:hy "user"}'
t'{1: abcd "{1}" }'
t'{1: abcd "{'aa'}" }'
t'{1=: "abcd {'aa'}}'
t'{x:a{z:hy "user"}} \'\'\''
# Changing the outer quotes is fine because the format-spec is in a nested expression.
t'{t'{z=:hy "user"}'} \'\'\''
# We have to be careful about changing the quotes if the t-string has a debug expression because it is inserted verbatim.
t'{1=: "abcd \'\'}' # Don't change the outer quotes, or it results in a syntax error
t'{1=: abcd \'\'}' # Changing the quotes here is fine because the inner quotes aren't the opposite quotes
t'{1=: abcd \"\"}' # Changing the quotes here is fine because the inner quotes are escaped
# Don't change the quotes in the following cases:
t'{x=:hy "user"} \'\'\''
t'{x=:a{y:hy "user"}} \'\'\''
t'{x=:a{y:{z:hy "user"}}} \'\'\''
t'{x:a{y=:{z:hy "user"}}} \'\'\''
# This is fine because the debug expression and format spec are in a nested expression
t"""{1=: "this" is fine}"""
t'''{1=: "this" is fine}''' # Change quotes to double quotes because they're preferred
t'{1=: {'ab"cd"'}}' # It's okay if the quotes are in an expression part.
# Regression tests for https://github.com/astral-sh/ruff/issues/15459
print(t"{ {1, 2, 3} - {2} }")
print(t"{ {1: 2}.keys() }")
print(t"{({1, 2, 3}) - ({2})}")
print(t"{1, 2, {3} }")
print(t"{(1, 2, {3})}")
# Regression tests for https://github.com/astral-sh/ruff/issues/15535
print(t"{ {}, }") # A single item tuple gets parenthesized
print(t"{ {}.values(), }")
print(t"{ {}, 1 }") # A tuple with multiple elements doesn't get parenthesized
print(t"{ # Tuple with multiple elements that doesn't fit on a single line gets parenthesized
{}, 1,
}")
# Regression tests for https://github.com/astral-sh/ruff/issues/15536
print(t"{ {}, 1, }")

View file

@ -205,14 +205,14 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> {
pub(crate) fn finish(&mut self) -> FormatResult<()> {
self.result.and_then(|()| {
// Don't add a magic trailing comma when formatting an f-string expression
// Don't add a magic trailing comma when formatting an f-string or t-string expression
// that always must be flat because the `expand_parent` forces enclosing
// groups to expand, e.g. `print(f"{(a,)} ")` would format the f-string in
// flat mode but the `print` call gets expanded because of the `expand_parent`.
if self
.fmt
.context()
.f_string_state()
.interpolated_string_state()
.can_contain_line_breaks()
== Some(false)
{

View file

@ -314,15 +314,14 @@ fn handle_enclosed_comment<'a>(
AnyNodeRef::StmtImportFrom(import_from) => handle_import_from_comment(comment, import_from),
AnyNodeRef::StmtWith(with_) => handle_with_comment(comment, with_),
AnyNodeRef::ExprCall(_) => handle_call_comment(comment),
AnyNodeRef::ExprStringLiteral(_) => {
if let Some(AnyNodeRef::FString(fstring)) = comment.enclosing_parent() {
CommentPlacement::dangling(fstring, comment)
} else {
CommentPlacement::Default(comment)
}
}
AnyNodeRef::ExprStringLiteral(_) => match comment.enclosing_parent() {
Some(AnyNodeRef::FString(fstring)) => CommentPlacement::dangling(fstring, comment),
Some(AnyNodeRef::TString(tstring)) => CommentPlacement::dangling(tstring, comment),
_ => CommentPlacement::Default(comment),
},
AnyNodeRef::FString(fstring) => CommentPlacement::dangling(fstring, comment),
AnyNodeRef::FStringExpressionElement(_) => {
AnyNodeRef::TString(tstring) => CommentPlacement::dangling(tstring, comment),
AnyNodeRef::InterpolatedElement(_) => {
// Handle comments after the format specifier (should be rare):
//
// ```python
@ -336,7 +335,8 @@ fn handle_enclosed_comment<'a>(
if matches!(
comment.preceding_node(),
Some(
AnyNodeRef::FStringExpressionElement(_) | AnyNodeRef::FStringLiteralElement(_)
AnyNodeRef::InterpolatedElement(_)
| AnyNodeRef::InterpolatedStringLiteralElement(_)
)
) {
CommentPlacement::trailing(comment.enclosing_node(), comment)
@ -344,6 +344,7 @@ fn handle_enclosed_comment<'a>(
handle_bracketed_end_of_line_comment(comment, source)
}
}
AnyNodeRef::ExprList(_)
| AnyNodeRef::ExprSet(_)
| AnyNodeRef::ExprListComp(_)

View file

@ -7,7 +7,7 @@ use ruff_python_parser::Tokens;
use crate::PyFormatOptions;
use crate::comments::Comments;
use crate::other::f_string_element::FStringExpressionElementContext;
use crate::other::interpolated_string_element::InterpolatedElementContext;
pub struct PyFormatContext<'a> {
options: PyFormatOptions,
@ -25,8 +25,8 @@ pub struct PyFormatContext<'a> {
/// quote style that is inverted from the one here in order to ensure that
/// the formatted Python code will be valid.
docstring: Option<Quote>,
/// The state of the formatter with respect to f-strings.
f_string_state: FStringState,
/// The state of the formatter with respect to f-strings and t-strings.
interpolated_string_state: InterpolatedStringState,
}
impl<'a> PyFormatContext<'a> {
@ -44,7 +44,7 @@ impl<'a> PyFormatContext<'a> {
node_level: NodeLevel::TopLevel(TopLevelStatementPosition::Other),
indent_level: IndentLevel::new(0),
docstring: None,
f_string_state: FStringState::Outside,
interpolated_string_state: InterpolatedStringState::Outside,
}
}
@ -97,12 +97,15 @@ impl<'a> PyFormatContext<'a> {
}
}
pub(crate) fn f_string_state(&self) -> FStringState {
self.f_string_state
pub(crate) fn interpolated_string_state(&self) -> InterpolatedStringState {
self.interpolated_string_state
}
pub(crate) fn set_f_string_state(&mut self, f_string_state: FStringState) {
self.f_string_state = f_string_state;
pub(crate) fn set_interpolated_string_state(
&mut self,
interpolated_string_state: InterpolatedStringState,
) {
self.interpolated_string_state = interpolated_string_state;
}
/// Returns `true` if preview mode is enabled.
@ -135,24 +138,24 @@ impl Debug for PyFormatContext<'_> {
}
#[derive(Clone, Copy, Debug, Default)]
pub(crate) enum FStringState {
pub(crate) enum InterpolatedStringState {
/// The formatter is inside an f-string expression element i.e., between the
/// curly brace in `f"foo {x}"`.
///
/// The containing `FStringContext` is the surrounding f-string context.
InsideExpressionElement(FStringExpressionElementContext),
InsideInterpolatedElement(InterpolatedElementContext),
/// The formatter is outside an f-string.
#[default]
Outside,
}
impl FStringState {
impl InterpolatedStringState {
pub(crate) fn can_contain_line_breaks(self) -> Option<bool> {
match self {
FStringState::InsideExpressionElement(context) => {
InterpolatedStringState::InsideInterpolatedElement(context) => {
Some(context.can_contain_line_breaks())
}
FStringState::Outside => None,
InterpolatedStringState::Outside => None,
}
}
}
@ -375,25 +378,25 @@ where
}
}
pub(crate) struct WithFStringState<'a, B, D>
pub(crate) struct WithInterpolatedStringState<'a, B, D>
where
D: DerefMut<Target = B>,
B: Buffer<Context = PyFormatContext<'a>>,
{
buffer: D,
saved_location: FStringState,
saved_location: InterpolatedStringState,
}
impl<'a, B, D> WithFStringState<'a, B, D>
impl<'a, B, D> WithInterpolatedStringState<'a, B, D>
where
D: DerefMut<Target = B>,
B: Buffer<Context = PyFormatContext<'a>>,
{
pub(crate) fn new(expr_location: FStringState, mut buffer: D) -> Self {
pub(crate) fn new(expr_location: InterpolatedStringState, mut buffer: D) -> Self {
let context = buffer.state_mut().context_mut();
let saved_location = context.f_string_state();
let saved_location = context.interpolated_string_state();
context.set_f_string_state(expr_location);
context.set_interpolated_string_state(expr_location);
Self {
buffer,
@ -402,7 +405,7 @@ where
}
}
impl<'a, B, D> Deref for WithFStringState<'a, B, D>
impl<'a, B, D> Deref for WithInterpolatedStringState<'a, B, D>
where
D: DerefMut<Target = B>,
B: Buffer<Context = PyFormatContext<'a>>,
@ -414,7 +417,7 @@ where
}
}
impl<'a, B, D> DerefMut for WithFStringState<'a, B, D>
impl<'a, B, D> DerefMut for WithInterpolatedStringState<'a, B, D>
where
D: DerefMut<Target = B>,
B: Buffer<Context = PyFormatContext<'a>>,
@ -424,7 +427,7 @@ where
}
}
impl<'a, B, D> Drop for WithFStringState<'a, B, D>
impl<'a, B, D> Drop for WithInterpolatedStringState<'a, B, D>
where
D: DerefMut<Target = B>,
B: Buffer<Context = PyFormatContext<'a>>,
@ -433,6 +436,6 @@ where
self.buffer
.state_mut()
.context_mut()
.set_f_string_state(self.saved_location);
.set_interpolated_string_state(self.saved_location);
}
}

View file

@ -3,7 +3,7 @@ use ruff_python_ast::{AnyNodeRef, ExprFString, StringLike};
use crate::expression::parentheses::{
NeedsParentheses, OptionalParentheses, in_parentheses_only_group,
};
use crate::other::f_string::FStringLayout;
use crate::other::interpolated_string::InterpolatedStringLayout;
use crate::prelude::*;
use crate::string::StringLikeExtensions;
use crate::string::implicit::{
@ -41,7 +41,11 @@ impl NeedsParentheses for ExprFString {
if let Some(fstring_part) = self.as_single_part_fstring() {
// The f-string is not implicitly concatenated
if StringLike::FString(self).is_multiline(context)
|| FStringLayout::from_f_string(fstring_part, context.source()).is_multiline()
|| InterpolatedStringLayout::from_interpolated_string_elements(
&fstring_part.elements,
context.source(),
)
.is_multiline()
{
OptionalParentheses::Never
} else {

View file

@ -0,0 +1,59 @@
use ruff_python_ast::{AnyNodeRef, ExprTString, StringLike};
use crate::expression::parentheses::{
NeedsParentheses, OptionalParentheses, in_parentheses_only_group,
};
use crate::other::interpolated_string::InterpolatedStringLayout;
use crate::prelude::*;
use crate::string::StringLikeExtensions;
use crate::string::implicit::{
FormatImplicitConcatenatedString, FormatImplicitConcatenatedStringFlat,
};
#[derive(Default)]
pub struct FormatExprTString;
impl FormatNodeRule<ExprTString> for FormatExprTString {
fn fmt_fields(&self, item: &ExprTString, f: &mut PyFormatter) -> FormatResult<()> {
if let Some(t_string) = item.as_single_part_tstring() {
t_string.format().fmt(f)
} else {
// Always join tstrings 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)
}
}
}
impl NeedsParentheses for ExprTString {
fn needs_parentheses(
&self,
_parent: AnyNodeRef,
context: &PyFormatContext,
) -> OptionalParentheses {
if let Some(tstring_part) = self.as_single_part_tstring() {
// The t-string is not implicitly concatenated
if StringLike::TString(self).is_multiline(context)
|| InterpolatedStringLayout::from_interpolated_string_elements(
&tstring_part.elements,
context.source(),
)
.is_multiline()
{
OptionalParentheses::Never
} else {
OptionalParentheses::BestFit
}
} else {
// The t-string is implicitly concatenated
OptionalParentheses::Multiline
}
}
}

View file

@ -50,6 +50,7 @@ pub(crate) mod expr_slice;
pub(crate) mod expr_starred;
pub(crate) mod expr_string_literal;
pub(crate) mod expr_subscript;
pub(crate) mod expr_t_string;
pub(crate) mod expr_tuple;
pub(crate) mod expr_unary_op;
pub(crate) mod expr_yield;
@ -94,6 +95,7 @@ impl FormatRule<Expr, PyFormatContext<'_>> for FormatExpr {
Expr::Compare(expr) => expr.format().fmt(f),
Expr::Call(expr) => expr.format().fmt(f),
Expr::FString(expr) => expr.format().fmt(f),
Expr::TString(expr) => expr.format().fmt(f),
Expr::StringLiteral(expr) => expr.format().fmt(f),
Expr::BytesLiteral(expr) => expr.format().fmt(f),
Expr::NumberLiteral(expr) => expr.format().fmt(f),
@ -282,6 +284,7 @@ fn format_with_parentheses_comments(
Expr::Compare(expr) => FormatNodeRule::fmt_fields(expr.format().rule(), expr, f),
Expr::Call(expr) => FormatNodeRule::fmt_fields(expr.format().rule(), expr, f),
Expr::FString(expr) => FormatNodeRule::fmt_fields(expr.format().rule(), expr, f),
Expr::TString(expr) => FormatNodeRule::fmt_fields(expr.format().rule(), expr, f),
Expr::StringLiteral(expr) => FormatNodeRule::fmt_fields(expr.format().rule(), expr, f),
Expr::BytesLiteral(expr) => FormatNodeRule::fmt_fields(expr.format().rule(), expr, f),
Expr::NumberLiteral(expr) => FormatNodeRule::fmt_fields(expr.format().rule(), expr, f),
@ -480,6 +483,7 @@ impl NeedsParentheses for Expr {
Expr::Compare(expr) => expr.needs_parentheses(parent, context),
Expr::Call(expr) => expr.needs_parentheses(parent, context),
Expr::FString(expr) => expr.needs_parentheses(parent, context),
Expr::TString(expr) => expr.needs_parentheses(parent, context),
Expr::StringLiteral(expr) => expr.needs_parentheses(parent, context),
Expr::BytesLiteral(expr) => expr.needs_parentheses(parent, context),
Expr::NumberLiteral(expr) => expr.needs_parentheses(parent, context),
@ -775,6 +779,7 @@ impl<'input> CanOmitOptionalParenthesesVisitor<'input> {
// Terminal nodes or nodes that wrap a sub-expression (where the sub expression can never be at the end).
Expr::FString(_)
| Expr::TString(_)
| Expr::StringLiteral(_)
| Expr::BytesLiteral(_)
| Expr::NumberLiteral(_)
@ -1126,6 +1131,7 @@ pub(crate) fn is_expression_huggable(expr: &Expr, context: &PyFormatContext) ->
| Expr::StringLiteral(_)
| Expr::BytesLiteral(_)
| Expr::FString(_)
| Expr::TString(_)
| Expr::EllipsisLiteral(_) => false,
}
}
@ -1221,6 +1227,7 @@ pub(crate) fn is_splittable_expression(expr: &Expr, context: &PyFormatContext) -
// String like literals can expand if they are implicit concatenated.
Expr::FString(fstring) => fstring.value.is_implicit_concatenated(),
Expr::TString(tstring) => tstring.value.is_implicit_concatenated(),
Expr::StringLiteral(string) => string.value.is_implicit_concatenated(),
Expr::BytesLiteral(bytes) => bytes.value.is_implicit_concatenated(),
@ -1278,6 +1285,7 @@ pub(crate) fn left_most<'expr>(
| Expr::Name(_)
| Expr::Starred(_)
| Expr::FString(_)
| Expr::TString(_)
| Expr::StringLiteral(_)
| Expr::BytesLiteral(_)
| Expr::NumberLiteral(_)

View file

@ -1562,6 +1562,42 @@ impl<'ast> IntoFormat<PyFormatContext<'ast>> for ast::ExprFString {
}
}
impl FormatRule<ast::ExprTString, PyFormatContext<'_>>
for crate::expression::expr_t_string::FormatExprTString
{
#[inline]
fn fmt(&self, node: &ast::ExprTString, f: &mut PyFormatter) -> FormatResult<()> {
FormatNodeRule::<ast::ExprTString>::fmt(self, node, f)
}
}
impl<'ast> AsFormat<PyFormatContext<'ast>> for ast::ExprTString {
type Format<'a> = FormatRefWithRule<
'a,
ast::ExprTString,
crate::expression::expr_t_string::FormatExprTString,
PyFormatContext<'ast>,
>;
fn format(&self) -> Self::Format<'_> {
FormatRefWithRule::new(
self,
crate::expression::expr_t_string::FormatExprTString::default(),
)
}
}
impl<'ast> IntoFormat<PyFormatContext<'ast>> for ast::ExprTString {
type Format = FormatOwnedWithRule<
ast::ExprTString,
crate::expression::expr_t_string::FormatExprTString,
PyFormatContext<'ast>,
>;
fn into_format(self) -> Self::Format {
FormatOwnedWithRule::new(
self,
crate::expression::expr_t_string::FormatExprTString::default(),
)
}
}
impl FormatRule<ast::ExprStringLiteral, PyFormatContext<'_>>
for crate::expression::expr_string_literal::FormatExprStringLiteral
{
@ -2963,6 +2999,34 @@ impl<'ast> IntoFormat<PyFormatContext<'ast>> for ast::FString {
}
}
impl FormatRule<ast::TString, PyFormatContext<'_>> for crate::other::t_string::FormatTString {
#[inline]
fn fmt(&self, node: &ast::TString, f: &mut PyFormatter) -> FormatResult<()> {
FormatNodeRule::<ast::TString>::fmt(self, node, f)
}
}
impl<'ast> AsFormat<PyFormatContext<'ast>> for ast::TString {
type Format<'a> = FormatRefWithRule<
'a,
ast::TString,
crate::other::t_string::FormatTString,
PyFormatContext<'ast>,
>;
fn format(&self) -> Self::Format<'_> {
FormatRefWithRule::new(self, crate::other::t_string::FormatTString::default())
}
}
impl<'ast> IntoFormat<PyFormatContext<'ast>> for ast::TString {
type Format = FormatOwnedWithRule<
ast::TString,
crate::other::t_string::FormatTString,
PyFormatContext<'ast>,
>;
fn into_format(self) -> Self::Format {
FormatOwnedWithRule::new(self, crate::other::t_string::FormatTString::default())
}
}
impl FormatRule<ast::StringLiteral, PyFormatContext<'_>>
for crate::other::string_literal::FormatStringLiteral
{

View file

@ -1,12 +1,9 @@
use ruff_formatter::write;
use ruff_python_ast::{AnyStringFlags, FString, StringFlags};
use ruff_source_file::LineRanges;
use ruff_text_size::Ranged;
use super::interpolated_string_element::FormatInterpolatedStringElement;
use crate::other::interpolated_string::{InterpolatedStringContext, InterpolatedStringLayout};
use crate::prelude::*;
use crate::string::{StringNormalizer, StringQuotes};
use super::f_string_element::FormatFStringElement;
use ruff_formatter::write;
use ruff_python_ast::{FString, StringFlags};
/// Formats an f-string which is part of a larger f-string expression.
///
@ -21,9 +18,12 @@ impl FormatNodeRule<FString> for FormatFString {
let string_kind = normalizer.choose_quotes(item.into()).flags();
let context = FStringContext::new(
let context = InterpolatedStringContext::new(
string_kind,
FStringLayout::from_f_string(item, f.context().source()),
InterpolatedStringLayout::from_interpolated_string_elements(
&item.elements,
f.context().source(),
),
);
// Starting prefix and quote
@ -31,78 +31,10 @@ impl FormatNodeRule<FString> for FormatFString {
write!(f, [string_kind.prefix(), quotes])?;
for element in &item.elements {
FormatFStringElement::new(element, context).fmt(f)?;
FormatInterpolatedStringElement::new(element, context).fmt(f)?;
}
// Ending quote
quotes.fmt(f)
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct FStringContext {
/// The string flags of the enclosing f-string part.
enclosing_flags: AnyStringFlags,
layout: FStringLayout,
}
impl FStringContext {
pub(crate) const fn new(flags: AnyStringFlags, layout: FStringLayout) -> Self {
Self {
enclosing_flags: flags,
layout,
}
}
pub(crate) fn flags(self) -> AnyStringFlags {
self.enclosing_flags
}
pub(crate) const fn layout(self) -> FStringLayout {
self.layout
}
}
#[derive(Copy, Clone, Debug)]
pub(crate) enum FStringLayout {
/// Original f-string is flat.
/// Don't break expressions to keep the string flat.
Flat,
/// Original f-string has multiline expressions in the replacement fields.
/// Allow breaking expressions across multiple lines.
Multiline,
}
impl FStringLayout {
pub(crate) fn from_f_string(f_string: &FString, source: &str) -> Self {
// Heuristic: Allow breaking the f-string expressions across multiple lines
// 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
// f-string expressions across multiple lines or not. This is similar to
// how Prettier does it for template literals in JavaScript.
//
// If it's single quoted f-string and it contains a multiline expression, then we
// assume that the target version of Python supports it (3.12+). If there are comments
// used in any of the expression of the f-string, then it's always going to be multiline
// and we assume that the target version of Python supports it (3.12+).
//
// Reference: https://prettier.io/docs/en/next/rationale.html#template-literals
if f_string
.elements
.expressions()
.any(|expr| source.contains_line_break(expr.range()))
{
Self::Multiline
} else {
Self::Flat
}
}
pub(crate) const fn is_flat(self) -> bool {
matches!(self, FStringLayout::Flat)
}
pub(crate) const fn is_multiline(self) -> bool {
matches!(self, FStringLayout::Multiline)
}
}

View file

@ -0,0 +1,73 @@
use ruff_python_ast::{AnyStringFlags, InterpolatedStringElements};
use ruff_source_file::LineRanges;
use ruff_text_size::Ranged;
#[derive(Clone, Copy, Debug)]
pub(crate) struct InterpolatedStringContext {
/// The string flags of the enclosing f/t-string part.
enclosing_flags: AnyStringFlags,
layout: InterpolatedStringLayout,
}
impl InterpolatedStringContext {
pub(crate) const fn new(flags: AnyStringFlags, layout: InterpolatedStringLayout) -> Self {
Self {
enclosing_flags: flags,
layout,
}
}
pub(crate) fn flags(self) -> AnyStringFlags {
self.enclosing_flags
}
pub(crate) const fn layout(self) -> InterpolatedStringLayout {
self.layout
}
}
#[derive(Copy, Clone, Debug)]
pub(crate) enum InterpolatedStringLayout {
/// Original f/t-string is flat.
/// Don't break expressions to keep the string flat.
Flat,
/// Original f/t-string has multiline expressions in the replacement fields.
/// Allow breaking expressions across multiple lines.
Multiline,
}
impl InterpolatedStringLayout {
// Heuristic: Allow breaking the f/t-string expressions across multiple lines
// 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
// f/t-string expressions across multiple lines or not. This is similar to
// how Prettier does it for template literals in JavaScript.
//
// If it's single quoted f-string and it contains a multiline expression, then we
// assume that the target version of Python supports it (3.12+). If there are comments
// used in any of the expression of the f-string, then it's always going to be multiline
// and we assume that the target version of Python supports it (3.12+).
//
// Reference: https://prettier.io/docs/en/next/rationale.html#template-literals
pub(crate) fn from_interpolated_string_elements(
elements: &InterpolatedStringElements,
source: &str,
) -> Self {
if elements
.interpolations()
.any(|expr| source.contains_line_break(expr.range()))
{
Self::Multiline
} else {
Self::Flat
}
}
pub(crate) const fn is_flat(self) -> bool {
matches!(self, InterpolatedStringLayout::Flat)
}
pub(crate) const fn is_multiline(self) -> bool {
matches!(self, InterpolatedStringLayout::Multiline)
}
}

View file

@ -2,42 +2,47 @@ use std::borrow::Cow;
use ruff_formatter::{Buffer, RemoveSoftLinesBuffer, format_args, write};
use ruff_python_ast::{
AnyStringFlags, ConversionFlag, Expr, FStringElement, FStringExpressionElement,
FStringLiteralElement, StringFlags,
AnyStringFlags, ConversionFlag, Expr, InterpolatedElement, InterpolatedStringElement,
InterpolatedStringLiteralElement, StringFlags,
};
use ruff_text_size::{Ranged, TextSlice};
use crate::comments::{dangling_open_parenthesis_comments, trailing_comments};
use crate::context::{FStringState, NodeLevel, WithFStringState, WithNodeLevel};
use crate::context::{
InterpolatedStringState, NodeLevel, WithInterpolatedStringState, WithNodeLevel,
};
use crate::expression::left_most;
use crate::prelude::*;
use crate::string::normalize_string;
use crate::verbatim::verbatim_text;
use super::f_string::FStringContext;
use super::interpolated_string::InterpolatedStringContext;
/// Formats an f-string element which is either a literal or a formatted expression.
///
/// This delegates the actual formatting to the appropriate formatter.
pub(crate) struct FormatFStringElement<'a> {
element: &'a FStringElement,
context: FStringContext,
pub(crate) struct FormatInterpolatedStringElement<'a> {
element: &'a InterpolatedStringElement,
context: InterpolatedStringContext,
}
impl<'a> FormatFStringElement<'a> {
pub(crate) fn new(element: &'a FStringElement, context: FStringContext) -> Self {
impl<'a> FormatInterpolatedStringElement<'a> {
pub(crate) fn new(
element: &'a InterpolatedStringElement,
context: InterpolatedStringContext,
) -> Self {
Self { element, context }
}
}
impl Format<PyFormatContext<'_>> for FormatFStringElement<'_> {
impl Format<PyFormatContext<'_>> for FormatInterpolatedStringElement<'_> {
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
match self.element {
FStringElement::Literal(string_literal) => {
InterpolatedStringElement::Literal(string_literal) => {
FormatFStringLiteralElement::new(string_literal, self.context.flags()).fmt(f)
}
FStringElement::Expression(expression) => {
FormatFStringExpressionElement::new(expression, self.context).fmt(f)
InterpolatedStringElement::Interpolation(expression) => {
FormatInterpolatedElement::new(expression, self.context).fmt(f)
}
}
}
@ -45,13 +50,16 @@ impl Format<PyFormatContext<'_>> for FormatFStringElement<'_> {
/// Formats an f-string literal element.
pub(crate) struct FormatFStringLiteralElement<'a> {
element: &'a FStringLiteralElement,
element: &'a InterpolatedStringLiteralElement,
/// Flags of the enclosing F-string part
fstring_flags: AnyStringFlags,
}
impl<'a> FormatFStringLiteralElement<'a> {
pub(crate) fn new(element: &'a FStringLiteralElement, fstring_flags: AnyStringFlags) -> Self {
pub(crate) fn new(
element: &'a InterpolatedStringLiteralElement,
fstring_flags: AnyStringFlags,
) -> Self {
Self {
element,
fstring_flags,
@ -72,16 +80,16 @@ impl Format<PyFormatContext<'_>> for FormatFStringLiteralElement<'_> {
/// Context representing an f-string expression element.
#[derive(Clone, Copy, Debug)]
pub(crate) struct FStringExpressionElementContext {
pub(crate) struct InterpolatedElementContext {
/// The context of the parent f-string containing this expression element.
parent_context: FStringContext,
parent_context: InterpolatedStringContext,
/// Indicates whether this expression element has format specifier or not.
has_format_spec: bool,
}
impl FStringExpressionElementContext {
/// Returns the [`FStringContext`] containing this expression element.
pub(crate) fn f_string(self) -> FStringContext {
impl InterpolatedElementContext {
/// Returns the [`InterpolatedStringContext`] containing this expression element.
pub(crate) fn interpolated_string(self) -> InterpolatedStringContext {
self.parent_context
}
@ -113,16 +121,19 @@ impl FStringExpressionElementContext {
}
/// Formats an f-string expression element.
pub(crate) struct FormatFStringExpressionElement<'a> {
element: &'a FStringExpressionElement,
context: FStringExpressionElementContext,
pub(crate) struct FormatInterpolatedElement<'a> {
element: &'a InterpolatedElement,
context: InterpolatedElementContext,
}
impl<'a> FormatFStringExpressionElement<'a> {
pub(crate) fn new(element: &'a FStringExpressionElement, context: FStringContext) -> Self {
impl<'a> FormatInterpolatedElement<'a> {
pub(crate) fn new(
element: &'a InterpolatedElement,
context: InterpolatedStringContext,
) -> Self {
Self {
element,
context: FStringExpressionElementContext {
context: InterpolatedElementContext {
parent_context: context,
has_format_spec: element.format_spec.is_some(),
},
@ -130,9 +141,9 @@ impl<'a> FormatFStringExpressionElement<'a> {
}
}
impl Format<PyFormatContext<'_>> for FormatFStringExpressionElement<'_> {
impl Format<PyFormatContext<'_>> for FormatInterpolatedElement<'_> {
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
let FStringExpressionElement {
let InterpolatedElement {
expression,
debug_text,
conversion,
@ -214,8 +225,8 @@ impl Format<PyFormatContext<'_>> for FormatFStringExpressionElement<'_> {
let item = format_with(|f: &mut PyFormatter| {
// Update the context to be inside the f-string expression element.
let f = &mut WithFStringState::new(
FStringState::InsideExpressionElement(self.context),
let f = &mut WithInterpolatedStringState::new(
InterpolatedStringState::InsideInterpolatedElement(self.context),
f,
);
@ -233,7 +244,11 @@ impl Format<PyFormatContext<'_>> for FormatFStringExpressionElement<'_> {
token(":").fmt(f)?;
for element in &format_spec.elements {
FormatFStringElement::new(element, self.context.f_string()).fmt(f)?;
FormatInterpolatedStringElement::new(
element,
self.context.interpolated_string(),
)
.fmt(f)?;
}
// These trailing comments can only occur if the format specifier is

View file

@ -7,12 +7,14 @@ pub(crate) mod decorator;
pub(crate) mod elif_else_clause;
pub(crate) mod except_handler_except_handler;
pub(crate) mod f_string;
pub(crate) mod f_string_element;
pub(crate) mod identifier;
pub(crate) mod interpolated_string;
pub(crate) mod interpolated_string_element;
pub(crate) mod keyword;
pub(crate) mod match_case;
pub(crate) mod parameter;
pub(crate) mod parameter_with_default;
pub(crate) mod parameters;
pub(crate) mod string_literal;
pub(crate) mod t_string;
pub(crate) mod with_item;

View file

@ -0,0 +1,40 @@
use super::interpolated_string_element::FormatInterpolatedStringElement;
use crate::other::interpolated_string::{InterpolatedStringContext, InterpolatedStringLayout};
use crate::prelude::*;
use crate::string::{StringNormalizer, StringQuotes};
use ruff_formatter::write;
use ruff_python_ast::{StringFlags, TString};
/// Formats a t-string which is part of a larger t-string expression.
///
/// For example, this would be used to format the t-string part in `"foo" t"bar {x}"`
/// or the standalone t-string in `t"foo {x} bar"`.
#[derive(Default)]
pub struct FormatTString;
impl FormatNodeRule<TString> for FormatTString {
fn fmt_fields(&self, item: &TString, f: &mut PyFormatter) -> FormatResult<()> {
let normalizer = StringNormalizer::from_context(f.context());
let string_kind = normalizer.choose_quotes(item.into()).flags();
let context = InterpolatedStringContext::new(
string_kind,
InterpolatedStringLayout::from_interpolated_string_elements(
&item.elements,
f.context().source(),
),
);
// Starting prefix and quote
let quotes = StringQuotes::from(string_kind);
write!(f, [string_kind.prefix(), quotes])?;
for element in &item.elements {
FormatInterpolatedStringElement::new(element, context).fmt(f)?;
}
// Ending quote
quotes.fmt(f)
}
}

View file

@ -293,6 +293,7 @@ impl<'a> CanOmitOptionalParenthesesVisitor<'a> {
// 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.
Expr::FString(_) |
Expr::TString(_)|
Expr::NumberLiteral(_) | Expr::Attribute(_) | Expr::UnaryOp(_) => {
// require no state update other than visit_pattern does.
}
@ -306,7 +307,7 @@ impl<'a> CanOmitOptionalParenthesesVisitor<'a> {
_ => {
debug_assert!(
false,
"Unsupported expression in pattern mach value: {:?}",
"Unsupported expression in pattern match value: {:?}",
value.value
);
}

View file

@ -659,10 +659,11 @@ impl Format<PyFormatContext<'_>> for FormatEnclosingNode<'_> {
| AnyNodeRef::ExprYieldFrom(_)
| AnyNodeRef::ExprCompare(_)
| AnyNodeRef::ExprCall(_)
| AnyNodeRef::FStringExpressionElement(_)
| AnyNodeRef::FStringLiteralElement(_)
| AnyNodeRef::FStringFormatSpec(_)
| AnyNodeRef::InterpolatedElement(_)
| AnyNodeRef::InterpolatedStringLiteralElement(_)
| AnyNodeRef::InterpolatedStringFormatSpec(_)
| AnyNodeRef::ExprFString(_)
| AnyNodeRef::ExprTString(_)
| AnyNodeRef::ExprStringLiteral(_)
| AnyNodeRef::ExprBytesLiteral(_)
| AnyNodeRef::ExprNumberLiteral(_)
@ -679,6 +680,7 @@ impl Format<PyFormatContext<'_>> for FormatEnclosingNode<'_> {
| AnyNodeRef::ExprIpyEscapeCommand(_)
| AnyNodeRef::FString(_)
| AnyNodeRef::StringLiteral(_)
| AnyNodeRef::TString(_)
| AnyNodeRef::PatternMatchValue(_)
| AnyNodeRef::PatternMatchSingleton(_)
| AnyNodeRef::PatternMatchSequence(_)

View file

@ -1,6 +1,6 @@
use ruff_formatter::{FormatError, RemoveSoftLinesBuffer, format_args, write};
use ruff_python_ast::{
AnyNodeRef, Expr, ExprAttribute, ExprCall, FString, Operator, StmtAssign, StringLike,
AnyNodeRef, Expr, ExprAttribute, ExprCall, FString, Operator, StmtAssign, StringLike, TString,
TypeParams,
};
@ -17,7 +17,7 @@ use crate::expression::{
can_omit_optional_parentheses, has_own_parentheses, has_parentheses,
maybe_parenthesize_expression,
};
use crate::other::f_string::FStringLayout;
use crate::other::interpolated_string::InterpolatedStringLayout;
use crate::statement::trailing_semicolon;
use crate::string::StringLikeExtensions;
use crate::string::implicit::{
@ -291,15 +291,16 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
let can_inline_comment = should_inline_comments(value, *statement, f.context());
let string_like = StringLike::try_from(*value).ok();
let format_f_string =
string_like.and_then(|string| format_f_string_assignment(string, f.context()));
let format_interpolated_string = string_like
.and_then(|string| format_interpolated_string_assignment(string, f.context()));
let format_implicit_flat = string_like.and_then(|string| {
FormatImplicitConcatenatedStringFlat::new(string, f.context())
});
if !can_inline_comment
&& format_implicit_flat.is_none()
&& format_f_string.is_none()
&& format_interpolated_string.is_none()
{
return maybe_parenthesize_expression(
value,
@ -351,7 +352,7 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
let string = flat.string();
let flat = format_with(|f| {
if string.is_fstring() {
if string.is_interpolated_string() {
let mut buffer = RemoveSoftLinesBuffer::new(&mut *f);
write!(buffer, [flat])
@ -361,7 +362,7 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
})
.memoized();
// F-String containing an expression with a magic trailing comma, a comment, or a
// F-string or T-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{[
@ -369,7 +370,7 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
// 2,
// ]}" "more"
// ```
if string.is_fstring() && flat.inspect(f)?.will_break() {
if string.is_interpolated_string() && flat.inspect(f)?.will_break() {
inline_comments.mark_unformatted();
return write!(
@ -446,24 +447,23 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
best_fitting![single_line, joined_parenthesized, implicit_expanded]
.with_mode(BestFittingMode::AllLines)
.fmt(f)?;
} else if let Some(format_f_string) = format_f_string {
} else if let Some(format_interpolated_string) = format_interpolated_string {
inline_comments.mark_formatted();
let f_string_flat = format_with(|f| {
let interpolated_string_flat = format_with(|f| {
let mut buffer = RemoveSoftLinesBuffer::new(&mut *f);
write!(buffer, [format_f_string.format()])
write!(buffer, [format_interpolated_string])
})
.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.
// F/T-String containing an interpolation with a magic trailing comma, a comment, or a
// multiline debug interpolation should never be joined. Use the default layout.
// ```python
// aaaa = f"aaaa {[
// 1, 2,
// ]} bbbb"
// ```
if f_string_flat.inspect(f)?.will_break() {
if interpolated_string_flat.inspect(f)?.will_break() {
inline_comments.mark_unformatted();
return write!(
@ -482,23 +482,26 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
// expression}moreeeeeeeeeeeeeeeee"
// ```
// Flatten the f-string.
// Flatten the f/t-string.
// ```python
// aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee"
// ```
let single_line =
format_with(|f| write!(f, [f_string_flat, inline_comments]));
format_with(|f| write!(f, [interpolated_string_flat, inline_comments]));
// Parenthesize the f-string and flatten the f-string.
// Parenthesize the t-string and flatten the t-string.
// ```python
// aaaaaaaaaaaaaaaaaa = (
// f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee"
// t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee"
// )
// ```
let joined_parenthesized = format_with(|f| {
group(&format_args![
token("("),
soft_block_indent(&format_args![f_string_flat, inline_comments]),
soft_block_indent(&format_args![
interpolated_string_flat,
inline_comments
]),
token(")"),
])
.with_id(Some(group_id))
@ -506,19 +509,24 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
.fmt(f)
});
// Avoid flattening or parenthesizing the f-string, keep the original
// f-string formatting.
// Avoid flattening or parenthesizing the f/t-string, keep the original
// f/t-string formatting.
// ```python
// aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
// aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
// expression
// }moreeeeeeeeeeeeeeeee"
// ```
let format_f_string =
format_with(|f| write!(f, [format_f_string.format(), inline_comments]));
let format_interpolated_string = format_with(|f| {
write!(f, [format_interpolated_string, inline_comments])
});
best_fitting![single_line, joined_parenthesized, format_f_string]
.with_mode(BestFittingMode::AllLines)
.fmt(f)?;
best_fitting![
single_line,
joined_parenthesized,
format_interpolated_string
]
.with_mode(BestFittingMode::AllLines)
.fmt(f)?;
} else {
best_fit_parenthesize(&format_once(|f| {
inline_comments.mark_formatted();
@ -559,17 +567,16 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
let should_inline_comments = should_inline_comments(value, *statement, f.context());
let string_like = StringLike::try_from(*value).ok();
let format_f_string =
string_like.and_then(|string| format_f_string_assignment(string, f.context()));
let format_interpolated_string = string_like
.and_then(|string| format_interpolated_string_assignment(string, f.context()));
let format_implicit_flat = string_like.and_then(|string| {
FormatImplicitConcatenatedStringFlat::new(string, f.context())
});
// Use the normal `maybe_parenthesize_layout` for splittable `value`s.
if !should_inline_comments
&& !should_non_inlineable_use_best_fit(value, *statement, f.context())
&& format_implicit_flat.is_none()
&& format_f_string.is_none()
&& format_interpolated_string.is_none()
{
return write!(
f,
@ -593,7 +600,7 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
// Don't inline comments for attribute and call expressions for black compatibility
let inline_comments = if should_inline_comments
|| format_implicit_flat.is_some()
|| format_f_string.is_some()
|| format_interpolated_string.is_some()
{
OptionalParenthesesInlinedComments::new(
&expression_comments,
@ -633,7 +640,9 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
// 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
// can ever fit because the left breaks.
if format_implicit_flat.is_none() && format_f_string.is_none() && last_target_breaks
if format_implicit_flat.is_none()
&& format_interpolated_string.is_none()
&& last_target_breaks
{
return write!(
f,
@ -650,7 +659,7 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
let format_value = format_with(|f| {
if let Some(format_implicit_flat) = format_implicit_flat.as_ref() {
if format_implicit_flat.string().is_fstring() {
if format_implicit_flat.string().is_interpolated_string() {
// 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
@ -660,11 +669,13 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
} else {
format_implicit_flat.fmt(f)
}
} else if let Some(format_f_string) = format_f_string.as_ref() {
} else if let Some(format_interpolated_string) =
format_interpolated_string.as_ref()
{
// Similar to above, remove any soft line breaks emitted by the f-string
// formatting.
let mut buffer = RemoveSoftLinesBuffer::new(&mut *f);
write!(buffer, [format_f_string.format()])
write!(buffer, [format_interpolated_string])
} else {
value.format().with_options(Parentheses::Never).fmt(f)
}
@ -766,7 +777,7 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
// 2,
// ]}" "more"
// ```
if format_implicit_flat.string().is_fstring()
if format_implicit_flat.string().is_interpolated_string()
&& format_value.inspect(f)?.will_break()
{
inline_comments.mark_unformatted();
@ -905,12 +916,12 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
.with_mode(BestFittingMode::AllLines)
.fmt(f)
}
} else if let Some(format_f_string) = &format_f_string {
// F-String containing an expression with a magic trailing comma, a comment, or a
} else if let Some(format_interpolated_string) = &format_interpolated_string {
// F/T-String containing an interpolation with a magic trailing comma, a comment, or a
// multiline debug expression should never be joined. Use the default layout.
//
// ```python
// aaaa, bbbb = f"aaaa {[
// aaaa, bbbb = t"aaaa {[
// 1, 2,
// ]} bbbb"
// ```
@ -933,40 +944,46 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
);
}
let format_f_string =
format_with(|f| write!(f, [format_f_string.format(), inline_comments]))
let format_interpolated_string =
format_with(|f| write!(f, [format_interpolated_string, inline_comments]))
.memoized();
// Considering the following initial source:
//
// ```python
// aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = (
// f"aaaaaaaaaaaaaaaaaaa {
// t"aaaaaaaaaaaaaaaaaaa {
// aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd"
// )
// ```
//
// Keep the target flat, and use the regular f-string formatting.
// Keep the target flat, and use the regular f/t-string formatting.
//
// ```python
// aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = f"aaaaaaaaaaaaaaaaaaa {
// aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = t"aaaaaaaaaaaaaaaaaaa {
// aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc
// } ddddddddddddddddddd"
// ```
let flat_target_regular_f_string = format_with(|f| {
let flat_target_regular_interpolated_string = format_with(|f| {
write!(
f,
[last_target, space(), operator, space(), format_f_string]
[
last_target,
space(),
operator,
space(),
format_interpolated_string
]
)
});
// Expand the parent and parenthesize the flattened f-string.
// Expand the parent and parenthesize the flattened f/t-string.
//
// ```python
// aaaaaaaaaaaa[
// "bbbbbbbbbbbbbbbb"
// ] = (
// f"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd"
// t"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd"
// )
// ```
let split_target_value_parenthesized_flat = format_with(|f| {
@ -988,16 +1005,16 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
)
});
// Expand the parent, and use the regular f-string formatting.
// Expand the parent, and use the regular f/t-string formatting.
//
// ```python
// aaaaaaaaaaaa[
// "bbbbbbbbbbbbbbbb"
// ] = f"aaaaaaaaaaaaaaaaaaa {
// ] = t"aaaaaaaaaaaaaaaaaaa {
// aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc
// } ddddddddddddddddddd"
// ```
let split_target_regular_f_string = format_with(|f| {
let split_target_regular_interpolated_string = format_with(|f| {
write!(
f,
[
@ -1005,7 +1022,7 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
space(),
operator,
space(),
format_f_string,
format_interpolated_string,
]
)
});
@ -1016,7 +1033,7 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
best_fitting![
split_target_flat_value,
split_target_value_parenthesized_flat,
split_target_regular_f_string,
split_target_regular_interpolated_string,
]
.with_mode(BestFittingMode::AllLines)
.fmt(f)
@ -1024,10 +1041,10 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
best_fitting![
single_line,
flat_target_parenthesize_value,
flat_target_regular_f_string,
flat_target_regular_interpolated_string,
split_target_flat_value,
split_target_value_parenthesized_flat,
split_target_regular_f_string,
split_target_regular_interpolated_string,
]
.with_mode(BestFittingMode::AllLines)
.fmt(f)
@ -1045,13 +1062,31 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
}
}
/// Formats an f-string that is at the value position of an assignment statement.
#[derive(Debug, Copy, Clone)]
enum InterpolatedString<'a> {
FString(&'a FString),
TString(&'a TString),
}
impl Format<PyFormatContext<'_>> for InterpolatedString<'_> {
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
match self {
InterpolatedString::FString(string) => string.format().fmt(f),
InterpolatedString::TString(string) => string.format().fmt(f),
}
}
}
/// Formats an f/t-string that is at the value position of an assignment statement.
///
/// This is just a wrapper around [`FormatFString`] while considering a special case when the
/// f-string is at an assignment statement's value position.
/// For legibility, we discuss only the case of f-strings below, but the
/// same comments apply to t-strings.
///
/// This is necessary to prevent an instability where an f-string contains a multiline expression
/// and the f-string fits on the line, but only when it's surrounded by parentheses.
/// This is just a wrapper around [`FormatFString`] while considering a special
/// case when the f-string is at an assignment statement's value position.
/// This is necessary to prevent an instability where an f-string contains a
/// multiline expression and the f-string fits on the line, but only when it's
/// surrounded by parentheses.
///
/// ```python
/// aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
@ -1099,30 +1134,40 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
/// The reason for this is because (a) f-string already has a multiline expression thus it tries to
/// break the expression and (b) the `BestFit` layout doesn't considers the layout where the
/// multiline f-string isn't surrounded by parentheses.
fn format_f_string_assignment<'a>(
fn format_interpolated_string_assignment<'a>(
string: StringLike<'a>,
context: &PyFormatContext,
) -> Option<&'a FString> {
let StringLike::FString(expr) = string else {
return None;
) -> Option<InterpolatedString<'a>> {
let (interpolated_string, elements) = match string {
StringLike::TString(expr) => {
let t_string = expr.as_single_part_tstring()?;
(InterpolatedString::TString(t_string), &t_string.elements)
}
StringLike::FString(expr) => {
let f_string = expr.as_single_part_fstring()?;
(InterpolatedString::FString(f_string), &f_string.elements)
}
_ => {
return None;
}
};
let f_string = expr.as_single_part_fstring()?;
// If the f-string is flat, there are no breakpoints from which it can be made multiline.
// This is the case when the f-string has no expressions or if it does then the expressions
// If the f/t-string is flat, there are no breakpoints from which it can be made multiline.
// This is the case when the f/t-string has no expressions or if it does then the expressions
// are flat (no newlines).
if FStringLayout::from_f_string(f_string, context.source()).is_flat() {
if InterpolatedStringLayout::from_interpolated_string_elements(elements, context.source())
.is_flat()
{
return None;
}
// This checks whether the f-string is multi-line and it can *never* be flattened. Thus,
// This checks whether the f/t-string is multi-line and it can *never* be flattened. Thus,
// it's useless to try the flattened layout.
if string.is_multiline(context) {
return None;
}
Some(f_string)
Some(interpolated_string)
}
#[derive(Debug, Default)]
@ -1277,6 +1322,9 @@ fn should_inline_comments(
Expr::FString(fstring) => {
fstring.needs_parentheses(parent, context) == OptionalParentheses::BestFit
}
Expr::TString(tstring) => {
tstring.needs_parentheses(parent, context) == OptionalParentheses::BestFit
}
_ => false,
}
}

View file

@ -2,28 +2,31 @@ use itertools::Itertools;
use ruff_formatter::{FormatContext, format_args, write};
use ruff_python_ast::str::{Quote, TripleQuotes};
use ruff_python_ast::str_prefix::{
AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix,
AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix, TStringPrefix,
};
use ruff_python_ast::{
AnyStringFlags, FString, InterpolatedStringElement, StringFlags, StringLike, StringLikePart,
TString,
};
use ruff_python_ast::{AnyStringFlags, FStringElement, StringFlags, StringLike, StringLikePart};
use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextRange};
use std::borrow::Cow;
use crate::comments::{leading_comments, trailing_comments};
use crate::expression::parentheses::in_parentheses_only_soft_line_break_or_space;
use crate::other::f_string::{FStringContext, FStringLayout};
use crate::other::f_string_element::FormatFStringExpressionElement;
use crate::other::interpolated_string::{InterpolatedStringContext, InterpolatedStringLayout};
use crate::other::interpolated_string_element::FormatInterpolatedElement;
use crate::prelude::*;
use crate::string::docstring::needs_chaperone_space;
use crate::string::normalize::{
QuoteMetadata, is_fstring_with_quoted_debug_expression,
is_fstring_with_quoted_format_spec_and_debug,
is_fstring_with_triple_quoted_literal_expression_containing_quotes,
is_interpolated_string_with_quoted_format_spec_and_debug,
};
use crate::string::{StringLikeExtensions, StringNormalizer, StringQuotes, normalize_string};
/// Formats any implicitly concatenated string. This could be any valid combination
/// of string, bytes or f-string literals.
/// of string, bytes, f-string, or t-string literals.
pub(crate) struct FormatImplicitConcatenatedString<'a> {
string: StringLike<'a>,
}
@ -98,6 +101,7 @@ impl Format<PyFormatContext<'_>> for FormatImplicitConcatenatedStringExpanded<'_
StringLikePart::String(part) => part.format().fmt(f),
StringLikePart::Bytes(bytes_literal) => bytes_literal.format().fmt(f),
StringLikePart::FString(part) => part.format().fmt(f),
StringLikePart::TString(part) => part.format().fmt(f),
});
let part_comments = comments.leading_dangling_trailing(part);
@ -138,7 +142,7 @@ impl<'a> FormatImplicitConcatenatedStringFlat<'a> {
let first_part = string.parts().next()?;
// The string is either a regular string, f-string, or bytes string.
// The string is either a regular string, f-string, t-string, or bytes string.
let normalizer = StringNormalizer::from_context(context);
// Some if a part requires preserving its quotes.
@ -164,9 +168,34 @@ impl<'a> FormatImplicitConcatenatedStringFlat<'a> {
return None;
}
if let StringLikePart::FString(fstring) = part {
if context.options().target_version().supports_pep_701() {
if is_fstring_with_quoted_format_spec_and_debug(fstring, context) {
match part {
StringLikePart::FString(fstring) => {
if matches!(string, StringLike::TString(_)) {
// Don't concatenate t-strings and f-strings
return None;
}
if context.options().target_version().supports_pep_701() {
if is_interpolated_string_with_quoted_format_spec_and_debug(
&fstring.elements,
fstring.flags.into(),
context,
) {
if preserve_quotes_requirement
.is_some_and(|quote| quote != part.flags().quote_style())
{
return None;
}
preserve_quotes_requirement = Some(part.flags().quote_style());
}
}
// 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 " """}'`
else 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())
{
@ -175,21 +204,21 @@ impl<'a> FormatImplicitConcatenatedStringFlat<'a> {
preserve_quotes_requirement = Some(part.flags().quote_style());
}
}
// 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 " """}'`
else 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;
StringLikePart::TString(tstring) => {
if is_interpolated_string_with_quoted_format_spec_and_debug(
&tstring.elements,
tstring.flags.into(),
context,
) {
if preserve_quotes_requirement
.is_some_and(|quote| quote != part.flags().quote_style())
{
return None;
}
preserve_quotes_requirement = Some(part.flags().quote_style());
}
preserve_quotes_requirement = Some(part.flags().quote_style());
}
StringLikePart::Bytes(_) | StringLikePart::String(_) => {}
}
}
@ -203,6 +232,7 @@ impl<'a> FormatImplicitConcatenatedStringFlat<'a> {
StringLike::String(_) => AnyStringPrefix::Regular(StringLiteralPrefix::Empty),
StringLike::Bytes(_) => AnyStringPrefix::Bytes(ByteStringPrefix::Regular),
StringLike::FString(_) => AnyStringPrefix::Format(FStringPrefix::Regular),
StringLike::TString(_) => AnyStringPrefix::Template(TStringPrefix::Regular),
};
let quote = if let Some(quote) = preserve_quotes_requirement {
@ -287,7 +317,7 @@ impl Format<PyFormatContext<'_>> for FormatImplicitConcatenatedStringFlat<'_> {
FormatLiteralContent {
range: part.content_range(),
flags: self.flags,
is_fstring: false,
is_interpolated_string: false,
trim_start: first_non_empty && self.docstring,
trim_end: self.docstring && parts.peek().is_none(),
}
@ -300,28 +330,32 @@ impl Format<PyFormatContext<'_>> for FormatImplicitConcatenatedStringFlat<'_> {
}
}
StringLikePart::FString(f_string) => {
for element in &f_string.elements {
StringLikePart::FString(FString { elements, .. })
| StringLikePart::TString(TString { elements, .. }) => {
for element in elements {
match element {
FStringElement::Literal(literal) => {
InterpolatedStringElement::Literal(literal) => {
FormatLiteralContent {
range: literal.range(),
flags: self.flags,
is_fstring: true,
is_interpolated_string: 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(
// because we assert that the f/t-string never contains any comments.
InterpolatedStringElement::Interpolation(expression) => {
let context = InterpolatedStringContext::new(
self.flags,
FStringLayout::from_f_string(f_string, f.context().source()),
InterpolatedStringLayout::from_interpolated_string_elements(
elements,
f.context().source(),
),
);
FormatFStringExpressionElement::new(expression, context).fmt(f)?;
FormatInterpolatedElement::new(expression, context).fmt(f)?;
}
}
}
@ -336,7 +370,7 @@ impl Format<PyFormatContext<'_>> for FormatImplicitConcatenatedStringFlat<'_> {
struct FormatLiteralContent {
range: TextRange,
flags: AnyStringFlags,
is_fstring: bool,
is_interpolated_string: bool,
trim_start: bool,
trim_end: bool,
}
@ -348,7 +382,7 @@ impl Format<PyFormatContext<'_>> for FormatLiteralContent {
content,
0,
self.flags,
self.flags.is_f_string() && !self.is_fstring,
self.flags.is_interpolated_string() && !self.is_interpolated_string,
);
// Trim the start and end of the string if it's the first or last part of a docstring.

View file

@ -85,57 +85,55 @@ pub(crate) trait StringLikeExtensions {
impl StringLikeExtensions for ast::StringLike<'_> {
fn is_multiline(&self, context: &PyFormatContext) -> bool {
// Helper for f-string and t-string parts
fn contains_line_break_or_comments(
elements: &ast::InterpolatedStringElements,
context: &PyFormatContext,
triple_quotes: TripleQuotes,
) -> bool {
elements.iter().any(|element| match element {
ast::InterpolatedStringElement::Literal(literal) => {
triple_quotes.is_yes() && context.source().contains_line_break(literal.range())
}
ast::InterpolatedStringElement::Interpolation(expression) => {
// Expressions containing comments can't be joined.
//
// Format specifiers needs to be checked as well. For example, the
// following should be considered multiline because the literal
// part of the format specifier contains a newline at the end
// (`.3f\n`):
//
// ```py
// x = f"hello {a + b + c + d:.3f
// } world"
// ```
context.comments().contains_comments(expression.into())
|| expression.format_spec.as_deref().is_some_and(|spec| {
contains_line_break_or_comments(&spec.elements, context, triple_quotes)
})
|| expression.debug_text.as_ref().is_some_and(|debug_text| {
memchr2(b'\n', b'\r', debug_text.leading.as_bytes()).is_some()
|| memchr2(b'\n', b'\r', debug_text.trailing.as_bytes()).is_some()
})
}
})
}
self.parts().any(|part| match part {
StringLikePart::String(_) | StringLikePart::Bytes(_) => {
part.flags().is_triple_quoted()
&& context.source().contains_line_break(part.range())
}
StringLikePart::FString(f_string) => {
fn contains_line_break_or_comments(
elements: &ast::FStringElements,
context: &PyFormatContext,
triple_quotes: TripleQuotes,
) -> bool {
elements.iter().any(|element| match element {
ast::FStringElement::Literal(literal) => {
triple_quotes.is_yes()
&& context.source().contains_line_break(literal.range())
}
ast::FStringElement::Expression(expression) => {
// Expressions containing comments can't be joined.
//
// Format specifiers needs to be checked as well. For example, the
// following should be considered multiline because the literal
// part of the format specifier contains a newline at the end
// (`.3f\n`):
//
// ```py
// x = f"hello {a + b + c + d:.3f
// } world"
// ```
context.comments().contains_comments(expression.into())
|| expression.format_spec.as_deref().is_some_and(|spec| {
contains_line_break_or_comments(
&spec.elements,
context,
triple_quotes,
)
})
|| expression.debug_text.as_ref().is_some_and(|debug_text| {
memchr2(b'\n', b'\r', debug_text.leading.as_bytes()).is_some()
|| memchr2(b'\n', b'\r', debug_text.trailing.as_bytes())
.is_some()
})
}
})
}
contains_line_break_or_comments(
&f_string.elements,
context,
f_string.flags.triple_quotes(),
)
}
StringLikePart::FString(f_string) => contains_line_break_or_comments(
&f_string.elements,
context,
f_string.flags.triple_quotes(),
),
StringLikePart::TString(t_string) => contains_line_break_or_comments(
&t_string.elements,
context,
t_string.flags.triple_quotes(),
),
})
}
}

View file

@ -5,16 +5,15 @@ use std::iter::FusedIterator;
use ruff_formatter::FormatContext;
use ruff_python_ast::visitor::source_order::SourceOrderVisitor;
use ruff_python_ast::{
AnyStringFlags, BytesLiteral, FString, FStringElement, FStringElements, FStringFlags,
AnyStringFlags, BytesLiteral, FString, InterpolatedStringElement, InterpolatedStringElements,
StringFlags, StringLikePart, StringLiteral,
str::{Quote, TripleQuotes},
};
use ruff_text_size::{Ranged, TextRange, TextSlice};
use crate::QuoteStyle;
use crate::context::FStringState;
use crate::context::InterpolatedStringState;
use crate::prelude::*;
use crate::string::StringQuotes;
use crate::string::{Quote, StringQuotes, TripleQuotes};
pub(crate) struct StringNormalizer<'a, 'src> {
preferred_quote_style: Option<QuoteStyle>,
@ -47,11 +46,11 @@ impl<'a, 'src> StringNormalizer<'a, 'src> {
.unwrap_or(self.context.options().quote_style());
let supports_pep_701 = self.context.options().target_version().supports_pep_701();
// For f-strings prefer alternating the quotes unless The outer string is triple quoted and the inner isn't.
if let FStringState::InsideExpressionElement(parent_context) = self.context.f_string_state()
// For f-strings and t-strings prefer alternating the quotes unless The outer string is triple quoted and the inner isn't.
if let InterpolatedStringState::InsideInterpolatedElement(parent_context) =
self.context.interpolated_string_state()
{
let parent_flags = parent_context.f_string().flags();
let parent_flags = parent_context.interpolated_string().flags();
if !parent_flags.is_triple_quoted() || string.flags().is_triple_quoted() {
// This logic is even necessary when using preserve and the target python version doesn't support PEP701 because
// we might end up joining two f-strings that have different quote styles, in which case we need to alternate the quotes
@ -67,33 +66,49 @@ impl<'a, 'src> StringNormalizer<'a, 'src> {
return QuoteStyle::Preserve;
}
// There are cases where it is necessary to preserve the quotes to prevent an invalid f-string.
if let StringLikePart::FString(fstring) = string {
// There are two cases where it's necessary to preserve the quotes if the
// target version is pre 3.12 and the part is an f-string.
if !supports_pep_701 {
// An f-string expression contains a debug text with a quote character
// because the formatter will emit the debug expression **exactly** the
// same as in the source text.
if is_fstring_with_quoted_debug_expression(fstring, self.context) {
return QuoteStyle::Preserve;
// There are cases where it is necessary to preserve the quotes to prevent an invalid f-string or t-string.
match string {
StringLikePart::FString(fstring) => {
// There are two cases where it's necessary to preserve the quotes if the
// target version is pre 3.12 and the part is an f-string.
if !supports_pep_701 {
// An f-string expression contains a debug text with a quote character
// because the formatter will emit the debug expression **exactly** the
// same as in the source text.
if is_fstring_with_quoted_debug_expression(fstring, self.context) {
return QuoteStyle::Preserve;
}
// An f-string expression that contains a triple quoted string literal
// expression that contains a quote.
if is_fstring_with_triple_quoted_literal_expression_containing_quotes(
fstring,
self.context,
) {
return QuoteStyle::Preserve;
}
}
// An f-string expression that contains a triple quoted string literal
// expression that contains a quote.
if is_fstring_with_triple_quoted_literal_expression_containing_quotes(
fstring,
// An f-string expression element contains a debug text and the corresponding
// format specifier has a literal element with a quote character.
if is_interpolated_string_with_quoted_format_spec_and_debug(
&fstring.elements,
fstring.flags.into(),
self.context,
) {
return QuoteStyle::Preserve;
}
}
// An f-string expression element contains a debug text and the corresponding
// format specifier has a literal element with a quote character.
if is_fstring_with_quoted_format_spec_and_debug(fstring, self.context) {
return QuoteStyle::Preserve;
StringLikePart::TString(tstring) => {
if is_interpolated_string_with_quoted_format_spec_and_debug(
&tstring.elements,
tstring.flags.into(),
self.context,
) {
return QuoteStyle::Preserve;
}
}
_ => {}
}
// Per PEP 8, always prefer double quotes for triple-quoted strings.
@ -172,7 +187,7 @@ impl<'a, 'src> StringNormalizer<'a, 'src> {
// The preferred quote style is single or double quotes, and the string contains a quote or
// another character that may require escaping
(Ok(preferred_quote), Some(first_quote_or_normalized_char_offset)) => {
let metadata = if string.is_fstring() {
let metadata = if string.is_interpolated_string() {
QuoteMetadata::from_part(string, self.context, preferred_quote)
} else {
QuoteMetadata::from_str(
@ -262,9 +277,19 @@ impl QuoteMetadata {
StringLikePart::FString(fstring) => {
let metadata = QuoteMetadata::from_str("", part.flags(), preferred_quote);
metadata.merge_fstring_elements(
metadata.merge_interpolated_string_elements(
&fstring.elements,
fstring.flags,
fstring.flags.into(),
context,
preferred_quote,
)
}
StringLikePart::TString(tstring) => {
let metadata = QuoteMetadata::from_str("", part.flags(), preferred_quote);
metadata.merge_interpolated_string_elements(
&tstring.elements,
tstring.flags.into(),
context,
preferred_quote,
)
@ -369,7 +394,7 @@ impl QuoteMetadata {
})
}
/// For f-strings, only consider the quotes inside string-literals but ignore
/// For f-strings and t-strings, only consider the quotes inside string-literals but ignore
/// quotes inside expressions (except inside the format spec). This allows both the outer and the nested literals
/// to make the optimal local-choice to reduce the total number of quotes necessary.
/// This doesn't require any pre 312 special handling because an expression
@ -377,10 +402,10 @@ impl QuoteMetadata {
/// ```python
/// f"{'escaping a quote like this \" is a syntax error pre 312'}"
/// ```
fn merge_fstring_elements(
fn merge_interpolated_string_elements(
self,
elements: &FStringElements,
flags: FStringFlags,
elements: &InterpolatedStringElements,
flags: AnyStringFlags,
context: &PyFormatContext,
preferred_quote: Quote,
) -> Self {
@ -388,19 +413,19 @@ impl QuoteMetadata {
for element in elements {
match element {
FStringElement::Literal(literal) => {
InterpolatedStringElement::Literal(literal) => {
merged = merged
.merge(&QuoteMetadata::from_str(
context.source().slice(literal),
flags.into(),
flags,
preferred_quote,
))
.expect("Merge to succeed because all parts have the same flags");
}
FStringElement::Expression(expression) => {
InterpolatedStringElement::Interpolation(expression) => {
if let Some(spec) = expression.format_spec.as_deref() {
if expression.debug_text.is_none() {
merged = merged.merge_fstring_elements(
merged = merged.merge_interpolated_string_elements(
&spec.elements,
flags,
context,
@ -879,7 +904,7 @@ pub(super) fn is_fstring_with_quoted_debug_expression(
fstring: &FString,
context: &PyFormatContext,
) -> bool {
fstring.elements.expressions().any(|expression| {
fstring.elements.interpolations().any(|expression| {
if expression.debug_text.is_some() {
let content = context.source().slice(expression);
contains_opposite_quote(content, fstring.flags.into())
@ -889,58 +914,6 @@ pub(super) fn is_fstring_with_quoted_debug_expression(
})
}
/// Returns `true` if `string` has any f-string expression element (direct or nested) with a debug expression and a format spec
/// that contains the opposite quote. It's important to preserve the quote style for those f-strings
/// because changing the quote style would result in invalid syntax.
///
/// ```python
/// f'{1=: "abcd \'\'}'
/// f'{x=:a{y:"abcd"}}'
/// f'{x=:a{y:{z:"abcd"}}}'
/// ```
pub(super) fn is_fstring_with_quoted_format_spec_and_debug(
fstring: &FString,
context: &PyFormatContext,
) -> bool {
fn has_format_spec_with_opposite_quote(
elements: &FStringElements,
flags: FStringFlags,
context: &PyFormatContext,
in_debug: bool,
) -> bool {
elements.iter().any(|element| match element {
FStringElement::Literal(literal) => {
let content = context.source().slice(literal);
in_debug && contains_opposite_quote(content, flags.into())
}
FStringElement::Expression(expression) => {
expression.format_spec.as_deref().is_some_and(|spec| {
has_format_spec_with_opposite_quote(
&spec.elements,
flags,
context,
in_debug || expression.debug_text.is_some(),
)
})
}
})
}
fstring.elements.expressions().any(|expression| {
if let Some(spec) = expression.format_spec.as_deref() {
return has_format_spec_with_opposite_quote(
&spec.elements,
fstring.flags,
context,
expression.debug_text.is_some(),
);
}
false
})
}
/// Tests if the `fstring` contains any triple quoted string, byte, or f-string literal that
/// contains a quote character opposite to its own quote character.
///
@ -980,6 +953,17 @@ pub(super) fn is_fstring_with_triple_quoted_literal_expression_containing_quotes
}
}
contains_quotes
}
StringLikePart::TString(tstring) => {
let mut contains_quotes = false;
for literal in tstring.elements.literals() {
if self.contains_quote(literal.range(), tstring.flags.into()) {
contains_quotes = true;
break;
}
}
contains_quotes
}
};
@ -1018,6 +1002,59 @@ pub(super) fn is_fstring_with_triple_quoted_literal_expression_containing_quotes
visitor.found
}
/// Returns `true` if `string` has any f/t-string interpolation element (direct or nested) with a debug expression and a format spec
/// that contains the opposite quote. It's important to preserve the quote style for those f/t-strings
/// because changing the quote style would result in invalid syntax.
///
/// ```python
/// t'{1=: "abcd \'\'}'
/// t'{x=:a{y:"abcd"}}'
/// t'{x=:a{y:{z:"abcd"}}}'
/// ```
pub(super) fn is_interpolated_string_with_quoted_format_spec_and_debug(
elements: &InterpolatedStringElements,
flags: AnyStringFlags,
context: &PyFormatContext,
) -> bool {
fn has_format_spec_with_opposite_quote(
elements: &InterpolatedStringElements,
flags: AnyStringFlags,
context: &PyFormatContext,
in_debug: bool,
) -> bool {
elements.iter().any(|element| match element {
InterpolatedStringElement::Literal(literal) => {
let content = context.source().slice(literal);
in_debug && contains_opposite_quote(content, flags)
}
InterpolatedStringElement::Interpolation(expression) => {
expression.format_spec.as_deref().is_some_and(|spec| {
has_format_spec_with_opposite_quote(
&spec.elements,
flags,
context,
in_debug || expression.debug_text.is_some(),
)
})
}
})
}
elements.interpolations().any(|expression| {
if let Some(spec) = expression.format_spec.as_deref() {
return has_format_spec_with_opposite_quote(
&spec.elements,
flags,
context,
expression.debug_text.is_some(),
);
}
false
})
}
fn contains_opposite_quote(content: &str, flags: AnyStringFlags) -> bool {
if flags.is_triple_quoted() {
match flags.quote_style() {

View file

@ -6,8 +6,8 @@ use {
use ruff_python_ast::visitor::transformer::Transformer;
use ruff_python_ast::{
self as ast, BytesLiteralFlags, Expr, FStringElement, FStringFlags, FStringLiteralElement,
FStringPart, Stmt, StringFlags,
self as ast, BytesLiteralFlags, Expr, FStringFlags, FStringPart, InterpolatedStringElement,
InterpolatedStringLiteralElement, Stmt, StringFlags,
};
use ruff_python_ast::{StringLiteralFlags, visitor::transformer};
use ruff_text_size::{Ranged, TextRange};
@ -117,7 +117,7 @@ impl Transformer for Normalizer {
if can_join {
#[derive(Default)]
struct Collector {
elements: Vec<FStringElement>,
elements: Vec<InterpolatedStringElement>,
}
impl Collector {
@ -127,7 +127,7 @@ impl Transformer for Normalizer {
// `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)) =
if let Some(InterpolatedStringElement::Literal(existing_literal)) =
self.elements.last_mut()
{
let value = std::mem::take(&mut existing_literal.value);
@ -137,8 +137,8 @@ impl Transformer for Normalizer {
existing_literal.range =
TextRange::new(existing_literal.start(), range.end());
} else {
self.elements.push(FStringElement::Literal(
FStringLiteralElement {
self.elements.push(InterpolatedStringElement::Literal(
InterpolatedStringLiteralElement {
range,
value: literal.into(),
},
@ -146,11 +146,9 @@ impl Transformer for Normalizer {
}
}
fn push_expression(
&mut self,
expression: ast::FStringExpressionElement,
) {
self.elements.push(FStringElement::Expression(expression));
fn push_expression(&mut self, expression: ast::InterpolatedElement) {
self.elements
.push(InterpolatedStringElement::Interpolation(expression));
}
}
@ -165,11 +163,13 @@ impl Transformer for Normalizer {
ast::FStringPart::FString(fstring) => {
for element in &fstring.elements {
match element {
ast::FStringElement::Literal(literal) => {
ast::InterpolatedStringElement::Literal(literal) => {
collector
.push_literal(&literal.value, literal.range);
}
ast::FStringElement::Expression(expression) => {
ast::InterpolatedStringElement::Interpolation(
expression,
) => {
collector.push_expression(expression.clone());
}
}

View file

@ -437,6 +437,19 @@ f"hellooooooooooooooooooooooo \
worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb)
aaaaaaaaaaa = f"hellooooooooooooooooooooooo \
worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb)
# This t-string should be flattened
xxxxxxxxxxxxxxxx = t"aaaaaaaaaaaaaaaaaaaaa {
expression } bbbbbbbbbbbbbbbbbbbbbbbb" + (
yyyyyyyyyyyyyy + zzzzzzzzzzz
)
# This is not a multiline t-string, but the expression is too long so it should be
# wrapped in parentheses.
t"hellooooooooooooooooooooooo \
worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb)
aaaaaaaaaaa = t"hellooooooooooooooooooooooo \
worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb)
```
## Output
@ -927,4 +940,22 @@ aaaaaaaaaaa = (
worlddddddddddddddddddddddddddddddddd"
+ (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb)
)
# This t-string should be flattened
xxxxxxxxxxxxxxxx = t"aaaaaaaaaaaaaaaaaaaaa {expression} bbbbbbbbbbbbbbbbbbbbbbbb" + (
yyyyyyyyyyyyyy + zzzzzzzzzzz
)
# This is not a multiline t-string, but the expression is too long so it should be
# wrapped in parentheses.
(
t"hellooooooooooooooooooooooo \
worlddddddddddddddddddddddddddddddddd"
+ (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb)
)
aaaaaaaaaaa = (
t"hellooooooooooooooooooooooo \
worlddddddddddddddddddddddddddddddddd"
+ (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb)
)
```

View file

@ -106,6 +106,55 @@ f"{10 + len('bar')=}" f'no debug{10}' f"{10 + len('bar')=}"
f"{10 + len('bar')=}" f'{10 + len("bar")=}'
##############################################################################
# T-strings
##############################################################################
# Escape `{` and `}` when merging a t-string with a string
"a {not_a_variable}" t"b {10}" "c"
# Join, and break expressions
t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{
expression
}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" t"cccccccccccccccccccc {20999}" "more"
# Join, but don't break the expressions
t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" t"cccccccccccccccccccc {20999}" "more"
t"test{
expression
}flat" t"can be {
joined
} together"
aaaaaaaaaaa = t"test{
expression
}flat" t"cean beeeeeeee {
joined
} eeeeeeeeeeeeeeeeeeeeeeeeeeeee" # inline
t"single quoted '{x}'" t'double quoted "{x}"' # Same number of quotes => use preferred quote style
t"single quote ' {x}" t'double quoted "{x}"' # More double quotes => use single quotes
t"single quoted '{x}'" t'double quote " {x}"' # More single quotes => use double quotes
# Different triple quoted strings
t"{'''test'''}" t'{"""other"""}'
# Now with inner quotes
t"{'''test ' '''}" t'{"""other " """}'
t"{some_where_nested('''test ' ''')}" t'{"""other " """ + "more"}'
t"{b'''test ' '''}" t'{b"""other " """}'
t"{t'''test ' '''}" t'{t"""other " """}'
# debug expressions containing quotes
t"{10 + len('bar')=}" t"{10 + len('bar')=}"
t"{10 + len('bar')=}" t'no debug{10}' t"{10 + len('bar')=}"
# We can't safely merge this pre Python 3.12 without altering the debug expression.
t"{10 + len('bar')=}" t'{10 + len("bar")=}'
##############################################################################
# Don't join raw strings
##############################################################################
@ -116,6 +165,9 @@ R"a" "normal"
f"test" fr"test"
f"test" fR"test"
t"test" tr"test"
t"test" tR"test"
##############################################################################
# Don't join triple quoted strings
@ -125,9 +177,22 @@ f"test" fR"test"
"single" f""""single"""
"single" t""""single"""
b"single" b"""triple"""
##############################################################################
# Don't join t-strings and f-strings
##############################################################################
t"{interp}" f"{expr}"
f"{expr}" t"{interp}"
f"{expr}" "string" t"{interp}"
##############################################################################
# Join strings in with statements
##############################################################################
@ -452,6 +517,50 @@ f"{10 + len('bar')=}no debug{10}{10 + len('bar')=}"
f"{10 + len('bar')=}" f'{10 + len("bar")=}'
##############################################################################
# T-strings
##############################################################################
# Escape `{` and `}` when merging a t-string with a string
t"a {{not_a_variable}}b {10}c"
# Join, and break expressions
t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{
expression
}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccc {20999}more"
# Join, but don't break the expressions
t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccc {20999}more"
t"test{expression}flatcan be {joined} together"
aaaaaaaaaaa = (
t"test{expression}flat"
t"cean beeeeeeee {joined} eeeeeeeeeeeeeeeeeeeeeeeeeeeee"
) # inline
t"single quoted '{x}'double quoted \"{x}\"" # Same number of quotes => use preferred quote style
t'single quote \' {x}double quoted "{x}"' # More double quotes => use single quotes
t"single quoted '{x}'double quote \" {x}\"" # More single quotes => use double quotes
# Different triple quoted strings
t"{'''test'''}{'''other'''}"
# Now with inner quotes
t"{'''test ' '''}{'''other " '''}"
t"{some_where_nested('''test ' ''')}{'''other " ''' + 'more'}"
t"{b'''test ' '''}{b'''other " '''}"
t"{t'''test ' '''}{t'''other " '''}"
# debug expressions containing quotes
t"{10 + len('bar')=}{10 + len('bar')=}"
t"{10 + len('bar')=}no debug{10}{10 + len('bar')=}"
# We can't safely merge this pre Python 3.12 without altering the debug expression.
t"{10 + len('bar')=}{10 + len("bar")=}"
##############################################################################
# Don't join raw strings
##############################################################################
@ -462,6 +571,9 @@ R"a" "normal"
f"test" rf"test"
f"test" Rf"test"
t"test" rt"test"
t"test" Rt"test"
##############################################################################
# Don't join triple quoted strings
@ -471,9 +583,22 @@ f"test" Rf"test"
"single" f""""single"""
"single" t""""single"""
b"single" b"""triple"""
##############################################################################
# Don't join t-strings and f-strings
##############################################################################
t"{interp}" f"{expr}"
f"{expr}" t"{interp}"
f"{expr}" "string" t"{interp}"
##############################################################################
# Join strings in with statements
##############################################################################
@ -780,7 +905,7 @@ f"aaaaaaaaaaaaaaaa \
```diff
--- Stable
+++ Preview
@@ -242,9 +242,12 @@
@@ -302,9 +302,12 @@
##############################################################################
# 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.

View file

@ -299,6 +299,155 @@ aaaaa[aaaaaaaaaaa] = (
)
#############################################################
# T-Strings
#############################################################
# Flatten and join the t-string
aaaaaaaaaaa = t"test{
expression}flat" t"cean beeeeeeee {joined} eeeeeeeeeeeeeeeee" # inline
# Parenthesize the value and join it, inline the comment
aaaaaaaaaaa = t"test{
expression}flat" t"cean beeeeeeee {joined} eeeeeeeeeeeeeeeeeeeeeeeeeee" # inline
# Parenthesize the t-string and keep it multiline because it doesn't fit on a single line including the comment
aaaaaaaaaaa = t"test{
expression
}flat" t"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,
] = t"ccccc{
expression}ccccccccccc" t"cccccccccccccccccccccccccccccccccccccccccc" # comment
# Same but starting with a joined string. They should both result in the same formatting.
[
aaaaaaa,
b,
] = t"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,
] = t"ccccc{
expression}cccccccccccccccccccc" t"cccccccccccccccccccccccccccccccccccccccccc" # comment
# The target should be flat
# The string should be joined because it fits into the line length
a[
aaaaaaa,
b
] = (
t"ccccc{
expression}ccccccccccc" "cccccccccccccccccccccccc" # comment
)
# Same but starting with a joined string. They should both result in the same formatting.
a[
aaaaaaa,
b
] = t"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
] = t"ccccc{
expression}ccccccccccc" "ccccccccccccccccccccccccccccccccccccccccccc" # comment
# Split an overlong target, but join the string if it fits
a[
aaaaaaa,
b
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
t"ccccc{
expression}ccccccccccc" "cccccccccccccccccccccccccccccc" # comment
)
# Split both if necessary and keep multiline
a[
aaaaaaa,
b
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
t"ccccc{
expression}cccccccccccccccccccccccccccccccc" "ccccccccccccccccccccccccccccccc" # comment
)
# Don't inline t-strings that contain expressions that are guaranteed to split, e.b. because of a magic trailing comma
aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
)
aaaaa[aaaaaaaaaaa] = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
aaaaa[aaaaaaaaaaa] = (t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[a,]
}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment
)
# Don't inline t-strings that contain commented expressions
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{[
a # comment
]}" "moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{[
a # comment
]}" "moreeeeeeeeeeeeeeeeeetest" # comment
)
# Don't inline t-strings with multiline debug expressions:
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
a=}" "moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a +
b=}" "moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a
=}" "moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
a=}" "moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a
=}" "moreeeeeeeeeeeeeeeeeetest" # comment
)
# Trailing last-part comments
a = (
@ -380,7 +529,8 @@ self._attr_unique_id = (
return (
f"Exception in {call_back_name} when handling msg on "
f"'{msg.topic}': '{msg.payload}'" # type: ignore[str-bytes-safe]
)```
)
```
## Output
```python
@ -704,6 +854,172 @@ aaaaa[aaaaaaaaaaa] = (
)
#############################################################
# T-Strings
#############################################################
# Flatten and join the t-string
aaaaaaaaaaa = t"test{expression}flatcean beeeeeeee {joined} eeeeeeeeeeeeeeeee" # inline
# Parenthesize the value and join it, inline the comment
aaaaaaaaaaa = (
t"test{expression}flatcean beeeeeeee {joined} eeeeeeeeeeeeeeeeeeeeeeeeeee" # inline
)
# Parenthesize the t-string and keep it multiline because it doesn't fit on a single line including the comment
aaaaaaaaaaa = (
t"test{expression}flat"
t"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,
] = t"ccccc{expression}ccccccccccccccccccccccccccccccccccccccccccccccccccccc" # comment
# Same but starting with a joined string. They should both result in the same formatting.
[
aaaaaaa,
b,
] = t"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,
] = (
t"ccccc{expression}cccccccccccccccccccc"
t"cccccccccccccccccccccccccccccccccccccccccc"
) # comment
# The target should be flat
# The string should be joined because it fits into the line length
a[aaaaaaa, b] = t"ccccc{expression}ccccccccccccccccccccccccccccccccccc" # comment
# Same but starting with a joined string. They should both result in the same formatting.
a[aaaaaaa, b] = t"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] = (
t"ccccc{expression}ccccccccccc"
"ccccccccccccccccccccccccccccccccccccccccccc"
) # comment
# Split an overlong target, but join the string if it fits
a[
aaaaaaa, b
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
t"ccccc{expression}ccccccccccccccccccccccccccccccccccccccccc" # comment
)
# Split both if necessary and keep multiline
a[
aaaaaaa, b
].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
t"ccccc{expression}cccccccccccccccccccccccccccccccc"
"ccccccccccccccccccccccccccccccc"
) # comment
# Don't inline t-strings that contain expressions that are guaranteed to split, e.b. because of a magic trailing comma
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[
a,
]
}"
"moreeeeeeeeeeeeeeeeeeee"
"test"
) # comment
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[
a,
]
}"
"moreeeeeeeeeeeeeeeeeeee"
"test" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[
a,
]
}"
"moreeeeeeeeeeeeeeeeeeee"
"test"
) # comment
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{
[
a,
]
}"
"moreeeeeeeeeeeeeeeeeeee"
"test" # comment
)
# Don't inline t-strings that contain commented expressions
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
[
a # comment
]
}"
"moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
[
a # comment
]
}"
"moreeeeeeeeeeeeeeeeeetest" # comment
)
# Don't inline t-strings with multiline debug expressions:
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
a=}"
"moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a +
b=}"
"moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaaaaaaaaaaaaaaa = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a
=}"
"moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
a=}"
"moreeeeeeeeeeeeeeeeeetest" # comment
)
aaaaa[aaaaaaaaaaa] = (
t"testeeeeeeeeeeeeeeeeeeeeeeeee{a
=}"
"moreeeeeeeeeeeeeeeeeetest" # comment
)
# Trailing last-part comments
a = (

File diff suppressed because it is too large Load diff