From 67977672360659f203664d23cfc52b5e07e4381a Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Thu, 2 May 2019 07:03:45 -0400 Subject: [PATCH 01/53] WIP: Mostly works, except repr isn't called (so only string expressions currently work). --- Include/Python-ast.h | 7 ++++--- Include/ceval.h | 7 ++++--- Parser/Python.asdl | 2 +- Python/Python-ast.c | 35 +++++++++++++++++++++++++++++------ Python/ast.c | 23 +++++++++++++++-------- Python/ceval.c | 15 ++++++++++++--- Python/compile.c | 44 ++++++++++++++++++++++++++++++++++---------- 7 files changed, 99 insertions(+), 34 deletions(-) diff --git a/Include/Python-ast.h b/Include/Python-ast.h index 0c739db6d14124..4d461930d02aeb 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_source; } 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_source, 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/Include/ceval.h b/Include/ceval.h index 11283c0a570b7e..9bf28664ad1ca1 100644 --- a/Include/ceval.h +++ b/Include/ceval.h @@ -225,13 +225,14 @@ PyAPI_FUNC(void) _PyEval_SignalAsyncExc(void); #endif /* Masks and values used by FORMAT_VALUE opcode. */ -#define FVC_MASK 0x3 +#define FVC_MASK 0x7 #define FVC_NONE 0x0 #define FVC_STR 0x1 #define FVC_REPR 0x2 #define FVC_ASCII 0x3 -#define FVS_MASK 0x4 -#define FVS_HAVE_SPEC 0x4 +#define FVC_DEBUG 0x4 +#define FVS_MASK 0x8 +#define FVS_HAVE_SPEC 0x8 #ifdef __cplusplus } diff --git a/Parser/Python.asdl b/Parser/Python.asdl index 668d3c938090f4..d1a5c80c3f66d5 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_source) | JoinedStr(expr* values) | Constant(constant value, string? kind) diff --git a/Python/Python-ast.c b/Python/Python-ast.c index 6c8488f8fe680a..ff113ad20c91d8 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_source); static char *FormattedValue_fields[]={ "value", "conversion", "format_spec", + "expr_source", }; 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_source, 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_source = expr_source; 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_source); + if (!value) goto failed; + if (_PyObject_SetAttrId(result, &PyId_expr_source, 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_source; 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_source, &tmp) < 0) { + return 1; + } + if (tmp == NULL || tmp == Py_None) { + Py_CLEAR(tmp); + expr_source = NULL; + } + else { + int res; + res = obj2ast_string(tmp, &expr_source, arena); + if (res != 0) goto failed; + Py_CLEAR(tmp); + } + *out = FormattedValue(value, conversion, format_spec, expr_source, + 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 4687f8178b0244..d71425b993dd7f 100644 --- a/Python/ast.c +++ b/Python/ast.c @@ -4997,9 +4997,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 !x 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 @@ -5017,6 +5017,7 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, expr_ty simple_expression; expr_ty format_spec = NULL; /* Optional format specifier. */ int conversion = -1; /* The conversion char. -1 if not specified. */ + PyObject *expr_source = NULL; /* The text of the expression. */ /* 0 if we're not in a string, else the quote char we're trying to match (single or double quote). */ @@ -5183,12 +5184,18 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, /* Validate the conversion. */ if (!(conversion == 's' || conversion == 'r' - || conversion == 'a')) { + || conversion == 'a' || conversion == 'x')) { ast_error(c, n, "f-string: invalid conversion character: " - "expected 's', 'r', or 'a'"); + "expected 's', 'r', 'a', or 'x'"); return -1; } + + /* If !x, then save the source to the expression. */ + if (conversion == 'x') { + expr_source = PyUnicode_FromStringAndSize(expr_start, + expr_end-expr_start); + } } /* Check for the format spec, if present. */ @@ -5216,9 +5223,9 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, /* 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_source, LINENO(n), + n->n_col_offset, n->n_end_lineno, + n->n_end_col_offset, c->c_arena); if (!*expression) return -1; diff --git a/Python/ceval.c b/Python/ceval.c index 8ae273e0820d2a..39b27c219dc3b9 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -3436,13 +3436,18 @@ _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; + /* !x is a special case, handled later. */ + case FVC_DEBUG: conv_fn = NULL; 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 @@ -3456,6 +3461,10 @@ _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag) goto error; } value = result; + } else if (which_conversion == FVC_DEBUG) { + /* If we're using !x, do the formatting, replace value with + the result. */ + printf("debug\n"); } /* If value is a unicode object, and there's no fmt_spec, diff --git a/Python/compile.c b/Python/compile.c index 86f2a09ffb3a68..e518f295c203fd 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -212,7 +212,7 @@ static int compiler_async_comprehension_generator( static PyCodeObject *assemble(struct compiler *, int addNone); static PyObject *__doc__, *__annotations__; - +static PyObject *equal_str; #define CAPSULE_NAME "compile.c compiler unit" PyObject * @@ -3946,26 +3946,50 @@ 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 - !s : 001 0x1 FVC_STR - !r : 010 0x2 FVC_REPR - !a : 011 0x3 FVC_ASCII + Convert the conversion char to 3 bits: + None: 0000 0x0 FVC_NONE + !s : 0001 0x1 FVC_STR + !r : 0010 0x2 FVC_REPR + !a : 0011 0x3 FVC_ASCII + !x : 0100 0x4 FVC_DEBUG next bit is whether or not we have a format spec: - yes : 100 0x4 - no : 000 0x0 + yes : 1000 0x8 + no : 0000 0x0 */ int oparg; - /* Evaluate the expression to be formatted. */ - VISIT(c, expr, e->v.FormattedValue.value); + if (!equal_str) { + equal_str = PyUnicode_InternFromString("="); + if (!equal_str) + return 0; + } + + if (e->v.FormattedValue.conversion == 'x') { + /* Special handling here to generate basically: + format( + '=' + repr(value), format_spec) */ + assert(e->v.FormattedValue.expr_source != NULL); + + /* The text of the expression. */ + ADDOP_LOAD_CONST(c, e->v.FormattedValue.expr_source); + /* The equal sign. */ + ADDOP_LOAD_CONST(c, equal_str); + /* Evaluate the expression to be formatted. */ + VISIT(c, expr, e->v.FormattedValue.value); + /* Call repr on it. */ + /* 3 for the text, the "=", and the value. */ + ADDOP_I(c, BUILD_STRING, 3); + } else { + /* Just evaluate the expression to be formatted. */ + VISIT(c, expr, e->v.FormattedValue.value); + } switch (e->v.FormattedValue.conversion) { case 's': oparg = FVC_STR; break; case 'r': oparg = FVC_REPR; break; case 'a': oparg = FVC_ASCII; break; + case 'x': oparg = FVC_NONE; break; /* Already handled above. */ case -1: oparg = FVC_NONE; break; default: PyErr_SetString(PyExc_SystemError, From 93df01742e6396c0a46d230644db4672bc98ae11 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Thu, 2 May 2019 07:19:44 -0400 Subject: [PATCH 02/53] WIP: Change !x to !d, to match an earlier discussion on python-ideas. --- Python/ast.c | 6 +++--- Python/compile.c | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Python/ast.c b/Python/ast.c index d71425b993dd7f..b90337ce6d64cc 100644 --- a/Python/ast.c +++ b/Python/ast.c @@ -5184,15 +5184,15 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, /* Validate the conversion. */ if (!(conversion == 's' || conversion == 'r' - || conversion == 'a' || conversion == 'x')) { + || conversion == 'a' || conversion == 'd')) { ast_error(c, n, "f-string: invalid conversion character: " - "expected 's', 'r', 'a', or 'x'"); + "expected 's', 'r', 'a', or 'd'"); return -1; } /* If !x, then save the source to the expression. */ - if (conversion == 'x') { + if (conversion == 'd') { expr_source = PyUnicode_FromStringAndSize(expr_start, expr_end-expr_start); } diff --git a/Python/compile.c b/Python/compile.c index e518f295c203fd..82e9e5fab81936 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -3966,7 +3966,7 @@ compiler_formatted_value(struct compiler *c, expr_ty e) return 0; } - if (e->v.FormattedValue.conversion == 'x') { + if (e->v.FormattedValue.conversion == 'd') { /* Special handling here to generate basically: format( + '=' + repr(value), format_spec) */ assert(e->v.FormattedValue.expr_source != NULL); @@ -3989,7 +3989,7 @@ compiler_formatted_value(struct compiler *c, expr_ty e) case 's': oparg = FVC_STR; break; case 'r': oparg = FVC_REPR; break; case 'a': oparg = FVC_ASCII; break; - case 'x': oparg = FVC_NONE; break; /* Already handled above. */ + case 'd': oparg = FVC_NONE; break; /* Already handled above. */ case -1: oparg = FVC_NONE; break; default: PyErr_SetString(PyExc_SystemError, From 99283dc0b369aa954e38a59aebc7ba6ea2f272f3 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Thu, 2 May 2019 09:02:02 -0400 Subject: [PATCH 03/53] Call repr on the resulting expression. Change expr_source to expr_text. --- Include/Python-ast.h | 4 ++-- Parser/Python.asdl | 2 +- Python/Python-ast.c | 22 +++++++++++----------- Python/ast.c | 8 ++++---- Python/compile.c | 11 +++++++---- 5 files changed, 25 insertions(+), 22 deletions(-) diff --git a/Include/Python-ast.h b/Include/Python-ast.h index 4d461930d02aeb..08d50ffcddf609 100644 --- a/Include/Python-ast.h +++ b/Include/Python-ast.h @@ -330,7 +330,7 @@ struct _expr { expr_ty value; int conversion; expr_ty format_spec; - string expr_source; + string expr_text; } FormattedValue; struct { @@ -640,7 +640,7 @@ expr_ty _Py_Call(expr_ty func, asdl_seq * args, asdl_seq * keywords, int PyArena *arena); #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, - string expr_source, int lineno, int col_offset, int + 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 diff --git a/Parser/Python.asdl b/Parser/Python.asdl index d1a5c80c3f66d5..626fa4fede476b 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, string? expr_source) + | 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 ff113ad20c91d8..cb53a41cdf35bd 100644 --- a/Python/Python-ast.c +++ b/Python/Python-ast.c @@ -314,12 +314,12 @@ static char *Call_fields[]={ static PyTypeObject *FormattedValue_type; _Py_IDENTIFIER(conversion); _Py_IDENTIFIER(format_spec); -_Py_IDENTIFIER(expr_source); +_Py_IDENTIFIER(expr_text); static char *FormattedValue_fields[]={ "value", "conversion", "format_spec", - "expr_source", + "expr_text", }; static PyTypeObject *JoinedStr_type; static char *JoinedStr_fields[]={ @@ -2252,7 +2252,7 @@ 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, string - expr_source, int lineno, int col_offset, int end_lineno, int + expr_text, int lineno, int col_offset, int end_lineno, int end_col_offset, PyArena *arena) { expr_ty p; @@ -2268,7 +2268,7 @@ FormattedValue(expr_ty value, int conversion, expr_ty format_spec, string p->v.FormattedValue.value = value; p->v.FormattedValue.conversion = conversion; p->v.FormattedValue.format_spec = format_spec; - p->v.FormattedValue.expr_source = expr_source; + p->v.FormattedValue.expr_text = expr_text; p->lineno = lineno; p->col_offset = col_offset; p->end_lineno = end_lineno; @@ -3499,9 +3499,9 @@ 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_source); + value = ast2obj_string(o->v.FormattedValue.expr_text); if (!value) goto failed; - if (_PyObject_SetAttrId(result, &PyId_expr_source, value) == -1) + if (_PyObject_SetAttrId(result, &PyId_expr_text, value) == -1) goto failed; Py_DECREF(value); break; @@ -7156,7 +7156,7 @@ obj2ast_expr(PyObject* obj, expr_ty* out, PyArena* arena) expr_ty value; int conversion; expr_ty format_spec; - string expr_source; + string expr_text; if (_PyObject_LookupAttrId(obj, &PyId_value, &tmp) < 0) { return 1; @@ -7197,20 +7197,20 @@ obj2ast_expr(PyObject* obj, expr_ty* out, PyArena* arena) if (res != 0) goto failed; Py_CLEAR(tmp); } - if (_PyObject_LookupAttrId(obj, &PyId_expr_source, &tmp) < 0) { + if (_PyObject_LookupAttrId(obj, &PyId_expr_text, &tmp) < 0) { return 1; } if (tmp == NULL || tmp == Py_None) { Py_CLEAR(tmp); - expr_source = NULL; + expr_text = NULL; } else { int res; - res = obj2ast_string(tmp, &expr_source, arena); + res = obj2ast_string(tmp, &expr_text, arena); if (res != 0) goto failed; Py_CLEAR(tmp); } - *out = FormattedValue(value, conversion, format_spec, expr_source, + *out = FormattedValue(value, conversion, format_spec, expr_text, lineno, col_offset, end_lineno, end_col_offset, arena); if (*out == NULL) goto failed; diff --git a/Python/ast.c b/Python/ast.c index b90337ce6d64cc..1c1c5d91fb02d1 100644 --- a/Python/ast.c +++ b/Python/ast.c @@ -5017,7 +5017,7 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, expr_ty simple_expression; expr_ty format_spec = NULL; /* Optional format specifier. */ int conversion = -1; /* The conversion char. -1 if not specified. */ - PyObject *expr_source = NULL; /* The text of the expression. */ + PyObject *expr_text = NULL; /* The text of the expression, used for !d. */ /* 0 if we're not in a string, else the quote char we're trying to match (single or double quote). */ @@ -5193,8 +5193,8 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, /* If !x, then save the source to the expression. */ if (conversion == 'd') { - expr_source = PyUnicode_FromStringAndSize(expr_start, - expr_end-expr_start); + expr_text = PyUnicode_FromStringAndSize(expr_start, + expr_end-expr_start); } } @@ -5223,7 +5223,7 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, /* 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_source, LINENO(n), + format_spec, expr_text, LINENO(n), n->n_col_offset, n->n_end_lineno, n->n_end_col_offset, c->c_arena); if (!*expression) diff --git a/Python/compile.c b/Python/compile.c index 82e9e5fab81936..28e08f93594b79 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -3969,16 +3969,19 @@ compiler_formatted_value(struct compiler *c, expr_ty e) if (e->v.FormattedValue.conversion == 'd') { /* Special handling here to generate basically: format( + '=' + repr(value), format_spec) */ - assert(e->v.FormattedValue.expr_source != NULL); + assert(e->v.FormattedValue.expr_text != NULL); /* The text of the expression. */ - ADDOP_LOAD_CONST(c, e->v.FormattedValue.expr_source); + ADDOP_LOAD_CONST(c, e->v.FormattedValue.expr_text); /* The equal sign. */ ADDOP_LOAD_CONST(c, equal_str); /* Evaluate the expression to be formatted. */ VISIT(c, expr, e->v.FormattedValue.value); - /* Call repr on it. */ - /* 3 for the text, the "=", and the value. */ + /* Call repr on it, by using FORMAT_VALUE with FVC_REPR. */ + ADDOP_I(c, FORMAT_VALUE, FVC_REPR); + + /* Now combine the 3 strings on top of the stack: the text of the + expression, the "=", and the repr'd value. */ ADDOP_I(c, BUILD_STRING, 3); } else { /* Just evaluate the expression to be formatted. */ From 1b29503a2d6df8ccfecf24518d20c97373259da9 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Thu, 2 May 2019 09:31:00 -0400 Subject: [PATCH 04/53] Add simple tests. --- Lib/test/test_fstring.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index 9d60be3a29a176..bfb59602e0849f 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -1049,6 +1049,14 @@ 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!d}', 'x=' + repr(x)) + self.assertEqual(f'{x !d}', 'x =' + repr(x)) + + x = 9 + self.assertEqual(f'{3*x+15!d}', '3*x+15=42') + if __name__ == '__main__': unittest.main() From 2684897c952da145f34e6519e27b87df9de95209 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Thu, 2 May 2019 11:21:09 -0400 Subject: [PATCH 05/53] Remove FVC_DEBUG, which I thought I was going to use for this feature, but it ended up not being needed. --- Include/ceval.h | 7 +++---- Python/ceval.c | 6 ------ Python/compile.c | 15 +++++++-------- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/Include/ceval.h b/Include/ceval.h index 9bf28664ad1ca1..11283c0a570b7e 100644 --- a/Include/ceval.h +++ b/Include/ceval.h @@ -225,14 +225,13 @@ PyAPI_FUNC(void) _PyEval_SignalAsyncExc(void); #endif /* Masks and values used by FORMAT_VALUE opcode. */ -#define FVC_MASK 0x7 +#define FVC_MASK 0x3 #define FVC_NONE 0x0 #define FVC_STR 0x1 #define FVC_REPR 0x2 #define FVC_ASCII 0x3 -#define FVC_DEBUG 0x4 -#define FVS_MASK 0x8 -#define FVS_HAVE_SPEC 0x8 +#define FVS_MASK 0x4 +#define FVS_HAVE_SPEC 0x4 #ifdef __cplusplus } diff --git a/Python/ceval.c b/Python/ceval.c index 39b27c219dc3b9..8eadda9723bc24 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -3440,8 +3440,6 @@ _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag) case FVC_STR: conv_fn = PyObject_Str; break; case FVC_REPR: conv_fn = PyObject_Repr; break; case FVC_ASCII: conv_fn = PyObject_ASCII; break; - /* !x is a special case, handled later. */ - case FVC_DEBUG: conv_fn = NULL; break; default: PyErr_Format(PyExc_SystemError, @@ -3461,10 +3459,6 @@ _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag) goto error; } value = result; - } else if (which_conversion == FVC_DEBUG) { - /* If we're using !x, do the formatting, replace value with - the result. */ - printf("debug\n"); } /* If value is a unicode object, and there's no fmt_spec, diff --git a/Python/compile.c b/Python/compile.c index 28e08f93594b79..6b517e65f1aa72 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -3946,16 +3946,15 @@ 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 3 bits: - None: 0000 0x0 FVC_NONE - !s : 0001 0x1 FVC_STR - !r : 0010 0x2 FVC_REPR - !a : 0011 0x3 FVC_ASCII - !x : 0100 0x4 FVC_DEBUG + Convert the conversion char to 2 bit: + None: 000 0x0 FVC_NONE + !s : 001 0x1 FVC_STR + !r : 010 0x2 FVC_REPR + !a : 011 0x3 FVC_ASCII next bit is whether or not we have a format spec: - yes : 1000 0x8 - no : 0000 0x0 + yes : 100 0x4 + no : 000 0x0 */ int oparg; From 42582d5633dc09a2ccfd0de889803b8e0f26a67e Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Thu, 2 May 2019 11:27:46 -0400 Subject: [PATCH 06/53] Fix some comments and formatting. --- Python/ast.c | 4 ++-- Python/compile.c | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Python/ast.c b/Python/ast.c index 1c1c5d91fb02d1..dd98d484195b0a 100644 --- a/Python/ast.c +++ b/Python/ast.c @@ -4999,7 +4999,7 @@ fstring_parse(const char **str, const char *end, int raw, int recurse_lvl, /* 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, format_spec expression, and - optionally the text of the expression (if !x is used). + optionally the text of the expression (if !d 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 @@ -5191,7 +5191,7 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, return -1; } - /* If !x, then save the source to the expression. */ + /* If !d, then save the source to the expression. */ if (conversion == 'd') { expr_text = PyUnicode_FromStringAndSize(expr_start, expr_end-expr_start); diff --git a/Python/compile.c b/Python/compile.c index 6b517e65f1aa72..629d697cf7346d 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -211,8 +211,8 @@ static int compiler_async_comprehension_generator( expr_ty elt, expr_ty val, int type); static PyCodeObject *assemble(struct compiler *, int addNone); -static PyObject *__doc__, *__annotations__; -static PyObject *equal_str; +static PyObject *__doc__, *__annotations__, *equal_str; + #define CAPSULE_NAME "compile.c compiler unit" PyObject * From 1ffa2a162750853b3a2b990a9fc966df33664f5a Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Thu, 2 May 2019 11:48:18 -0400 Subject: [PATCH 07/53] Added blurb. --- .../2019-05-02-11-48-08.bpo-36774.ZqbJ1J.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2019-05-02-11-48-08.bpo-36774.ZqbJ1J.rst 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 00000000000000..7af8193aa264ee --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2019-05-02-11-48-08.bpo-36774.ZqbJ1J.rst @@ -0,0 +1,4 @@ +Add a !x conversion specifier to f-strings. This is similar to !s and !r. It +produces the text of the expression, followed by an equal sign, followed by +the value of the expression. So ``f'{3*9+15!d}'`` would be equal to the +string ``3*9+15=42``. From 14af246a48a15447262a1d829e5afbe5e3053625 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Thu, 2 May 2019 11:50:51 -0400 Subject: [PATCH 08/53] Use quotes around the string result in the blurb text. --- .../Core and Builtins/2019-05-02-11-48-08.bpo-36774.ZqbJ1J.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 7af8193aa264ee..21d717c1406386 100644 --- 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 @@ -1,4 +1,4 @@ Add a !x conversion specifier to f-strings. This is similar to !s and !r. It produces the text of the expression, followed by an equal sign, followed by the value of the expression. So ``f'{3*9+15!d}'`` would be equal to the -string ``3*9+15=42``. +string ``'3*9+15=42'``. From f985b106251712ad4d334a2bcaaaa86e8d729bd7 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Thu, 2 May 2019 11:51:45 -0400 Subject: [PATCH 09/53] Call out the conversion specifiers in monospace. --- .../2019-05-02-11-48-08.bpo-36774.ZqbJ1J.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 index 21d717c1406386..5a1adfda05bd21 100644 --- 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 @@ -1,4 +1,4 @@ -Add a !x conversion specifier to f-strings. This is similar to !s and !r. It -produces the text of the expression, followed by an equal sign, followed by -the value of the expression. So ``f'{3*9+15!d}'`` would be equal to the -string ``'3*9+15=42'``. +Add a ``!x`` conversion specifier to f-strings. This is similar to +``!s`` and ``!r``. It produces the text of the expression, followed by +an equal sign, followed by the value of the expression. So +``f'{3*9+15!d}'`` would be equal to the string ``'3*9+15=42'``. From 45564212050aeb7486cea43ff64d50214c6c2b87 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Thu, 2 May 2019 11:57:40 -0400 Subject: [PATCH 10/53] Note that the repr of the expression is used. --- .../Core and Builtins/2019-05-02-11-48-08.bpo-36774.ZqbJ1J.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 5a1adfda05bd21..19b224a2b8f92a 100644 --- 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 @@ -1,4 +1,4 @@ Add a ``!x`` conversion specifier to f-strings. This is similar to ``!s`` and ``!r``. It produces the text of the expression, followed by -an equal sign, followed by the value of the expression. So +an equal sign, followed by the repr of the value of the expression. So ``f'{3*9+15!d}'`` would be equal to the string ``'3*9+15=42'``. From 856485a89d7242e54ad9d33c2e6d1ed338e6393c Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Thu, 2 May 2019 14:11:06 -0400 Subject: [PATCH 11/53] Fix conversion character. --- .../Core and Builtins/2019-05-02-11-48-08.bpo-36774.ZqbJ1J.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 19b224a2b8f92a..2f5c7b140124dc 100644 --- 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 @@ -1,4 +1,4 @@ -Add a ``!x`` conversion specifier to f-strings. This is similar to +Add a ``!d`` conversion specifier to f-strings. This is similar to ``!s`` and ``!r``. 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!d}'`` would be equal to the string ``'3*9+15=42'``. From a38451687425bb18806991762ced262b10b95397 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Thu, 2 May 2019 16:47:25 -0400 Subject: [PATCH 12/53] Added Whats New entry. --- Doc/whatsnew/3.8.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst index bbc55ddd63418d..72c53a63e852a2 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 !d for quick and dirty debugging +------------------------------------------------------- + +Add ``!d`` conversion specifier to f-strings. ``f'{expr!d}'`` 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!d}') + +Would print ``x*9 + 15=42``. + +(Contributed by Eric V. Smith in :issue:`36774`.) + Other Language Changes ====================== From 6107b54e6290ff7fa9157032bb0b1c28af71c243 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Thu, 2 May 2019 17:30:14 -0400 Subject: [PATCH 13/53] Don't allow format_spec with !d. --- Python/ast.c | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/Python/ast.c b/Python/ast.c index dd98d484195b0a..4346c7e61598c6 100644 --- a/Python/ast.c +++ b/Python/ast.c @@ -5034,7 +5034,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 @@ -5062,7 +5062,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. */ @@ -5107,7 +5107,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++; @@ -5115,7 +5115,7 @@ 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 @@ -5130,7 +5130,7 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, } 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]; @@ -5142,7 +5142,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. */ @@ -5155,12 +5155,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) @@ -5171,7 +5171,7 @@ 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 a conversion char, if present. */ if (**str == '!') { @@ -5188,13 +5188,15 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, ast_error(c, n, "f-string: invalid conversion character: " "expected 's', 'r', 'a', or 'd'"); - return -1; + goto error; } /* If !d, then save the source to the expression. */ if (conversion == 'd') { expr_text = PyUnicode_FromStringAndSize(expr_start, expr_end-expr_start); + if (!expr_text) + goto error; } } @@ -5209,7 +5211,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 != '}') @@ -5220,6 +5222,13 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, assert(**str == '}'); *str += 1; + /* Can't have !d and a format spec. */ + if (conversion == 'd' && format_spec != NULL) { + PyErr_SetString(PyExc_ValueError, + "cannot specify a format spec with !d"); + goto error; + } + /* And now create the FormattedValue node that represents this entire expression with the conversion and format spec. */ *expression = FormattedValue(simple_expression, conversion, @@ -5227,13 +5236,18 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, 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. From 3f292c5fe6d0a5977c85c71fed0d02199cf6fb32 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Thu, 2 May 2019 19:55:13 -0400 Subject: [PATCH 14/53] Test that we don't allow a format spec. --- Lib/test/test_fstring.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index bfb59602e0849f..e519c0f57e537f 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -1057,6 +1057,10 @@ def test_debug_conversion(self): x = 9 self.assertEqual(f'{3*x+15!d}', '3*x+15=42') + # Don't allow any format spec. + with self.assertRaisesRegex(SyntaxError, 'cannot specify a format spec with !d'): + eval("f'{0!d:0}'") + if __name__ == '__main__': unittest.main() From 0046262372ce72007f9f13dfce39c28abe51ef1a Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Fri, 3 May 2019 04:12:27 -0400 Subject: [PATCH 15/53] Use repr(expr) if no format spec, use format(expr, spec) if one is. --- Lib/test/test_fstring.py | 19 ++++++++++++++++--- Python/ast.c | 7 ------- Python/compile.c | 28 ++++++++++++++++++++++------ 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index e519c0f57e537f..6edcc598e1bc8b 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -1057,9 +1057,22 @@ def test_debug_conversion(self): x = 9 self.assertEqual(f'{3*x+15!d}', '3*x+15=42') - # Don't allow any format spec. - with self.assertRaisesRegex(SyntaxError, 'cannot specify a format spec with !d'): - eval("f'{0!d:0}'") + def test_debug_conversion_calls_format(self): + # Test that !d calls format on the expression's value, if a + # format spec is also provided. + + class C: + def __repr__(self): + return 'my repr' + + def __format__(self, spec): + return f'F{spec}' + + # __format__ is called if a format spec is provided. + self.assertEqual(f'{C()!d:abc}', 'C()=Fabc') + + # But __repr__ is called if one isn't. + self.assertEqual(f'{C()!d}', 'C()=my repr') if __name__ == '__main__': diff --git a/Python/ast.c b/Python/ast.c index 4346c7e61598c6..4f4783310d2afd 100644 --- a/Python/ast.c +++ b/Python/ast.c @@ -5222,13 +5222,6 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, assert(**str == '}'); *str += 1; - /* Can't have !d and a format spec. */ - if (conversion == 'd' && format_spec != NULL) { - PyErr_SetString(PyExc_ValueError, - "cannot specify a format spec with !d"); - goto error; - } - /* And now create the FormattedValue node that represents this entire expression with the conversion and format spec. */ *expression = FormattedValue(simple_expression, conversion, diff --git a/Python/compile.c b/Python/compile.c index 629d697cf7346d..f77a1568319a0a 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -3958,6 +3958,7 @@ compiler_formatted_value(struct compiler *c, expr_ty e) */ int oparg; + int use_format_spec=1; if (!equal_str) { equal_str = PyUnicode_InternFromString("="); @@ -3966,8 +3967,12 @@ compiler_formatted_value(struct compiler *c, expr_ty e) } if (e->v.FormattedValue.conversion == 'd') { - /* Special handling here to generate basically: - format( + '=' + repr(value), format_spec) */ + /* If there is a format spec, then generate: + + '=' + format(value, format_spec) + If there's no format spec, then generate: + + '=' + repr(value) + */ + assert(e->v.FormattedValue.expr_text != NULL); /* The text of the expression. */ @@ -3976,11 +3981,22 @@ compiler_formatted_value(struct compiler *c, expr_ty e) ADDOP_LOAD_CONST(c, equal_str); /* Evaluate the expression to be formatted. */ VISIT(c, expr, e->v.FormattedValue.value); - /* Call repr on it, by using FORMAT_VALUE with FVC_REPR. */ - ADDOP_I(c, FORMAT_VALUE, FVC_REPR); + + /* Now format the expression value. */ + if (e->v.FormattedValue.format_spec) { + /* Call format on it, using FORMAT_VALUE with the format_spec. */ + VISIT(c, expr, e->v.FormattedValue.format_spec); + ADDOP_I(c, FORMAT_VALUE, FVS_HAVE_SPEC); + /* We've already used the format spec, don't use it again + below. */ + use_format_spec = 0; + } else { + /* Call repr on it, by using FORMAT_VALUE with FVC_REPR. */ + ADDOP_I(c, FORMAT_VALUE, FVC_REPR); + } /* Now combine the 3 strings on top of the stack: the text of the - expression, the "=", and the repr'd value. */ + expression, the "=", and the formatted value, which is a string. */ ADDOP_I(c, BUILD_STRING, 3); } else { /* Just evaluate the expression to be formatted. */ @@ -3998,7 +4014,7 @@ compiler_formatted_value(struct compiler *c, expr_ty e) "Unrecognized conversion character"); return 0; } - if (e->v.FormattedValue.format_spec) { + if (e->v.FormattedValue.format_spec && use_format_spec) { /* Evaluate the format spec, and update our opcode arg. */ VISIT(c, expr, e->v.FormattedValue.format_spec); oparg |= FVS_HAVE_SPEC; From a697d02e1b1514bb7cdc83ac6ead3d0e7e408b12 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Fri, 3 May 2019 23:19:23 -0400 Subject: [PATCH 16/53] Pre-append the '=' to the end of expr_text at compile time, instead of adding it at runtime. --- Lib/test/test_fstring.py | 6 +++++ Python/ast.c | 18 +++++++++++-- Python/compile.c | 55 ++++++++++++++++++++-------------------- 3 files changed, 50 insertions(+), 29 deletions(-) diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index 6edcc598e1bc8b..49e84ad18da3d3 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -1057,6 +1057,12 @@ def test_debug_conversion(self): x = 9 self.assertEqual(f'{3*x+15!d}', '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π!d:.2f}', 'tenπ=31.40') + + def test_debug_conversion_calls_format(self): # Test that !d calls format on the expression's value, if a # format spec is also provided. diff --git a/Python/ast.c b/Python/ast.c index 4f4783310d2afd..9d8aea1088d7e8 100644 --- a/Python/ast.c +++ b/Python/ast.c @@ -5193,10 +5193,24 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, /* If !d, then save the source to the expression. */ if (conversion == 'd') { - expr_text = PyUnicode_FromStringAndSize(expr_start, - expr_end-expr_start); + /* Add one for the = we're going to append. This is the length of + the input in bytes, UTF-8 encoded. */ + Py_ssize_t len = expr_end-expr_start+1; + + /* This can't read off the end, because there must be at least one + more char, the ! character. */ + expr_text = PyUnicode_FromStringAndSize(expr_start, len); if (!expr_text) goto error; + + /* Get the length in chars now (no longer bytes, so this might + change if the exppression had any encoded unicode chars in + it). */ + len = PyUnicode_GET_LENGTH(expr_text); + assert(PyUnicode_ReadChar(expr_text, len-1) == '!'); + + /* Change the last char to an '='. */ + PyUnicode_WriteChar(expr_text, len-1, '='); } } diff --git a/Python/compile.c b/Python/compile.c index f77a1568319a0a..6b27b70bf08025 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -211,7 +211,7 @@ static int compiler_async_comprehension_generator( expr_ty elt, expr_ty val, int type); static PyCodeObject *assemble(struct compiler *, int addNone); -static PyObject *__doc__, *__annotations__, *equal_str; +static PyObject *__doc__, *__annotations__; #define CAPSULE_NAME "compile.c compiler unit" @@ -3958,63 +3958,64 @@ compiler_formatted_value(struct compiler *c, expr_ty e) */ int oparg; - int use_format_spec=1; - - if (!equal_str) { - equal_str = PyUnicode_InternFromString("="); - if (!equal_str) - return 0; - } if (e->v.FormattedValue.conversion == 'd') { + /* Note that expr_text already has a trailing equal sign, so we don't + need to add it here. */ /* If there is a format spec, then generate: - + '=' + format(value, format_spec) + expr_text + format(value, format_spec) If there's no format spec, then generate: - + '=' + repr(value) + expr_text + repr(value) */ + /* This can't be an assert, because although ast.c will never generate + an incorrect FormattedValue node, it's possible to create such a + node programatically. So, make this a runtime check. */ + if (e->v.FormattedValue.expr_text == NULL) { + PyErr_SetString(PyExc_ValueError, + "!d conversion without expr_text set"); + return 0; + } - assert(e->v.FormattedValue.expr_text != NULL); - - /* The text of the expression. */ + /* Push the text of the expression (with an equal sign at the end. */ ADDOP_LOAD_CONST(c, e->v.FormattedValue.expr_text); - /* The equal sign. */ - ADDOP_LOAD_CONST(c, equal_str); + /* Evaluate the expression to be formatted. */ VISIT(c, expr, e->v.FormattedValue.value); - /* Now format the expression value. */ + /* Now format the expression value (which forces it to be a + * string). */ if (e->v.FormattedValue.format_spec) { /* Call format on it, using FORMAT_VALUE with the format_spec. */ VISIT(c, expr, e->v.FormattedValue.format_spec); ADDOP_I(c, FORMAT_VALUE, FVS_HAVE_SPEC); - /* We've already used the format spec, don't use it again - below. */ - use_format_spec = 0; } else { /* Call repr on it, by using FORMAT_VALUE with FVC_REPR. */ ADDOP_I(c, FORMAT_VALUE, FVC_REPR); } - /* Now combine the 3 strings on top of the stack: the text of the - expression, the "=", and the formatted value, which is a string. */ - ADDOP_I(c, BUILD_STRING, 3); - } else { - /* Just evaluate the expression to be formatted. */ - VISIT(c, expr, e->v.FormattedValue.value); + /* Now combine the 2 strings on top of the stack: the text of the + expression (which already ends with an '=') and the formatted + value, which is a string. */ + ADDOP_I(c, BUILD_STRING, 2); + return 1; } + /* Now we handle all conversions (including none), except 'd'. */ + + /* The expression to be formatted. */ + VISIT(c, expr, e->v.FormattedValue.value); + switch (e->v.FormattedValue.conversion) { case 's': oparg = FVC_STR; break; case 'r': oparg = FVC_REPR; break; case 'a': oparg = FVC_ASCII; break; - case 'd': oparg = FVC_NONE; break; /* Already handled above. */ case -1: oparg = FVC_NONE; break; default: PyErr_SetString(PyExc_SystemError, "Unrecognized conversion character"); return 0; } - if (e->v.FormattedValue.format_spec && use_format_spec) { + if (e->v.FormattedValue.format_spec) { /* Evaluate the format spec, and update our opcode arg. */ VISIT(c, expr, e->v.FormattedValue.format_spec); oparg |= FVS_HAVE_SPEC; From 49d03e4c2d8c85df7f8e42275d5363d8e74fb045 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Sat, 4 May 2019 04:11:37 -0400 Subject: [PATCH 17/53] Add a coding comment, a note about why I'm using a unicode identifier, and a test with a unicode literal. --- Lib/test/test_fstring.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index 49e84ad18da3d3..fcf1e3ceab8709 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 @@ -1062,6 +1071,8 @@ def test_debug_conversion(self): tenπ = 31.4 self.assertEqual(f'{tenπ!d:.2f}', 'tenπ=31.40') + # Also test with non-identifiers. + self.assertEqual(f'{"Σ"!d}', '"Σ"=\'Σ\'') def test_debug_conversion_calls_format(self): # Test that !d calls format on the expression's value, if a From f79e21df254f13bfe2ef65e1899f24b1657309bf Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Sat, 4 May 2019 11:02:05 -0400 Subject: [PATCH 18/53] Added tests for nested f-strings and for pre- and post-text. --- Lib/test/test_fstring.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index fcf1e3ceab8709..c7fc0cd3a5386c 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -1074,6 +1074,13 @@ def test_debug_conversion(self): # Also test with non-identifiers. self.assertEqual(f'{"Σ"!d}', '"Σ"=\'Σ\'') + # Make sure nested still works. + self.assertEqual(f'{f"{3.1415!d:.1f}":*^20}', '*****3.1415=3.1*****') + + # Make sure text before and after !d works correctly. + pi = 'π' + self.assertEqual(f'alpha α {pi!d} ω omega', "alpha α pi='π' ω omega") + def test_debug_conversion_calls_format(self): # Test that !d calls format on the expression's value, if a # format spec is also provided. From ed82083c5d08c8e359af93ce8f94445f26211b03 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Sat, 4 May 2019 16:00:17 -0400 Subject: [PATCH 19/53] Add a test for newlines in expressions. --- Lib/test/test_fstring.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index c7fc0cd3a5386c..bbf0276b07d9ec 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -1081,6 +1081,11 @@ def test_debug_conversion(self): pi = 'π' self.assertEqual(f'alpha α {pi!d} ω omega', "alpha α pi='π' ω omega") + # Check multi-lines. + self.assertEqual(f'''{ +3 +!d}''', '\n3\n=3') + def test_debug_conversion_calls_format(self): # Test that !d calls format on the expression's value, if a # format spec is also provided. From aeef3dba83207f0f14da6e68f7c55d5a9972d16e Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Sat, 4 May 2019 16:02:22 -0400 Subject: [PATCH 20/53] Clarify a comment. --- Lib/test/test_fstring.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index bbf0276b07d9ec..137cb529fdb2d9 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -1081,7 +1081,7 @@ def test_debug_conversion(self): pi = 'π' self.assertEqual(f'alpha α {pi!d} ω omega', "alpha α pi='π' ω omega") - # Check multi-lines. + # Check multi-line expressions. self.assertEqual(f'''{ 3 !d}''', '\n3\n=3') From caf3221afdbb61e944b275975256bb18771a4101 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Sat, 4 May 2019 22:41:51 -0400 Subject: [PATCH 21/53] Change from di to =. --- Lib/test/test_fstring.py | 39 +++++++++-------------- Python/ast.c | 67 ++++++++++++++++++++++++---------------- Python/compile.c | 48 +++++----------------------- 3 files changed, 62 insertions(+), 92 deletions(-) diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index 137cb529fdb2d9..6a8820b5ab2fa4 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -887,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 ') @@ -1060,48 +1066,31 @@ def test_backslash_char(self): def test_debug_conversion(self): x = 'A string' - self.assertEqual(f'{x!d}', 'x=' + repr(x)) - self.assertEqual(f'{x !d}', 'x =' + repr(x)) + self.assertEqual(f'{x=}', 'x=' + repr(x)) + self.assertEqual(f'{x =}', 'x =' + repr(x)) x = 9 - self.assertEqual(f'{3*x+15!d}', '3*x+15=42') + 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π!d:.2f}', 'tenπ=31.40') + self.assertEqual(f'{tenπ=!f:.2f}', 'tenπ=31.40') # Also test with non-identifiers. - self.assertEqual(f'{"Σ"!d}', '"Σ"=\'Σ\'') + self.assertEqual(f'{"Σ"=}', '"Σ"=\'Σ\'') # Make sure nested still works. - self.assertEqual(f'{f"{3.1415!d:.1f}":*^20}', '*****3.1415=3.1*****') + self.assertEqual(f'{f"{3.1415=!f:.1f}":*^20}', '*****3.1415=3.1*****') # Make sure text before and after !d works correctly. pi = 'π' - self.assertEqual(f'alpha α {pi!d} ω omega', "alpha α pi='π' ω omega") + self.assertEqual(f'alpha α {pi=} ω omega', "alpha α pi='π' ω omega") # Check multi-line expressions. self.assertEqual(f'''{ 3 -!d}''', '\n3\n=3') - - def test_debug_conversion_calls_format(self): - # Test that !d calls format on the expression's value, if a - # format spec is also provided. - - class C: - def __repr__(self): - return 'my repr' - - def __format__(self, spec): - return f'F{spec}' - - # __format__ is called if a format spec is provided. - self.assertEqual(f'{C()!d:abc}', 'C()=Fabc') - - # But __repr__ is called if one isn't. - self.assertEqual(f'{C()!d}', 'C()=my repr') +=}''', '\n3\n=3') if __name__ == '__main__': diff --git a/Python/ast.c b/Python/ast.c index 9d8aea1088d7e8..8ec4ed847ecb8a 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, @@ -5016,8 +5017,11 @@ 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 = 'f'; /* The conversion char. 'f', the default, is no + conversion, use format(). */ + int equal_conversion = 0; /* Are we using the = conversion? */ PyObject *expr_text = NULL; /* The text of the expression, used for !d. */ + 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). */ @@ -5117,14 +5121,24 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, ast_error(c, n, "f-string expression part cannot include '#'"); goto error; } else if (nested_depth == 0 && - (ch == '!' || ch == ':' || ch == '}')) { + (ch == '!' || 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. */ + /* This is !=, not a conversion character. Skip the next char + and continue. */ + *str += 1; continue; } + /* Also, test for '=='. */ + if (ch == '=' && *str+1 < end && *(*str+1) == '=') { + /* This is ==, not an = connversion. Skip the next char and + continue. */ + *str += 1; + continue; + } + /* Normal way out of this loop. */ break; } else if (ch == ']' || ch == '}' || ch == ')') { @@ -5173,6 +5187,20 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, if (!simple_expression) goto error; + /* Check for = conversion. */ + if (**str == '=') { + *str += 1; + equal_conversion = 1; + conversion = 'r'; /* Default for = is to use repr. */ + + /* Skip over whitespace. No need to test for end of string here, + since we know there's at least a } somewhere ahead. */ + while (**str == ' ') { + *str += 1; + } + expr_text_end = *str; + } + /* Check for a conversion char, if present. */ if (**str == '!') { *str += 1; @@ -5184,34 +5212,19 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, /* Validate the conversion. */ if (!(conversion == 's' || conversion == 'r' - || conversion == 'a' || conversion == 'd')) { + || conversion == 'a' || conversion == 'f')) { ast_error(c, n, "f-string: invalid conversion character: " - "expected 's', 'r', 'a', or 'd'"); + "expected 's', 'r', 'a', or 'f'"); goto error; } - /* If !d, then save the source to the expression. */ - if (conversion == 'd') { - /* Add one for the = we're going to append. This is the length of - the input in bytes, UTF-8 encoded. */ - Py_ssize_t len = expr_end-expr_start+1; - - /* This can't read off the end, because there must be at least one - more char, the ! character. */ - expr_text = PyUnicode_FromStringAndSize(expr_start, len); - if (!expr_text) - goto error; - - /* Get the length in chars now (no longer bytes, so this might - change if the exppression had any encoded unicode chars in - it). */ - len = PyUnicode_GET_LENGTH(expr_text); - assert(PyUnicode_ReadChar(expr_text, len-1) == '!'); - - /* Change the last char to an '='. */ - PyUnicode_WriteChar(expr_text, len-1, '='); - } + } + if (equal_conversion) { + 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. */ diff --git a/Python/compile.c b/Python/compile.c index 6b27b70bf08025..1b46fb79c99ac8 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -3959,49 +3959,11 @@ compiler_formatted_value(struct compiler *c, expr_ty e) int oparg; - if (e->v.FormattedValue.conversion == 'd') { - /* Note that expr_text already has a trailing equal sign, so we don't - need to add it here. */ - /* If there is a format spec, then generate: - expr_text + format(value, format_spec) - If there's no format spec, then generate: - expr_text + repr(value) - */ - /* This can't be an assert, because although ast.c will never generate - an incorrect FormattedValue node, it's possible to create such a - node programatically. So, make this a runtime check. */ - if (e->v.FormattedValue.expr_text == NULL) { - PyErr_SetString(PyExc_ValueError, - "!d conversion without expr_text set"); - return 0; - } - + if (e->v.FormattedValue.expr_text) { /* Push the text of the expression (with an equal sign at the end. */ ADDOP_LOAD_CONST(c, e->v.FormattedValue.expr_text); - - /* Evaluate the expression to be formatted. */ - VISIT(c, expr, e->v.FormattedValue.value); - - /* Now format the expression value (which forces it to be a - * string). */ - if (e->v.FormattedValue.format_spec) { - /* Call format on it, using FORMAT_VALUE with the format_spec. */ - VISIT(c, expr, e->v.FormattedValue.format_spec); - ADDOP_I(c, FORMAT_VALUE, FVS_HAVE_SPEC); - } else { - /* Call repr on it, by using FORMAT_VALUE with FVC_REPR. */ - ADDOP_I(c, FORMAT_VALUE, FVC_REPR); - } - - /* Now combine the 2 strings on top of the stack: the text of the - expression (which already ends with an '=') and the formatted - value, which is a string. */ - ADDOP_I(c, BUILD_STRING, 2); - return 1; } - /* Now we handle all conversions (including none), except 'd'. */ - /* The expression to be formatted. */ VISIT(c, expr, e->v.FormattedValue.value); @@ -4009,7 +3971,7 @@ compiler_formatted_value(struct compiler *c, expr_ty e) case 's': oparg = FVC_STR; break; case 'r': oparg = FVC_REPR; break; case 'a': oparg = FVC_ASCII; break; - case -1: oparg = FVC_NONE; break; + case 'f': oparg = FVC_NONE; break; default: PyErr_SetString(PyExc_SystemError, "Unrecognized conversion character"); @@ -4023,6 +3985,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; } From 921d8a1b5400387212509ad3ab713c9f32bec55e Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Sat, 4 May 2019 22:59:45 -0400 Subject: [PATCH 22/53] Fix a comment. --- Python/compile.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/compile.c b/Python/compile.c index 1b46fb79c99ac8..c253da5790985d 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -3947,7 +3947,7 @@ compiler_formatted_value(struct compiler *c, expr_ty e) character, and whether or not a format_spec was provided. Convert the conversion char to 2 bit: - None: 000 0x0 FVC_NONE + !f : 000 0x0 FVC_NONE The default. !s : 001 0x1 FVC_STR !r : 010 0x2 FVC_REPR !a : 011 0x3 FVC_ASCII From bd65ec10968255899f43d1492f03ffbe7c6d60a9 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Sun, 5 May 2019 19:37:47 -0400 Subject: [PATCH 23/53] Fix comment. --- Python/compile.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/compile.c b/Python/compile.c index c253da5790985d..f898b41f40ccd5 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -3947,7 +3947,7 @@ compiler_formatted_value(struct compiler *c, expr_ty e) character, and whether or not a format_spec was provided. Convert the conversion char to 2 bit: - !f : 000 0x0 FVC_NONE The default. + !f : 000 0x0 FVC_NONE The default if nothing specified. !s : 001 0x1 FVC_STR !r : 010 0x2 FVC_REPR !a : 011 0x3 FVC_ASCII From 1acc3863425dfdf7aaef4be821927b4f3a181c01 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Sun, 5 May 2019 19:39:58 -0400 Subject: [PATCH 24/53] Fix comments. --- Python/ast.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Python/ast.c b/Python/ast.c index 8ec4ed847ecb8a..e08a5bf5f48e3d 100644 --- a/Python/ast.c +++ b/Python/ast.c @@ -5000,7 +5000,7 @@ fstring_parse(const char **str, const char *end, int raw, int recurse_lvl, /* 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, format_spec expression, and - optionally the text of the expression (if !d is used). + 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 @@ -5020,7 +5020,7 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, int conversion = 'f'; /* The conversion char. 'f', the default, is no conversion, use format(). */ int equal_conversion = 0; /* Are we using the = conversion? */ - PyObject *expr_text = NULL; /* The text of the expression, used for !d. */ + 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 From d46f83c1a9ec23d31db21ae2cc2ad6fbf231ddab Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Mon, 6 May 2019 02:03:47 -0400 Subject: [PATCH 25/53] Skip all whitespace after the =, not just spaces. Handle unparsing. --- Python/ast.c | 2 +- Python/ast_unparse.c | 3 +++ Tools/parser/unparse.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Python/ast.c b/Python/ast.c index e08a5bf5f48e3d..ab19e3df27cf31 100644 --- a/Python/ast.c +++ b/Python/ast.c @@ -5195,7 +5195,7 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, /* Skip over whitespace. No need to test for end of string here, since we know there's at least a } somewhere ahead. */ - while (**str == ' ') { + while (isspace(**str)) { *str += 1; } expr_text_end = *str; diff --git a/Python/ast_unparse.c b/Python/ast_unparse.c index 916ad5f97f0ce1..34485e323160c3 100644 --- a/Python/ast_unparse.c +++ b/Python/ast_unparse.c @@ -666,6 +666,9 @@ append_formattedvalue(_PyUnicodeWriter *writer, expr_ty e, bool is_format_spec) case 's': conversion = "!s"; break; + case 'f': + conversion = "!f"; + break; default: PyErr_SetString(PyExc_SystemError, "unknown f-value conversion kind"); diff --git a/Tools/parser/unparse.py b/Tools/parser/unparse.py index 385902ef4bc5e7..20b7ee5857d445 100644 --- a/Tools/parser/unparse.py +++ b/Tools/parser/unparse.py @@ -368,7 +368,7 @@ def _fstring_FormattedValue(self, t, write): write(expr) if t.conversion != -1: conversion = chr(t.conversion) - assert conversion in "sra" + assert conversion in "sraf" write(f"!{conversion}") if t.format_spec: write(":") From abff2cb3f20688b9ec34732e278cc6e0b116ea1e Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Mon, 6 May 2019 08:04:43 -0400 Subject: [PATCH 26/53] Hack: Modify tests for f-string annotations. I'm not sure these are even valid tests any more. --- Include/ceval.h | 8 ++++---- Lib/test/test_future.py | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Include/ceval.h b/Include/ceval.h index 11283c0a570b7e..b4ce4c107ea6f0 100644 --- a/Include/ceval.h +++ b/Include/ceval.h @@ -225,13 +225,13 @@ PyAPI_FUNC(void) _PyEval_SignalAsyncExc(void); #endif /* Masks and values used by FORMAT_VALUE opcode. */ -#define FVC_MASK 0x3 +#define FVC_MASK 0x7 #define FVC_NONE 0x0 #define FVC_STR 0x1 #define FVC_REPR 0x2 -#define FVC_ASCII 0x3 -#define FVS_MASK 0x4 -#define FVS_HAVE_SPEC 0x4 +#define FVC_FORMAT 0x3 +#define FVS_MASK 0x8 +#define FVS_HAVE_SPEC 0x8 #ifdef __cplusplus } diff --git a/Lib/test/test_future.py b/Lib/test/test_future.py index c60a016f01f4de..3965731134ea69 100644 --- a/Lib/test/test_future.py +++ b/Lib/test/test_future.py @@ -250,11 +250,11 @@ def test_annotations(self): eq('str or None if sys.version_info[0] > (3,) else str or bytes or None') eq("f'f-string without formatted values is just a string'") eq("f'{{NOT a formatted value}}'") - eq("f'some f-string with {a} {few():.2f} {formatted.values!r}'") - eq('''f"{f'{nested} inner'} outer"''') - 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'some f-string with {a!f} {few()!f:.2f} {formatted.values!r}'") + eq('''f"{f'{nested!f} inner'!f} outer"''') + eq("f'space between opening braces: { {a for a in (1, 2, 3)}!f}'") + eq("f'{(lambda x: x)!f}'") + eq("f'{(None if a else lambda x: x)!f}'") eq('(yield from outside_of_generator)') eq('(yield)') eq('(yield a + b)') From 75ef1acde97aad225ee3effa2a25d0d67829d0c9 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Mon, 6 May 2019 10:22:21 -0400 Subject: [PATCH 27/53] Add missing FVC_ASCII. --- Include/ceval.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Include/ceval.h b/Include/ceval.h index b4ce4c107ea6f0..9b7a7455f91987 100644 --- a/Include/ceval.h +++ b/Include/ceval.h @@ -229,7 +229,8 @@ PyAPI_FUNC(void) _PyEval_SignalAsyncExc(void); #define FVC_NONE 0x0 #define FVC_STR 0x1 #define FVC_REPR 0x2 -#define FVC_FORMAT 0x3 +#define FVC_ASCII 0x3 +#define FVC_FORMAT 0x4 #define FVS_MASK 0x8 #define FVS_HAVE_SPEC 0x8 From e688fbf9a35dad78e8812f8d8a66885d9b43874e Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Mon, 6 May 2019 10:50:43 -0400 Subject: [PATCH 28/53] Move the mapping from "no converion specified" to the exact conversion function in to the compile phase. --- Include/ceval.h | 7 +++---- Python/ast.c | 4 +--- Python/ceval.c | 1 - Python/compile.c | 28 +++++++++++++++++++--------- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/Include/ceval.h b/Include/ceval.h index 9b7a7455f91987..11283c0a570b7e 100644 --- a/Include/ceval.h +++ b/Include/ceval.h @@ -225,14 +225,13 @@ PyAPI_FUNC(void) _PyEval_SignalAsyncExc(void); #endif /* Masks and values used by FORMAT_VALUE opcode. */ -#define FVC_MASK 0x7 +#define FVC_MASK 0x3 #define FVC_NONE 0x0 #define FVC_STR 0x1 #define FVC_REPR 0x2 #define FVC_ASCII 0x3 -#define FVC_FORMAT 0x4 -#define FVS_MASK 0x8 -#define FVS_HAVE_SPEC 0x8 +#define FVS_MASK 0x4 +#define FVS_HAVE_SPEC 0x4 #ifdef __cplusplus } diff --git a/Python/ast.c b/Python/ast.c index ab19e3df27cf31..187779c1c5ae83 100644 --- a/Python/ast.c +++ b/Python/ast.c @@ -5017,8 +5017,7 @@ 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 = 'f'; /* The conversion char. 'f', the default, is no - conversion, use format(). */ + int conversion = -1; /* The conversion char. Default is not specified. */ int equal_conversion = 0; /* Are we using the = conversion? */ PyObject *expr_text = NULL; /* The text of the expression, used for =. */ const char *expr_text_end; @@ -5191,7 +5190,6 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, if (**str == '=') { *str += 1; equal_conversion = 1; - conversion = 'r'; /* Default for = is to use repr. */ /* Skip over whitespace. No need to test for end of string here, since we know there's at least a } somewhere ahead. */ diff --git a/Python/ceval.c b/Python/ceval.c index 8eadda9723bc24..ab0e3b5542176d 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -3440,7 +3440,6 @@ _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag) case FVC_STR: conv_fn = PyObject_Str; break; case FVC_REPR: conv_fn = PyObject_Repr; break; case FVC_ASCII: conv_fn = PyObject_ASCII; break; - default: PyErr_Format(PyExc_SystemError, "unexpected conversion flag %d", diff --git a/Python/compile.c b/Python/compile.c index f898b41f40ccd5..6b5a4afbc16d69 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -3947,34 +3947,44 @@ compiler_formatted_value(struct compiler *c, expr_ty e) character, and whether or not a format_spec was provided. Convert the conversion char to 2 bit: - !f : 000 0x0 FVC_NONE The default if nothing specified. - !s : 001 0x1 FVC_STR - !r : 010 0x2 FVC_REPR - !a : 011 0x3 FVC_ASCII + : 0000 0x0 FVC_NONE The default if nothing specified. + !s : 0001 0x1 FVC_STR + !r : 0010 0x2 FVC_REPR + !a : 0011 0x3 FVC_ASCII + !a : 0100 0x4 FVC_ASCII next bit is whether or not we have a format spec: - yes : 100 0x4 - no : 000 0x0 + yes : 1000 0x8 + no : 0000 0x0 */ + int conversion = e->v.FormattedValue.conversion; int oparg; if (e->v.FormattedValue.expr_text) { /* Push the text of the expression (with an equal sign at the end. */ ADDOP_LOAD_CONST(c, e->v.FormattedValue.expr_text); + if (conversion == -1) { + /* Unspecified, we default to !r if = specified. */ + conversion = 'r'; + } + } else { + if (conversion == -1) { + conversion = 'f'; + } } /* 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 'f': 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) { From 3e7e9ff9bf235b6d2d9857c48d7f0ab276e3fcea Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Mon, 6 May 2019 11:48:40 -0400 Subject: [PATCH 29/53] Make ast_unparse understand =. --- Lib/test/test_future.py | 10 ++++++++++ Python/ast_unparse.c | 5 +++++ Python/compile.c | 16 ++++++++-------- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_future.py b/Lib/test/test_future.py index 3965731134ea69..b3f31610699687 100644 --- a/Lib/test/test_future.py +++ b/Lib/test/test_future.py @@ -255,6 +255,16 @@ def test_annotations(self): eq("f'space between opening braces: { {a for a in (1, 2, 3)}!f}'") eq("f'{(lambda x: x)!f}'") eq("f'{(None if a else lambda x: x)!f}'") + eq("f'{x}'") + eq("f'{x!f}'") + eq("f'{x!r}'") + eq("f'{x!a}'") + eq("f'{x=}'") + eq("f'{x=!f}'") + eq("f'{x=!r}'") + eq("f'{x=!a}'") + eq("f'{x=!f:.2f}'") + eq("f'{x=!s:*^20}'") eq('(yield from outside_of_generator)') eq('(yield)') eq('(yield a + b)') diff --git a/Python/ast_unparse.c b/Python/ast_unparse.c index 34485e323160c3..829d792d1c7eb4 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 = debug conversion. */ + APPEND_STR("="); + } + if (e->v.FormattedValue.conversion > 0) { switch (e->v.FormattedValue.conversion) { case 'a': diff --git a/Python/compile.c b/Python/compile.c index 6b5a4afbc16d69..f4b75d02b54dd5 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -3962,16 +3962,16 @@ compiler_formatted_value(struct compiler *c, expr_ty e) int oparg; if (e->v.FormattedValue.expr_text) { - /* Push the text of the expression (with an equal sign at the end. */ + /* Push the text of the expression (which already has the '=' in + it. */ ADDOP_LOAD_CONST(c, e->v.FormattedValue.expr_text); - if (conversion == -1) { - /* Unspecified, we default to !r if = specified. */ - conversion = 'r'; - } + + /* If unspecified conversion, default to !r since we know we're using + '='. */ + conversion = conversion == -1 ? 'r' : conversion; } else { - if (conversion == -1) { - conversion = 'f'; - } + /* Unspecified conversion, default to !f. */ + conversion = conversion == -1 ? 'f' : conversion; } /* The expression to be formatted. */ From 6a39185ed3ec85c8d79212bc6fb156b61181c5b4 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Mon, 6 May 2019 12:15:24 -0400 Subject: [PATCH 30/53] Add a test for !s. --- Lib/test/test_fstring.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index 6a8820b5ab2fa4..dc95eb515feb31 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -1068,6 +1068,7 @@ 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)) x = 9 self.assertEqual(f'{3*x+15=}', '3*x+15=42') @@ -1093,5 +1094,6 @@ def test_debug_conversion(self): =}''', '\n3\n=3') + if __name__ == '__main__': unittest.main() From e32563c40e0db0ec96bfcacf897bdc361c87277f Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Mon, 6 May 2019 12:19:32 -0400 Subject: [PATCH 31/53] Improve comments. --- Lib/test/test_fstring.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index dc95eb515feb31..66fcf0599abb07 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -1078,10 +1078,10 @@ def test_debug_conversion(self): tenπ = 31.4 self.assertEqual(f'{tenπ=!f:.2f}', 'tenπ=31.40') - # Also test with non-identifiers. + # Also test with Unicode in non-identifiers. self.assertEqual(f'{"Σ"=}', '"Σ"=\'Σ\'') - # Make sure nested still works. + # Make sure nested fstrings still work. self.assertEqual(f'{f"{3.1415=!f:.1f}":*^20}', '*****3.1415=3.1*****') # Make sure text before and after !d works correctly. From 9fe98cf465417ae268fcd4d3960cf542c01c81ce Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Mon, 6 May 2019 14:37:34 -0400 Subject: [PATCH 32/53] Update blurb for = syntax. --- .../2019-05-02-11-48-08.bpo-36774.ZqbJ1J.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 index 2f5c7b140124dc..9e0b18b28fd20a 100644 --- 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 @@ -1,4 +1,7 @@ -Add a ``!d`` conversion specifier to f-strings. This is similar to -``!s`` and ``!r``. It produces the text of the expression, followed by +Add a ``=`` conversion feature to f-strings. 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!d}'`` would be equal to the string ``'3*9+15=42'``. +``f'{3*9+15=}'`` would be equal to the string ``'3*9+15=42'``. If +``=`` is specified, the default conversion is set to ``!r``. Added a +``!f`` conversion to switch back to the previous format behavior, if +needed. From 6c62fce62b7244b37cf06ec961a1494204747625 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Mon, 6 May 2019 14:39:49 -0400 Subject: [PATCH 33/53] Fix whatsnew. --- Doc/whatsnew/3.8.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst index 72c53a63e852a2..9871e454f4ecdc 100644 --- a/Doc/whatsnew/3.8.rst +++ b/Doc/whatsnew/3.8.rst @@ -148,19 +148,19 @@ 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 !d for quick and dirty debugging -------------------------------------------------------- +f-strings now support = for quick and easy debugging +----------------------------------------------------- -Add ``!d`` conversion specifier to f-strings. ``f'{expr!d}'`` expands +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!d}') + print(f'{x*9 + 15=}') Would print ``x*9 + 15=42``. -(Contributed by Eric V. Smith in :issue:`36774`.) +(Contributed by Eric V. Smith and Larry Hastings in :issue:`36817`.) Other Language Changes From d453f033eff1f389ffbcdc1176ab29bb1f2738ce Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Mon, 6 May 2019 14:52:06 -0400 Subject: [PATCH 34/53] Fix comments about FVC_ values. --- Python/compile.c | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Python/compile.c b/Python/compile.c index f4b75d02b54dd5..dbb6e0aa3e01e5 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -3946,16 +3946,15 @@ 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 bit: - : 0000 0x0 FVC_NONE The default if nothing specified. - !s : 0001 0x1 FVC_STR - !r : 0010 0x2 FVC_REPR - !a : 0011 0x3 FVC_ASCII - !a : 0100 0x4 FVC_ASCII + Convert the conversion char to 3 bits: + !f : 000 0x0 FVC_NONE The default if nothing specified. + !s : 001 0x1 FVC_STR + !r : 010 0x2 FVC_REPR + !a : 011 0x3 FVC_ASCII next bit is whether or not we have a format spec: - yes : 1000 0x8 - no : 0000 0x0 + yes : 100 0x4 + no : 000 0x0 */ int conversion = e->v.FormattedValue.conversion; From bb7d2e28186f5aac7adb695d13cd5d96a81150dc Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Tue, 7 May 2019 04:44:50 -0400 Subject: [PATCH 35/53] Restored some f-string tests, this time without !f. --- Lib/test/test_future.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_future.py b/Lib/test/test_future.py index b3f31610699687..152aea7fafa5c8 100644 --- a/Lib/test/test_future.py +++ b/Lib/test/test_future.py @@ -250,11 +250,11 @@ def test_annotations(self): eq('str or None if sys.version_info[0] > (3,) else str or bytes or None') eq("f'f-string without formatted values is just a string'") eq("f'{{NOT a formatted value}}'") - eq("f'some f-string with {a!f} {few()!f:.2f} {formatted.values!r}'") - eq('''f"{f'{nested!f} inner'!f} outer"''') - eq("f'space between opening braces: { {a for a in (1, 2, 3)}!f}'") - eq("f'{(lambda x: x)!f}'") - eq("f'{(None if a else lambda x: x)!f}'") + eq("f'some f-string with {a} {few():.2f} {formatted.values!r}'") + eq('''f"{f'{nested} inner'} outer"''') + 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!f}'") eq("f'{x!r}'") From 40231e6d84688bbb06ef8d959f0ccdb82eb56ee5 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Tue, 7 May 2019 06:52:22 -0400 Subject: [PATCH 36/53] Fix and improve a comment. --- Python/ast.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Python/ast.c b/Python/ast.c index 187779c1c5ae83..d6ef4aab28ea32 100644 --- a/Python/ast.c +++ b/Python/ast.c @@ -5017,7 +5017,8 @@ 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. Default is not specified. */ + int conversion = -1; /* The conversion char. Use default if not + specified: !f, or !r if using =. */ int equal_conversion = 0; /* Are we using the = conversion? */ PyObject *expr_text = NULL; /* The text of the expression, used for =. */ const char *expr_text_end; From 34c2cf913d204add5088f3702e1c3e9aa7086f48 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Tue, 7 May 2019 06:58:43 -0400 Subject: [PATCH 37/53] Use Py_ISSPACE instead of isspace. --- Python/ast.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/ast.c b/Python/ast.c index d6ef4aab28ea32..e6d2251070f979 100644 --- a/Python/ast.c +++ b/Python/ast.c @@ -5194,7 +5194,7 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, /* Skip over whitespace. No need to test for end of string here, since we know there's at least a } somewhere ahead. */ - while (isspace(**str)) { + while (Py_ISSPACE(**str)) { *str += 1; } expr_text_end = *str; From 2dde0dfc69118e07569c755012476a9733dd16ff Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Tue, 7 May 2019 08:01:34 -0400 Subject: [PATCH 38/53] Fix parsing error with >= and <=. Add test for operators containing =. --- Lib/test/test_fstring.py | 14 ++++++++++++++ Python/ast.c | 37 +++++++++++++++++++------------------ 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index 66fcf0599abb07..ca272d0b5f284c 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -1093,6 +1093,20 @@ def test_debug_conversion(self): 3 =}''', '\n3\n=3') + # Since = is handled specially, make sure all existing uses of + # it stil 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 + self.assertEqual(f'{x:=10}', ' 20') if __name__ == '__main__': diff --git a/Python/ast.c b/Python/ast.c index e6d2251070f979..5a098f5576640d 100644 --- a/Python/ast.c +++ b/Python/ast.c @@ -5121,22 +5121,23 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, ast_error(c, n, "f-string expression part cannot include '#'"); goto error; } else if (nested_depth == 0 && - (ch == '!' || 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 is !=, not a conversion character. Skip the next char - and continue. */ - *str += 1; - continue; - } - /* Also, test for '=='. */ - if (ch == '=' && *str+1 < end && *(*str+1) == '=') { - /* This is ==, not an = connversion. Skip the next char and - continue. */ - *str += 1; - continue; + (ch == '!' || ch == ':' || ch == '}' || + ch == '=' || ch == '>' || ch == '<')) { + /* See if there's a next character. */ + if (*str+1 < end) { + char next = *(*str+1); + + /* First, test for the special case of "!=". 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; + } } /* Normal way out of this loop. */ @@ -5192,8 +5193,8 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, *str += 1; equal_conversion = 1; - /* Skip over whitespace. No need to test for end of string here, - since we know there's at least a } somewhere ahead. */ + /* Skip over ASCII whitespace. No need to test for end of string + here, since we know there's at least a } somewhere ahead. */ while (Py_ISSPACE(**str)) { *str += 1; } From 92a0b1a86452c76279bc1000eefc6d7ce244c087 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Tue, 7 May 2019 08:39:32 -0400 Subject: [PATCH 39/53] < and > without a trailing = don't end an expression. --- Python/ast.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Python/ast.c b/Python/ast.c index 5a098f5576640d..aad262d6862dc5 100644 --- a/Python/ast.c +++ b/Python/ast.c @@ -5138,6 +5138,12 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, *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. */ From 0015f118f9547a29bef0e9f0a08490825fa2e783 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Tue, 7 May 2019 08:50:29 -0400 Subject: [PATCH 40/53] Add a clarifying comment about a test. --- Lib/test/test_fstring.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index ca272d0b5f284c..2461be0608a60b 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -1105,7 +1105,10 @@ def test_debug_conversion(self): 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'. self.assertEqual(f'{x:=10}', ' 20') From 46d4611e2f9d5cb1109ba44341949c74f11dde89 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Tue, 7 May 2019 09:23:33 -0400 Subject: [PATCH 41/53] Add tests for named parameters, to exercise the = parsing. --- Lib/test/test_fstring.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index 2461be0608a60b..8fc3636bfccc1c 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -1111,6 +1111,18 @@ def test_debug_conversion(self): # spec of '=10'. self.assertEqual(f'{x:=10}', ' 20') + 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') + + if __name__ == '__main__': unittest.main() From 1bfb6e917a06a161a3739ebf4aeafaeba9224f34 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Tue, 7 May 2019 09:28:58 -0400 Subject: [PATCH 42/53] Improve the tests for named parameters. --- Lib/test/test_fstring.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index 8fc3636bfccc1c..e09085aac6431b 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -1111,16 +1111,18 @@ def test_debug_conversion(self): # spec of '=10'. 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') + self.assertEqual(f'{f(a="3=")}', '0') + self.assertEqual(x, '3=') + self.assertEqual(f'{f(a=4)}', '3=') + self.assertEqual(x, 4) From 5fa0eac1fb5f1ceb5e8d895db9c32913bc49803f Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Tue, 7 May 2019 09:33:49 -0400 Subject: [PATCH 43/53] Fix a typo in a comment. --- Lib/test/test_fstring.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index e09085aac6431b..489aa50eb2c331 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -1094,7 +1094,7 @@ def test_debug_conversion(self): =}''', '\n3\n=3') # Since = is handled specially, make sure all existing uses of - # it stil work. + # it still work. self.assertEqual(f'{0==1}', 'False') self.assertEqual(f'{0!=1}', 'True') From 451cd2765a8719074b0c5cb13151496a3f872fae Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Tue, 7 May 2019 11:41:09 -0400 Subject: [PATCH 44/53] Removed !f conversion. Switch to repr if using = and a format spec is supplied. --- Lib/test/test_fstring.py | 12 ++++++++++-- Python/ast.c | 19 ++++++++++++------- Python/compile.c | 9 +-------- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index 489aa50eb2c331..e12032617f7bdb 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -1069,6 +1069,14 @@ def test_debug_conversion(self): 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=!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') @@ -1076,13 +1084,13 @@ def test_debug_conversion(self): # 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π=!f:.2f}', 'tenπ=31.40') + 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=!f:.1f}":*^20}', '*****3.1415=3.1*****') + self.assertEqual(f'{f"{3.1415=:.1f}":*^20}', '*****3.1415=3.1*****') # Make sure text before and after !d works correctly. pi = 'π' diff --git a/Python/ast.c b/Python/ast.c index aad262d6862dc5..db5363a6e3a532 100644 --- a/Python/ast.c +++ b/Python/ast.c @@ -5018,7 +5018,8 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, expr_ty simple_expression; expr_ty format_spec = NULL; /* Optional format specifier. */ int conversion = -1; /* The conversion char. Use default if not - specified: !f, or !r if using =. */ + specified, or !r if using = and no format + spec. */ int equal_conversion = 0; /* Are we using the = conversion? */ PyObject *expr_text = NULL; /* The text of the expression, used for =. */ const char *expr_text_end; @@ -5127,9 +5128,8 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, if (*str+1 < end) { char next = *(*str+1); - /* First, test for the special case of "!=". Since '=' is not - an allowed conversion character, nothing is lost in this - test. */ + /* For "!=". since '=' is not an allowed conversion character, + nothing is lost in this test. */ if ((ch == '!' && next == '=') || /* != */ (ch == '=' && next == '=') || /* == */ (ch == '<' && next == '=') || /* <= */ @@ -5217,11 +5217,10 @@ 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' || conversion == 'f')) { + if (!(conversion == 's' || conversion == 'r' || conversion == 'a')) { ast_error(c, n, "f-string: invalid conversion character: " - "expected 's', 'r', 'a', or 'f'"); + "expected 's', 'r', or 'a'"); goto error; } @@ -5255,6 +5254,12 @@ 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_conversion && 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, diff --git a/Python/compile.c b/Python/compile.c index dbb6e0aa3e01e5..03693e55b67927 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -3964,13 +3964,6 @@ compiler_formatted_value(struct compiler *c, expr_ty e) /* Push the text of the expression (which already has the '=' in it. */ ADDOP_LOAD_CONST(c, e->v.FormattedValue.expr_text); - - /* If unspecified conversion, default to !r since we know we're using - '='. */ - conversion = conversion == -1 ? 'r' : conversion; - } else { - /* Unspecified conversion, default to !f. */ - conversion = conversion == -1 ? 'f' : conversion; } /* The expression to be formatted. */ @@ -3980,7 +3973,7 @@ compiler_formatted_value(struct compiler *c, expr_ty e) case 's': oparg = FVC_STR; break; case 'r': oparg = FVC_REPR; break; case 'a': oparg = FVC_ASCII; break; - case 'f': oparg = FVC_NONE; break; + case -1: oparg = FVC_NONE; break; default: PyErr_Format(PyExc_SystemError, "Unrecognized conversion character %d", conversion); From 8e5a7c5380ac34993965c1522a75b075dcc7b5f1 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Tue, 7 May 2019 11:46:39 -0400 Subject: [PATCH 45/53] Add more tests for = and repr/format. --- Lib/test/test_fstring.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index e12032617f7bdb..b0718feccea53d 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -1074,6 +1074,7 @@ def test_debug_conversion(self): 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')) @@ -1132,6 +1133,20 @@ def f(a): 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********') + if __name__ == '__main__': From 8e739485efb4d7f81c335b681b28f750e86e40ed Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Tue, 7 May 2019 11:58:29 -0400 Subject: [PATCH 46/53] Fixed string annotations checks. --- Lib/test/test_future.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_future.py b/Lib/test/test_future.py index 152aea7fafa5c8..38de3dfdafcdb1 100644 --- a/Lib/test/test_future.py +++ b/Lib/test/test_future.py @@ -256,14 +256,13 @@ def test_annotations(self): eq("f'{(lambda x: x)}'") eq("f'{(None if a else lambda x: x)}'") eq("f'{x}'") - eq("f'{x!f}'") eq("f'{x!r}'") eq("f'{x!a}'") - eq("f'{x=}'") - eq("f'{x=!f}'") + eq("f'{x=!r}'") + eq("f'{x=:}'") + eq("f'{x=:.2f}'") eq("f'{x=!r}'") eq("f'{x=!a}'") - eq("f'{x=!f:.2f}'") eq("f'{x=!s:*^20}'") eq('(yield from outside_of_generator)') eq('(yield)') From 0ec4daead4d5b3c42613bd210a51da60932035f3 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Tue, 7 May 2019 12:17:32 -0400 Subject: [PATCH 47/53] Removed unreachable code left over from !f. --- Python/ast_unparse.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/Python/ast_unparse.c b/Python/ast_unparse.c index 829d792d1c7eb4..7b795b7cb01d27 100644 --- a/Python/ast_unparse.c +++ b/Python/ast_unparse.c @@ -671,9 +671,6 @@ append_formattedvalue(_PyUnicodeWriter *writer, expr_ty e, bool is_format_spec) case 's': conversion = "!s"; break; - case 'f': - conversion = "!f"; - break; default: PyErr_SetString(PyExc_SystemError, "unknown f-value conversion kind"); From 975f0f8450c43ea8ee9a9eb4bb0002d9359e4af8 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Tue, 7 May 2019 12:28:16 -0400 Subject: [PATCH 48/53] Removed 'f' type conversion. --- Tools/parser/unparse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tools/parser/unparse.py b/Tools/parser/unparse.py index 20b7ee5857d445..385902ef4bc5e7 100644 --- a/Tools/parser/unparse.py +++ b/Tools/parser/unparse.py @@ -368,7 +368,7 @@ def _fstring_FormattedValue(self, t, write): write(expr) if t.conversion != -1: conversion = chr(t.conversion) - assert conversion in "sraf" + assert conversion in "sra" write(f"!{conversion}") if t.format_spec: write(":") From 19fb7fa27c0a0c38c08b7beaa7d731d288270f7e Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Tue, 7 May 2019 12:37:03 -0400 Subject: [PATCH 49/53] Fixed a spurious space. --- Doc/whatsnew/3.8.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst index 9871e454f4ecdc..6b11525392378b 100644 --- a/Doc/whatsnew/3.8.rst +++ b/Doc/whatsnew/3.8.rst @@ -160,7 +160,7 @@ evaluated expression. So:: Would print ``x*9 + 15=42``. -(Contributed by Eric V. Smith and Larry Hastings in :issue:`36817`.) +(Contributed by Eric V. Smith and Larry Hastings in :issue:`36817`.) Other Language Changes From c3a8f122a74554c462a64157284c9bcd17ae5562 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Tue, 7 May 2019 15:35:44 -0400 Subject: [PATCH 50/53] Fixed whitespace. --- Lib/test/test_fstring.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index b0718feccea53d..d2d5d2b6695b28 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -1123,10 +1123,10 @@ def test_debug_conversion(self): # Test named function parameters, to make sure '=' parsing works # there. def f(a): - nonlocal x - oldx = x - x = a - return oldx + nonlocal x + oldx = x + x = a + return oldx x = 0 self.assertEqual(f'{f(a="3=")}', '0') self.assertEqual(x, '3=') From dba7421aeb3b2b5903acc1be3cd641e0e48b60b3 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Tue, 7 May 2019 21:04:36 -0400 Subject: [PATCH 51/53] Address review findings. --- Lib/test/test_fstring.py | 14 ++++++++++++-- .../2019-05-02-11-48-08.bpo-36774.ZqbJ1J.rst | 8 ++++---- Python/ast.c | 5 +++-- Python/ast_unparse.c | 2 +- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index d2d5d2b6695b28..76b76ca14d1874 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -1093,7 +1093,8 @@ def test_debug_conversion(self): # Make sure nested fstrings still work. self.assertEqual(f'{f"{3.1415=:.1f}":*^20}', '*****3.1415=3.1*****') - # Make sure text before and after !d works correctly. + # Make sure text before and after an expression with = works + # correctly. pi = 'π' self.assertEqual(f'alpha α {pi=} ω omega', "alpha α pi='π' ω omega") @@ -1117,7 +1118,7 @@ def test_debug_conversion(self): x = 20 # This isn't an assignment expression, it's 'x', with a format - # spec of '=10'. + # 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 @@ -1147,6 +1148,15 @@ def __repr__(self): 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'. See test_walrus: you need to use parens. + 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__': 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 index 9e0b18b28fd20a..b73547c84a7dea 100644 --- 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 @@ -1,7 +1,7 @@ -Add a ``=`` conversion feature to f-strings. This can precede ``!s``, +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``. Added a -``!f`` conversion to switch back to the previous format behavior, if -needed. +``=`` 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/Python/ast.c b/Python/ast.c index db5363a6e3a532..40243512f3570e 100644 --- a/Python/ast.c +++ b/Python/ast.c @@ -5020,7 +5020,7 @@ 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_conversion = 0; /* Are we using the = conversion? */ + int equal_conversion = 0; /* Are we using the = feature? */ PyObject *expr_text = NULL; /* The text of the expression, used for =. */ const char *expr_text_end; @@ -5194,7 +5194,8 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, if (!simple_expression) goto error; - /* Check for = conversion. */ + /* Check for =, which puts the text value of the expression in + expr_text. */ if (**str == '=') { *str += 1; equal_conversion = 1; diff --git a/Python/ast_unparse.c b/Python/ast_unparse.c index 7b795b7cb01d27..25a5c698a1db9e 100644 --- a/Python/ast_unparse.c +++ b/Python/ast_unparse.c @@ -656,7 +656,7 @@ append_formattedvalue(_PyUnicodeWriter *writer, expr_ty e, bool is_format_spec) Py_DECREF(temp_fv_str); if (e->v.FormattedValue.expr_text) { - /* Use the = debug conversion. */ + /* Use the = for debug text expansion. */ APPEND_STR("="); } From 1a53ccd42d41ee55ed049ae6894a2db3da8eca91 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Wed, 8 May 2019 07:08:50 -0400 Subject: [PATCH 52/53] Minor comments and variable renaming. --- Lib/test/test_fstring.py | 2 +- Python/ast.c | 11 ++++++----- Python/compile.c | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index 76b76ca14d1874..a0fae50d172074 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -1151,7 +1151,7 @@ def __repr__(self): def test_walrus(self): 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. + # spec of '=10'. self.assertEqual(f'{x:=10}', ' 20') # This is an assignment expression, which requires parens. diff --git a/Python/ast.c b/Python/ast.c index 40243512f3570e..21abd7e88d8458 100644 --- a/Python/ast.c +++ b/Python/ast.c @@ -5020,7 +5020,7 @@ 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_conversion = 0; /* Are we using the = feature? */ + 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; @@ -5198,10 +5198,11 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, expr_text. */ if (**str == '=') { *str += 1; - equal_conversion = 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 } somewhere ahead. */ + here, since we know there's at least a trailing quote somewhere + ahead. */ while (Py_ISSPACE(**str)) { *str += 1; } @@ -5226,7 +5227,7 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, } } - if (equal_conversion) { + if (equal_flag) { Py_ssize_t len = expr_text_end-expr_start; expr_text = PyUnicode_FromStringAndSize(expr_start, len); if (!expr_text) @@ -5257,7 +5258,7 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, /* If we're in = mode, and have no format spec and no explict conversion, set the conversion to 'r'. */ - if (equal_conversion && format_spec == NULL && conversion == -1) { + if (equal_flag && format_spec == NULL && conversion == -1) { conversion = 'r'; } diff --git a/Python/compile.c b/Python/compile.c index 03693e55b67927..dd27ba840f758c 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -3947,7 +3947,7 @@ compiler_formatted_value(struct compiler *c, expr_ty e) character, and whether or not a format_spec was provided. Convert the conversion char to 3 bits: - !f : 000 0x0 FVC_NONE The default if nothing specified. + : 000 0x0 FVC_NONE The default if nothing specified. !s : 001 0x1 FVC_STR !r : 010 0x2 FVC_REPR !a : 011 0x3 FVC_ASCII From 19dc0d02438e08207da23b2cec79dcfd14b0c5c0 Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Wed, 8 May 2019 11:35:10 -0500 Subject: [PATCH 53/53] bpo-36816: Update the self-signed.pythontest.net cert (GH-13192) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We updated the server, our testsuite must match. https://bugs.python.org/issue36816 ✈️ CLE -> DEN ✈️ #pycon2019 (cherry picked from commit 6bd81734de0b73f1431880d6a75fb71bcbc65fa1) --- Lib/test/selfsigned_pythontestdotnet.pem | 46 +++++++++++++------ .../2019-05-08-15-55-46.bpo-36816.WBKRGZ.rst | 1 + 2 files changed, 33 insertions(+), 14 deletions(-) create mode 100644 Misc/NEWS.d/next/Tests/2019-05-08-15-55-46.bpo-36816.WBKRGZ.rst diff --git a/Lib/test/selfsigned_pythontestdotnet.pem b/Lib/test/selfsigned_pythontestdotnet.pem index b6d259bcb23680..2b1760747bce30 100644 --- a/Lib/test/selfsigned_pythontestdotnet.pem +++ b/Lib/test/selfsigned_pythontestdotnet.pem @@ -1,16 +1,34 @@ -----BEGIN CERTIFICATE----- -MIIClTCCAf6gAwIBAgIJAKGU95wKR8pTMA0GCSqGSIb3DQEBBQUAMHAxCzAJBgNV -BAYTAlhZMRcwFQYDVQQHDA5DYXN0bGUgQW50aHJheDEjMCEGA1UECgwaUHl0aG9u -IFNvZnR3YXJlIEZvdW5kYXRpb24xIzAhBgNVBAMMGnNlbGYtc2lnbmVkLnB5dGhv -bnRlc3QubmV0MB4XDTE0MTEwMjE4MDkyOVoXDTI0MTAzMDE4MDkyOVowcDELMAkG -A1UEBhMCWFkxFzAVBgNVBAcMDkNhc3RsZSBBbnRocmF4MSMwIQYDVQQKDBpQeXRo -b24gU29mdHdhcmUgRm91bmRhdGlvbjEjMCEGA1UEAwwac2VsZi1zaWduZWQucHl0 -aG9udGVzdC5uZXQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANDXQXW9tjyZ -Xt0Iv2tLL1+jinr4wGg36ioLDLFkMf+2Y1GL0v0BnKYG4N1OKlAU15LXGeGer8vm -Sv/yIvmdrELvhAbbo3w4a9TMYQA4XkIVLdvu3mvNOAet+8PMJxn26dbDhG809ALv -EHY57lQsBS3G59RZyBPVqAqmImWNJnVzAgMBAAGjNzA1MCUGA1UdEQQeMByCGnNl -bGYtc2lnbmVkLnB5dGhvbnRlc3QubmV0MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcN -AQEFBQADgYEAIuzAhgMouJpNdf3URCHIineyoSt6WK/9+eyUcjlKOrDoXNZaD72h -TXMeKYoWvJyVcSLKL8ckPtDobgP2OTt0UkyAaj0n+ZHaqq1lH2yVfGUA1ILJv515 -C8BqbvVZuqm3i7ygmw3bqE/lYMgOrYtXXnqOrz6nvsE6Yc9V9rFflOM= +MIIF9zCCA9+gAwIBAgIUH98b4Fw/DyugC9cV7VK7ZODzHsIwDQYJKoZIhvcNAQEL +BQAwgYoxCzAJBgNVBAYTAlhZMRcwFQYDVQQIDA5DYXN0bGUgQW50aHJheDEYMBYG +A1UEBwwPQXJndW1lbnQgQ2xpbmljMSMwIQYDVQQKDBpQeXRob24gU29mdHdhcmUg +Rm91bmRhdGlvbjEjMCEGA1UEAwwac2VsZi1zaWduZWQucHl0aG9udGVzdC5uZXQw +HhcNMTkwNTA4MDEwMjQzWhcNMjcwNzI0MDEwMjQzWjCBijELMAkGA1UEBhMCWFkx +FzAVBgNVBAgMDkNhc3RsZSBBbnRocmF4MRgwFgYDVQQHDA9Bcmd1bWVudCBDbGlu +aWMxIzAhBgNVBAoMGlB5dGhvbiBTb2Z0d2FyZSBGb3VuZGF0aW9uMSMwIQYDVQQD +DBpzZWxmLXNpZ25lZC5weXRob250ZXN0Lm5ldDCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAMKdJlyCThkahwoBb7pl5q64Pe9Fn5jrIvzsveHTc97TpjV2 +RLfICnXKrltPk/ohkVl6K5SUZQZwMVzFubkyxE0nZPHYHlpiKWQxbsYVkYv01rix +IFdLvaxxbGYke2jwQao31s4o61AdlsfK1SdpHQUynBBMssqI3SB4XPmcA7e+wEEx +jxjVish4ixA1vuIZOx8yibu+CFCf/geEjoBMF3QPdzULzlrCSw8k/45iZCSoNbvK +DoL4TVV07PHOxpheDh8ZQmepGvU6pVqhb9m4lgmV0OGWHgozd5Ur9CbTVDmxIEz3 +TSoRtNJK7qtyZdGNqwjksQxgZTjM/d/Lm/BJG99AiOmYOjsl9gbQMZgvQmMAtUsI +aMJnQuZ6R+KEpW/TR5qSKLWZSG45z/op+tzI2m+cE6HwTRVAWbcuJxcAA55MZjqU +OOOu3BBYMjS5nf2sQ9uoXsVBFH7i0mQqoW1SLzr9opI8KsWwFxQmO2vBxWYaN+lH +OmwBZBwyODIsmI1YGXmTp09NxRYz3Qe5GCgFzYowpMrcxUC24iduIdMwwhRM7rKg +7GtIWMSrFfuI1XCLRmSlhDbhNN6fVg2f8Bo9PdH9ihiIyxSrc+FOUasUYCCJvlSZ +8hFUlLvcmrZlWuazohm0lsXuMK1JflmQr/DA/uXxP9xzFfRy+RU3jDyxJbRHAgMB +AAGjUzBRMB0GA1UdDgQWBBSQJyxiPMRK01i+0BsV9zUwDiBaHzAfBgNVHSMEGDAW +gBSQJyxiPMRK01i+0BsV9zUwDiBaHzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4ICAQCR+7a7N/m+WLkxPPIA/CB4MOr2Uf8ixTv435Nyv6rXOun0+lTP +ExSZ0uYQ+L0WylItI3cQHULldDueD+s8TGzxf5woaLKf6tqyr0NYhKs+UeNEzDnN +9PHQIhX0SZw3XyXGUgPNBfRCg2ZDdtMMdOU4XlQN/IN/9hbYTrueyY7eXq9hmtI9 +1srftAMqr9SR1JP7aHI6DVgrEsZVMTDnfT8WmLSGLlY1HmGfdEn1Ip5sbo9uSkiH +AEPgPfjYIvR5LqTOMn4KsrlZyBbFIDh9Sl99M1kZzgH6zUGVLCDg1y6Cms69fx/e +W1HoIeVkY4b4TY7Bk7JsqyNhIuqu7ARaxkdaZWhYaA2YyknwANdFfNpfH+elCLIk +BUt5S3f4i7DaUePTvKukCZiCq4Oyln7RcOn5If73wCeLB/ZM9Ei1HforyLWP1CN8 +XLfpHaoeoPSWIveI0XHUl65LsPN2UbMbul/F23hwl+h8+BLmyAS680Yhn4zEN6Ku +B7Po90HoFa1Du3bmx4jsN73UkT/dwMTi6K072FbipnC1904oGlWmLwvAHvrtxxmL +Pl3pvEaZIu8wa/PNF6Y7J7VIewikIJq6Ta6FrWeFfzMWOj2qA1ZZi6fUaDSNYvuV +J5quYKCc/O+I/yDDf8wyBbZ/gvUXzUHTMYGG+bFrn1p7XDbYYeEJ6R/xEg== -----END CERTIFICATE----- diff --git a/Misc/NEWS.d/next/Tests/2019-05-08-15-55-46.bpo-36816.WBKRGZ.rst b/Misc/NEWS.d/next/Tests/2019-05-08-15-55-46.bpo-36816.WBKRGZ.rst new file mode 100644 index 00000000000000..420dfe8323663b --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2019-05-08-15-55-46.bpo-36816.WBKRGZ.rst @@ -0,0 +1 @@ +Update Lib/test/selfsigned_pythontestdotnet.pem to match self-signed.pythontest.net's new TLS certificate. \ No newline at end of file