bpo-36817: Add f-string debugging using '='. (GH-13123)
If a "=" is specified a the end of an f-string expression, the f-string will evaluate to the text of the expression, followed by '=', followed by the repr of the value of the expression.
This commit is contained in:
parent
65d98d0f53
commit
9a4135e939
|
@ -148,6 +148,20 @@ extensions compiled in release mode and for C extensions compiled with the
|
||||||
stable ABI.
|
stable ABI.
|
||||||
(Contributed by Victor Stinner in :issue:`36722`.)
|
(Contributed by Victor Stinner in :issue:`36722`.)
|
||||||
|
|
||||||
|
f-strings now support = for quick and easy debugging
|
||||||
|
-----------------------------------------------------
|
||||||
|
|
||||||
|
Add ``=`` specifier to f-strings. ``f'{expr=}'`` expands
|
||||||
|
to the text of the expression, an equal sign, then the repr of the
|
||||||
|
evaluated expression. So::
|
||||||
|
|
||||||
|
x = 3
|
||||||
|
print(f'{x*9 + 15=}')
|
||||||
|
|
||||||
|
Would print ``x*9 + 15=42``.
|
||||||
|
|
||||||
|
(Contributed by Eric V. Smith and Larry Hastings in :issue:`36817`.)
|
||||||
|
|
||||||
|
|
||||||
Other Language Changes
|
Other Language Changes
|
||||||
======================
|
======================
|
||||||
|
|
|
@ -330,6 +330,7 @@ struct _expr {
|
||||||
expr_ty value;
|
expr_ty value;
|
||||||
int conversion;
|
int conversion;
|
||||||
expr_ty format_spec;
|
expr_ty format_spec;
|
||||||
|
string expr_text;
|
||||||
} FormattedValue;
|
} FormattedValue;
|
||||||
|
|
||||||
struct {
|
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
|
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,
|
lineno, int col_offset, int end_lineno, int end_col_offset,
|
||||||
PyArena *arena);
|
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,
|
expr_ty _Py_FormattedValue(expr_ty value, int conversion, expr_ty format_spec,
|
||||||
int lineno, int col_offset, int end_lineno, int
|
string expr_text, int lineno, int col_offset, int
|
||||||
end_col_offset, PyArena *arena);
|
end_lineno, int end_col_offset, PyArena *arena);
|
||||||
#define JoinedStr(a0, a1, a2, a3, a4, a5) _Py_JoinedStr(a0, a1, a2, a3, a4, a5)
|
#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
|
expr_ty _Py_JoinedStr(asdl_seq * values, int lineno, int col_offset, int
|
||||||
end_lineno, int end_col_offset, PyArena *arena);
|
end_lineno, int end_col_offset, PyArena *arena);
|
||||||
|
|
|
@ -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 ast
|
||||||
import types
|
import types
|
||||||
import decimal
|
import decimal
|
||||||
|
@ -878,6 +887,12 @@ non-important content
|
||||||
self.assertEqual(f'{3!=4!s}', 'True')
|
self.assertEqual(f'{3!=4!s}', 'True')
|
||||||
self.assertEqual(f'{3!=4!s:.3}', 'Tru')
|
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):
|
def test_conversions(self):
|
||||||
self.assertEqual(f'{3.14:10.10}', ' 3.14')
|
self.assertEqual(f'{3.14:10.10}', ' 3.14')
|
||||||
self.assertEqual(f'{3.14!s:10.10}', '3.14 ')
|
self.assertEqual(f'{3.14!s:10.10}', '3.14 ')
|
||||||
|
@ -1049,6 +1064,100 @@ non-important content
|
||||||
self.assertEqual(eval('f"\\\n"'), '')
|
self.assertEqual(eval('f"\\\n"'), '')
|
||||||
self.assertEqual(eval('f"\\\r"'), '')
|
self.assertEqual(eval('f"\\\r"'), '')
|
||||||
|
|
||||||
|
def test_debug_conversion(self):
|
||||||
|
x = 'A string'
|
||||||
|
self.assertEqual(f'{x=}', 'x=' + repr(x))
|
||||||
|
self.assertEqual(f'{x =}', 'x =' + repr(x))
|
||||||
|
self.assertEqual(f'{x=!s}', 'x=' + str(x))
|
||||||
|
self.assertEqual(f'{x=!r}', 'x=' + repr(x))
|
||||||
|
self.assertEqual(f'{x=!a}', 'x=' + ascii(x))
|
||||||
|
|
||||||
|
x = 2.71828
|
||||||
|
self.assertEqual(f'{x=:.2f}', 'x=' + format(x, '.2f'))
|
||||||
|
self.assertEqual(f'{x=:}', 'x=' + format(x, ''))
|
||||||
|
self.assertEqual(f'{x=!r:^20}', 'x=' + format(repr(x), '^20'))
|
||||||
|
self.assertEqual(f'{x=!s:^20}', 'x=' + format(str(x), '^20'))
|
||||||
|
self.assertEqual(f'{x=!a:^20}', 'x=' + format(ascii(x), '^20'))
|
||||||
|
|
||||||
|
x = 9
|
||||||
|
self.assertEqual(f'{3*x+15=}', '3*x+15=42')
|
||||||
|
|
||||||
|
# There is code in ast.c that deals with non-ascii expression values. So,
|
||||||
|
# use a unicode identifier to trigger that.
|
||||||
|
tenπ = 31.4
|
||||||
|
self.assertEqual(f'{tenπ=:.2f}', 'tenπ=31.40')
|
||||||
|
|
||||||
|
# Also test with Unicode in non-identifiers.
|
||||||
|
self.assertEqual(f'{"Σ"=}', '"Σ"=\'Σ\'')
|
||||||
|
|
||||||
|
# Make sure nested fstrings still work.
|
||||||
|
self.assertEqual(f'{f"{3.1415=:.1f}":*^20}', '*****3.1415=3.1*****')
|
||||||
|
|
||||||
|
# Make sure text before and after an expression with = works
|
||||||
|
# correctly.
|
||||||
|
pi = 'π'
|
||||||
|
self.assertEqual(f'alpha α {pi=} ω omega', "alpha α pi='π' ω omega")
|
||||||
|
|
||||||
|
# Check multi-line expressions.
|
||||||
|
self.assertEqual(f'''{
|
||||||
|
3
|
||||||
|
=}''', '\n3\n=3')
|
||||||
|
|
||||||
|
# Since = is handled specially, make sure all existing uses of
|
||||||
|
# it still work.
|
||||||
|
|
||||||
|
self.assertEqual(f'{0==1}', 'False')
|
||||||
|
self.assertEqual(f'{0!=1}', 'True')
|
||||||
|
self.assertEqual(f'{0<=1}', 'True')
|
||||||
|
self.assertEqual(f'{0>=1}', 'False')
|
||||||
|
self.assertEqual(f'{(x:="5")}', '5')
|
||||||
|
self.assertEqual(x, '5')
|
||||||
|
self.assertEqual(f'{(x:=5)}', '5')
|
||||||
|
self.assertEqual(x, 5)
|
||||||
|
self.assertEqual(f'{"="}', '=')
|
||||||
|
|
||||||
|
x = 20
|
||||||
|
# This isn't an assignment expression, it's 'x', with a format
|
||||||
|
# spec of '=10'. See test_walrus: you need to use parens.
|
||||||
|
self.assertEqual(f'{x:=10}', ' 20')
|
||||||
|
|
||||||
|
# Test named function parameters, to make sure '=' parsing works
|
||||||
|
# there.
|
||||||
|
def f(a):
|
||||||
|
nonlocal x
|
||||||
|
oldx = x
|
||||||
|
x = a
|
||||||
|
return oldx
|
||||||
|
x = 0
|
||||||
|
self.assertEqual(f'{f(a="3=")}', '0')
|
||||||
|
self.assertEqual(x, '3=')
|
||||||
|
self.assertEqual(f'{f(a=4)}', '3=')
|
||||||
|
self.assertEqual(x, 4)
|
||||||
|
|
||||||
|
# Make sure __format__ is being called.
|
||||||
|
class C:
|
||||||
|
def __format__(self, s):
|
||||||
|
return f'FORMAT-{s}'
|
||||||
|
def __repr__(self):
|
||||||
|
return 'REPR'
|
||||||
|
|
||||||
|
self.assertEqual(f'{C()=}', 'C()=REPR')
|
||||||
|
self.assertEqual(f'{C()=!r}', 'C()=REPR')
|
||||||
|
self.assertEqual(f'{C()=:}', 'C()=FORMAT-')
|
||||||
|
self.assertEqual(f'{C()=: }', 'C()=FORMAT- ')
|
||||||
|
self.assertEqual(f'{C()=:x}', 'C()=FORMAT-x')
|
||||||
|
self.assertEqual(f'{C()=!r:*^20}', 'C()=********REPR********')
|
||||||
|
|
||||||
|
def test_walrus(self):
|
||||||
|
x = 20
|
||||||
|
# This isn't an assignment expression, it's 'x', with a format
|
||||||
|
# spec of '=10'.
|
||||||
|
self.assertEqual(f'{x:=10}', ' 20')
|
||||||
|
|
||||||
|
# This is an assignment expression, which requires parens.
|
||||||
|
self.assertEqual(f'{(x:=10)}', '10')
|
||||||
|
self.assertEqual(x, 10)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -255,6 +255,15 @@ class AnnotationsFutureTestCase(unittest.TestCase):
|
||||||
eq("f'space between opening braces: { {a for a in (1, 2, 3)}}'")
|
eq("f'space between opening braces: { {a for a in (1, 2, 3)}}'")
|
||||||
eq("f'{(lambda x: x)}'")
|
eq("f'{(lambda x: x)}'")
|
||||||
eq("f'{(None if a else lambda x: x)}'")
|
eq("f'{(None if a else lambda x: x)}'")
|
||||||
|
eq("f'{x}'")
|
||||||
|
eq("f'{x!r}'")
|
||||||
|
eq("f'{x!a}'")
|
||||||
|
eq("f'{x=!r}'")
|
||||||
|
eq("f'{x=:}'")
|
||||||
|
eq("f'{x=:.2f}'")
|
||||||
|
eq("f'{x=!r}'")
|
||||||
|
eq("f'{x=!a}'")
|
||||||
|
eq("f'{x=!s:*^20}'")
|
||||||
eq('(yield from outside_of_generator)')
|
eq('(yield from outside_of_generator)')
|
||||||
eq('(yield)')
|
eq('(yield)')
|
||||||
eq('(yield a + b)')
|
eq('(yield a + b)')
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
Add a ``=`` feature f-strings for debugging. This can precede ``!s``,
|
||||||
|
``!r``, or ``!a``. It produces the text of the expression, followed by
|
||||||
|
an equal sign, followed by the repr of the value of the expression. So
|
||||||
|
``f'{3*9+15=}'`` would be equal to the string ``'3*9+15=42'``. If
|
||||||
|
``=`` is specified, the default conversion is set to ``!r``, unless a
|
||||||
|
format spec is given, in which case the formatting behavior is
|
||||||
|
unchanged, and __format__ will be used.
|
|
@ -76,7 +76,7 @@ module Python
|
||||||
-- x < 4 < 3 and (x < 4) < 3
|
-- x < 4 < 3 and (x < 4) < 3
|
||||||
| Compare(expr left, cmpop* ops, expr* comparators)
|
| Compare(expr left, cmpop* ops, expr* comparators)
|
||||||
| Call(expr func, expr* args, keyword* keywords)
|
| Call(expr func, expr* args, keyword* keywords)
|
||||||
| FormattedValue(expr value, int? conversion, expr? format_spec)
|
| FormattedValue(expr value, int? conversion, expr? format_spec, string? expr_text)
|
||||||
| JoinedStr(expr* values)
|
| JoinedStr(expr* values)
|
||||||
| Constant(constant value, string? kind)
|
| Constant(constant value, string? kind)
|
||||||
|
|
||||||
|
|
|
@ -314,10 +314,12 @@ static char *Call_fields[]={
|
||||||
static PyTypeObject *FormattedValue_type;
|
static PyTypeObject *FormattedValue_type;
|
||||||
_Py_IDENTIFIER(conversion);
|
_Py_IDENTIFIER(conversion);
|
||||||
_Py_IDENTIFIER(format_spec);
|
_Py_IDENTIFIER(format_spec);
|
||||||
|
_Py_IDENTIFIER(expr_text);
|
||||||
static char *FormattedValue_fields[]={
|
static char *FormattedValue_fields[]={
|
||||||
"value",
|
"value",
|
||||||
"conversion",
|
"conversion",
|
||||||
"format_spec",
|
"format_spec",
|
||||||
|
"expr_text",
|
||||||
};
|
};
|
||||||
static PyTypeObject *JoinedStr_type;
|
static PyTypeObject *JoinedStr_type;
|
||||||
static char *JoinedStr_fields[]={
|
static char *JoinedStr_fields[]={
|
||||||
|
@ -950,7 +952,7 @@ static int init_types(void)
|
||||||
Call_type = make_type("Call", expr_type, Call_fields, 3);
|
Call_type = make_type("Call", expr_type, Call_fields, 3);
|
||||||
if (!Call_type) return 0;
|
if (!Call_type) return 0;
|
||||||
FormattedValue_type = make_type("FormattedValue", expr_type,
|
FormattedValue_type = make_type("FormattedValue", expr_type,
|
||||||
FormattedValue_fields, 3);
|
FormattedValue_fields, 4);
|
||||||
if (!FormattedValue_type) return 0;
|
if (!FormattedValue_type) return 0;
|
||||||
JoinedStr_type = make_type("JoinedStr", expr_type, JoinedStr_fields, 1);
|
JoinedStr_type = make_type("JoinedStr", expr_type, JoinedStr_fields, 1);
|
||||||
if (!JoinedStr_type) return 0;
|
if (!JoinedStr_type) return 0;
|
||||||
|
@ -2249,9 +2251,9 @@ Call(expr_ty func, asdl_seq * args, asdl_seq * keywords, int lineno, int
|
||||||
}
|
}
|
||||||
|
|
||||||
expr_ty
|
expr_ty
|
||||||
FormattedValue(expr_ty value, int conversion, expr_ty format_spec, int lineno,
|
FormattedValue(expr_ty value, int conversion, expr_ty format_spec, string
|
||||||
int col_offset, int end_lineno, int end_col_offset, PyArena
|
expr_text, int lineno, int col_offset, int end_lineno, int
|
||||||
*arena)
|
end_col_offset, PyArena *arena)
|
||||||
{
|
{
|
||||||
expr_ty p;
|
expr_ty p;
|
||||||
if (!value) {
|
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.value = value;
|
||||||
p->v.FormattedValue.conversion = conversion;
|
p->v.FormattedValue.conversion = conversion;
|
||||||
p->v.FormattedValue.format_spec = format_spec;
|
p->v.FormattedValue.format_spec = format_spec;
|
||||||
|
p->v.FormattedValue.expr_text = expr_text;
|
||||||
p->lineno = lineno;
|
p->lineno = lineno;
|
||||||
p->col_offset = col_offset;
|
p->col_offset = col_offset;
|
||||||
p->end_lineno = end_lineno;
|
p->end_lineno = end_lineno;
|
||||||
|
@ -3496,6 +3499,11 @@ ast2obj_expr(void* _o)
|
||||||
if (_PyObject_SetAttrId(result, &PyId_format_spec, value) == -1)
|
if (_PyObject_SetAttrId(result, &PyId_format_spec, value) == -1)
|
||||||
goto failed;
|
goto failed;
|
||||||
Py_DECREF(value);
|
Py_DECREF(value);
|
||||||
|
value = ast2obj_string(o->v.FormattedValue.expr_text);
|
||||||
|
if (!value) goto failed;
|
||||||
|
if (_PyObject_SetAttrId(result, &PyId_expr_text, value) == -1)
|
||||||
|
goto failed;
|
||||||
|
Py_DECREF(value);
|
||||||
break;
|
break;
|
||||||
case JoinedStr_kind:
|
case JoinedStr_kind:
|
||||||
result = PyType_GenericNew(JoinedStr_type, NULL, NULL);
|
result = PyType_GenericNew(JoinedStr_type, NULL, NULL);
|
||||||
|
@ -7148,6 +7156,7 @@ obj2ast_expr(PyObject* obj, expr_ty* out, PyArena* arena)
|
||||||
expr_ty value;
|
expr_ty value;
|
||||||
int conversion;
|
int conversion;
|
||||||
expr_ty format_spec;
|
expr_ty format_spec;
|
||||||
|
string expr_text;
|
||||||
|
|
||||||
if (_PyObject_LookupAttrId(obj, &PyId_value, &tmp) < 0) {
|
if (_PyObject_LookupAttrId(obj, &PyId_value, &tmp) < 0) {
|
||||||
return 1;
|
return 1;
|
||||||
|
@ -7188,8 +7197,22 @@ obj2ast_expr(PyObject* obj, expr_ty* out, PyArena* arena)
|
||||||
if (res != 0) goto failed;
|
if (res != 0) goto failed;
|
||||||
Py_CLEAR(tmp);
|
Py_CLEAR(tmp);
|
||||||
}
|
}
|
||||||
*out = FormattedValue(value, conversion, format_spec, lineno,
|
if (_PyObject_LookupAttrId(obj, &PyId_expr_text, &tmp) < 0) {
|
||||||
col_offset, end_lineno, end_col_offset, arena);
|
return 1;
|
||||||
|
}
|
||||||
|
if (tmp == NULL || tmp == Py_None) {
|
||||||
|
Py_CLEAR(tmp);
|
||||||
|
expr_text = NULL;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
int res;
|
||||||
|
res = obj2ast_string(tmp, &expr_text, arena);
|
||||||
|
if (res != 0) goto failed;
|
||||||
|
Py_CLEAR(tmp);
|
||||||
|
}
|
||||||
|
*out = FormattedValue(value, conversion, format_spec, expr_text,
|
||||||
|
lineno, col_offset, end_lineno, end_col_offset,
|
||||||
|
arena);
|
||||||
if (*out == NULL) goto failed;
|
if (*out == NULL) goto failed;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
112
Python/ast.c
112
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_end >= expr_start);
|
||||||
assert(*(expr_start-1) == '{');
|
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
|
/* If the substring is all whitespace, it's an error. We need to catch this
|
||||||
here, and not when we call PyParser_SimpleParseStringFlagsFilename,
|
here, and not when we call PyParser_SimpleParseStringFlagsFilename,
|
||||||
|
@ -4997,9 +4998,9 @@ fstring_parse(const char **str, const char *end, int raw, int recurse_lvl,
|
||||||
struct compiling *c, const node *n);
|
struct compiling *c, const node *n);
|
||||||
|
|
||||||
/* Parse the f-string at *str, ending at end. We know *str starts an
|
/* Parse the f-string at *str, ending at end. We know *str starts an
|
||||||
expression (so it must be a '{'). Returns the FormattedValue node,
|
expression (so it must be a '{'). Returns the FormattedValue node, which
|
||||||
which includes the expression, conversion character, and
|
includes the expression, conversion character, format_spec expression, and
|
||||||
format_spec expression.
|
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
|
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
|
closing brace doesn't match an opening paren, for example. It
|
||||||
|
@ -5016,7 +5017,12 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl,
|
||||||
const char *expr_end;
|
const char *expr_end;
|
||||||
expr_ty simple_expression;
|
expr_ty simple_expression;
|
||||||
expr_ty format_spec = NULL; /* Optional format specifier. */
|
expr_ty format_spec = NULL; /* Optional format specifier. */
|
||||||
int conversion = -1; /* The conversion char. -1 if not specified. */
|
int conversion = -1; /* The conversion char. Use default if not
|
||||||
|
specified, or !r if using = and no format
|
||||||
|
spec. */
|
||||||
|
int equal_flag = 0; /* Are we using the = feature? */
|
||||||
|
PyObject *expr_text = NULL; /* The text of the expression, used for =. */
|
||||||
|
const char *expr_text_end;
|
||||||
|
|
||||||
/* 0 if we're not in a string, else the quote char we're trying to
|
/* 0 if we're not in a string, else the quote char we're trying to
|
||||||
match (single or double quote). */
|
match (single or double quote). */
|
||||||
|
@ -5033,7 +5039,7 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl,
|
||||||
/* Can only nest one level deep. */
|
/* Can only nest one level deep. */
|
||||||
if (recurse_lvl >= 2) {
|
if (recurse_lvl >= 2) {
|
||||||
ast_error(c, n, "f-string: expressions nested too deeply");
|
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
|
/* The first char must be a left brace, or we wouldn't have gotten
|
||||||
|
@ -5061,7 +5067,7 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl,
|
||||||
ast_error(c, n,
|
ast_error(c, n,
|
||||||
"f-string expression part "
|
"f-string expression part "
|
||||||
"cannot include a backslash");
|
"cannot include a backslash");
|
||||||
return -1;
|
goto error;
|
||||||
}
|
}
|
||||||
if (quote_char) {
|
if (quote_char) {
|
||||||
/* We're inside a string. See if we're at the end. */
|
/* We're inside a string. See if we're at the end. */
|
||||||
|
@ -5106,7 +5112,7 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl,
|
||||||
} else if (ch == '[' || ch == '{' || ch == '(') {
|
} else if (ch == '[' || ch == '{' || ch == '(') {
|
||||||
if (nested_depth >= MAXLEVEL) {
|
if (nested_depth >= MAXLEVEL) {
|
||||||
ast_error(c, n, "f-string: too many nested parenthesis");
|
ast_error(c, n, "f-string: too many nested parenthesis");
|
||||||
return -1;
|
goto error;
|
||||||
}
|
}
|
||||||
parenstack[nested_depth] = ch;
|
parenstack[nested_depth] = ch;
|
||||||
nested_depth++;
|
nested_depth++;
|
||||||
|
@ -5114,22 +5120,38 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl,
|
||||||
/* Error: can't include a comment character, inside parens
|
/* Error: can't include a comment character, inside parens
|
||||||
or not. */
|
or not. */
|
||||||
ast_error(c, n, "f-string expression part cannot include '#'");
|
ast_error(c, n, "f-string expression part cannot include '#'");
|
||||||
return -1;
|
goto error;
|
||||||
} else if (nested_depth == 0 &&
|
} else if (nested_depth == 0 &&
|
||||||
(ch == '!' || ch == ':' || ch == '}')) {
|
(ch == '!' || ch == ':' || ch == '}' ||
|
||||||
/* First, test for the special case of "!=". Since '=' is
|
ch == '=' || ch == '>' || ch == '<')) {
|
||||||
not an allowed conversion character, nothing is lost in
|
/* See if there's a next character. */
|
||||||
this test. */
|
if (*str+1 < end) {
|
||||||
if (ch == '!' && *str+1 < end && *(*str+1) == '=') {
|
char next = *(*str+1);
|
||||||
/* This isn't a conversion character, just continue. */
|
|
||||||
continue;
|
/* For "!=". since '=' is not an allowed conversion character,
|
||||||
|
nothing is lost in this test. */
|
||||||
|
if ((ch == '!' && next == '=') || /* != */
|
||||||
|
(ch == '=' && next == '=') || /* == */
|
||||||
|
(ch == '<' && next == '=') || /* <= */
|
||||||
|
(ch == '>' && next == '=') /* >= */
|
||||||
|
) {
|
||||||
|
*str += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
/* Don't get out of the loop for these, if they're single
|
||||||
|
chars (not part of 2-char tokens). If by themselves, they
|
||||||
|
don't end an expression (unlike say '!'). */
|
||||||
|
if (ch == '>' || ch == '<') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Normal way out of this loop. */
|
/* Normal way out of this loop. */
|
||||||
break;
|
break;
|
||||||
} else if (ch == ']' || ch == '}' || ch == ')') {
|
} else if (ch == ']' || ch == '}' || ch == ')') {
|
||||||
if (!nested_depth) {
|
if (!nested_depth) {
|
||||||
ast_error(c, n, "f-string: unmatched '%c'", ch);
|
ast_error(c, n, "f-string: unmatched '%c'", ch);
|
||||||
return -1;
|
goto error;
|
||||||
}
|
}
|
||||||
nested_depth--;
|
nested_depth--;
|
||||||
int opening = parenstack[nested_depth];
|
int opening = parenstack[nested_depth];
|
||||||
|
@ -5141,7 +5163,7 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl,
|
||||||
"f-string: closing parenthesis '%c' "
|
"f-string: closing parenthesis '%c' "
|
||||||
"does not match opening parenthesis '%c'",
|
"does not match opening parenthesis '%c'",
|
||||||
ch, opening);
|
ch, opening);
|
||||||
return -1;
|
goto error;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
/* Just consume this char and loop around. */
|
/* Just consume this char and loop around. */
|
||||||
|
@ -5154,12 +5176,12 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl,
|
||||||
let's just do that.*/
|
let's just do that.*/
|
||||||
if (quote_char) {
|
if (quote_char) {
|
||||||
ast_error(c, n, "f-string: unterminated string");
|
ast_error(c, n, "f-string: unterminated string");
|
||||||
return -1;
|
goto error;
|
||||||
}
|
}
|
||||||
if (nested_depth) {
|
if (nested_depth) {
|
||||||
int opening = parenstack[nested_depth - 1];
|
int opening = parenstack[nested_depth - 1];
|
||||||
ast_error(c, n, "f-string: unmatched '%c'", opening);
|
ast_error(c, n, "f-string: unmatched '%c'", opening);
|
||||||
return -1;
|
goto error;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (*str >= end)
|
if (*str >= end)
|
||||||
|
@ -5170,7 +5192,22 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl,
|
||||||
conversion or format_spec. */
|
conversion or format_spec. */
|
||||||
simple_expression = fstring_compile_expr(expr_start, expr_end, c, n);
|
simple_expression = fstring_compile_expr(expr_start, expr_end, c, n);
|
||||||
if (!simple_expression)
|
if (!simple_expression)
|
||||||
return -1;
|
goto error;
|
||||||
|
|
||||||
|
/* Check for =, which puts the text value of the expression in
|
||||||
|
expr_text. */
|
||||||
|
if (**str == '=') {
|
||||||
|
*str += 1;
|
||||||
|
equal_flag = 1;
|
||||||
|
|
||||||
|
/* Skip over ASCII whitespace. No need to test for end of string
|
||||||
|
here, since we know there's at least a trailing quote somewhere
|
||||||
|
ahead. */
|
||||||
|
while (Py_ISSPACE(**str)) {
|
||||||
|
*str += 1;
|
||||||
|
}
|
||||||
|
expr_text_end = *str;
|
||||||
|
}
|
||||||
|
|
||||||
/* Check for a conversion char, if present. */
|
/* Check for a conversion char, if present. */
|
||||||
if (**str == '!') {
|
if (**str == '!') {
|
||||||
|
@ -5182,13 +5219,19 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl,
|
||||||
*str += 1;
|
*str += 1;
|
||||||
|
|
||||||
/* Validate the conversion. */
|
/* Validate the conversion. */
|
||||||
if (!(conversion == 's' || conversion == 'r'
|
if (!(conversion == 's' || conversion == 'r' || conversion == 'a')) {
|
||||||
|| conversion == 'a')) {
|
|
||||||
ast_error(c, n,
|
ast_error(c, n,
|
||||||
"f-string: invalid conversion character: "
|
"f-string: invalid conversion character: "
|
||||||
"expected 's', 'r', or 'a'");
|
"expected 's', 'r', or 'a'");
|
||||||
return -1;
|
goto error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
if (equal_flag) {
|
||||||
|
Py_ssize_t len = expr_text_end-expr_start;
|
||||||
|
expr_text = PyUnicode_FromStringAndSize(expr_start, len);
|
||||||
|
if (!expr_text)
|
||||||
|
goto error;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Check for the format spec, if present. */
|
/* Check for the format spec, if present. */
|
||||||
|
@ -5202,7 +5245,7 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl,
|
||||||
/* Parse the format spec. */
|
/* Parse the format spec. */
|
||||||
format_spec = fstring_parse(str, end, raw, recurse_lvl+1, c, n);
|
format_spec = fstring_parse(str, end, raw, recurse_lvl+1, c, n);
|
||||||
if (!format_spec)
|
if (!format_spec)
|
||||||
return -1;
|
goto error;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (*str >= end || **str != '}')
|
if (*str >= end || **str != '}')
|
||||||
|
@ -5213,20 +5256,31 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl,
|
||||||
assert(**str == '}');
|
assert(**str == '}');
|
||||||
*str += 1;
|
*str += 1;
|
||||||
|
|
||||||
|
/* If we're in = mode, and have no format spec and no explict conversion,
|
||||||
|
set the conversion to 'r'. */
|
||||||
|
if (equal_flag && format_spec == NULL && conversion == -1) {
|
||||||
|
conversion = 'r';
|
||||||
|
}
|
||||||
|
|
||||||
/* And now create the FormattedValue node that represents this
|
/* And now create the FormattedValue node that represents this
|
||||||
entire expression with the conversion and format spec. */
|
entire expression with the conversion and format spec. */
|
||||||
*expression = FormattedValue(simple_expression, conversion,
|
*expression = FormattedValue(simple_expression, conversion,
|
||||||
format_spec, LINENO(n), n->n_col_offset,
|
format_spec, expr_text, LINENO(n),
|
||||||
n->n_end_lineno, n->n_end_col_offset,
|
n->n_col_offset, n->n_end_lineno,
|
||||||
c->c_arena);
|
n->n_end_col_offset, c->c_arena);
|
||||||
if (!*expression)
|
if (!*expression)
|
||||||
return -1;
|
goto error;
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
unexpected_end_of_string:
|
unexpected_end_of_string:
|
||||||
ast_error(c, n, "f-string: expecting '}'");
|
ast_error(c, n, "f-string: expecting '}'");
|
||||||
|
/* Falls through to error. */
|
||||||
|
|
||||||
|
error:
|
||||||
|
Py_XDECREF(expr_text);
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Return -1 on error.
|
/* Return -1 on error.
|
||||||
|
|
|
@ -655,6 +655,11 @@ append_formattedvalue(_PyUnicodeWriter *writer, expr_ty e, bool is_format_spec)
|
||||||
}
|
}
|
||||||
Py_DECREF(temp_fv_str);
|
Py_DECREF(temp_fv_str);
|
||||||
|
|
||||||
|
if (e->v.FormattedValue.expr_text) {
|
||||||
|
/* Use the = for debug text expansion. */
|
||||||
|
APPEND_STR("=");
|
||||||
|
}
|
||||||
|
|
||||||
if (e->v.FormattedValue.conversion > 0) {
|
if (e->v.FormattedValue.conversion > 0) {
|
||||||
switch (e->v.FormattedValue.conversion) {
|
switch (e->v.FormattedValue.conversion) {
|
||||||
case 'a':
|
case 'a':
|
||||||
|
|
|
@ -3435,13 +3435,15 @@ main_loop:
|
||||||
|
|
||||||
/* See if any conversion is specified. */
|
/* See if any conversion is specified. */
|
||||||
switch (which_conversion) {
|
switch (which_conversion) {
|
||||||
|
case FVC_NONE: conv_fn = NULL; break;
|
||||||
case FVC_STR: conv_fn = PyObject_Str; break;
|
case FVC_STR: conv_fn = PyObject_Str; break;
|
||||||
case FVC_REPR: conv_fn = PyObject_Repr; break;
|
case FVC_REPR: conv_fn = PyObject_Repr; break;
|
||||||
case FVC_ASCII: conv_fn = PyObject_ASCII; break;
|
case FVC_ASCII: conv_fn = PyObject_ASCII; break;
|
||||||
|
default:
|
||||||
/* Must be 0 (meaning no conversion), since only four
|
PyErr_Format(PyExc_SystemError,
|
||||||
values are allowed by (oparg & FVC_MASK). */
|
"unexpected conversion flag %d",
|
||||||
default: conv_fn = NULL; break;
|
which_conversion);
|
||||||
|
goto error;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* If there's a conversion function, call it and replace
|
/* If there's a conversion function, call it and replace
|
||||||
|
|
|
@ -3946,8 +3946,8 @@ compiler_formatted_value(struct compiler *c, expr_ty e)
|
||||||
/* Our oparg encodes 2 pieces of information: the conversion
|
/* Our oparg encodes 2 pieces of information: the conversion
|
||||||
character, and whether or not a format_spec was provided.
|
character, and whether or not a format_spec was provided.
|
||||||
|
|
||||||
Convert the conversion char to 2 bits:
|
Convert the conversion char to 3 bits:
|
||||||
None: 000 0x0 FVC_NONE
|
: 000 0x0 FVC_NONE The default if nothing specified.
|
||||||
!s : 001 0x1 FVC_STR
|
!s : 001 0x1 FVC_STR
|
||||||
!r : 010 0x2 FVC_REPR
|
!r : 010 0x2 FVC_REPR
|
||||||
!a : 011 0x3 FVC_ASCII
|
!a : 011 0x3 FVC_ASCII
|
||||||
|
@ -3957,19 +3957,26 @@ compiler_formatted_value(struct compiler *c, expr_ty e)
|
||||||
no : 000 0x0
|
no : 000 0x0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
int conversion = e->v.FormattedValue.conversion;
|
||||||
int oparg;
|
int oparg;
|
||||||
|
|
||||||
/* Evaluate the expression to be formatted. */
|
if (e->v.FormattedValue.expr_text) {
|
||||||
|
/* Push the text of the expression (which already has the '=' in
|
||||||
|
it. */
|
||||||
|
ADDOP_LOAD_CONST(c, e->v.FormattedValue.expr_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The expression to be formatted. */
|
||||||
VISIT(c, expr, e->v.FormattedValue.value);
|
VISIT(c, expr, e->v.FormattedValue.value);
|
||||||
|
|
||||||
switch (e->v.FormattedValue.conversion) {
|
switch (conversion) {
|
||||||
case 's': oparg = FVC_STR; break;
|
case 's': oparg = FVC_STR; break;
|
||||||
case 'r': oparg = FVC_REPR; break;
|
case 'r': oparg = FVC_REPR; break;
|
||||||
case 'a': oparg = FVC_ASCII; break;
|
case 'a': oparg = FVC_ASCII; break;
|
||||||
case -1: oparg = FVC_NONE; break;
|
case -1: oparg = FVC_NONE; break;
|
||||||
default:
|
default:
|
||||||
PyErr_SetString(PyExc_SystemError,
|
PyErr_Format(PyExc_SystemError,
|
||||||
"Unrecognized conversion character");
|
"Unrecognized conversion character %d", conversion);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (e->v.FormattedValue.format_spec) {
|
if (e->v.FormattedValue.format_spec) {
|
||||||
|
@ -3980,6 +3987,12 @@ compiler_formatted_value(struct compiler *c, expr_ty e)
|
||||||
|
|
||||||
/* And push our opcode and oparg */
|
/* And push our opcode and oparg */
|
||||||
ADDOP_I(c, FORMAT_VALUE, 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;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue