Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Lib/test/test_fstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -1657,6 +1657,7 @@ def __repr__(self):
self.assertEqual(f'{C()=:x}', 'C()=FORMAT-x')
self.assertEqual(f'{C()=!r:*^20}', 'C()=********REPR********')
self.assertEqual(f"{C():{20=}}", 'FORMAT-20=20')
self.assertEqual(f"{C():{C():{4=}}}", 'FORMAT-FORMAT-4=4')

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

Expand Down Expand Up @@ -1890,6 +1891,9 @@ def __format__(self, format):
self.assertEqual(f"{UnchangedFormat():{r'\xFF'}}", '\\xFF')
self.assertEqual(rf"{UnchangedFormat():{r'\xFF'}}", '\\xFF')

self.assertEqual(rf"{UnchangedFormat():{f'\xFF'}}\n", 'ÿ\\n')
self.assertEqual(f"{UnchangedFormat():{rf'\xFF'}}\n", '\\xFF\n')

# Test continuation character in format specs
self.assertEqual(f"""{UnchangedFormat():{'a'\
'b'}}""", 'ab')
Expand Down
15 changes: 15 additions & 0 deletions Lib/test/test_tokenize.py
Original file line number Diff line number Diff line change
Expand Up @@ -2592,6 +2592,21 @@ def test_incomplete_formatted_string_comment_after_carriage_return(self):
("unexpected EOF in multi-line statement", (1, 7)),
)

def test_formatted_string_nesting_limit(self):
def nested_string(depth, prefix):
source = "'x'"
for _ in range(depth):
source = f'{prefix}"{{{source}}}"'
return source

for prefix in ("f", "t"):
with self.subTest(prefix=prefix):
self._get_tokens(nested_string(149, prefix))
with self.assertRaisesRegex(
tokenize.TokenError,
"too many nested f-strings or t-strings"):
self._get_tokens(nested_string(150, prefix))

def test_escaped_fstring_brace_has_a_position_gap(self):
tokens = self._get_tokens('f"a{{"', extra_tokens=True)
self.assertEqual(
Expand Down
21 changes: 21 additions & 0 deletions Lib/test/test_tstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,15 @@ def test_debug_specifier(self):
)
self.assertEqual(fstring(t), "Value: value = 42")

class C:
def __format__(self, spec):
return f"FORMAT-{spec}"

x = y = C()
t = t"{x:{y:{value=}}}"
self.assertEqual(t.interpolations[0].format_spec,
"FORMAT-value=42")

def test_raw_tstrings(self):
path = r"C:\Users"
t = rt"{path}\Documents"
Expand All @@ -150,6 +159,14 @@ def test_raw_tstrings(self):
t = tr"{path}\Documents"
self.assertTStringEqual(t, ("", r"\Documents"), [(path, "path")])

value = 42
t = rt"{value:{f'\xFF'}}\n"
self.assertTStringEqual(
t, ("", "\\n"), [(value, "value", None, 'ÿ')])
t = t"{value:{rf'\xFF'}}\n"
self.assertTStringEqual(
t, ("", "\n"), [(value, "value", None, '\\xFF')])

def test_template_concatenation(self):
# Test template + template
t1 = t"Hello, "
Expand Down Expand Up @@ -217,6 +234,10 @@ def test_syntax_errors(self):
("t'{x=!}'", "t-string: missing conversion character"),
("t'{x!z}'", "t-string: invalid conversion character 'z': "
"expected 's', 'r', or 'a'"),
("f\"{t'{x!z}'}\"", "t-string: invalid conversion character 'z': "
"expected 's', 'r', or 'a'"),
("t'{f\"{x!z}\"}'", "f-string: invalid conversion character 'z': "
"expected 's', 'r', or 'a'"),
("t'{lambda:1}'", "t-string: lambda expressions are not allowed "
"without parentheses"),
("t'{x:{;}}'", "t-string: expecting a valid expression after '{'"),
Expand Down
45 changes: 30 additions & 15 deletions Parser/action_helpers.c
Original file line number Diff line number Diff line change
Expand Up @@ -1001,14 +1001,33 @@ result_token_with_metadata(Parser *p, void *result, PyObject *metadata)
return res;
}

