Skip to content
Merged
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
7 changes: 3 additions & 4 deletions Include/Python-ast.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions Lib/test/test_fstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -1150,6 +1150,24 @@ def __repr__(self):

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

# Make sure leading and following text works.
x = 'foo'
self.assertEqual(f'X{x=}Y', 'Xx='+repr(x)+'Y')

# Make sure whitespace around the = works.
self.assertEqual(f'X{x =}Y', 'Xx ='+repr(x)+'Y')
self.assertEqual(f'X{x= }Y', 'Xx= '+repr(x)+'Y')
self.assertEqual(f'X{x = }Y', 'Xx = '+repr(x)+'Y')

# These next lines contains tabs. Backslash escapes don't
# work in f-strings.
# patchcheck doens't like these tabs. So the only way to test
# this will be to dynamically created and exec the f-strings. But
# that's such a hassle I'll save it for another day. For now, convert
# the tabs to spaces just to shut up patchcheck.
#self.assertEqual(f'X{x =}Y', 'Xx\t='+repr(x)+'Y')
#self.assertEqual(f'X{x = }Y', 'Xx\t=\t'+repr(x)+'Y')

def test_walrus(self):
x = 20
# This isn't an assignment expression, it's 'x', with a format
Expand Down
15 changes: 9 additions & 6 deletions Lib/test/test_future.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,12 +270,6 @@ def test_annotations(self):
eq("f'{x}'")
eq("f'{x!r}'")
eq("f'{x!a}'")
eq("f'{x=!r}'")
eq("f'{x=:}'")
eq("f'{x=:.2f}'")
eq("f'{x=!r}'")
eq("f'{x=!a}'")
eq("f'{x=!s:*^20}'")
eq('(yield from outside_of_generator)')
eq('(yield)')
eq('(yield a + b)')
Expand All @@ -290,6 +284,15 @@ def test_annotations(self):
eq("(x:=10)")
eq("f'{(x:=10):=10}'")

# f-strings with '=' don't round trip very well, so set the expected

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.

I think it is worth to move this into separate method. It can even be decorated as cpython_only. I think that if an alternate implementation is smart enough to roundtrip these case, it is still a valid Python implementation.

# result explicitely.
self.assertAnnotationEqual("f'{x=!r}'", expected="f'x={x!r}'")
self.assertAnnotationEqual("f'{x=:}'", expected="f'x={x:}'")
self.assertAnnotationEqual("f'{x=:.2f}'", expected="f'x={x:.2f}'")
self.assertAnnotationEqual("f'{x=!r}'", expected="f'x={x!r}'")
self.assertAnnotationEqual("f'{x=!a}'", expected="f'x={x!a}'")
self.assertAnnotationEqual("f'{x=!s:*^20}'", expected="f'x={x!s:*^20}'")


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Improve the AST for "debug" f-strings, which use '=' to print out the source
of the expression being evaluated. Delete expr_text from the FormattedValue
node, and instead use a Constant string node (possibly merged with adjacent
constant expressions inside the f-string).
2 changes: 1 addition & 1 deletion Parser/Python.asdl
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ module Python
-- x < 4 < 3 and (x < 4) < 3
| Compare(expr left, cmpop* ops, expr* comparators)
| Call(expr func, expr* args, keyword* keywords)
| FormattedValue(expr value, int? conversion, expr? format_spec, string? expr_text)
| FormattedValue(expr value, int? conversion, expr? format_spec)
| JoinedStr(expr* values)
| Constant(constant value, string? kind)

Expand Down
35 changes: 6 additions & 29 deletions Python/Python-ast.c

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

90 changes: 46 additions & 44 deletions Python/ast.c
Original file line number Diff line number Diff line change
Expand Up @@ -5006,10 +5006,16 @@ fstring_parse(const char **str, const char *end, int raw, int recurse_lvl,
closing brace doesn't match an opening paren, for example. It
doesn't need to error on all invalid expressions, just correctly
find the end of all valid ones. Any errors inside the expression
will be caught when we parse it later. */
will be caught when we parse it later.

*expression is set to the expression. For an '=' "debug" expression,
*expr_text is set to the debug text (the original text of the expression,
*including the '=' and any whitespace around it, as a string object). If

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.

Suggested change
*including the '=' and any whitespace around it, as a string object). If
including the '=' and any whitespace around it, as a string object). If

*not a debug expression, *expr_text set to NULL. */

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.

Suggested change
*not a debug expression, *expr_text set to NULL. */
not a debug expression, *expr_text set to NULL. */

