Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 163 additions & 28 deletions Lib/test/test_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -661,55 +661,190 @@ def test_g_format_has_no_trailing_zeros(self):
self.assertEqual(format(12300050.0, "#.6g"), "1.23000e+07")

def test_with_two_commas_in_format_specifier(self):
error_msg = re.escape("Cannot specify ',' with ','.")
error_msg = re.escape(
"Cannot specify grouping character ',' more than once")
with self.assertRaisesRegex(ValueError, error_msg):
'{:,,}'.format(1)
with self.assertRaisesRegex(ValueError, error_msg):
'{:.,,}'.format(1.1)
with self.assertRaisesRegex(ValueError, error_msg):
'{:.,,f}'.format(1.1)

def test_with_two_underscore_in_format_specifier(self):
error_msg = re.escape("Cannot specify '_' with '_'.")
error_msg = re.escape(
"Cannot specify grouping character '_' more than once")
with self.assertRaisesRegex(ValueError, error_msg):
'{:__}'.format(1)
with self.assertRaisesRegex(ValueError, error_msg):
'{:.__}'.format(1.1)
with self.assertRaisesRegex(ValueError, error_msg):
'{:.__f}'.format(1.1)

def test_with_a_commas_and_an_underscore_in_format_specifier(self):
error_msg = re.escape("Cannot specify both ',' and '_'.")
def test_with_a_comma_and_an_underscore_in_format_specifier(self):
error_msg = re.escape("Cannot specify both ',' and '_'")
with self.assertRaisesRegex(ValueError, error_msg):
'{:,_}'.format(1)
with self.assertRaisesRegex(ValueError, error_msg):
'{:.,_}'.format(1.1)
with self.assertRaisesRegex(ValueError, error_msg):
'{:.,_f}'.format(1.1)

def test_with_an_underscore_and_a_comma_in_format_specifier(self):
error_msg = re.escape("Cannot specify both ',' and '_'.")
error_msg = re.escape("Cannot specify both ',' and '_'")
with self.assertRaisesRegex(ValueError, error_msg):
'{:_,}'.format(1)
with self.assertRaisesRegex(ValueError, error_msg):
'{:._,}'.format(1.1)
with self.assertRaisesRegex(ValueError, error_msg):
'{:._,f}'.format(1.1)

def test_better_error_message_format(self):
def test_invalid_format_specifier_error_message(self):
# https://bugs.python.org/issue20524
for value in [12j, 12, 12.0, "12"]:
with self.subTest(value=value):
for bad_spec in ["%M", "ЫйXЯЧ", "\n'\\"]:
# The format spec must be invalid for all types we're testing.
# '%M' will suffice.
bad_format_spec = '%M'
err = re.escape("Invalid format specifier "
f"'{bad_format_spec}' for object of type "
f"'{type(value).__name__}'")
with self.assertRaisesRegex(ValueError, err):
f"xx{{value:{bad_format_spec}}}yy".format(value=value)

# Also test the builtin format() function.
with self.assertRaisesRegex(ValueError, err):
format(value, bad_format_spec)

# Also test f-strings.
with self.assertRaisesRegex(ValueError, err):
eval("f'xx{value:{bad_format_spec}}yy'")

def test_unicode_in_error_message(self):
str_err = re.escape(
"Invalid format specifier '%ЫйЯЧ' for object of type 'str'")
with self.assertRaisesRegex(ValueError, str_err):
"{a:%ЫйЯЧ}".format(a='a')
with self.subTest(value=value, bad_spec=bad_spec):
err = re.escape("Invalid format specifier "
f"{bad_spec!r} for object of type "
f"'{type(value).__name__}'")
with self.assertRaisesRegex(ValueError, err):
f"xx{{value:{bad_spec}}}yy".format(value=value)

# Also test the builtin format() function.
with self.assertRaisesRegex(ValueError, err):
format(value, bad_spec)

# Also test f-strings.
with self.assertRaisesRegex(ValueError, err):
eval("f'xx{value:{bad_spec}}yy'")

