Skip to content

Commit 9e645ae

Browse files
committed
gh-153569: keep active formatted-string frames
1 parent 4c8ba41 commit 9e645ae

12 files changed

Lines changed: 360 additions & 353 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_tokenize.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2592,6 +2592,21 @@ def test_incomplete_formatted_string_comment_after_carriage_return(self):
25922592
("unexpected EOF in multi-line statement", (1, 7)),
25932593
)
25942594

2595+
def test_formatted_string_nesting_limit(self):
2596+
def nested_string(depth, prefix):
2597+
source = "'x'"
2598+
for _ in range(depth):
2599+
source = f'{prefix}"{{{source}}}"'
2600+
return source
2601+
2602+
for prefix in ("f", "t"):
2603+
with self.subTest(prefix=prefix):
2604+
self._get_tokens(nested_string(149, prefix))
2605+
with self.assertRaisesRegex(
2606+
tokenize.TokenError,
2607+
"too many nested f-strings or t-strings"):
2608+
self._get_tokens(nested_string(150, prefix))
2609+
25952610
def test_escaped_fstring_brace_has_a_position_gap(self):
25962611
tokens = self._get_tokens('f"a{{"', extra_tokens=True)
25972612
self.assertEqual(

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: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,14 +1001,21 @@ result_token_with_metadata(Parser *p, void *result, PyObject *metadata)
10011001
return res;
10021002
}
10031003

1004+
static char
1005+
formatted_string_prefix(const Parser *p)
1006+
{
1007+
const ftstring_state *state = _PyLexer_CurrentFTString(p->tok);
1008+
return state == NULL ? 'f' : _PyLexer_StringPrefix(state->kind);
1009+
}
1010+
10041011
ResultTokenWithMetadata *
10051012
_PyPegen_check_fstring_conversion(Parser *p, Token* conv_token, expr_ty conv)
10061013
{
10071014
if (conv_token->lineno != conv->lineno || conv_token->end_col_offset != conv->col_offset) {
10081015
return RAISE_SYNTAX_ERROR_KNOWN_RANGE(
10091016
conv_token, conv,
10101017
"%c-string: conversion type must come right after the exclamation mark",
1011-
TOK_GET_STRING_PREFIX(p->tok)
1018+
formatted_string_prefix(p)
10121019
);
10131020
}
10141021

@@ -1017,7 +1024,7 @@ _PyPegen_check_fstring_conversion(Parser *p, Token* conv_token, expr_ty conv)
10171024
!(first == 's' || first == 'r' || first == 'a')) {
10181025
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(conv,
10191026
"%c-string: invalid conversion character %R: expected 's', 'r', or 'a'",
1020-
TOK_GET_STRING_PREFIX(p->tok),
1027+
formatted_string_prefix(p),
10211028
conv->v.Name.id);
10221029
return NULL;
10231030
}
@@ -1344,7 +1351,8 @@ _PyPegen_decode_fstring_part(Parser* p, int is_raw, expr_ty constant, Token* tok
13441351
}
13451352

