Skip to content

Commit 9d3e7ce

Browse files
committed
gh-153569: finish nested formatted-string expressions
1 parent d87b26a commit 9d3e7ce

6 files changed

Lines changed: 31 additions & 32 deletions

File tree

Lib/test/test_fstring.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1657,6 +1657,7 @@ def __repr__(self):
16571657
self.assertEqual(f'{C()=:x}', 'C()=FORMAT-x')
16581658
self.assertEqual(f'{C()=!r:*^20}', 'C()=********REPR********')
16591659
self.assertEqual(f"{C():{20=}}", 'FORMAT-20=20')
1660+
self.assertEqual(f"{C():{C():{4=}}}", 'FORMAT-FORMAT-4=4')
16601661

16611662
self.assertRaises(SyntaxError, eval, "f'{C=]'")
16621663

Lib/test/test_tstring.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,15 @@ def test_debug_specifier(self):
140140
)
141141
self.assertEqual(fstring(t), "Value: value = 42")
142142

143+
class C:
144+
def __format__(self, spec):
145+
return f"FORMAT-{spec}"
146+
147+
x = y = C()
148+
t = t"{x:{y:{value=}}}"
149+
self.assertEqual(t.interpolations[0].format_spec,
150+
"FORMAT-value=42")
151+
143152
def test_raw_tstrings(self):
144153
path = r"C:\Users"
145154
t = rt"{path}\Documents"

Parser/action_helpers.c

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1390,10 +1390,8 @@ _get_resized_exprs(Parser *p, Token *a, asdl_expr_seq *raw_expressions,
13901390
for (Py_ssize_t i = 0; i < n_items; i++) {
13911391
expr_ty item = asdl_seq_GET(raw_expressions, i);
13921392

1393-
// This should correspond to a JoinedStr node of two elements
1394-
// created _PyPegen_formatted_value. This situation can only be the result of
1395-
// a (f|t)-string debug expression where the first element is a constant with the text and the second
1396-
// a formatted value with the expression.
1393+
/* Debug expressions arrive as JoinedStr(text, value); flatten them
1394+
into the surrounding string. */
13971395
if (item->kind == JoinedStr_kind) {
13981396
asdl_expr_seq *values = item->v.JoinedStr.values;
13991397
if (asdl_seq_LEN(values) != 2) {

Parser/lexer/lexer.c

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -550,26 +550,21 @@ _PyLexer_get_normal(struct tok_state *tok, ftstring_state *current, struct token
550550
int is_punctuation = (c == ':' || c == '}' || c == '!');
551551
if (is_punctuation && current != NULL) {
552552
int bracket_depth = _PyLexer_FTStringBracketDepth(tok, current);
553-
int cursor = bracket_depth - 1;
554-
int cursor_in_format_with_debug =
555-
cursor == 1 && current->debug_expr;
556-
int cursor_valid = cursor == 0 || cursor_in_format_with_debug;
557-
if (cursor_valid && c == '!') {
553+
int at_expression_boundary =
554+
bracket_depth == current->replacement_depth;
555+
if (at_expression_boundary && c == '!') {
558556
int c2 = tok_nextc(tok);
559557
if (c2 == '=') {
560-
cursor_valid = 0;
558+
at_expression_boundary = 0;
561559
}
562560
tok_backup(tok, c2);
563561
}
564-
if (cursor_valid) {
565-
_PyLexer_finish_ftstring_expr(tok, current);
566-
}
567-
if (cursor_valid &&
568-
_PyLexer_set_ftstring_expr_metadata(tok, current, token)) {
562+
if (at_expression_boundary &&
563+
_PyLexer_finish_ftstring_expr(tok, current, token)) {
569564
return MAKE_TOKEN(ERRORTOKEN);
570565
}
571566

572-
if (c == ':' && bracket_depth == current->replacement_depth) {
567+
if (c == ':' && at_expression_boundary) {
573568
current->mode = FTSTRING_MODE_FORMAT_SPEC;
574569
p_start = tok->start;
575570
p_end = tok->cur;

Parser/lexer/lexer_internal.h

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,10 @@ tok_failed(const struct tok_state *tok)
4141

4242
int _PyLexer_nextc(struct tok_state *);
4343
void _PyLexer_backup(struct tok_state *, int);
44-
void _PyLexer_finish_ftstring_expr(struct tok_state *, ftstring_state *);
4544
int _PyLexer_record_ftstring_comment(
4645
struct tok_state *, ftstring_state *, const char *, const char *);
47-
int _PyLexer_set_ftstring_expr_metadata(
48-
struct tok_state *, const ftstring_state *, struct token *);
46+
int _PyLexer_finish_ftstring_expr(
47+
struct tok_state *, ftstring_state *, struct token *);
4948
int _PyLexer_scan_prefixed_string(
5049
struct tok_state *, struct token *, int, unsigned int);
5150
int _PyLexer_scan_number(struct tok_state *, struct token *, int, int);

Parser/lexer/string.c

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,19 @@ _PyLexer_record_ftstring_comment(struct tok_state *tok, ftstring_state *state,
5454
}
5555

5656
int
57-
_PyLexer_set_ftstring_expr_metadata(struct tok_state *tok,
58-
const ftstring_state *state,
59-
struct token *token)
57+
_PyLexer_finish_ftstring_expr(struct tok_state *tok, ftstring_state *state,
58+
struct token *token)
6059
{
6160
assert(token != NULL);
6261

63-
if (!(state->debug_expr || _PyLexer_IsTString(state->kind)) ||
64-
token->metadata) {
62+
if (state->expr_span.end >= 0) {
63+
return 0;
64+
}
65+
assert(state->expr_span.start >= 0);
66+
state->expr_span.end = _PyLexer_BufferOffset(tok, tok->start);
67+
int tstring_interpolation = _PyLexer_IsTString(state->kind) &&
68+
state->replacement_depth == 1;
69+
if (!(state->debug_expr || tstring_interpolation) || token->metadata) {
6570
return 0;
6671
}
6772
Py_ssize_t expr_len;
@@ -120,14 +125,6 @@ _PyLexer_set_ftstring_expr_metadata(struct tok_state *tok,
120125
return 0;
121126
}
122127

123-
void
124-
_PyLexer_finish_ftstring_expr(struct tok_state *tok, ftstring_state *state)
125-
{
126-
if (state->expr_span.end < 0) {
127-
state->expr_span.end = _PyLexer_BufferOffset(tok, tok->start);
128-
}
129-
}
130-
131128
static int
132129
check_string_prefixes(struct tok_state *tok, unsigned int prefixes)
133130
{

0 commit comments

Comments
 (0)