[Python-checkins] bpo-36817: Add f-string debugging using '='. (GH-13123)

Eric V. Smith webhook-mailer at python.org
Wed May 8 16:28:55 EDT 2019


https://github.com/python/cpython/commit/9a4135e939bc223f592045a38e0f927ba170da32
commit: 9a4135e939bc223f592045a38e0f927ba170da32
branch: master
author: Eric V. Smith <ericvsmith at users.noreply.github.com>
committer: GitHub <noreply at github.com>
date: 2019-05-08T16:28:48-04:00
summary:

bpo-36817: Add f-string debugging using '='. (GH-13123)

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.

files:
A Misc/NEWS.d/next/Core and Builtins/2019-05-02-11-48-08.bpo-36774.ZqbJ1J.rst
M Doc/whatsnew/3.8.rst
M Include/Python-ast.h
M Lib/test/test_fstring.py
M Lib/test/test_future.py
M Parser/Python.asdl
M Python/Python-ast.c
M Python/ast.c
M Python/ast_unparse.c
M Python/ceval.c
M Python/compile.c

diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst
index d6388f8faaba..874b9b129432 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 0c739db6d141..08d50ffcddf6 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 9d60be3a29a1..a0fae50d1720 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 @@ def test_not_equal(self):
         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 @@ def test_backslash_char(self):
         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 c60a016f01f4..38de3dfdafcd 100644
--- a/Lib/test/test_future.py
+++ b/Lib/test/test_future.py
@@ -255,6 +255,15 @@ def test_annotations(self):
         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 000000000000..b73547c84a7d
--- /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 668d3c938090..626fa4fede47 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 6c8488f8fe68..cb53a41cdf35 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 4687f8178b02..21abd7e88d84 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 916ad5f97f0c..25a5c698a1db 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 e616a3f53989..4e43df2713d8 100644
--- a/Python/ceval.c
+++ b/Python/ceval.c
@@ -3435,13 +3435,15 @@ _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
 
             /* 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 86f2a09ffb3a..dd27ba840f75 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;
 }
 



More information about the Python-checkins mailing list