Skip to content

Commit

Permalink
bpo-28307: Tests and fixes for optimization of C-style formatting (py…
Browse files Browse the repository at this point in the history
…thonGH-26318)

Fix errors:
* "%10.s" should be equal to "%10.0s", not "%10s".
* Tuples with starred expressions caused a SyntaxError.
  • Loading branch information
serhiy-storchaka committed May 23, 2021
1 parent bd7476d commit 8b01067
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 8 deletions.
76 changes: 76 additions & 0 deletions Lib/test/test_peepholer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import dis
from itertools import combinations, product
import unittest

from test.support.bytecode_helper import BytecodeTestCase
Expand Down Expand Up @@ -494,6 +495,81 @@ def genexpr():
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):

Expand Down
27 changes: 19 additions & 8 deletions Python/ast_opt.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit 8b01067

Please sign in to comment.