Skip to content

Commit 6f6ff8a

Browse files
authored
bpo-37050: Remove expr_text from FormattedValue ast node, use Constant node instead (GH-13597)
When using the "=" debug functionality of f-strings, use another Constant node (or a merged constant node) instead of adding expr_text to the FormattedValue node.
1 parent 695b1dd commit 6f6ff8a

File tree

9 files changed

+87
-100
lines changed

9 files changed

+87
-100
lines changed

Include/Python-ast.h

Lines changed: 3 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_fstring.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1150,6 +1150,24 @@ def __repr__(self):
11501150

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

1153+
# Make sure leading and following text works.
1154+
x = 'foo'
1155+
self.assertEqual(f'X{x=}Y', 'Xx='+repr(x)+'Y')
1156+
1157+
# Make sure whitespace around the = works.
1158+
self.assertEqual(f'X{x =}Y', 'Xx ='+repr(x)+'Y')
1159+
self.assertEqual(f'X{x= }Y', 'Xx= '+repr(x)+'Y')
1160+
self.assertEqual(f'X{x = }Y', 'Xx = '+repr(x)+'Y')
1161+
1162+
# These next lines contains tabs. Backslash escapes don't
1163+
# work in f-strings.
1164+
# patchcheck doens't like these tabs. So the only way to test
1165+
# this will be to dynamically created and exec the f-strings. But
1166+
# that's such a hassle I'll save it for another day. For now, convert
1167+
# the tabs to spaces just to shut up patchcheck.
1168+
#self.assertEqual(f'X{x =}Y', 'Xx\t='+repr(x)+'Y')
1169+
#self.assertEqual(f'X{x = }Y', 'Xx\t=\t'+repr(x)+'Y')
1170+
11531171
def test_walrus(self):
11541172
x = 20
11551173
# This isn't an assignment expression, it's 'x', with a format

Lib/test/test_future.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -270,12 +270,6 @@ def test_annotations(self):
270270
eq("f'{x}'")
271271
eq("f'{x!r}'")
272272
eq("f'{x!a}'")
273-
eq("f'{x=!r}'")
274-
eq("f'{x=:}'")
275-
eq("f'{x=:.2f}'")
276-
eq("f'{x=!r}'")
277-
eq("f'{x=!a}'")
278-
eq("f'{x=!s:*^20}'")
279273
eq('(yield from outside_of_generator)')
280274
eq('(yield)')
281275
eq('(yield a + b)')
@@ -290,6 +284,15 @@ def test_annotations(self):
290284
eq("(x:=10)")
291285
eq("f'{(x:=10):=10}'")
292286

287+
# f-strings with '=' don't round trip very well, so set the expected
288+
# result explicitely.
289+
self.assertAnnotationEqual("f'{x=!r}'", expected="f'x={x!r}'")
290+
self.assertAnnotationEqual("f'{x=:}'", expected="f'x={x:}'")
291+
self.assertAnnotationEqual("f'{x=:.2f}'", expected="f'x={x:.2f}'")
292+
self.assertAnnotationEqual("f'{x=!r}'", expected="f'x={x!r}'")
293+
self.assertAnnotationEqual("f'{x=!a}'", expected="f'x={x!a}'")
294+
self.assertAnnotationEqual("f'{x=!s:*^20}'", expected="f'x={x!s:*^20}'")
295+
293296

294297
if __name__ == "__main__":
295298
unittest.main()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Improve the AST for "debug" f-strings, which use '=' to print out the source
2+
of the expression being evaluated. Delete expr_text from the FormattedValue
3+
node, and instead use a Constant string node (possibly merged with adjacent
4+
constant expressions inside the f-string).

Parser/Python.asdl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ module Python
7676
-- x < 4 < 3 and (x < 4) < 3
7777
| Compare(expr left, cmpop* ops, expr* comparators)
7878
| Call(expr func, expr* args, keyword* keywords)
79-
| FormattedValue(expr value, int? conversion, expr? format_spec, string? expr_text)
79+
| FormattedValue(expr value, int? conversion, expr? format_spec)
8080
| JoinedStr(expr* values)
8181
| Constant(constant value, string? kind)
8282

Python/Python-ast.c

Lines changed: 6 additions & 29 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Python/ast.c

