Skip to content

Commit f705197

Browse files
t-string support (#4805)
* [WIP] t-string support * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix failing test case on 3.14 * oh yeah we do string normalization now * Add changelog entry * fix mypy * use py314 branch of pytokens in pre-commit * Add more test cases * use published pytokens * Add `T_STRINGS` feature --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 994e184 commit f705197

File tree

16 files changed

+154
-22
lines changed

16 files changed

+154
-22
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ repos:
5353
# v8.2 has breaking changes. We work around them at runtime, but we need the newer stubs.
5454
- packaging >= 22.0
5555
- platformdirs >= 2.1.0
56-
- pytokens >= 0.1.10
56+
- pytokens >= 0.3.0
5757
- pytest
5858
- hypothesis
5959
- aiohttp >= 3.7.4

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<!-- Include any especially major or disruptive changes here -->
1111

1212
- Enable base 3.14 support (#4804)
13+
- Add support for the new Python 3.14 t-string syntax introduced by PEP 750 (#4805)
1314

1415
### Stable style
1516

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ dependencies = [
7070
"packaging>=22.0",
7171
"pathspec>=0.9.0",
7272
"platformdirs>=2",
73-
"pytokens>=0.1.10",
73+
"pytokens>=0.3.0",
7474
"tomli>=1.1.0; python_version < '3.11'",
7575
"typing_extensions>=4.0.1; python_version < '3.11'",
7676
]

src/black/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1248,6 +1248,7 @@ def _format_str_once(
12481248
for feature in {
12491249
Feature.PARENTHESIZED_CONTEXT_MANAGERS,
12501250
Feature.UNPARENTHESIZED_EXCEPT_TYPES,
1251+
Feature.T_STRINGS,
12511252
}
12521253
if supports_feature(versions, feature)
12531254
}
@@ -1364,6 +1365,8 @@ def get_features_used( # noqa: C901
13641365
for n in node.pre_order():
13651366
if n.type == token.FSTRING_START:
13661367
features.add(Feature.F_STRINGS)
1368+
elif n.type == token.TSTRING_START:
1369+
features.add(Feature.T_STRINGS)
13671370
elif (
13681371
n.type == token.RBRACE
13691372
and n.parent is not None

src/black/linegen.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
WHITESPACE,
3939
Visitor,
4040
ensure_visible,
41-
fstring_to_string,
41+
fstring_tstring_to_string,
4242
get_annotation_type,
4343
has_sibling_with_type,
4444
is_arith_like,
@@ -560,7 +560,22 @@ def visit_atom(self, node: Node) -> Iterator[Line]:
560560

561561
def visit_fstring(self, node: Node) -> Iterator[Line]:
562562
# currently we don't want to format and split f-strings at all.
563-
string_leaf = fstring_to_string(node)
563+
string_leaf = fstring_tstring_to_string(node)
564+
node.replace(string_leaf)
565+
if "\\" in string_leaf.value and any(
566+
"\\" in str(child)
567+
for child in node.children
568+
if child.type == syms.fstring_replacement_field
569+
):
570+
# string normalization doesn't account for nested quotes,
571+
# causing breakages. skip normalization when nested quotes exist
572+
yield from self.visit_default(string_leaf)
573+
return
574+
yield from self.visit_STRING(string_leaf)
575+
576+
def visit_tstring(self, node: Node) -> Iterator[Line]:
577+
# currently we don't want to format and split t-strings at all.
578+
string_leaf = fstring_tstring_to_string(node)
564579
node.replace(string_leaf)
565580
if "\\" in string_leaf.value and any(
566581
"\\" in str(child)

src/black/lines.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ def append(
6464
"""
6565
has_value = (
6666
leaf.type in BRACKETS
67-
# empty fstring-middles must not be truncated
68-
or leaf.type == token.FSTRING_MIDDLE
67+
# empty fstring and tstring middles must not be truncated
68+
or leaf.type in (token.FSTRING_MIDDLE, token.TSTRING_MIDDLE)
6969
or bool(leaf.value.strip())
7070
)
7171
if not has_value:

src/black/mode.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,10 @@ class Feature(Enum):
5252
DEBUG_F_STRINGS = 16
5353
PARENTHESIZED_CONTEXT_MANAGERS = 17
5454
TYPE_PARAMS = 18
55-
FSTRING_PARSING = 19
55+
# FSTRING_PARSING = 19 # unused
5656
TYPE_PARAM_DEFAULTS = 20
5757
UNPARENTHESIZED_EXCEPT_TYPES = 21
58+
T_STRINGS = 22
5859
FORCE_OPTIONAL_PARENTHESES = 50
5960

6061
# __future__ flags
@@ -165,7 +166,6 @@ class Feature(Enum):
165166
Feature.EXCEPT_STAR,
166167
Feature.VARIADIC_GENERICS,
167168
Feature.TYPE_PARAMS,
168-
Feature.FSTRING_PARSING,
169169
},
170170
TargetVersion.PY313: {
171171
Feature.F_STRINGS,
@@ -185,7 +185,6 @@ class Feature(Enum):
185185
Feature.EXCEPT_STAR,
186186
Feature.VARIADIC_GENERICS,
187187
Feature.TYPE_PARAMS,
188-
Feature.FSTRING_PARSING,
189188
Feature.TYPE_PARAM_DEFAULTS,
190189
},
191190
TargetVersion.PY314: {
@@ -206,9 +205,9 @@ class Feature(Enum):
206205
Feature.EXCEPT_STAR,
207206
Feature.VARIADIC_GENERICS,
208207
Feature.TYPE_PARAMS,
209-
Feature.FSTRING_PARSING,
210208
Feature.TYPE_PARAM_DEFAULTS,
211209
Feature.UNPARENTHESIZED_EXCEPT_TYPES,
210+
Feature.T_STRINGS,
212211
},
213212
}
214213

src/black/nodes.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@
140140
STANDALONE_COMMENT,
141141
token.FSTRING_MIDDLE,
142142
token.FSTRING_END,
143+
token.TSTRING_MIDDLE,
144+
token.TSTRING_END,
143145
token.BANG,
144146
}
145147

@@ -207,7 +209,10 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool, mode: Mode) -> str: # no
207209
}:
208210
return NO
209211

210-
if t == token.LBRACE and p.type == syms.fstring_replacement_field:
212+
if t == token.LBRACE and p.type in (
213+
syms.fstring_replacement_field,
214+
syms.tstring_replacement_field,
215+
):
211216
return NO
212217

213218
prev = leaf.prev_sibling
@@ -395,7 +400,6 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool, mode: Mode) -> str: # no
395400
elif prevp.type == token.EQUAL and prevp_parent.type == syms.argument:
396401
return NO
397402

398-
# TODO: add fstring here?
399403
elif t in {token.NAME, token.NUMBER, token.STRING}:
400404
return NO
401405

@@ -789,8 +793,8 @@ def is_fstring(node: Node) -> bool:
789793
return node.type == syms.fstring
790794

791795

792-
def fstring_to_string(node: Node) -> Leaf:
793-
"""Converts an fstring node back to a string node."""
796+
def fstring_tstring_to_string(node: Node) -> Leaf:
797+
"""Converts an fstring or tstring node back to a string node."""
794798
string_without_prefix = str(node)[len(node.prefix) :]
795799
string_leaf = Leaf(token.STRING, string_without_prefix, prefix=node.prefix)
796800
string_leaf.lineno = node.get_lineno() or 0
@@ -800,7 +804,7 @@ def fstring_to_string(node: Node) -> Leaf:
800804
def is_multiline_string(node: LN) -> bool:
801805
"""Return True if `leaf` is a multiline string that actually spans many lines."""
802806
if isinstance(node, Node) and is_fstring(node):
803-
leaf = fstring_to_string(node)
807+
leaf = fstring_tstring_to_string(node)
804808
elif isinstance(node, Leaf):
805809
leaf = node
806810
else:

src/black/strings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from black._width_table import WIDTH_TABLE
1212
from blib2to3.pytree import Leaf
1313

14-
STRING_PREFIX_CHARS: Final = "furbFURB" # All possible string prefix characters.
14+
STRING_PREFIX_CHARS: Final = "fturbFTURB" # All possible string prefix characters.
1515
STRING_PREFIX_RE: Final = re.compile(
1616
r"^([" + STRING_PREFIX_CHARS + r"]*)(.*)$", re.DOTALL
1717
)

src/blib2to3/Grammar.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ atom: ('(' [yield_expr|testlist_gexp] ')' |
163163
'[' [listmaker] ']' |
164164
'{' [dictsetmaker] '}' |
165165
'`' testlist1 '`' |
166-
NAME | NUMBER | (STRING | fstring)+ | '.' '.' '.')
166+
NAME | NUMBER | (STRING | fstring | tstring)+ | '.' '.' '.')
167167
listmaker: (namedexpr_test|star_expr) ( old_comp_for | (',' (namedexpr_test|star_expr))* [','] )
168168
testlist_gexp: (namedexpr_test|star_expr) ( old_comp_for | (',' (namedexpr_test|star_expr))* [','] )
169169
lambdef: 'lambda' [varargslist] ':' test
@@ -259,3 +259,8 @@ fstring: FSTRING_START fstring_middle* FSTRING_END
259259
fstring_middle: fstring_replacement_field | FSTRING_MIDDLE
260260
fstring_replacement_field: '{' (yield_expr | testlist_star_expr) ['='] [ "!" NAME ] [ ':' fstring_format_spec* ] '}'
261261
fstring_format_spec: FSTRING_MIDDLE | fstring_replacement_field
262+
263+
tstring: TSTRING_START tstring_middle* TSTRING_END
264+
tstring_middle: tstring_replacement_field | TSTRING_MIDDLE
265+
tstring_replacement_field: '{' (yield_expr | testlist_star_expr) ['='] [ "!" NAME ] [ ':' tstring_format_spec* ] '}'
266+
tstring_format_spec: TSTRING_MIDDLE | tstring_replacement_field

0 commit comments

Comments
 (0)