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
28 changes: 28 additions & 0 deletions Lib/test/test_fstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,18 @@ def build_fstr(n, extra=''):
s = "f'{1}' 'x' 'y'" * 1024
self.assertEqual(eval(s), '1xy' * 1024)

@support.requires_resource('cpu')
def test_many_fstrings_in_module(self):
fields = ''.join(f'{{x{i}}}' for i in range(100))
source = ''.join(
f"value_{i} = f'{fields}'\n" for i in range(1_000)
)
namespace = {f'x{i}': str(i) for i in range(100)}
expected = ''.join(str(i) for i in range(100))
exec(source, namespace)
self.assertEqual(namespace['value_0'], expected)
self.assertEqual(namespace['value_999'], expected)

def test_format_specifier_expressions(self):
width = 10
precision = 4
Expand Down Expand Up @@ -1348,6 +1360,9 @@ def test_not_equal(self):
self.assertEqual(f'{3!=4:}', 'True')
self.assertEqual(f'{3!=4!s}', 'True')
self.assertEqual(f'{3!=4!s:.3}', 'Tru')
a = 3
b = 4
self.assertEqual(f'{a!=b=:>10}', 'a!=b= 1')

def test_equal_equal(self):
# Because an expression ending in = has special meaning,
Expand Down Expand Up @@ -1796,6 +1811,19 @@ def test_debug_in_file(self):
self.assertEqual(stdout.decode('utf-8').strip().replace('\r\n', '\n').replace('\r', '\n'),
"3\n=3")

def test_debug_in_file_after_buffer_resize(self):
expression = "(\n" + (" " * 64 + "\n") * 256 + "1\n)"
expected = expression + "=1"
with temp_cwd():
script = 'script.py'
source = (
f"result = f'''{{{expression}=}}'''\n"
f"assert result == {expected!r}\n"
)
with open(script, 'w') as f:
f.write(source)
assert_python_ok(script)

def test_syntax_warning_infinite_recursion_in_file(self):
with temp_cwd():
script = 'script.py'
Expand Down
36 changes: 36 additions & 0 deletions Lib/test/test_tstring.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import unittest

from test import support
from test.support.os_helper import temp_cwd
from test.support.script_helper import assert_python_ok
from test.test_string._support import TStringBaseCase, fstring


Expand Down Expand Up @@ -79,6 +82,31 @@ def upper(self):
)
self.assertEqual(fstring(t), "Name: Bob, Age: 30")

def test_interpolation_expression_in_file_after_buffer_resize(self):
expression = "(\n" + (" " * 64 + "\n") * 256 + "1\n)"
with temp_cwd():
script = 'script.py'
source = (
f"template = t'''{{{expression}}}'''\n"
"interpolation = template.interpolations[0]\n"
f"assert interpolation.expression == {expression!r}\n"
)
with open(script, 'w') as f:
f.write(source)
assert_python_ok(script)

@support.requires_resource('cpu')
def test_many_tstrings_in_module(self):
fields = ''.join(f'{{x{i}}}' for i in range(100))
source = ''.join(
f"value_{i} = t'{fields}'\n" for i in range(1_000)
)
namespace = {f'x{i}': str(i) for i in range(100)}
expected = ''.join(str(i) for i in range(100))
exec(source, namespace)
self.assertEqual(fstring(namespace['value_0']), expected)
self.assertEqual(fstring(namespace['value_999']), expected)

def test_format_specifiers(self):
# Test basic format specifiers
value = 3.14159
Expand All @@ -88,6 +116,14 @@ def test_format_specifiers(self):
)
self.assertEqual(fstring(t), "Pi: 3.14")

a = 3
b = 4
t = t"{a!=b:>10}"
self.assertTStringEqual(
t, ("", ""), [(a != b, "a!=b", None, ">10")]
)
self.assertEqual(fstring(t), " 1")