def test_invalid_specifier_type_error_message(self):
for value in [12j, 12, 12.0, "12"]:
for bad_spec, repr in [
("M", "'M'"),
("10$", "'$'"),
("\t", "U+0009"),
(",\x7f", "U+007F"),
("о", "'о' (U+043E)"),
("+#020,🐍", "'🐍' (U+1F40D)")
]:
with self.subTest(value=value, bad_spec=bad_spec):
err = re.escape("Unknown format code "
f"{repr} for object of type "
f"'{type(value).__name__}'")
with self.assertRaisesRegex(ValueError, err):
f"xx{{value:{bad_spec}}}yy".format(value=value)

# Also test the builtin format() function.
with self.assertRaisesRegex(ValueError, err):
format(value, bad_spec)

# Also test f-strings.
with self.assertRaisesRegex(ValueError, err):
eval("f'xx{value:{bad_spec}}yy'")

def test_specifier_grouping_with_types(self):
def assertEqualGroup(spec, value, expected):
with self.subTest(spec=spec, value=value):
self.assertEqual(("{:%s}" % spec).format(value), expected)
self.assertEqual(format(value, spec), expected)
self.assertEqual(f"{value:{spec}}", expected)

def assertRaisesGroup(spec, value, error_msg):
with self.subTest(spec=spec, value=value):
error_msg = re.escape(error_msg)
with self.assertRaisesRegex(ValueError, error_msg):
("{:%s}" % spec).format(value)
with self.assertRaisesRegex(ValueError, error_msg):
format(value, spec)
with self.assertRaisesRegex(ValueError, error_msg):
f"{value:{spec}}"

value = 1234567
assertEqualGroup(",", value, "1,234,567")
assertRaisesGroup("._", value,
"Cannot specify '_' in fractional part with 'd'")
assertEqualGroup(",d", value, "1,234,567")
assertRaisesGroup("._d", value,
"Cannot specify '_' in fractional part with 'd'")
assertEqualGroup(",e", value, "1.234567e+06")
assertEqualGroup("._e", value, "1.234_567e+06")
assertRaisesGroup(",b", value, "Cannot specify ',' with 'b'")
assertEqualGroup("_b", 1234, "100_1101_0010")
assertRaisesGroup("._b", value,
"Cannot specify '_' in fractional part with 'b'")
assertRaisesGroup(",s", value, "Cannot specify ',' with 's'")
assertRaisesGroup("._s", value,
"Cannot specify '_' in fractional part with 's'")
assertRaisesGroup(",n", value, "Cannot specify ',' with 'n'")
assertRaisesGroup("._n", value,
"Cannot specify '_' in fractional part with 'n'")

value = 1234567.1234567
assertEqualGroup(",", value, "1,234,567.1234567")
assertEqualGroup("._", value, "1234567.123_456_7")
assertRaisesGroup(",d", value,
"Unknown format code 'd' for object of type 'float'")
assertRaisesGroup("._d", value,
"Cannot specify '_' in fractional part with 'd'")
assertEqualGroup(",e", value, "1.234567e+06")
assertEqualGroup("._e", value, "1.234_567e+06")
assertRaisesGroup(",b", value, "Cannot specify ',' with 'b'")
assertRaisesGroup("_b", value,
"Unknown format code 'b' for object of type 'float'")
assertRaisesGroup("._b", value,
"Cannot specify '_' in fractional part with 'b'")
assertRaisesGroup(",s", value, "Cannot specify ',' with 's'")
assertRaisesGroup("._s", value,
"Cannot specify '_' in fractional part with 's'")
assertRaisesGroup(",n", value, "Cannot specify ',' with 'n'")
assertRaisesGroup("._n", value,
"Cannot specify '_' in fractional part with 'n'")

value = 1234567.1234567+1234567.1234567j
assertEqualGroup(",", value, "(1,234,567.1234567+1,234,567.1234567j)")
assertEqualGroup("._", value, "(1234567.123_456_7+1234567.123_456_7j)")
assertRaisesGroup(",d", value,
"Unknown format code 'd' for object of type 'complex'")
assertRaisesGroup("._d", value,
"Cannot specify '_' in fractional part with 'd'")
assertEqualGroup(",e", value, "1.234567e+06+1.234567e+06j")
assertEqualGroup("._e", value, "1.234_567e+06+1.234_567e+06j")
assertRaisesGroup(",b", value, "Cannot specify ',' with 'b'")
assertRaisesGroup("_b", value,
"Unknown format code 'b' for object of type 'complex'")
assertRaisesGroup("._b", value,
"Cannot specify '_' in fractional part with 'b'")
assertRaisesGroup(",s", value, "Cannot specify ',' with 's'")
assertRaisesGroup("._s", value,
"Cannot specify '_' in fractional part with 's'")
assertRaisesGroup(",n", value, "Cannot specify ',' with 'n'")
assertRaisesGroup("._n", value,
"Cannot specify '_' in fractional part with 'n'")