Lines changed: 46 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5006,10 +5006,16 @@ fstring_parse(const char **str, const char *end, int raw, int recurse_lvl,
50065006
closing brace doesn't match an opening paren, for example. It
50075007
doesn't need to error on all invalid expressions, just correctly
50085008
find the end of all valid ones. Any errors inside the expression
5009-
will be caught when we parse it later. */
5009+
will be caught when we parse it later.
5010+
5011+
*expression is set to the expression. For an '=' "debug" expression,
5012+
*expr_text is set to the debug text (the original text of the expression,
5013+
*including the '=' and any whitespace around it, as a string object). If
5014+
*not a debug expression, *expr_text set to NULL. */
50105015
static int
50115016
fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl,
5012-
expr_ty *expression, struct compiling *c, const node *n)
5017+
PyObject **expr_text, expr_ty *expression,
5018+
struct compiling *c, const node *n)
50135019
{
50145020
/* Return -1 on error, else 0. */
50155021

@@ -5020,9 +5026,6 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl,
50205026
int conversion = -1; /* The conversion char. Use default if not
50215027
specified, or !r if using = and no format
50225028
spec. */
5023-
int equal_flag = 0; /* Are we using the = feature? */
5024-
PyObject *expr_text = NULL; /* The text of the expression, used for =. */
5025-
const char *expr_text_end;
50265029

50275030
/* 0 if we're not in a string, else the quote char we're trying to
50285031
match (single or double quote). */
@@ -5198,15 +5201,21 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl,
51985201
expr_text. */
51995202
if (**str == '=') {
52005203
*str += 1;
5201-
equal_flag = 1;
52025204

52035205
/* Skip over ASCII whitespace. No need to test for end of string
52045206
here, since we know there's at least a trailing quote somewhere
52055207
ahead. */
52065208
while (Py_ISSPACE(**str)) {
52075209
*str += 1;
52085210
}
5209-
expr_text_end = *str;
5211+
5212+
/* Set *expr_text to the text of the expression. */
5213+
*expr_text = PyUnicode_FromStringAndSize(expr_start, *str-expr_start);
5214+
if (!*expr_text) {
5215+
goto error;
5216+
}
5217+
} else {
5218+
*expr_text = NULL;
52105219
}
52115220

52125221
/* Check for a conversion char, if present. */
@@ -5227,17 +5236,6 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl,
52275236
}
52285237

52295238
}
5230-
if (equal_flag) {
5231-
Py_ssize_t len = expr_text_end - expr_start;
5232-
expr_text = PyUnicode_FromStringAndSize(expr_start, len);
5233-
if (!expr_text) {
5234-
goto error;
5235-
}
5236-
if (PyArena_AddPyObject(c->c_arena, expr_text) < 0) {
5237-
Py_DECREF(expr_text);
5238-
goto error;
5239-
}
5240-
}
52415239

52425240
/* Check for the format spec, if present. */
52435241
if (*str >= end)
@@ -5261,16 +5259,16 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl,
52615259
assert(**str == '}');
52625260
*str += 1;
52635261

