If a "=" is specified a the end of an f-string expression, the f-string will evaluate to the text of the expression, followed by '=', followed by the repr of the value of the expression.
stable ABI.
(Contributed by Victor Stinner in :issue:`36722`.)
+f-strings now support = for quick and easy debugging
+-----------------------------------------------------
+
+Add ``=`` specifier to f-strings. ``f'{expr=}'`` expands
+to the text of the expression, an equal sign, then the repr of the
+evaluated expression. So::
+
+ x = 3
+ print(f'{x*9 + 15=}')
+
+Would print ``x*9 + 15=42``.
+
+(Contributed by Eric V. Smith and Larry Hastings in :issue:`36817`.)
+
Other Language Changes
======================
expr_ty value;
int conversion;
expr_ty format_spec;
+ string expr_text;
} FormattedValue;
struct {
expr_ty _Py_Call(expr_ty func, asdl_seq * args, asdl_seq * keywords, int
lineno, int col_offset, int end_lineno, int end_col_offset,
PyArena *arena);
-#define FormattedValue(a0, a1, a2, a3, a4, a5, a6, a7) _Py_FormattedValue(a0, a1, a2, a3, a4, a5, a6, a7)
+#define FormattedValue(a0, a1, a2, a3, a4, a5, a6, a7, a8) _Py_FormattedValue(a0, a1, a2, a3, a4, a5, a6, a7, a8)
expr_ty _Py_FormattedValue(expr_ty value, int conversion, expr_ty format_spec,
- int lineno, int col_offset, int end_lineno, int
- end_col_offset, PyArena *arena);
+ string expr_text, int lineno, int col_offset, int
+ end_lineno, int end_col_offset, PyArena *arena);
#define JoinedStr(a0, a1, a2, a3, a4, a5) _Py_JoinedStr(a0, a1, a2, a3, a4, a5)
expr_ty _Py_JoinedStr(asdl_seq * values, int lineno, int col_offset, int
end_lineno, int end_col_offset, PyArena *arena);
+# -*- coding: utf-8 -*-
+# There are tests here with unicode string literals and
+# identifiers. There's a code in ast.c that was added because of a
+# failure with a non-ascii-only expression. So, I have tests for
+# that. There are workarounds that would let me run tests for that
+# code without unicode identifiers and strings, but just using them
+# directly seems like the easiest and therefore safest thing to do.
+# Unicode identifiers in tests is allowed by PEP 3131.
+
import ast
import types
import decimal
self.assertEqual(f'{3!=4!s}', 'True')
self.assertEqual(f'{3!=4!s:.3}', 'Tru')
+ def test_equal_equal(self):
+ # Because an expression ending in = has special meaning,
+ # there's a special test for ==. Make sure it works.
+
+ self.assertEqual(f'{0==1}', 'False')
+
def test_conversions(self):
self.assertEqual(f'{3.14:10.10}', ' 3.14')
self.assertEqual(f'{3.14!s:10.10}', '3.14 ')
self.assertEqual(eval('f"\\\n"'), '')
self.assertEqual(eval('f"\\\r"'), '')
+ def test_debug_conversion(self):
+ x = 'A string'
+ self.assertEqual(f'{x=}', 'x=' + repr(x))
+ self.assertEqual(f'{x =}', 'x =' + repr(x))
+ self.assertEqual(f'{x=!s}', 'x=' + str(x))
+ self.assertEqual(f'{x=!r}', 'x=' + repr(x))
+ self.assertEqual(f'{x=!a}', 'x=' + ascii(x))
+
+ x = 2.71828
+ self.assertEqual(f'{x=:.2f}', 'x=' + format(x, '.2f'))
+ self.assertEqual(f'{x=:}', 'x=' + format(x, ''))
+ self.assertEqual(f'{x=!r:^20}', 'x=' + format(repr(x), '^20'))
+ self.assertEqual(f'{x=!s:^20}', 'x=' + format(str(x), '^20'))
+ self.assertEqual(f'{x=!a:^20}', 'x=' + format(ascii(x), '^20'))
+
+ x = 9
+ self.assertEqual(f'{3*x+15=}', '3*x+15=42')
+
+ # There is code in ast.c that deals with non-ascii expression values. So,
+ # use a unicode identifier to trigger that.
+ tenπ = 31.4
+ self.assertEqual(f'{tenπ=:.2f}', 'tenπ=31.40')
+
+ # Also test with Unicode in non-identifiers.
+ self.assertEqual(f'{"Σ"=}', '"Σ"=\'Σ\'')
+
+ # Make sure nested fstrings still work.
+ self.assertEqual(f'{f"{3.1415=:.1f}":*^20}', '*****3.1415=3.1*****')
+
+ # Make sure text before and after an expression with = works
+ # correctly.
+ pi = 'π'
+ self.assertEqual(f'alpha α {pi=} ω omega', "alpha α pi='π' ω omega")
+
+ # Check multi-line expressions.
+ self.assertEqual(f'''{
+3
+=}''', '\n3\n=3')
+
+ # Since = is handled specially, make sure all existing uses of
+ # it still work.
+
+ self.assertEqual(f'{0==1}', 'False')
+ self.assertEqual(f'{0!=1}', 'True')
+ self.assertEqual(f'{0<=1}', 'True')
+ self.assertEqual(f'{0>=1}', 'False')
+ self.assertEqual(f'{(x:="5")}', '5')
+ self.assertEqual(x, '5')
+ self.assertEqual(f'{(x:=5)}', '5')
+ self.assertEqual(x, 5)
+ self.assertEqual(f'{"="}', '=')
+
+ x = 20
+ # This isn't an assignment expression, it's 'x', with a format
+ # spec of '=10'. See test_walrus: you need to use parens.
+ self.assertEqual(f'{x:=10}', ' 20')
+
+ # Test named function parameters, to make sure '=' parsing works
+ # there.
+ def f(a):
+ nonlocal x
+ oldx = x
+ x = a
+ return oldx
+ x = 0
+ self.assertEqual(f'{f(a="3=")}', '0')
+ self.assertEqual(x, '3=')
+ self.assertEqual(f'{f(a=4)}', '3=')
+ self.assertEqual(x, 4)
+
+ # Make sure __format__ is being called.
+ class C:
+ def __format__(self, s):
+ return f'FORMAT-{s}'
+ def __repr__(self):
+ return 'REPR'
+
+ self.assertEqual(f'{C()=}', 'C()=REPR')
+ self.assertEqual(f'{C()=!r}', 'C()=REPR')
+ self.assertEqual(f'{C()=:}', 'C()=FORMAT-')
+ self.assertEqual(f'{C()=: }', 'C()=FORMAT- ')
+ self.assertEqual(f'{C()=:x}', 'C()=FORMAT-x')
+ self.assertEqual(f'{C()=!r:*^20}', 'C()=********REPR********')
+
+ def test_walrus(self):
+ x = 20
+ # This isn't an assignment expression, it's 'x', with a format
+ # spec of '=10'.
+ self.assertEqual(f'{x:=10}', ' 20')
+
+ # This is an assignment expression, which requires parens.
+ self.assertEqual(f'{(x:=10)}', '10')
+ self.assertEqual(x, 10)
+
if __name__ == '__main__':
unittest.main()
eq("f'space between opening braces: { {a for a in (1, 2, 3)}}'")
eq("f'{(lambda x: x)}'")
eq("f'{(None if a else lambda x: x)}'")
+ 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)')
--- /dev/null
+Add a ``=`` feature f-strings for debugging. This can precede ``!s``,
+``!r``, or ``!a``. It produces the text of the expression, followed by
+an equal sign, followed by the repr of the value of the expression. So
+``f'{3*9+15=}'`` would be equal to the string ``'3*9+15=42'``. If
+``=`` is specified, the default conversion is set to ``!r``, unless a
+format spec is given, in which case the formatting behavior is
+unchanged, and __format__ will be used.
-- 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)
+ | FormattedValue(expr value, int? conversion, expr? format_spec, string? expr_text)
| JoinedStr(expr* values)
| Constant(constant value, string? kind)
static PyTypeObject *FormattedValue_type;
_Py_IDENTIFIER(conversion);
_Py_IDENTIFIER(format_spec);
+_Py_IDENTIFIER(expr_text);
static char *FormattedValue_fields[]={
"value",
"conversion",
"format_spec",
+ "expr_text",
};
static PyTypeObject *JoinedStr_type;
static char *JoinedStr_fields[]={
Call_type = make_type("Call", expr_type, Call_fields, 3);
if (!Call_type) return 0;
FormattedValue_type = make_type("FormattedValue", expr_type,
- FormattedValue_fields, 3);
+ FormattedValue_fields, 4);
if (!FormattedValue_type) return 0;
JoinedStr_type = make_type("JoinedStr", expr_type, JoinedStr_fields, 1);
if (!JoinedStr_type) return 0;
}
expr_ty
-FormattedValue(expr_ty value, int conversion, expr_ty format_spec, int lineno,
- int col_offset, int end_lineno, int end_col_offset, PyArena
- *arena)
+FormattedValue(expr_ty value, int conversion, expr_ty format_spec, string
+ expr_text, int lineno, int col_offset, int end_lineno, int
+ end_col_offset, PyArena *arena)
{
expr_ty p;
if (!value) {
p->v.FormattedValue.value = value;
p->v.FormattedValue.conversion = conversion;
p->v.FormattedValue.format_spec = format_spec;
+ p->v.FormattedValue.expr_text = expr_text;
p->lineno = lineno;
p->col_offset = col_offset;
p->end_lineno = end_lineno;
if (_PyObject_SetAttrId(result, &PyId_format_spec, value) == -1)
goto failed;
Py_DECREF(value);
+ value = ast2obj_string(o->v.FormattedValue.expr_text);
+ if (!value) goto failed;
+ if (_PyObject_SetAttrId(result, &PyId_expr_text, value) == -1)
+ goto failed;
+ Py_DECREF(value);
break;
case JoinedStr_kind:
result = PyType_GenericNew(JoinedStr_type, NULL, NULL);
expr_ty value;
int conversion;
expr_ty format_spec;
+ string expr_text;
if (_PyObject_LookupAttrId(obj, &PyId_value, &tmp) < 0) {
return 1;
if (res != 0) goto failed;
Py_CLEAR(tmp);
}
- *out = FormattedValue(value, conversion, format_spec, lineno,
- col_offset, end_lineno, end_col_offset, arena);
+ if (_PyObject_LookupAttrId(obj, &PyId_expr_text, &tmp) < 0) {
+ return 1;
+ }
+ if (tmp == NULL || tmp == Py_None) {
+ Py_CLEAR(tmp);
+ expr_text = NULL;
+ }
+ else {
+ int res;
+ res = obj2ast_string(tmp, &expr_text, arena);
+ if (res != 0) goto failed;
+ Py_CLEAR(tmp);
+ }
+ *out = FormattedValue(value, conversion, format_spec, expr_text,
+ lineno, col_offset, end_lineno, end_col_offset,
+ arena);
if (*out == NULL) goto failed;
return 0;
}
assert(expr_end >= expr_start);
assert(*(expr_start-1) == '{');
- assert(*expr_end == '}' || *expr_end == '!' || *expr_end == ':');
+ assert(*expr_end == '}' || *expr_end == '!' || *expr_end == ':' ||
+ *expr_end == '=');
/* If the substring is all whitespace, it's an error. We need to catch this
here, and not when we call PyParser_SimpleParseStringFlagsFilename,
struct compiling *c, const node *n);
/* Parse the f-string at *str, ending at end. We know *str starts an
- expression (so it must be a '{'). Returns the FormattedValue node,
- which includes the expression, conversion character, and
- format_spec expression.
+ expression (so it must be a '{'). Returns the FormattedValue node, which
+ includes the expression, conversion character, format_spec expression, and
+ optionally the text of the expression (if = is used).
Note that I don't do a perfect job here: I don't make sure that a
closing brace doesn't match an opening paren, for example. It
const char *expr_end;
expr_ty simple_expression;
expr_ty format_spec = NULL; /* Optional format specifier. */
- int conversion = -1; /* The conversion char. -1 if not specified. */
+ 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). */
/* Can only nest one level deep. */
if (recurse_lvl >= 2) {
ast_error(c, n, "f-string: expressions nested too deeply");
- return -1;
+ goto error;
}
/* The first char must be a left brace, or we wouldn't have gotten
ast_error(c, n,
"f-string expression part "
"cannot include a backslash");
- return -1;
+ goto error;
}
if (quote_char) {
/* We're inside a string. See if we're at the end. */
} else if (ch == '[' || ch == '{' || ch == '(') {
if (nested_depth >= MAXLEVEL) {
ast_error(c, n, "f-string: too many nested parenthesis");
- return -1;
+ goto error;
}
parenstack[nested_depth] = ch;
nested_depth++;
/* Error: can't include a comment character, inside parens
or not. */
ast_error(c, n, "f-string expression part cannot include '#'");
- return -1;
+ goto error;
} else if (nested_depth == 0 &&
- (ch == '!' || ch == ':' || ch == '}')) {
- /* First, test for the special case of "!=". Since '=' is
- not an allowed conversion character, nothing is lost in
- this test. */
- if (ch == '!' && *str+1 < end && *(*str+1) == '=') {
- /* This isn't a conversion character, just continue. */
- continue;
+ (ch == '!' || ch == ':' || ch == '}' ||
+ ch == '=' || ch == '>' || ch == '<')) {
+ /* See if there's a next character. */
+ if (*str+1 < end) {
+ char next = *(*str+1);
+
+ /* For "!=". since '=' is not an allowed conversion character,
+ nothing is lost in this test. */
+ if ((ch == '!' && next == '=') || /* != */
+ (ch == '=' && next == '=') || /* == */
+ (ch == '<' && next == '=') || /* <= */
+ (ch == '>' && next == '=') /* >= */
+ ) {
+ *str += 1;
+ continue;
+ }
+ /* Don't get out of the loop for these, if they're single
+ chars (not part of 2-char tokens). If by themselves, they
+ don't end an expression (unlike say '!'). */
+ if (ch == '>' || ch == '<') {
+ continue;
+ }
}
+
/* Normal way out of this loop. */
break;
} else if (ch == ']' || ch == '}' || ch == ')') {
if (!nested_depth) {
ast_error(c, n, "f-string: unmatched '%c'", ch);
- return -1;
+ goto error;
}
nested_depth--;
int opening = parenstack[nested_depth];
"f-string: closing parenthesis '%c' "
"does not match opening parenthesis '%c'",
ch, opening);
- return -1;
+ goto error;
}
} else {
/* Just consume this char and loop around. */
let's just do that.*/
if (quote_char) {
ast_error(c, n, "f-string: unterminated string");
- return -1;
+ goto error;
}
if (nested_depth) {
int opening = parenstack[nested_depth - 1];
ast_error(c, n, "f-string: unmatched '%c'", opening);
- return -1;
+ goto error;
}
if (*str >= end)
conversion or format_spec. */
simple_expression = fstring_compile_expr(expr_start, expr_end, c, n);
if (!simple_expression)
- return -1;
+ goto error;
+
+ /* Check for =, which puts the text value of the expression in
+ 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;
+ }
/* Check for a conversion char, if present. */
if (**str == '!') {
*str += 1;
/* Validate the conversion. */
- if (!(conversion == 's' || conversion == 'r'
- || conversion == 'a')) {
+ if (!(conversion == 's' || conversion == 'r' || conversion == 'a')) {
ast_error(c, n,
"f-string: invalid conversion character: "
"expected 's', 'r', or 'a'");
- return -1;
+ goto error;
}
+
+ }
+ if (equal_flag) {
+ Py_ssize_t len = expr_text_end-expr_start;
+ expr_text = PyUnicode_FromStringAndSize(expr_start, len);
+ if (!expr_text)
+ goto error;
}
/* Check for the format spec, if present. */
/* Parse the format spec. */
format_spec = fstring_parse(str, end, raw, recurse_lvl+1, c, n);
if (!format_spec)
- return -1;
+ goto error;
}
if (*str >= end || **str != '}')
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) {
+ 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, LINENO(n), n->n_col_offset,
- n->n_end_lineno, n->n_end_col_offset,
- c->c_arena);
+ format_spec, expr_text, LINENO(n),
+ n->n_col_offset, n->n_end_lineno,
+ n->n_end_col_offset, c->c_arena);
if (!*expression)
- return -1;
+ goto error;
return 0;
unexpected_end_of_string:
ast_error(c, n, "f-string: expecting '}'");
+ /* Falls through to error. */
+
+error:
+ Py_XDECREF(expr_text);
return -1;
+
}
/* Return -1 on error.
}
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':
/* See if any conversion is specified. */
switch (which_conversion) {
+ case FVC_NONE: conv_fn = NULL; break;
case FVC_STR: conv_fn = PyObject_Str; break;
case FVC_REPR: conv_fn = PyObject_Repr; break;
case FVC_ASCII: conv_fn = PyObject_ASCII; break;
-
- /* Must be 0 (meaning no conversion), since only four
- values are allowed by (oparg & FVC_MASK). */
- default: conv_fn = NULL; break;
+ default:
+ PyErr_Format(PyExc_SystemError,
+ "unexpected conversion flag %d",
+ which_conversion);
+ goto error;
}
/* If there's a conversion function, call it and replace
/* Our oparg encodes 2 pieces of information: the conversion
character, and whether or not a format_spec was provided.
- Convert the conversion char to 2 bits:
- None: 000 0x0 FVC_NONE
+ Convert the conversion char to 3 bits:
+ : 000 0x0 FVC_NONE The default if nothing specified.
!s : 001 0x1 FVC_STR
!r : 010 0x2 FVC_REPR
!a : 011 0x3 FVC_ASCII
no : 000 0x0
*/
+ int conversion = e->v.FormattedValue.conversion;
int oparg;
- /* Evaluate the expression to be formatted. */
+ 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);
- switch (e->v.FormattedValue.conversion) {
+ switch (conversion) {
case 's': oparg = FVC_STR; break;
case 'r': oparg = FVC_REPR; break;
case 'a': oparg = FVC_ASCII; break;
case -1: oparg = FVC_NONE; break;
default:
- PyErr_SetString(PyExc_SystemError,
- "Unrecognized conversion character");
+ PyErr_Format(PyExc_SystemError,
+ "Unrecognized conversion character %d", conversion);
return 0;
}
if (e->v.FormattedValue.format_spec) {
/* 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;
}