Skip to content
Open
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
15 changes: 15 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 All @@ -1679,6 +1680,20 @@ def __repr__(self):

self.assertEqual(f'{" # nooo "=}', '" # nooo "=\' # nooo \'')
self.assertEqual(f'{" \" # nooo \" "=}', '" \\" # nooo \\" "=\' " # nooo " \'')
self.assertEqual(f'{"""a" # inside"""=}',
'"""a" # inside"""=\'a" # inside\'')
self.assertEqual(f"{'''a' # inside'''=}",
"'''a' # inside'''=\"a' # inside\"")
self.assertEqual(f'{"""a""""#" # outside
=}', '"""a""""#" \n=\'a#\'')

x, y = 1, 2
self.assertEqual(f'{x != y # outside
=}', 'x != y \n=True')

d = {'a#b': 42}
self.assertEqual(f'''{f"{d["a#b"]}"=}''',
'f"{d["a#b"]}"=\'42\'')

self.assertEqual(f'{ # some comment goes here
"""hello"""=}', ' \n """hello"""=\'hello\'')
Expand Down
4 changes: 4 additions & 0 deletions Lib/test/test_syntax.py
Original file line number Diff line number Diff line change
Expand Up @@ -3393,6 +3393,10 @@ def test_invalid_line_continuation_error_position(self):
self._check_error('\nfgdfgf\n1,\\#\n2\n',
"unexpected character after line continuation character",
lineno=3, offset=4)
for prefix in ("f", "t"):
self._check_error(f'{prefix}"""{{\n\\ x}}"""',
"unexpected character after line continuation character",
lineno=2, offset=2)

def test_invalid_line_continuation_left_recursive(self):
# Check bpo-42218: SyntaxErrors following left-recursive rules
Expand Down
34 changes: 34 additions & 0 deletions Lib/test/test_tokenize.py
Original file line number Diff line number Diff line change
Expand Up @@ -2573,6 +2573,40 @@ def test_degraded_fstring_format_spec(self):
("f-string: single '}' is not allowed", (1, 11)),
)

def test_carriage_return_after_debug_comment(self):
for prefix in ("f", "t"):
with self.subTest(prefix=prefix):
tokens = self._get_tokens(f"{prefix}'''{{x=# comment\r}}'''")
self.assertEqual(tokens[4].string, "# comment\r}")

def test_incomplete_formatted_string_comment_after_carriage_return(self):
for prefix in ("f", "t"):
with self.subTest(prefix=prefix):
for extra_tokens in (False, True):
with self.assertRaises(tokenize.TokenError) as caught:
self._get_tokens(
f"{prefix}'{{#\r!", extra_tokens=extra_tokens
)
self.assertEqual(
caught.exception.args,
("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
26 changes: 26 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 Down Expand Up @@ -287,5 +296,22 @@ def test_triple_quoted(self):
)
self.assertEqual(fstring(t), "\n Hello,\n Python\n ")

t = t'{"""a" # inside"""}'
self.assertEqual(t.interpolations[0].expression,
'"""a" # inside"""')

t = t'{"""a""""#" # outside
}'
self.assertEqual(t.interpolations[0].expression, '"""a""""#"')

x, y = 1, 2
t = t'{x != y # outside
}'
self.assertEqual(t.interpolations[0].expression, 'x != y')

d = {'a#b': 42}
t = t'''{f"{d["a#b"]}"}'''
self.assertEqual(t.interpolations[0].expression, 'f"{d["a#b"]}"')

if __name__ == '__main__':
unittest.main()
34 changes: 19 additions & 15 deletions Parser/action_helpers.c
Original file line number Diff line number Diff line change
Expand Up @@ -1001,14 +1001,21 @@ result_token_with_metadata(Parser *p, void *result, PyObject *metadata)
return res;
}

static char
formatted_string_prefix(const Parser *p)
{
const ftstring_state *state = _PyLexer_CurrentFTString(p->tok);
return state == NULL ? 'f' : _PyLexer_StringPrefix(state->kind);
}

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 +1024,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 +1351,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 +1378,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 +1396,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 +1468,8 @@ 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;
}
const ftstring_state *state = _PyLexer_CurrentFTString(p->tok);
int is_raw = state != NULL && _PyLexer_IsRawString(state->kind);

PyObject* str = _PyPegen_decode_string(p, is_raw, bstr, bsize, tok);
if (str == NULL) {
Expand Down
17 changes: 0 additions & 17 deletions Parser/lexer/buffer.c
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,6 @@ _PyLexer_SaveBufferPointers(struct tok_state *tok, const char *base,
? -1 : tok->start - tok->buf;
pointers->line_start_from_buf = tok->line_start == NULL
? -1 : tok->line_start - tok->buf;
pointers->multi_line_start_from_buf = tok->multi_line_start == NULL
? -1 : tok->multi_line_start - tok->buf;
for (int index = tok->tok_mode_stack_index; index > 0; --index) {
tokenizer_mode *mode = &tok->tok_mode_stack[index];
mode->start_offset = mode->start == NULL ? -1 : mode->start - tok->buf;
mode->multi_line_start_offset = mode->multi_line_start == NULL
? -1 : mode->multi_line_start - tok->buf;
}
}

void
Expand All @@ -34,13 +26,4 @@ _PyLexer_RestoreBufferPointers(struct tok_state *tok, char *base,
? NULL : tok->buf + pointers->start_from_buf;
tok->line_start = pointers->line_start_from_buf < 0
? NULL : tok->buf + pointers->line_start_from_buf;
tok->multi_line_start = pointers->multi_line_start_from_buf < 0
? NULL : tok->buf + pointers->multi_line_start_from_buf;
for (int index = tok->tok_mode_stack_index; index > 0; --index) {
tokenizer_mode *mode = &tok->tok_mode_stack[index];
mode->start = mode->start_offset < 0
? NULL : tok->buf + mode->start_offset;
mode->multi_line_start = mode->multi_line_start_offset < 0
? NULL : tok->buf + mode->multi_line_start_offset;
}
}
1 change: 0 additions & 1 deletion Parser/lexer/buffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ typedef struct {
Py_ssize_t inp_from_buf;
Py_ssize_t start_from_buf;
Py_ssize_t line_start_from_buf;
Py_ssize_t multi_line_start_from_buf;
} _PyLexer_BufferPointers;

void _PyLexer_SaveBufferPointers(
Expand Down
Loading
Loading