5264-
/* If we're in = mode, and have no format spec and no explict conversion,
5265-
set the conversion to 'r'. */
5266-
if (equal_flag && format_spec == NULL && conversion == -1) {
5262+
/* If we're in = mode (detected by non-NULL expr_text), and have no format
5263+
spec and no explict conversion, set the conversion to 'r'. */
5264+
if (*expr_text && format_spec == NULL && conversion == -1) {
52675265
conversion = 'r';
52685266
}
52695267

52705268
/* And now create the FormattedValue node that represents this
52715269
entire expression with the conversion and format spec. */
52725270
*expression = FormattedValue(simple_expression, conversion,
5273-
format_spec, expr_text, LINENO(n),
5271+
format_spec, LINENO(n),
52745272
n->n_col_offset, n->n_end_lineno,
52755273
n->n_end_col_offset, c->c_arena);
52765274
if (!*expression)
@@ -5313,7 +5311,7 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl,
53135311
static int
53145312
fstring_find_literal_and_expr(const char **str, const char *end, int raw,
53155313
int recurse_lvl, PyObject **literal,
5316-
expr_ty *expression,
5314+
PyObject **expr_text, expr_ty *expression,
53175315
struct compiling *c, const node *n)
53185316
{
53195317
int result;
@@ -5341,7 +5339,8 @@ fstring_find_literal_and_expr(const char **str, const char *end, int raw,
53415339
/* We must now be the start of an expression, on a '{'. */
53425340
assert(**str == '{');
53435341

5344-
if (fstring_find_expr(str, end, raw, recurse_lvl, expression, c, n) < 0)
5342+
if (fstring_find_expr(str, end, raw, recurse_lvl, expr_text,
5343+
expression, c, n) < 0)
53455344
goto error;
53465345

53475346
return 0;
@@ -5604,39 +5603,42 @@ FstringParser_ConcatFstring(FstringParser *state, const char **str,
56045603

56055604
/* Parse the f-string. */
56065605
while (1) {
5607-
PyObject *literal = NULL;
5606+
PyObject *literal[2] = {NULL, NULL};
56085607
expr_ty expression = NULL;
56095608

56105609
/* If there's a zero length literal in front of the
56115610
expression, literal will be NULL. If we're at the end of
56125611
the f-string, expression will be NULL (unless result == 1,
56135612
see below). */
56145613
int result = fstring_find_literal_and_expr(str, end, raw, recurse_lvl,
5615-
&literal, &expression,
5616-
c, n);
5614+
&literal[0], &literal[1],
5615+
&expression, c, n);
56175616
if (result < 0)
56185617
return -1;
56195618

5620-
/* Add the literal, if any. */
5621-
if (!literal) {
5622-
/* Do nothing. Just leave last_str alone (and possibly
5623-
NULL). */
5624-
} else if (!state->last_str) {
5625-
/* Note that the literal can be zero length, if the
5626-
input string is "\\\n" or "\\\r", among others. */
5627-
state->last_str = literal;
5628-
literal = NULL;
5629-
} else {
5630-
/* We have a literal, concatenate it. */
5631-
assert(PyUnicode_GET_LENGTH(literal) != 0);
5632-
if (FstringParser_ConcatAndDel(state, literal) < 0)
5633-
return -1;
5634-
literal = NULL;
5619+
/* Add the literals, if any. */
5620+
for (int i = 0; i < 2; i++) {
5621+
if (!literal[i]) {
5622+
/* Do nothing. Just leave last_str alone (and possibly
5623+
NULL). */
5624+
} else if (!state->last_str) {
5625+
/* Note that the literal can be zero length, if the
5626+
input string is "\\\n" or "\\\r", among others. */
5627+
state->last_str = literal[i];
5628+
literal[i] = NULL;
5629+
} else {
5630+
/* We have a literal, concatenate it. */
5631+
assert(PyUnicode_GET_LENGTH(literal[i]) != 0);
5632+
if (FstringParser_ConcatAndDel(state, literal[i]) < 0)
5633+
return -1;
5634+
literal[i] = NULL;
5635+
}
56355636
}
56365637

5637-
/* We've dealt with the literal now. It can't be leaked on further
5638+
/* We've dealt with the literals now. They can't be leaked on further
56385639
errors. */
5639-
assert(literal == NULL);
5640+
assert(literal[0] == NULL);
5641+
assert(literal[1] == NULL);
56405642

56415643
/* See if we should just loop around to get the next literal
56425644
and expression, while ignoring the expression this

Python/ast_unparse.c

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -665,11 +665,6 @@ append_formattedvalue(_PyUnicodeWriter *writer, expr_ty e, bool is_format_spec)
665665
}
666666
Py_DECREF(temp_fv_str);
667667

668-
if (e->v.FormattedValue.expr_text) {
669-
/* Use the = for debug text expansion. */
670-
APPEND_STR("=");
671-
}
672-
673668
if (e->v.FormattedValue.conversion > 0) {
674669
switch (e->v.FormattedValue.conversion) {
675670
case 'a':

Python/compile.c

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3963,12 +3963,6 @@ compiler_formatted_value(struct compiler *c, expr_ty e)
39633963
int conversion = e->v.FormattedValue.conversion;
39643964
int oparg;
39653965

3966-
if (e->v.FormattedValue.expr_text) {
3967-
/* Push the text of the expression (which already has the '=' in
3968-
it. */
3969-
ADDOP_LOAD_CONST(c, e->v.FormattedValue.expr_text);
3970-
}
3971-
39723966
/* The expression to be formatted. */
39733967
VISIT(c, expr, e->v.FormattedValue.value);
39743968

@@ -3991,11 +3985,6 @@ compiler_formatted_value(struct compiler *c, expr_ty e)
39913985
/* And push our opcode and oparg */
39923986
ADDOP_I(c, FORMAT_VALUE, oparg);
39933987

3994-
/* If we have expr_text, join the 2 strings on the stack. */
3995-
if (e->v.FormattedValue.expr_text) {
3996-
ADDOP_I(c, BUILD_STRING, 2);
3997-
}
3998-
39993988
return 1;
40003989
}
40013990

0 commit comments

Comments
 (0)