static char
formatted_string_prefix(const Parser *p)
{
int nested = 0;
for (int i = p->mark - 1; i >= 0; i--) {
int type = p->tokens[i]->type;
if (type == FSTRING_END || type == TSTRING_END) {
nested++;
}
else if (type == FSTRING_START || type == TSTRING_START) {
if (nested == 0) {
return type == TSTRING_START ? 't' : 'f';
}
nested--;
}
}
Py_UNREACHABLE();
}

ResultTokenWithMetadata *
_PyPegen_check_fstring_conversion(Parser *p, Token* conv_token, expr_ty conv)
{
if (conv_token->lineno != conv->lineno || conv_token->end_col_offset != conv->col_offset) {
return RAISE_SYNTAX_ERROR_KNOWN_RANGE(
conv_token, conv,
"%c-string: conversion type must come right after the exclamation mark",
TOK_GET_STRING_PREFIX(p->tok)
formatted_string_prefix(p)
);
}

Expand All @@ -1017,7 +1036,7 @@ _PyPegen_check_fstring_conversion(Parser *p, Token* conv_token, expr_ty conv)
!(first == 's' || first == 'r' || first == 'a')) {
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(conv,
"%c-string: invalid conversion character %R: expected 's', 'r', or 'a'",
TOK_GET_STRING_PREFIX(p->tok),
formatted_string_prefix(p),
conv->v.Name.id);
return NULL;
}
Expand Down Expand Up @@ -1344,7 +1363,8 @@ _PyPegen_decode_fstring_part(Parser* p, int is_raw, expr_ty constant, Token* tok
}