def test_conversions(self):
# Test !s conversion (str)
obj = object()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix quadratic-time tokenization of modules containing many f-strings or
t-strings.
4 changes: 4 additions & 0 deletions Parser/lexer/buffer.c
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ _PyLexer_remember_fstring_buffers(struct tok_state *tok)
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;
mode->last_expr_start_offset = mode->last_expr_start == NULL
? -1 : mode->last_expr_start - tok->buf;
}
}

Expand All @@ -29,6 +31,8 @@ _PyLexer_restore_fstring_buffers(struct tok_state *tok)
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;
mode->last_expr_start = mode->last_expr_start_offset < 0
? NULL : tok->buf + mode->last_expr_start_offset;
}
}

Expand Down
3 changes: 0 additions & 3 deletions Parser/lexer/lexer.c
Original file line number Diff line number Diff line change
Expand Up @@ -542,9 +542,6 @@ _PyLexer_get_normal_mode(struct tok_state *tok, tokenizer_mode* current_tok, str
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) && !_PyLexer_update_ftstring_expr(tok, c)) {
return MAKE_TOKEN(ENDMARKER);
}
if ((cursor_valid) && c != '{' && _PyLexer_set_ftstring_expr(tok, token, c)) {
return MAKE_TOKEN(ERRORTOKEN);
}
Expand Down
2 changes: 0 additions & 2 deletions Parser/lexer/lexer.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@

#include "state.h"

int _PyLexer_update_ftstring_expr(struct tok_state *tok, char cur);

int _PyTokenizer_Get(struct tok_state *, struct token *);

#endif
19 changes: 0 additions & 19 deletions Parser/lexer/state.c
Original file line number Diff line number Diff line change
Expand Up @@ -61,24 +61,6 @@ _PyTokenizer_tok_new(void)
return tok;
}

static void
free_fstring_expressions(struct tok_state *tok)
{
int index;
tokenizer_mode *mode;

for (index = tok->tok_mode_stack_index; index >= 0; --index) {
mode = &(tok->tok_mode_stack[index]);
if (mode->last_expr_buffer != NULL) {
PyMem_Free(mode->last_expr_buffer);
mode->last_expr_buffer = NULL;
mode->last_expr_size = 0;
mode->last_expr_end = -1;
mode->in_format_spec = 0;
}
}
}

/* Free a tok_state structure */
void
_PyTokenizer_Free(struct tok_state *tok)
Expand All @@ -90,7 +72,6 @@ _PyTokenizer_Free(struct tok_state *tok)
Py_XDECREF(tok->module);
_PyTok_ReaderFree(tok);
_PyTok_SourceClear(&tok->source);
free_fstring_expressions(tok);
PyMem_Free(tok);
}