13461353
static asdl_expr_seq *
1347-
_get_resized_exprs(Parser *p, Token *a, asdl_expr_seq *raw_expressions, Token *b, enum string_kind_t string_kind)
1354+
_get_resized_exprs(Parser *p, Token *a, asdl_expr_seq *raw_expressions,
1355+
Token *b, ftstring_kind string_kind)
13481356
{
13491357
Py_ssize_t n_items = asdl_seq_LEN(raw_expressions);
13501358
Py_ssize_t total_items = n_items;
@@ -1370,15 +1378,13 @@ _get_resized_exprs(Parser *p, Token *a, asdl_expr_seq *raw_expressions, Token *b
13701378
for (Py_ssize_t i = 0; i < n_items; i++) {
13711379
expr_ty item = asdl_seq_GET(raw_expressions, i);
13721380

1373-
// This should correspond to a JoinedStr node of two elements
1374-
// created _PyPegen_formatted_value. This situation can only be the result of
1375-
// a (f|t)-string debug expression where the first element is a constant with the text and the second
1376-
// a formatted value with the expression.
1381+
/* Debug expressions arrive as JoinedStr(text, value); flatten them
1382+
into the surrounding string. */
13771383
if (item->kind == JoinedStr_kind) {
13781384
asdl_expr_seq *values = item->v.JoinedStr.values;
13791385
if (asdl_seq_LEN(values) != 2) {
13801386
PyErr_Format(PyExc_SystemError,
1381-
string_kind == TSTRING
1387+
_PyLexer_IsTString(string_kind)
13821388
? "unexpected TemplateStr node without debug data in t-string at line %d"
13831389
: "unexpected JoinedStr node without debug data in f-string at line %d",
13841390
item->lineno);
@@ -1390,7 +1396,9 @@ _get_resized_exprs(Parser *p, Token *a, asdl_expr_seq *raw_expressions, Token *b
13901396
asdl_seq_SET(seq, index++, first);
13911397

13921398
expr_ty second = asdl_seq_GET(values, 1);
1393-
assert((string_kind == TSTRING && second->kind == Interpolation_kind) || second->kind == FormattedValue_kind);
1399+
assert((_PyLexer_IsTString(string_kind) &&
1400+
second->kind == Interpolation_kind) ||
1401+
second->kind == FormattedValue_kind);
13941402
asdl_seq_SET(seq, index++, second);
13951403

13961404
continue;
@@ -1460,12 +1468,8 @@ expr_ty _PyPegen_decoded_constant_from_token(Parser* p, Token* tok) {
14601468
return NULL;
14611469
}
14621470

1463-
// Check if we're inside a raw f-string for format spec decoding
1464-
int is_raw = 0;
1465-
if (INSIDE_FSTRING(p->tok)) {
1466-
tokenizer_mode *mode = TOK_GET_MODE(p->tok);
1467-
is_raw = mode->raw;
1468-
}
1471+
const ftstring_state *state = _PyLexer_CurrentFTString(p->tok);
1472+
int is_raw = state != NULL && _PyLexer_IsRawString(state->kind);
14691473

14701474
PyObject* str = _PyPegen_decode_string(p, is_raw, bstr, bsize, tok);
14711475
if (str == NULL) {

Parser/lexer/lexer.c

Lines changed: 50 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,11 @@ tok_continuation_line(struct tok_state *tok) {
154154

155155

156156
int
157-
_PyLexer_get_normal_mode(struct tok_state *tok, tokenizer_mode* current_tok, struct token *token)
157+
_PyLexer_get_normal(struct tok_state *tok, ftstring_state *current, struct token *token)
158158
{
159+
assert(current == NULL ||
160+
(current->mode == FTSTRING_MODE_EXPRESSION &&
161+
current->replacement_depth > 0));
159162
int c;
160163
int blankline, nonascii;
161164

@@ -317,13 +320,13 @@ _PyLexer_get_normal_mode(struct tok_state *tok, tokenizer_mode* current_tok, str
317320
c = tok_nextc(tok);
318321
}
319322

320-
if (INSIDE_FSTRING(tok) && INSIDE_FSTRING_EXPR(current_tok)) {
323+
if (current != NULL) {
321324
const char *comment_end = tok->cur;
322325
if (c == '\n' || c == '\r') {
323326
comment_end--;
324327
}
325328
if (_PyLexer_record_ftstring_comment(
326-
tok, tok->start, comment_end) < 0) {
329+
tok, current, tok->start, comment_end) < 0) {
327330
tok->done = E_NOMEM;
328331
return MAKE_TOKEN(ERRORTOKEN);
329332
}
@@ -547,34 +550,25 @@ _PyLexer_get_normal_mode(struct tok_state *tok, tokenizer_mode* current_tok, str
547550
}
548551

549552
/* Punctuation character */
550-
int is_punctuation = (c == ':' || c == '}' || c == '!' || c == '{');
551-
if (is_punctuation && INSIDE_FSTRING(tok) && INSIDE_FSTRING_EXPR(current_tok)) {
552-
/* This code block gets executed before the curly_bracket_depth is incremented
553-
* by the `{` case, so for ensuring that we are on the 0th level, we need
554-
* to adjust it manually */
555-
int cursor = current_tok->curly_bracket_depth - (c != '{');
556-
int in_format_spec = current_tok->in_format_spec;
557-
int cursor_in_format_with_debug =
558-
cursor == 1 && (current_tok->in_debug || in_format_spec);
559-
int cursor_valid = cursor == 0 || cursor_in_format_with_debug;
560-
if (cursor_valid && c == '!') {
553+
int is_punctuation = (c == ':' || c == '}' || c == '!');
554+
if (is_punctuation && current != NULL) {
555+
int bracket_depth = _PyLexer_FTStringBracketDepth(tok, current);
556+
int at_expression_boundary =
557+
bracket_depth == current->replacement_depth;
558+
if (at_expression_boundary && c == '!') {
561559
int c2 = tok_nextc(tok);
562560
if (c2 == '=') {
563-
cursor_valid = 0;
561+
at_expression_boundary = 0;
564562
}
565563
tok_backup(tok, c2);
566564
}
567-
if (cursor_valid) {
568-
_PyLexer_update_ftstring_expr(tok, c);
569-
}
570-
if (cursor_valid && c != '{' &&
571-
_PyLexer_set_ftstring_expr_metadata(tok, token)) {
565+
if (at_expression_boundary &&
566+
_PyLexer_finish_ftstring_expr(tok, current, token)) {
572567
return MAKE_TOKEN(ERRORTOKEN);
573568
}
574569

575-
if (c == ':' && cursor == current_tok->curly_bracket_expr_start_depth) {
576-
current_tok->kind = TOK_FSTRING_MODE;
577-
current_tok->in_format_spec = 1;
570+
if (c == ':' && at_expression_boundary) {
571+
current->mode = FTSTRING_MODE_FORMAT_SPEC;
578572
p_start = tok->start;
579573
p_end = tok->cur;
580574
return MAKE_TOKEN(_PyToken_OneChar(c));
@@ -613,16 +607,20 @@ _PyLexer_get_normal_mode(struct tok_state *tok, tokenizer_mode* current_tok, str
613607
tok->parenlinenostack[tok->level] = tok->lineno;
614608
tok->parencolstack[tok->level] = (int)(tok->start - tok->line_start);
615609
tok->level++;
616-
if (INSIDE_FSTRING(tok)) {
617-
current_tok->curly_bracket_depth++;
618-
}
619610
break;
620611
case ')':
621612
case ']':
622613
case '}':
623-
if (INSIDE_FSTRING(tok) && !current_tok->curly_bracket_depth && c == '}') {
624-
return MAKE_TOKEN(_PyTokenizer_syntaxerror(tok,
625-
"%c-string: single '}' is not allowed", TOK_GET_STRING_PREFIX(tok)));
614+
if (current != NULL &&
615+
_PyLexer_FTStringBracketDepth(tok, current) == 0) {
616+
if (c == '}') {
617+
return MAKE_TOKEN(_PyTokenizer_syntaxerror(tok,
618+
"%c-string: single '}' is not allowed",
619+
_PyLexer_StringPrefix(current->kind)));
620+
}
621+
return MAKE_TOKEN(_PyTokenizer_syntaxerror(
622+
tok, "%c-string: unmatched '%c'",
623+
_PyLexer_StringPrefix(current->kind), c));
626624
}
627625
if (!tok->tok_extra_tokens && !tok->level) {
628626
return MAKE_TOKEN(_PyTokenizer_syntaxerror(tok, "unmatched '%c'", c));
@@ -633,17 +631,15 @@ _PyLexer_get_normal_mode(struct tok_state *tok, tokenizer_mode* current_tok, str
633631
if (!tok->tok_extra_tokens && !((opening == '(' && c == ')') ||
634632
(opening == '[' && c == ']') ||
635633
(opening == '{' && c == '}'))) {
636-
/* If the opening bracket belongs to an f-string's expression
637-
part (e.g. f"{)}") and the closing bracket is an arbitrary
638-
nested expression, then instead of matching a different
639-
syntactical construct with it; we'll throw an unmatched
640-
parentheses error. */
641-
if (INSIDE_FSTRING(tok) && opening == '{') {
642-
assert(current_tok->curly_bracket_depth >= 0);
643-
int previous_bracket = current_tok->curly_bracket_depth - 1;
644-
if (previous_bracket == current_tok->curly_bracket_expr_start_depth) {
634+
/* Do not match a closer against the brace that opened the
635+
* current replacement field. */
636+
if (current != NULL && opening == '{') {
637+
int bracket_depth =
638+
_PyLexer_FTStringBracketDepth(tok, current);
639+
if (bracket_depth == current->replacement_depth - 1) {
645640
return MAKE_TOKEN(_PyTokenizer_syntaxerror(tok,
646-
"%c-string: unmatched '%c'", TOK_GET_STRING_PREFIX(tok), c));
641+
"%c-string: unmatched '%c'",
642+
_PyLexer_StringPrefix(current->kind), c));
647643
}
648644
}
649645
if (tok->parenlinenostack[tok->level] != tok->lineno) {
@@ -661,17 +657,16 @@ _PyLexer_get_normal_mode(struct tok_state *tok, tokenizer_mode* current_tok, str
661657
}
662658
}
663659

664-
if (INSIDE_FSTRING(tok)) {
665-
current_tok->curly_bracket_depth--;
666-
if (current_tok->curly_bracket_depth < 0) {
660+
if (current != NULL) {
661+
int bracket_depth = _PyLexer_FTStringBracketDepth(tok, current);
662+
if (bracket_depth < 0) {
667663
return MAKE_TOKEN(_PyTokenizer_syntaxerror(tok, "%c-string: unmatched '%c'",
668-
TOK_GET_STRING_PREFIX(tok), c));
664+
_PyLexer_StringPrefix(current->kind), c));
669665
}
670-
if (c == '}' && current_tok->curly_bracket_depth == current_tok->curly_bracket_expr_start_depth) {
671-
current_tok->curly_bracket_expr_start_depth--;
672-
current_tok->kind = TOK_FSTRING_MODE;
673-
current_tok->in_format_spec = 0;
674-
current_tok->in_debug = 0;
666+
if (c == '}' && bracket_depth == current->replacement_depth - 1) {
667+
current->replacement_depth--;
668+
current->mode = FTSTRING_MODE_MIDDLE;
669+
current->debug_expr = 0;
675670
}
676671
}
677672
break;
@@ -683,8 +678,9 @@ _PyLexer_get_normal_mode(struct tok_state *tok, tokenizer_mode* current_tok, str
683678
return MAKE_TOKEN(_PyTokenizer_syntaxerror(tok, "invalid non-printable character U+%04X", c));
684679
}
685680

686-
if( c == '=' && INSIDE_FSTRING_EXPR_AT_TOP(current_tok)) {
687-
current_tok->in_debug = 1;
681+
if (c == '=' && current != NULL &&
682+
_PyLexer_FTStringBracketDepth(tok, current) == current->replacement_depth) {
683+
current->debug_expr = 1;
688684
}
689685

690686
/* Punctuation character */
@@ -694,21 +690,13 @@ _PyLexer_get_normal_mode(struct tok_state *tok, tokenizer_mode* current_tok, str
694690
}
695691

696692

697-
static int
698-
tok_get(struct tok_state *tok, struct token *token)
699-
{
700-
tokenizer_mode *current_tok = TOK_GET_MODE(tok);
701-
if (current_tok->kind == TOK_REGULAR_MODE) {
702-
return _PyLexer_get_normal_mode(tok, current_tok, token);
703-
} else {
704-
return _PyLexer_get_fstring_mode(tok, current_tok, token);
705-
}
706-
}
707-
708693
int
709694
_PyTokenizer_Get(struct tok_state *tok, struct token *token)
710695
{
711-
int result = tok_get(tok, token);
696+
ftstring_state *current = _PyLexer_CurrentFTString(tok);
697+
int result = current == NULL || current->mode == FTSTRING_MODE_EXPRESSION
698+
? _PyLexer_get_normal(tok, current, token)
699+
: _PyLexer_get_ftstring(tok, current, token);
712700
if (tok_failed(tok)) {
713701
result = ERRORTOKEN;
714702
}

Parser/lexer/lexer_internal.h

Lines changed: 9 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,31 +17,10 @@
1717
|| c == '_'\
1818
|| (c >= 128))
1919

20-
#ifdef Py_DEBUG
21-
static inline tokenizer_mode *
22-
TOK_GET_MODE(struct tok_state *tok)
23-
{
24-
assert(tok->tok_mode_stack_index >= 0);
25-
assert(tok->tok_mode_stack_index < MAXFSTRINGLEVEL);
26-
return &tok->tok_mode_stack[tok->tok_mode_stack_index];
27-
}
28-
29-
static inline tokenizer_mode *
30-
TOK_NEXT_MODE(struct tok_state *tok)
31-
{
32-
assert(tok->tok_mode_stack_index >= 0);
33-
assert(tok->tok_mode_stack_index + 1 < MAXFSTRINGLEVEL);
34-
return &tok->tok_mode_stack[++tok->tok_mode_stack_index];
35-
}
36-
#else
37-
#define TOK_GET_MODE(tok) (&(tok)->tok_mode_stack[(tok)->tok_mode_stack_index])
38-
#define TOK_NEXT_MODE(tok) (&(tok)->tok_mode_stack[++(tok)->tok_mode_stack_index])
39-
#endif
40-
41-
#define FTSTRING_MIDDLE(tok_mode) ((tok_mode)->string_kind == TSTRING ? TSTRING_MIDDLE : FSTRING_MIDDLE)
42-
#define FTSTRING_END(tok_mode) ((tok_mode)->string_kind == TSTRING ? TSTRING_END : FSTRING_END)
43-
#define TOK_GET_STRING_PREFIX(tok) (TOK_GET_MODE(tok)->string_kind == TSTRING ? 't' : 'f')
44-
20+
#define FTSTRING_MIDDLE(state) \
21+
(_PyLexer_IsTString((state)->kind) ? TSTRING_MIDDLE : FSTRING_MIDDLE)
22+
#define FTSTRING_END(state) \
23+
(_PyLexer_IsTString((state)->kind) ? TSTRING_END : FSTRING_END)
4524
#define tok_nextc _PyLexer_nextc
4625
#define tok_backup _PyLexer_backup
4726

@@ -54,15 +33,15 @@ tok_failed(const struct tok_state *tok)
5433

5534
int _PyLexer_nextc(struct tok_state *);
5635
void _PyLexer_backup(struct tok_state *, int);
57-
void _PyLexer_update_ftstring_expr(struct tok_state *, char);
5836
int _PyLexer_record_ftstring_comment(
59-
struct tok_state *, const char *, const char *);
60-
int _PyLexer_set_ftstring_expr_metadata(struct tok_state *, struct token *);
37+
struct tok_state *, ftstring_state *, const char *, const char *);
38+
int _PyLexer_finish_ftstring_expr(
39+
struct tok_state *, ftstring_state *, struct token *);
6140
int _PyLexer_check_string_prefixes(struct tok_state *, int, int, int, int, int);
6241
int _PyLexer_scan_number(struct tok_state *, struct token *, int, int);
6342
int _PyLexer_scan_fstring_start(struct tok_state *, struct token *, int);
6443
int _PyLexer_scan_string(struct tok_state *, struct token *, int);
65-
int _PyLexer_get_normal_mode(struct tok_state *, tokenizer_mode *, struct token *);
66-
int _PyLexer_get_fstring_mode(struct tok_state *, tokenizer_mode *, struct token *);
44+
int _PyLexer_get_normal(struct tok_state *, ftstring_state *, struct token *);
45+
int _PyLexer_get_ftstring(struct tok_state *, ftstring_state *, struct token *);
6746

6847
#endif

0 commit comments

Comments
 (0)