value = "1234567"
assertRaisesGroup(",", value, "Cannot specify ',' with 's'")
assertRaisesGroup("._", value,
"Cannot specify '_' in fractional part with 's'")
assertRaisesGroup(",d", value,
"Unknown format code 'd' for object of type 'str'")
assertRaisesGroup("._d", value,
"Cannot specify '_' in fractional part with 'd'")
assertRaisesGroup(",e", value,
"Unknown format code 'e' for object of type 'str'")
assertRaisesGroup("._e", value,
"Unknown format code 'e' for object of type 'str'")
assertRaisesGroup(",b", value, "Cannot specify ',' with 'b'")
assertRaisesGroup("_b", value,
"Unknown format code 'b' for object of type 'str'")
assertRaisesGroup("._b", value,
"Cannot specify '_' in fractional part with 'b'")
assertRaisesGroup(",s", value, "Cannot specify ',' with 's'")
assertRaisesGroup("._s", value,
"Cannot specify '_' in fractional part with 's'")
assertRaisesGroup(",n", value, "Cannot specify ',' with 'n'")
assertRaisesGroup("._n", value,
"Cannot specify '_' in fractional part with 'n'")

def test_negative_zero(self):
## default behavior
Expand Down
20 changes: 15 additions & 5 deletions Lib/test/test_fstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -1697,24 +1697,34 @@ def test_invalid_syntax_error_message(self):
compile("f'{a $ b}'", "?", "exec")

def test_with_two_commas_in_format_specifier(self):
error_msg = re.escape("Cannot specify ',' with ','.")
error_msg = re.escape(
"Cannot specify grouping character ',' more than once")
with self.assertRaisesRegex(ValueError, error_msg):
f'{1:,,}'
with self.assertRaisesRegex(ValueError, error_msg):
f'{1.1:.,,}'

def test_with_two_underscore_in_format_specifier(self):
error_msg = re.escape("Cannot specify '_' with '_'.")
error_msg = re.escape(
"Cannot specify grouping character '_' more than once")
with self.assertRaisesRegex(ValueError, error_msg):
f'{1:__}'
with self.assertRaisesRegex(ValueError, error_msg):
f'{1.1:.__}'

def test_with_a_commas_and_an_underscore_in_format_specifier(self):
error_msg = re.escape("Cannot specify both ',' and '_'.")
def test_with_a_comma_and_an_underscore_in_format_specifier(self):
error_msg = re.escape("Cannot specify both ',' and '_'")
with self.assertRaisesRegex(ValueError, error_msg):
f'{1:,_}'
with self.assertRaisesRegex(ValueError, error_msg):
f'{1.1:.,_}'

def test_with_an_underscore_and_a_comma_in_format_specifier(self):
error_msg = re.escape("Cannot specify both ',' and '_'.")
error_msg = re.escape("Cannot specify both ',' and '_'")
with self.assertRaisesRegex(ValueError, error_msg):
f'{1:_,}'
with self.assertRaisesRegex(ValueError, error_msg):
f'{1.1:._,}'

def test_syntax_error_for_starred_expressions(self):
with self.assertRaisesRegex(SyntaxError, "can't use starred expression here"):
Expand Down
25 changes: 24 additions & 1 deletion Lib/test/test_str.py
Original file line number Diff line number Diff line change
Expand Up @@ -1262,6 +1262,14 @@ def __repr__(self):
self.assertEqual('{0!a}'.format(F('Hello')), 'F(Hello)')
self.assertEqual('{0!a}'.format(F('\u0374')), 'F(\\u0374)')

self.assertEqual('{0:10.10}'.format(1.25), ' 1.25')
self.assertEqual('{0!s:10.10}'.format(1.25), '1.25 ')
self.assertEqual('{0!r:10.10}'.format(1.25), '1.25 ')
self.assertEqual('{0!a:10.10}'.format(1.25), '1.25 ')

