diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst index d6388f8faab..874b9b12943 100644 --- a/Doc/whatsnew/3.8.rst +++ b/Doc/whatsnew/3.8.rst @@ -148,6 +148,20 @@ extensions compiled in release mode and for C extensions compiled with the 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 ====================== diff --git a/Include/Python-ast.h b/Include/Python-ast.h index 0c739db6d14..08d50ffcddf 100644 --- a/Include/Python-ast.h +++ b/Include/Python-ast.h @@ -330,6 +330,7 @@ struct _expr { expr_ty value; int conversion; expr_ty format_spec; + string expr_text; } FormattedValue; struct { @@ -637,10 +638,10 @@ expr_ty _Py_Compare(expr_ty left, asdl_int_seq * ops, asdl_seq * comparators, 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); diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index 9d60be3a29a..a0fae50d172 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -1,3 +1,12 @@ +# -*- 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 @@ -878,6 +887,12 @@ non-important content 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 ') @@ -1049,6 +1064,100 @@ non-important content 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() diff --git a/Lib/test/test_future.py b/Lib/test/test_future.py index c60a016f01f..38de3dfdafc 100644 --- a/Lib/test/test_future.py +++ b/Lib/test/test_future.py @@ -255,6 +255,15 @@ class AnnotationsFutureTestCase(unittest.TestCase): 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)') diff --git a/Misc/NEWS.d/next/Core and Builtins/2019-05-02-11-48-08.bpo-36774.ZqbJ1J.rst b/Misc/NEWS.d/next/Core and Builtins/2019-05-02-11-48-08.bpo-36774.ZqbJ1J.rst new file mode 100644 index 00000000000..b73547c84a7 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2019-05-02-11-48-08.bpo-36774.ZqbJ1J.rst @@ -0,0 +1,7 @@ +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. diff --git a/Parser/Python.asdl b/Parser/Python.asdl index 668d3c93809..626fa4fede4 100644 --- a/Parser/Python.asdl +++ b/Parser/Python.asdl @@ -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) + | FormattedValue(expr value, int? conversion, expr? format_spec, string? expr_text) | JoinedStr(expr* values) | Constant(constant value, string? kind) diff --git a/Python/Python-ast.c b/Python/Python-ast.c index 6c8488f8fe6..cb53a41cdf3 100644 --- a/Python/Python-ast.c +++ b/Python/Python-ast.c @@ -314,10 +314,12 @@ static char *Call_fields[]={ 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[]={ @@ -950,7 +952,7 @@ static int init_types(void) 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; @@ -2249,9 +2251,9 @@ Call(expr_ty func, asdl_seq * args, asdl_seq * keywords, int lineno, int } 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) { @@ -2266,6 +2268,7 @@ FormattedValue(expr_ty value, int conversion, expr_ty format_spec, int lineno, 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; @@ -3496,6 +3499,11 @@ ast2obj_expr(void* _o) 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); @@ -7148,6 +7156,7 @@ obj2ast_expr(PyObject* obj, expr_ty* out, PyArena* arena) expr_ty value; int conversion; expr_ty format_spec; + string expr_text; if (_PyObject_LookupAttrId(obj, &PyId_value, &tmp) < 0) { return 1; @@ -7188,8 +7197,22 @@ obj2ast_expr(PyObject* obj, expr_ty* out, PyArena* arena) 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; } diff --git a/Python/ast.c b/Python/ast.c index 4687f8178b0..21abd7e88d8 100644 --- a/Python/ast.c +++ b/Python/ast.c @@ -4854,7 +4854,8 @@ fstring_compile_expr(const char *expr_start, const char *expr_end, 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, @@ -4997,9 +4998,9 @@ fstring_parse(const char **str, const char *end, int raw, int recurse_lvl, 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 @@ -5016,7 +5017,12 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, 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). */ @@ -5033,7 +5039,7 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, /* 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 @@ -5061,7 +5067,7 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, 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. */ @@ -5106,7 +5112,7 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, } 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++; @@ -5114,22 +5120,38 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, /* 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]; @@ -5141,7 +5163,7 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, "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. */ @@ -5154,12 +5176,12 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, 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) @@ -5170,7 +5192,22 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, 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 == '!') { @@ -5182,13 +5219,19 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, *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. */ @@ -5202,7 +5245,7 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, /* 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 != '}') @@ -5213,20 +5256,31 @@ 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) { + 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. diff --git a/Python/ast_unparse.c b/Python/ast_unparse.c index 916ad5f97f0..25a5c698a1d 100644 --- a/Python/ast_unparse.c +++ b/Python/ast_unparse.c @@ -655,6 +655,11 @@ 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': diff --git a/Python/ceval.c b/Python/ceval.c index e616a3f5398..4e43df2713d 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -3435,13 +3435,15 @@ main_loop: /* 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 diff --git a/Python/compile.c b/Python/compile.c index 86f2a09ffb3..dd27ba840f7 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -3946,8 +3946,8 @@ compiler_formatted_value(struct compiler *c, expr_ty e) /* 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 @@ -3957,19 +3957,26 @@ compiler_formatted_value(struct compiler *c, expr_ty e) 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) { @@ -3980,6 +3987,12 @@ 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; }