From 8b010673185d36d13e69e5bf7d902a0b3fa63051 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 23 May 2021 19:06:48 +0300 Subject: bpo-28307: Tests and fixes for optimization of C-style formatting (GH-26318) Fix errors: * "%10.s" should be equal to "%10.0s", not "%10s". * Tuples with starred expressions caused a SyntaxError. --- Lib/test/test_peepholer.py | 76 ++++++++++++++++++++++++++++++++++++++++++++++ Python/ast_opt.c | 27 +++++++++++----- 2 files changed, 95 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_peepholer.py b/Lib/test/test_peepholer.py index 4034154..bfafc3e 100644 --- a/Lib/test/test_peepholer.py +++ b/Lib/test/test_peepholer.py @@ -1,4 +1,5 @@ import dis +from itertools import combinations, product import unittest from test.support.bytecode_helper import BytecodeTestCase @@ -494,6 +495,81 @@ class TestTranforms(BytecodeTestCase): return (y for x in a for y in [f(x)]) self.assertEqual(count_instr_recursively(genexpr, 'FOR_ITER'), 1) + def test_format_combinations(self): + flags = '-+ #0' + testcases = [ + *product(('', '1234', 'абвг'), 'sra'), + *product((1234, -1234), 'duioxX'), + *product((1234.5678901, -1234.5678901), 'duifegFEG'), + *product((float('inf'), -float('inf')), 'fegFEG'), + ] + width_precs = [ + *product(('', '1', '30'), ('', '.', '.0', '.2')), + ('', '.40'), + ('30', '.40'), + ] + for value, suffix in testcases: + for width, prec in width_precs: + for r in range(len(flags) + 1): + for spec in combinations(flags, r): + fmt = '%' + ''.join(spec) + width + prec + suffix + with self.subTest(fmt=fmt, value=value): + s1 = fmt % value + s2 = eval(f'{fmt!r} % (x,)', {'x': value}) + self.assertEqual(s2, s1, f'{fmt = }') + + def test_format_misc(self): + def format(fmt, *values): + vars = [f'x{i+1}' for i in range(len(values))] + if len(vars) == 1: + args = '(' + vars[0] + ',)' + else: + args = '(' + ', '.join(vars) + ')' + return eval(f'{fmt!r} % {args}', dict(zip(vars, values))) + + self.assertEqual(format('string'), 'string') + self.assertEqual(format('x = %s!', 1234), 'x = 1234!') + self.assertEqual(format('x = %d!', 1234), 'x = 1234!') + self.assertEqual(format('x = %x!', 1234), 'x = 4d2!') + self.assertEqual(format('x = %f!', 1234), 'x = 1234.000000!') + self.assertEqual(format('x = %s!', 1234.5678901), 'x = 1234.5678901!') + self.assertEqual(format('x = %f!', 1234.5678901), 'x = 1234.567890!') + self.assertEqual(format('x = %d!', 1234.5678901), 'x = 1234!') + self.assertEqual(format('x = %s%% %%%%', 1234), 'x = 1234% %%') + self.assertEqual(format('x = %s!', '%% %s'), 'x = %% %s!') + self.assertEqual(format('x = %s, y = %d', 12, 34), 'x = 12, y = 34') + + def test_format_errors(self): + with self.assertRaisesRegex(TypeError, + 'not enough arguments for format string'): + eval("'%s' % ()") + with self.assertRaisesRegex(TypeError, + 'not all arguments converted during string formatting'): + eval("'%s' % (x, y)", {'x': 1, 'y': 2}) + with self.assertRaisesRegex(ValueError, 'incomplete format'): + eval("'%s%' % (x,)", {'x': 1234}) + with self.assertRaisesRegex(ValueError, 'incomplete format'): + eval("'%s%%%' % (x,)", {'x': 1234}) + with self.assertRaisesRegex(TypeError, + 'not enough arguments for format string'): + eval("'%s%z' % (x,)", {'x': 1234}) + with self.assertRaisesRegex(ValueError, 'unsupported format character'): + eval("'%s%z' % (x, 5)", {'x': 1234}) + with self.assertRaisesRegex(TypeError, 'a real number is required, not str'): + eval("'%d' % (x,)", {'x': '1234'}) + with self.assertRaisesRegex(TypeError, 'an integer is required, not float'): + eval("'%x' % (x,)", {'x': 1234.56}) + with self.assertRaisesRegex(TypeError, 'an integer is required, not str'): + eval("'%x' % (x,)", {'x': '1234'}) + with self.assertRaisesRegex(TypeError, 'must be real number, not str'): + eval("'%f' % (x,)", {'x': '1234'}) + with self.assertRaisesRegex(TypeError, + 'not enough arguments for format string'): + eval("'%s, %s' % (x, *y)", {'x': 1, 'y': []}) + with self.assertRaisesRegex(TypeError, + 'not all arguments converted during string formatting'): + eval("'%s, %s' % (x, *y)", {'x': 1, 'y': [2, 3]}) + class TestBuglets(unittest.TestCase): diff --git a/Python/ast_opt.c b/Python/ast_opt.c index 53e089c..f6506ce 100644 --- a/Python/ast_opt.c +++ b/Python/ast_opt.c @@ -30,6 +30,20 @@ make_const(expr_ty node, PyObject *val, PyArena *arena) #define COPY_NODE(TO, FROM) (memcpy((TO), (FROM), sizeof(struct _expr))) +static int +has_starred(asdl_expr_seq *elts) +{ + Py_ssize_t n = asdl_seq_LEN(elts); + for (Py_ssize_t i = 0; i < n; i++) { + expr_ty e = (expr_ty)asdl_seq_GET(elts, i); + if (e->kind == Starred_kind) { + return 1; + } + } + return 0; +} + + static PyObject* unary_not(PyObject *v) { @@ -318,8 +332,8 @@ simple_format_arg_parse(PyObject *fmt, Py_ssize_t *ppos, if (ch == '.') { NEXTC; + *prec = 0; if ('0' <= ch && ch <= '9') { - *prec = 0; int digits = 0; while ('0' <= ch && ch <= '9') { *prec = *prec * 10 + (ch - '0'); @@ -445,7 +459,8 @@ fold_binop(expr_ty node, PyArena *arena, _PyASTOptimizeState *state) if (node->v.BinOp.op == Mod && rhs->kind == Tuple_kind && - PyUnicode_Check(lv)) + PyUnicode_Check(lv) && + !has_starred(rhs->v.Tuple.elts)) { return optimize_format(node, lv, rhs->v.Tuple.elts, arena); } @@ -572,12 +587,8 @@ fold_iter(expr_ty arg, PyArena *arena, _PyASTOptimizeState *state) if (arg->kind == List_kind) { /* First change a list into tuple. */ asdl_expr_seq *elts = arg->v.List.elts; - Py_ssize_t n = asdl_seq_LEN(elts); - for (Py_ssize_t i = 0; i < n; i++) { - expr_ty e = (expr_ty)asdl_seq_GET(elts, i); - if (e->kind == Starred_kind) { - return 1; - } + if (has_starred(elts)) { + return 1; } expr_context_ty ctx = arg->v.List.ctx; arg->kind = Tuple_kind; -- cgit v0.12