# Not a conversion, but show that ! is allowed in a format spec.
self.assertEqual('{0:!<10.10}'.format(3.14), '3.14!!!!!!')

# test fallback to object.__format__
self.assertEqual('{0}'.format({}), '{}')
self.assertEqual('{0}'.format([]), '[]')
Expand Down Expand Up @@ -1320,9 +1328,24 @@ def __repr__(self):
self.assertRaises(ValueError, "{0}}".format, 0)
self.assertRaises(KeyError, "{foo}".format, bar=3)
self.assertRaises(ValueError, "{0!x}".format, 3)
self.assertRaises(ValueError, '{0!A}'.format, 3)
self.assertRaises(ValueError, '{0!G}'.format, 3)
self.assertRaises(ValueError, '{0!ä}'.format, 3)
self.assertRaises(ValueError, '{0!ɐ}'.format, 3)
self.assertRaises(ValueError, '{0!3}'.format, 3)
self.assertRaises(ValueError, '{0!!}'.format, 3)
self.assertRaises(ValueError, "{0!}".format, 0)
self.assertRaises(ValueError, "{0!s }".format, 0)
self.assertRaises(ValueError, "{0!s :10}".format, 0)
self.assertRaises(ValueError, '{0! s}'.format, 0)
self.assertRaises(ValueError, '{0! s }'.format, 0)
self.assertRaises(ValueError, '{0!ss}'.format, 0)
self.assertRaises(ValueError, "{0!rs}".format, 0)
self.assertRaises(ValueError, "{!}".format)
self.assertRaises(ValueError, '{0!rs:}'.format, 0)
self.assertRaises(ValueError, '{0!rs:s}'.format, 0)
self.assertRaises(ValueError, "{!}".format, 0)
self.assertRaises(ValueError, "{!:}".format, 0)
self.assertRaises(ValueError, "{!:8}".format, 0)
self.assertRaises(IndexError, "{:}".format)
self.assertRaises(IndexError, "{:s}".format)
self.assertRaises(IndexError, "{}".format)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Improves error messages for ``{}``-style formatters for ``str``, ``float``,
``int``, and ``complex``. Make error messages handle Unicode characters
properly. Make grouping characters ``,`` and ``_`` in fractional part only
allowed for floating-point presentation types (``e``, ``f``, ``g``, ``E``,
``G``, ``%``, and ``F``).
34 changes: 19 additions & 15 deletions Objects/stringlib/unicode_format.h
Original file line number Diff line number Diff line change
Expand Up @@ -759,13 +759,9 @@ MarkupIterator_next(MarkupIterator *self, SubString *literal,
return 2;
}


/* do the !r or !s conversion on obj */
static PyObject *
do_conversion(PyObject *obj, Py_UCS4 conversion)
{
/* XXX in pre-3.0, do we need to convert this to unicode, since it
might have returned a string? */
switch (conversion) {
case 'r':
return PyObject_Repr(obj);
Expand All @@ -774,17 +770,25 @@ do_conversion(PyObject *obj, Py_UCS4 conversion)
case 'a':
return PyObject_ASCII(obj);
default:
if (conversion > 32 && conversion < 127) {
/* It's the ASCII subrange; casting to char is safe
(assuming the execution character set is an ASCII
superset). */
PyErr_Format(PyExc_ValueError,
"Unknown conversion specifier %c",
(char)conversion);
} else
PyErr_Format(PyExc_ValueError,
"Unknown conversion specifier \\x%x",
(unsigned int)conversion);
if (conversion == '\'') {
PyErr_SetString(PyExc_ValueError,
"Unknown conversion specifier \"'\"");
}
else if (conversion >= 32 && conversion < 127) {
PyErr_Format(PyExc_ValueError,
"Unknown conversion specifier '%c'",
(int)conversion);
}
else if (Py_UNICODE_ISPRINTABLE(conversion)) {
PyErr_Format(PyExc_ValueError,
"Unknown conversion specifier '%c' (U+%04X)",
(int)conversion, (int)conversion);
}
else {
PyErr_Format(PyExc_ValueError,
"Unknown conversion specifier U+%04X",
(int)conversion);
}
return NULL;
}
}
Expand Down
Loading
Loading