static asdl_expr_seq *
_get_resized_exprs(Parser *p, Token *a, asdl_expr_seq *raw_expressions, Token *b, enum string_kind_t string_kind)
_get_resized_exprs(Parser *p, Token *a, asdl_expr_seq *raw_expressions,
Token *b, ftstring_kind string_kind)
{
Py_ssize_t n_items = asdl_seq_LEN(raw_expressions);
Py_ssize_t total_items = n_items;
Expand All @@ -1370,15 +1390,13 @@ _get_resized_exprs(Parser *p, Token *a, asdl_expr_seq *raw_expressions, Token *b
for (Py_ssize_t i = 0; i < n_items; i++) {
expr_ty item = asdl_seq_GET(raw_expressions, i);

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

expr_ty second = asdl_seq_GET(values, 1);
assert((string_kind == TSTRING && second->kind == Interpolation_kind) || second->kind == FormattedValue_kind);
assert((_PyLexer_IsTString(string_kind) &&
second->kind == Interpolation_kind) ||
second->kind == FormattedValue_kind);
asdl_seq_SET(seq, index++, second);

continue;
Expand Down Expand Up @@ -1460,12 +1480,7 @@ expr_ty _PyPegen_decoded_constant_from_token(Parser* p, Token* tok) {
return NULL;
}

// Check if we're inside a raw f-string for format spec decoding
int is_raw = 0;
if (INSIDE_FSTRING(p->tok)) {
tokenizer_mode *mode = TOK_GET_MODE(p->tok);
is_raw = mode->raw;
}
int is_raw = tok->is_raw;

PyObject* str = _PyPegen_decode_string(p, is_raw, bstr, bsize, tok);
if (str == NULL) {
Expand Down
126 changes: 53 additions & 73 deletions Parser/lexer/lexer.c
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,11 @@ tok_continuation_line(struct tok_state *tok) {


int
_PyLexer_get_normal_mode(struct tok_state *tok, tokenizer_mode* current_tok, struct token *token)
_PyLexer_get_normal(struct tok_state *tok, ftstring_state *current, struct token *token)
{
assert(current == NULL ||
(current->mode == FTSTRING_MODE_EXPRESSION &&
current->replacement_depth > 0));
int c;
int blankline, nonascii;

Expand Down Expand Up @@ -318,13 +321,13 @@ _PyLexer_get_normal_mode(struct tok_state *tok, tokenizer_mode* current_tok, str
c = tok_nextc(tok);
}

if (INSIDE_FSTRING(tok) && INSIDE_FSTRING_EXPR(current_tok)) {
if (current != NULL) {
const char *comment_end = tok->cur;
if (c == '\n' || c == '\r') {
comment_end--;
}
if (_PyLexer_record_ftstring_comment(
tok, tok->start, comment_end) < 0) {
tok, current, tok->start, comment_end) < 0) {
tok->done = E_NOMEM;
return MAKE_TOKEN(ERRORTOKEN);
}
Expand Down Expand Up @@ -548,37 +551,16 @@ _PyLexer_get_normal_mode(struct tok_state *tok, tokenizer_mode* current_tok, str
}

/* Punctuation character */
int is_punctuation = (c == ':' || c == '}' || c == '!' || c == '{');
if (is_punctuation && INSIDE_FSTRING(tok) && INSIDE_FSTRING_EXPR(current_tok)) {
/* This code block gets executed before the curly_bracket_depth is incremented
* by the `{` case, so for ensuring that we are on the 0th level, we need
* to adjust it manually */
int cursor = current_tok->curly_bracket_depth - (c != '{');
int in_format_spec = current_tok->in_format_spec;
int cursor_in_format_with_debug =
cursor == 1 && (current_tok->in_debug || in_format_spec);
int cursor_valid = cursor == 0 || cursor_in_format_with_debug;
if (cursor_valid && c == '!') {
int c2 = tok_nextc(tok);
if (c2 == '=') {
cursor_valid = 0;
}
tok_backup(tok, c2);
}
if (cursor_valid) {
_PyLexer_update_ftstring_expr(tok, c);
}
if (cursor_valid && c != '{' &&
_PyLexer_set_ftstring_expr_metadata(tok, token)) {
int is_punctuation = (c == ':' || c == '}' || c == '!');
if (is_punctuation && current != NULL) {
int type = _PyLexer_ftstring_punctuation(tok, current, token, c);
if (type < 0) {
return MAKE_TOKEN(ERRORTOKEN);
}

if (c == ':' && cursor == current_tok->curly_bracket_expr_start_depth) {
current_tok->kind = TOK_FSTRING_MODE;
current_tok->in_format_spec = 1;
if (type != 0) {
p_start = tok->start;
p_end = tok->cur;
return MAKE_TOKEN(_PyToken_OneChar(c));
return MAKE_TOKEN(type);
}
}

Expand Down Expand Up @@ -614,16 +596,20 @@ _PyLexer_get_normal_mode(struct tok_state *tok, tokenizer_mode* current_tok, str
tok->parenlinenostack[tok->level] = tok->lineno;
tok->parencolstack[tok->level] = (int)(tok->start - tok->line_start);
tok->level++;
if (INSIDE_FSTRING(tok)) {
current_tok->curly_bracket_depth++;
}
break;
case ')':
case ']':
case '}':
if (INSIDE_FSTRING(tok) && !current_tok->curly_bracket_depth && c == '}') {
return MAKE_TOKEN(_PyTokenizer_syntaxerror(tok,
"%c-string: single '}' is not allowed", TOK_GET_STRING_PREFIX(tok)));
if (current != NULL &&
_PyLexer_FTStringBracketDepth(tok, current) == 0) {
if (c == '}') {
return MAKE_TOKEN(_PyTokenizer_syntaxerror(tok,
"%c-string: single '}' is not allowed",
_PyLexer_StringPrefix(current->kind)));
}
return MAKE_TOKEN(_PyTokenizer_syntaxerror(
tok, "%c-string: unmatched '%c'",
_PyLexer_StringPrefix(current->kind), c));
}
if (!tok->tok_extra_tokens && !tok->level) {
return MAKE_TOKEN(_PyTokenizer_syntaxerror(tok, "unmatched '%c'", c));
Expand All @@ -634,17 +620,15 @@ _PyLexer_get_normal_mode(struct tok_state *tok, tokenizer_mode* current_tok, str
if (!tok->tok_extra_tokens && !((opening == '(' && c == ')') ||
(opening == '[' && c == ']') ||
(opening == '{' && c == '}'))) {
/* If the opening bracket belongs to an f-string's expression
part (e.g. f"{)}") and the closing bracket is an arbitrary
nested expression, then instead of matching a different
syntactical construct with it; we'll throw an unmatched
parentheses error. */
if (INSIDE_FSTRING(tok) && opening == '{') {
assert(current_tok->curly_bracket_depth >= 0);
int previous_bracket = current_tok->curly_bracket_depth - 1;
if (previous_bracket == current_tok->curly_bracket_expr_start_depth) {
/* Do not match a closer against the brace that opened the
* current replacement field. */
if (current != NULL && opening == '{') {
int bracket_depth =
_PyLexer_FTStringBracketDepth(tok, current);
if (bracket_depth == current->replacement_depth - 1) {
return MAKE_TOKEN(_PyTokenizer_syntaxerror(tok,
"%c-string: unmatched '%c'", TOK_GET_STRING_PREFIX(tok), c));
"%c-string: unmatched '%c'",
_PyLexer_StringPrefix(current->kind), c));
}
}
if (tok->parenlinenostack[tok->level] != tok->lineno) {
Expand All @@ -661,19 +645,9 @@ _PyLexer_get_normal_mode(struct tok_state *tok, tokenizer_mode* current_tok, str
}
}
}

if (INSIDE_FSTRING(tok)) {
current_tok->curly_bracket_depth--;
if (current_tok->curly_bracket_depth < 0) {
return MAKE_TOKEN(_PyTokenizer_syntaxerror(tok, "%c-string: unmatched '%c'",
TOK_GET_STRING_PREFIX(tok), c));
}
if (c == '}' && current_tok->curly_bracket_depth == current_tok->curly_bracket_expr_start_depth) {
current_tok->curly_bracket_expr_start_depth--;
current_tok->kind = TOK_FSTRING_MODE;
current_tok->in_format_spec = 0;
current_tok->in_debug = 0;
}
if (current != NULL &&
_PyLexer_close_ftstring_expr(tok, current, c) < 0) {
return MAKE_TOKEN(ERRORTOKEN);
}
break;
default:
Expand All @@ -684,8 +658,8 @@ _PyLexer_get_normal_mode(struct tok_state *tok, tokenizer_mode* current_tok, str
return MAKE_TOKEN(_PyTokenizer_syntaxerror(tok, "invalid non-printable character U+%04X", c));
}

if( c == '=' && INSIDE_FSTRING_EXPR_AT_TOP(current_tok)) {
current_tok->in_debug = 1;
if (c == '=' && current != NULL) {
_PyLexer_mark_ftstring_debug(tok, current);
}

/* Punctuation character */
Expand All @@ -695,21 +669,27 @@ _PyLexer_get_normal_mode(struct tok_state *tok, tokenizer_mode* current_tok, str
}


static int
tok_get(struct tok_state *tok, struct token *token)
{
tokenizer_mode *current_tok = TOK_GET_MODE(tok);
if (current_tok->kind == TOK_REGULAR_MODE) {
return _PyLexer_get_normal_mode(tok, current_tok, token);
} else {
return _PyLexer_get_fstring_mode(tok, current_tok, token);
}
}

int
_PyTokenizer_Get(struct tok_state *tok, struct token *token)
{
int result = tok_get(tok, token);
ftstring_state *current = _PyLexer_CurrentFTString(tok);
int result;
if (current == NULL) {
result = _PyLexer_get_normal(tok, NULL, token);
}
else {
switch (current->mode) {
case FTSTRING_MODE_EXPRESSION:
result = _PyLexer_get_normal(tok, current, token);
break;
case FTSTRING_MODE_MIDDLE:
case FTSTRING_MODE_FORMAT_SPEC:
result = _PyLexer_get_ftstring(tok, current, token);
break;
default:
Py_UNREACHABLE();
}
}
if (tok_failed(tok)) {
result = ERRORTOKEN;
}
Expand Down
Loading
Loading