Expand Down
8 changes: 5 additions & 3 deletions Parser/lexer/state.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,11 @@ typedef struct _tokenizer_mode {
Py_ssize_t start_offset;
Py_ssize_t multi_line_start_offset;

Py_ssize_t last_expr_size;
Py_ssize_t last_expr_end;
char* last_expr_buffer;
/* Points into tok->buf: relies on _PyTok_ReaderUnderflow()
not resetting the buffer while INSIDE_FSTRING(tok) */
const char* last_expr_start;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: can we add a comment here saying that this points into tok->buf and relies on _PyTok_ReaderUnderflow never resetting the buffer while INSIDE_FSTRING(tok)? That invariant is what makes this work and it lives a bit far from here.

Py_ssize_t last_expr_start_offset;

int in_debug;
int in_format_spec;

Expand Down
108 changes: 23 additions & 85 deletions Parser/lexer/string.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,19 @@ _PyLexer_set_ftstring_expr(struct tok_state* tok, struct token *token, char c) {
if (!(tok_mode->in_debug || tok_mode->string_kind == TSTRING) || token->metadata) {
return 0;
}
const char *expression = tok_mode->last_expr_start;
assert(expression != NULL);
assert(expression <= tok->start);
Py_ssize_t expression_size = tok->start - expression;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless I am missing something, this also changes behaviour for a : after a !=: on main the ':' case only set last_expr_end when it was still -1, so for f'{a!=b=:>10}' the debug text was cut at the ! and t'{a!=b:>10}'.interpolations[0].expression was 'a'. Now we always take everything up to tok->start, which is the right thing, but can we add a test for both cases so we don't lose it?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless I am missing something

Nope, spot on. Added tests for both cases.

PyObject *res = NULL;

// Look for a # character outside of string literals
int hash_detected = 0;
int in_string = 0;
char quote_char = 0;

for (Py_ssize_t i = 0; i < tok_mode->last_expr_size - tok_mode->last_expr_end; i++) {
char ch = tok_mode->last_expr_buffer[i];
for (Py_ssize_t i = 0; i < expression_size; i++) {
char ch = expression[i];

// Skip escaped characters
if (ch == '\\') {
Expand Down Expand Up @@ -60,7 +64,8 @@ _PyLexer_set_ftstring_expr(struct tok_state* tok, struct token *token, char c) {
// If we found a # character in the expression, we need to handle comments
if (hash_detected) {
// Allocate buffer for processed result
char *result = (char *)PyMem_Malloc((tok_mode->last_expr_size - tok_mode->last_expr_end + 1) * sizeof(char));
char *result = (char *)PyMem_Malloc(
(expression_size + 1) * sizeof(char));
if (!result) {
return -1;
}
Expand All @@ -71,8 +76,8 @@ _PyLexer_set_ftstring_expr(struct tok_state* tok, struct token *token, char c) {
quote_char = 0; // Current string quote char

// Process each character
while (i < tok_mode->last_expr_size - tok_mode->last_expr_end) {
char ch = tok_mode->last_expr_buffer[i];
while (i < expression_size) {
char ch = expression[i];

// Handle string quotes
if (ch == '"' || ch == '\'') {
Expand All @@ -87,11 +92,10 @@ _PyLexer_set_ftstring_expr(struct tok_state* tok, struct token *token, char c) {
}
// Skip comments
else if (ch == '#' && !in_string) {
while (i < tok_mode->last_expr_size - tok_mode->last_expr_end &&
tok_mode->last_expr_buffer[i] != '\n') {
while (i < expression_size && expression[i] != '\n') {
i++;
}
if (i < tok_mode->last_expr_size - tok_mode->last_expr_end) {
if (i < expression_size) {
result[j++] = '\n';
}
}
Expand All @@ -106,11 +110,7 @@ _PyLexer_set_ftstring_expr(struct tok_state* tok, struct token *token, char c) {
res = PyUnicode_DecodeUTF8(result, j, NULL);
PyMem_Free(result);
} else {
res = PyUnicode_DecodeUTF8(
tok_mode->last_expr_buffer,
tok_mode->last_expr_size - tok_mode->last_expr_end,
NULL
);
res = PyUnicode_DecodeUTF8(expression, expression_size, NULL);
}

if (!res) {
Expand All @@ -120,61 +120,6 @@ _PyLexer_set_ftstring_expr(struct tok_state* tok, struct token *token, char c) {
return 0;
}

int
_PyLexer_update_ftstring_expr(struct tok_state *tok, char cur)
{
assert(tok->cur != NULL);

Py_ssize_t size = strlen(tok->cur);
tokenizer_mode *tok_mode = TOK_GET_MODE(tok);

switch (cur) {
case 0:
if (!tok_mode->last_expr_buffer || tok_mode->last_expr_end >= 0) {
return 1;
}
char *new_buffer = PyMem_Realloc(
tok_mode->last_expr_buffer,
tok_mode->last_expr_size + size
);
if (new_buffer == NULL) {
PyMem_Free(tok_mode->last_expr_buffer);
goto error;
}
tok_mode->last_expr_buffer = new_buffer;
strncpy(tok_mode->last_expr_buffer + tok_mode->last_expr_size, tok->cur, size);
tok_mode->last_expr_size += size;
break;
case '{':
if (tok_mode->last_expr_buffer != NULL) {
PyMem_Free(tok_mode->last_expr_buffer);
}
tok_mode->last_expr_buffer = PyMem_Malloc(size);
if (tok_mode->last_expr_buffer == NULL) {
goto error;
}
tok_mode->last_expr_size = size;
tok_mode->last_expr_end = -1;
strncpy(tok_mode->last_expr_buffer, tok->cur, size);
break;
case '}':
case '!':
tok_mode->last_expr_end = strlen(tok->start);
break;
case ':':
if (tok_mode->last_expr_end == -1) {
tok_mode->last_expr_end = strlen(tok->start);
}
break;
default:
Py_UNREACHABLE();
}
return 1;
error:
tok->done = E_NOMEM;
return 0;
}

int
_PyLexer_check_string_prefixes(struct tok_state *tok,
int saw_b, int saw_r, int saw_u,
Expand Down Expand Up @@ -268,9 +213,8 @@ _PyLexer_scan_fstring_start(struct tok_state *tok, struct token *token, int c)
the_current_tok->first_line = tok->lineno;
the_current_tok->start_offset = -1;
the_current_tok->multi_line_start_offset = -1;
the_current_tok->last_expr_buffer = NULL;
the_current_tok->last_expr_size = 0;
the_current_tok->last_expr_end = -1;
the_current_tok->last_expr_start = NULL;
the_current_tok->last_expr_start_offset = -1;
the_current_tok->in_format_spec = 0;
the_current_tok->in_debug = 0;

Expand Down Expand Up @@ -436,6 +380,9 @@ _PyLexer_get_fstring_mode(struct tok_state *tok, tokenizer_mode* current_tok, st
if (start_char == '{') {
int peek1 = tok_nextc(tok);
tok_backup(tok, peek1);
if (peek1 != '{') {
current_tok->last_expr_start = tok->cur;
}
tok_backup(tok, start_char);
if (peek1 != '{') {
current_tok->curly_bracket_expr_start_depth++;
Expand All @@ -460,13 +407,6 @@ _PyLexer_get_fstring_mode(struct tok_state *tok, tokenizer_mode* current_tok, st
}
}

if (current_tok->last_expr_buffer != NULL) {
PyMem_Free(current_tok->last_expr_buffer);
current_tok->last_expr_buffer = NULL;
current_tok->last_expr_size = 0;
current_tok->last_expr_end = -1;
}

p_start = tok->start;
p_end = tok->cur;
tok->tok_mode_stack_index--;
Expand Down Expand Up @@ -551,12 +491,10 @@ _PyLexer_get_fstring_mode(struct tok_state *tok, tokenizer_mode* current_tok, st
}

if (c == '{') {
if (!_PyLexer_update_ftstring_expr(tok, c)) {
return MAKE_TOKEN(ENDMARKER);
}
int peek = tok_nextc(tok);
if (peek != '{' || in_format_spec) {
tok_backup(tok, peek);
current_tok->last_expr_start = tok->cur;
tok_backup(tok, c);
current_tok->curly_bracket_expr_start_depth++;
if (current_tok->curly_bracket_expr_start_depth >= MAX_EXPR_NESTING) {
Expand All @@ -580,10 +518,10 @@ _PyLexer_get_fstring_mode(struct tok_state *tok, tokenizer_mode* current_tok, st
}
int peek = tok_nextc(tok);

// The tokenizer can only be in the format spec if we have already completed the expression
// scanning (indicated by the end of the expression being set) and we are not at the top level
// of the bracket stack (-1 is the top level). Since format specifiers can't legally use double
// brackets, we can bypass it here.
// The tokenizer can only be in the format spec if expression
// scanning is complete and we are not at the top level of the
// bracket stack (-1 is the top level). Since format specifiers
// can't legally use double brackets, we can bypass it here.
int cursor = current_tok->curly_bracket_depth;
if (peek == '}' && !in_format_spec && cursor == 0) {
p_start = tok->start;
Expand Down
Loading
Loading