static int
fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl,
expr_ty *expression, struct compiling *c, const node *n)
PyObject **expr_text, expr_ty *expression,
struct compiling *c, const node *n)
{
/* Return -1 on error, else 0. */

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

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

/* Skip over ASCII whitespace. No need to test for end of string
here, since we know there's at least a trailing quote somewhere
ahead. */
while (Py_ISSPACE(**str)) {
*str += 1;
}
expr_text_end = *str;

/* Set *expr_text to the text of the expression. */
*expr_text = PyUnicode_FromStringAndSize(expr_start, *str-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.

Note that *expr_text can be leaked at the error label.

if (!*expr_text) {
goto error;
}
} else {
*expr_text = NULL;
}

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

}
if (equal_flag) {
Py_ssize_t len = expr_text_end - expr_start;
expr_text = PyUnicode_FromStringAndSize(expr_start, len);
if (!expr_text) {
goto error;
}
if (PyArena_AddPyObject(c->c_arena, expr_text) < 0) {
Py_DECREF(expr_text);
goto error;
}
}

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

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

/* And now create the FormattedValue node that represents this
entire expression with the conversion and format spec. */
*expression = FormattedValue(simple_expression, conversion,
format_spec, expr_text, LINENO(n),
format_spec, LINENO(n),
n->n_col_offset, n->n_end_lineno,
n->n_end_col_offset, c->c_arena);
if (!*expression)
Expand Down Expand Up @@ -5313,7 +5311,7 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl,
static int
fstring_find_literal_and_expr(const char **str, const char *end, int raw,
int recurse_lvl, PyObject **literal,
expr_ty *expression,
PyObject **expr_text, expr_ty *expression,
struct compiling *c, const node *n)
{
int result;
Expand Down Expand Up @@ -5341,7 +5339,8 @@ fstring_find_literal_and_expr(const char **str, const char *end, int raw,
/* We must now be the start of an expression, on a '{'. */
assert(**str == '{');

if (fstring_find_expr(str, end, raw, recurse_lvl, expression, c, n) < 0)
if (fstring_find_expr(str, end, raw, recurse_lvl, expr_text,
expression, c, n) < 0)
goto error;

return 0;
Expand Down Expand Up @@ -5604,39 +5603,42 @@ FstringParser_ConcatFstring(FstringParser *state, const char **str,

/* Parse the f-string. */
while (1) {
PyObject *literal = NULL;
PyObject *literal[2] = {NULL, NULL};
expr_ty expression = NULL;

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

/* Add the literal, if any. */
if (!literal) {
/* Do nothing. Just leave last_str alone (and possibly
NULL). */
} else if (!state->last_str) {
/* Note that the literal can be zero length, if the
input string is "\\\n" or "\\\r", among others. */
state->last_str = literal;
literal = NULL;
} else {
/* We have a literal, concatenate it. */
assert(PyUnicode_GET_LENGTH(literal) != 0);
if (FstringParser_ConcatAndDel(state, literal) < 0)
return -1;
literal = NULL;
/* Add the literals, if any. */
for (int i = 0; i < 2; i++) {
if (!literal[i]) {
/* Do nothing. Just leave last_str alone (and possibly
NULL). */
} else if (!state->last_str) {
/* Note that the literal can be zero length, if the
input string is "\\\n" or "\\\r", among others. */
state->last_str = literal[i];
literal[i] = NULL;
} else {
/* We have a literal, concatenate it. */
assert(PyUnicode_GET_LENGTH(literal[i]) != 0);
if (FstringParser_ConcatAndDel(state, literal[i]) < 0)
return -1;

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.

literal[1] can be leaked here.

literal[i] = NULL;
}
}

/* We've dealt with the literal now. It can't be leaked on further
/* We've dealt with the literals now. They can't be leaked on further
errors. */
assert(literal == NULL);
assert(literal[0] == NULL);
assert(literal[1] == NULL);

/* See if we should just loop around to get the next literal
and expression, while ignoring the expression this
Expand Down
5 changes: 0 additions & 5 deletions Python/ast_unparse.c
Original file line number Diff line number Diff line change
Expand Up @@ -665,11 +665,6 @@ append_formattedvalue(_PyUnicodeWriter *writer, expr_ty e, bool is_format_spec)
}
Py_DECREF(temp_fv_str);

if (e->v.FormattedValue.expr_text) {
/* Use the = for debug text expansion. */
APPEND_STR("=");
}

if (e->v.FormattedValue.conversion > 0) {
switch (e->v.FormattedValue.conversion) {
case 'a':
Expand Down
11 changes: 0 additions & 11 deletions Python/compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -3963,12 +3963,6 @@ compiler_formatted_value(struct compiler *c, expr_ty e)
int conversion = e->v.FormattedValue.conversion;
int oparg;

if (e->v.FormattedValue.expr_text) {
/* Push the text of the expression (which already has the '=' in
it. */
ADDOP_LOAD_CONST(c, e->v.FormattedValue.expr_text);
}

/* The expression to be formatted. */
VISIT(c, expr, e->v.FormattedValue.value);

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

/* If we have expr_text, join the 2 strings on the stack. */
if (e->v.FormattedValue.expr_text) {
ADDOP_I(c, BUILD_STRING, 2);
}

return 1;
}

Expand Down