Skip to content

Commit 64ab5ed

Browse files
committed
gh-113804: Support "x" and "X" format types for floats
_Py_dg_dtoa_hex() helper is based on float.hex() with additional support for the precision setting and some format flags. Note (c.f. ``'a'`` format type of the C stdlib), that ``'#'`` option is used to control the prefix (off by default). Trailing zeros (and the dot) in fractional part are excluded. Examples: ```pycon >>> f'{-0.1:#x}' '-0x1.999999999999ap-4' >>> (-0.1).hex() '-0x1.999999999999ap-4' >>> f'{3.14159:+#X}' '+0X1.921F9F01B866EP+1' >>> f'{3.14159:.3x}' '1.922p+1' ``` Minor changes: * added Py_hexdigits_upper constant * tests for RaisingNumber are moved (to test also bytes)
1 parent efa738e commit 64ab5ed

File tree

20 files changed

+324
-113
lines changed

20 files changed

+324
-113
lines changed

Doc/c-api/conversion.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ The following functions provide locale-independent string to number conversions.
8888
*format_code*, *precision*, and *flags*.
8989
9090
*format_code* must be one of ``'e'``, ``'E'``, ``'f'``, ``'F'``,
91-
``'g'``, ``'G'`` or ``'r'``. For ``'r'``, the supplied *precision*
91+
``'g'``, ``'G'``, ``'x'``, ``'X'`` or ``'r'``. For ``'r'``, the supplied *precision*
9292
must be 0 and is ignored. The ``'r'`` format code specifies the
9393
standard :func:`repr` format.
9494
@@ -115,6 +115,9 @@ The following functions provide locale-independent string to number conversions.
115115
116116
.. versionadded:: 3.1
117117
118+
.. versionchanged:: 3.13
119+
Support ``'x'`` and ``'X'`` format types for :class:`float`.
120+
118121
119122
.. c:function:: int PyOS_stricmp(const char *s1, const char *s2)
120123

Doc/library/stdtypes.rst

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2430,9 +2430,9 @@ The conversion types are:
24302430
+------------+-----------------------------------------------------+-------+
24312431
| ``'u'`` | Obsolete type -- it is identical to ``'d'``. | \(6) |
24322432
+------------+-----------------------------------------------------+-------+
2433-
| ``'x'`` | Signed hexadecimal (lowercase). | \(2) |
2433+
| ``'x'`` | Signed hexadecimal integer or float (lowercase). | \(2) |
24342434
+------------+-----------------------------------------------------+-------+
2435-
| ``'X'`` | Signed hexadecimal (uppercase). | \(2) |
2435+
| ``'X'`` | Signed hexadecimal integer or float (uppercase). | \(2) |
24362436
+------------+-----------------------------------------------------+-------+
24372437
| ``'e'`` | Floating point exponential format (lowercase). | \(3) |
24382438
+------------+-----------------------------------------------------+-------+
@@ -2473,8 +2473,16 @@ Notes:
24732473
inserted before the first digit.
24742474

24752475
(2)
2476-
The alternate form causes a leading ``'0x'`` or ``'0X'`` (depending on whether
2477-
the ``'x'`` or ``'X'`` format was used) to be inserted before the first digit.
2476+
The alternate form for an integer causes a leading ``'0x'`` or ``'0X'``
2477+
(depending on whether the ``'x'`` or ``'X'`` format was used) to be
2478+
inserted before the first digit.
2479+
2480+
For floats, represent the number by a hexadecimal string in the form
2481+
``[±][0x]h[.hhh]p±d``, where there is one hexadecimal digit before the dot
2482+
and the fractional part either is exact or the number of its hexadecimal
2483+
digits is equal to the specified precision. The exponent ``d`` is written
2484+
in decimal, it always contains at least one digit, and it gives the power
2485+
of 2 by which to multiply the coefficient.
24782486

24792487
(3)
24802488
The alternate form causes the result to always contain a decimal point, even if
@@ -2505,6 +2513,9 @@ that ``'\0'`` is the end of the string.
25052513
``%f`` conversions for numbers whose absolute value is over 1e50 are no
25062514
longer replaced by ``%g`` conversions.
25072515

2516+
.. versionchanged:: 3.13
2517+
Support ``'x'`` and ``'X'`` format types for :class:`float`.
2518+
25082519

25092520
.. index::
25102521
single: buffer protocol; binary sequence types

Doc/library/string.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,30 @@ The available presentation types for :class:`float` and
588588
| | as altered by the other format modifiers. |
589589
+---------+----------------------------------------------------------+
590590

591+
Additionally, for :class:`float` available following representation types:
592+
593+
+---------+----------------------------------------------------------+
594+
| Type | Meaning |
595+
+=========+==========================================================+
596+
| ``'x'`` | Represent the number by a hexadecimal string in the |
597+
| | form ``[±][0x]h[.hhh]p±d``, where there is one |
598+
| | hexadecimal digit before the dot and the fractional part |
599+
| | either is exact or the number of its hexadecimal digits |
600+
| | is equal to the specified precision. The exponent ``d`` |
601+
| | is written in decimal, it always contains at least one |
602+
| | digit, and it gives the power of 2 by which to multiply |
603+
| | the coefficient. |
604+
| | |
605+
| | If the ``'#'`` option is specified, the prefix ``'0x'`` |
606+
| | will be inserted before an integer part. |
607+
+---------+----------------------------------------------------------+
608+
| ``'X'`` | Same as ``'x'``, but uses uppercase digits, the ``0X`` |
609+
| | prefix and ``'P'`` as the exponent separator. |
610+
+---------+----------------------------------------------------------+
611+
612+
.. versionchanged:: 3.13
613+
Support ``'x'`` and ``'X'`` format types for :class:`float`.
614+
591615

592616
.. _formatexamples:
593617

Include/codecs.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ PyAPI_FUNC(PyObject *) PyCodec_NameReplaceErrors(PyObject *exc);
168168

169169
#ifndef Py_LIMITED_API
170170
PyAPI_DATA(const char *) Py_hexdigits;
171+
PyAPI_DATA(const char *) Py_hexdigits_upper;
171172
#endif
172173

173174
#ifdef __cplusplus

Lib/test/test_complex.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -814,7 +814,7 @@ def test_format(self):
814814
self.assertRaises(ValueError, (1.5+3j).__format__, '=20')
815815

816816
# integer presentation types are an error
817-
for t in 'bcdoxX':
817+
for t in 'bcdo':
818818
self.assertRaises(ValueError, (1.5+0.5j).__format__, t)
819819

820820
# make sure everything works in ''.format()

Lib/test/test_float.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -700,12 +700,37 @@ def test_format(self):
700700
# % formatting
701701
self.assertEqual(format(-1.0, '%'), '-100.000000%')
702702

703+
# hexadecimal format
704+
x = float.fromhex('0x0.0030p+0')
705+
self.assertEqual(format(x, 'x'), '1.8p-11')
706+
self.assertEqual(format(x, 'X'), '1.8P-11')
707+
self.assertEqual(format(x, '.0x'), '1p-10')
708+
x = float.fromhex('0x1.7p+0')
709+
self.assertEqual(format(x, '.0x'), '1p+0')
710+
x = float.fromhex('0x0.1p-1022') # subnormal
711+
self.assertEqual(format(x, 'x'), '0.1p-1022')
712+
x = float.fromhex('0x0.0040p+0')
713+
self.assertEqual(format(x, 'x'), '1p-10')
714+
self.assertEqual(format(x, '>10x'), ' 1p-10')
715+
self.assertEqual(format(x, '>#10x'), ' 0x1p-10')
716+
self.assertEqual(format(x, '>010x'), '000001p-10')
717+
self.assertEqual(format(x, '>#010x'), '0000x1p-10')
718+
self.assertEqual(format(x, '#010x'), '0x0001p-10')
719+
self.assertEqual(format(x, '<10x'), '1p-10 ')
720+
self.assertEqual(format(x, '<#10x'), '0x1p-10 ')
721+
x = float.fromhex('0x1.fe12p0')
722+
self.assertEqual(format(x, 'x'), '1.fe12p+0')
723+
self.assertEqual(format(x, '#X'), '0X1.FE12P+0')
724+
self.assertEqual(format(x, '.3x'), '1.fe1p+0')
725+
self.assertEqual(format(x, '.1x'), '1p+1')
726+
self.assertEqual(format(x, '#.1x'), '0x1p+1')
727+
703728
# conversion to string should fail
704729
self.assertRaises(ValueError, format, 3.0, "s")
705730

706-
# confirm format options expected to fail on floats, such as integer
707-
# presentation types
708-
for format_spec in 'sbcdoxX':
731+
# confirm format options expected to fail on floats, such as some
732+
# integer presentation types
733+
for format_spec in 'sbcdo':
709734
self.assertRaises(ValueError, format, 0.0, format_spec)
710735
self.assertRaises(ValueError, format, 1.0, format_spec)
711736
self.assertRaises(ValueError, format, -1.0, format_spec)

Lib/test/test_format.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,17 @@ def test_common_format(self):
269269
testcommon('%g', 1.1, '1.1')
270270
testcommon('%#g', 1.1, '1.10000')
271271

272+
# hexadecimal floats
273+
testcommon("%x", 3.14, '1.91eb851eb851fp+1')
274+
testcommon("%#x", 3.14, '0x1.91eb851eb851fp+1')
275+
testcommon("%X", 3.14, '1.91EB851EB851FP+1')
276+
testcommon("%+.3x", 3.14, '+1.91fp+1')
277+
testcommon("%x", -0.5, '-1p-1')
278+
testcommon("%#x", -0.5, '-0x1p-1')
279+
x = float.fromhex('0x0.003p+0')
280+
testcommon("%040x", x, '0000000000000000000000000000000001.8p-11')
281+
testcommon("%#040x", x, '0x00000000000000000000000000000001.8p-11')
282+
272283
if verbose:
273284
print('Testing exceptions')
274285
test_exc_common('%', (), ValueError, "incomplete format")
@@ -279,9 +290,25 @@ def test_common_format(self):
279290
test_exc_common('%d', b'1', TypeError,
280291
"%d format: a real number is required, not bytes")
281292
test_exc_common('%x', '1', TypeError,
282-
"%x format: an integer is required, not str")
283-
test_exc_common('%x', 3.14, TypeError,
284-
"%x format: an integer is required, not float")
293+
"%x format: an integer or float is required, not str")
294+
test_exc_common('%d', 1j, TypeError,
295+
"%d format: a real number is required, not complex")
296+
test_exc_common('%x', 1j, TypeError,
297+
"%x format: an integer or float is required, not complex")
298+
299+
class RaisingNumber:
300+
def __int__(self):
301+
raise RuntimeError('int') # should not be `TypeError`
302+
def __index__(self):
303+
raise RuntimeError('index') # should not be `TypeError`
304+
305+
rn = RaisingNumber()
306+
test_exc_common('%d', rn, RuntimeError, 'int')
307+
test_exc_common('%i', rn, RuntimeError, 'int')
308+
test_exc_common('%u', rn, RuntimeError, 'int')
309+
test_exc_common('%x', rn, RuntimeError, 'index')
310+
test_exc_common('%X', rn, RuntimeError, 'index')
311+
test_exc_common('%o', rn, RuntimeError, 'index')
285312

286313
def test_str_format(self):
287314
testformat("%r", "\u0378", "'\\u0378'") # non printable

Lib/test/test_peepholer.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,7 @@ def format(fmt, *values):
567567
self.assertEqual(format('x = %s%% %%%%', 1234), 'x = 1234% %%')
568568
self.assertEqual(format('x = %s!', '%% %s'), 'x = %% %s!')
569569
self.assertEqual(format('x = %s, y = %d', 12, 34), 'x = 12, y = 34')
570+
self.assertEqual(format('x = %x', 1234.56), 'x = 1.34a3d70a3d70ap+10')
570571

571572
def test_format_errors(self):
572573
with self.assertRaisesRegex(TypeError,
@@ -586,9 +587,7 @@ def test_format_errors(self):
586587
eval("'%s%z' % (x, 5)", {'x': 1234})
587588
with self.assertRaisesRegex(TypeError, 'a real number is required, not str'):
588589
eval("'%d' % (x,)", {'x': '1234'})
589-
with self.assertRaisesRegex(TypeError, 'an integer is required, not float'):
590-
eval("'%x' % (x,)", {'x': 1234.56})
591-
with self.assertRaisesRegex(TypeError, 'an integer is required, not str'):
590+
with self.assertRaisesRegex(TypeError, 'an integer or float is required, not str'):
592591
eval("'%x' % (x,)", {'x': '1234'})
593592
with self.assertRaisesRegex(TypeError, 'must be real number, not str'):
594593
eval("'%f' % (x,)", {'x': '1234'})

Lib/test/test_random.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -599,16 +599,16 @@ def test_guaranteed_stable(self):
599599
'0x1.1ebb4352e4c4dp-1', '0x1.1a7422abf9c11p-1'])
600600
self.gen.seed("the quick brown fox", version=2)
601601
self.assertEqual([self.gen.random().hex() for i in range(4)],
602-
['0x1.1239ddfb11b7cp-3', '0x1.b3cbb5c51b120p-4',
602+
['0x1.1239ddfb11b7cp-3', '0x1.b3cbb5c51b12p-4',
603603
'0x1.8c4f55116b60fp-1', '0x1.63eb525174a27p-1'])
604604

605605
def test_bug_27706(self):
606606
# Verify that version 1 seeds are unaffected by hash randomization
607607

608608
self.gen.seed('nofar', version=1) # hash('nofar') == 5990528763808513177
609609
self.assertEqual([self.gen.random().hex() for i in range(4)],
610-
['0x1.8645314505ad7p-1', '0x1.afb1f82e40a40p-5',
611-
'0x1.2a59d2285e971p-1', '0x1.56977142a7880p-6'])
610+
['0x1.8645314505ad7p-1', '0x1.afb1f82e40a4p-5',
611+
'0x1.2a59d2285e971p-1', '0x1.56977142a788p-6'])
612612

613613
self.gen.seed('rachel', version=1) # hash('rachel') == -9091735575445484789
614614
self.assertEqual([self.gen.random().hex() for i in range(4)],
@@ -639,8 +639,8 @@ def test_bug_31482(self):
639639

640640
self.gen.seed(b'nofar', version=1) # hash('nofar') == 5990528763808513177
641641
self.assertEqual([self.gen.random().hex() for i in range(4)],
642-
['0x1.8645314505ad7p-1', '0x1.afb1f82e40a40p-5',
643-
'0x1.2a59d2285e971p-1', '0x1.56977142a7880p-6'])
642+
['0x1.8645314505ad7p-1', '0x1.afb1f82e40a4p-5',
643+
'0x1.2a59d2285e971p-1', '0x1.56977142a788p-6'])
644644

645645
self.gen.seed(b'rachel', version=1) # hash('rachel') == -9091735575445484789
646646
self.assertEqual([self.gen.random().hex() for i in range(4)],

Lib/test/test_str.py

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1321,10 +1321,6 @@ def __repr__(self):
13211321
self.assertRaises(ValueError, ("{[" + big + "]}").format, [0])
13221322

13231323
# test number formatter errors:
1324-
self.assertRaises(ValueError, '{0:x}'.format, 1j)
1325-
self.assertRaises(ValueError, '{0:x}'.format, 1.0)
1326-
self.assertRaises(ValueError, '{0:X}'.format, 1j)
1327-
self.assertRaises(ValueError, '{0:X}'.format, 1.0)
13281324
self.assertRaises(ValueError, '{0:o}'.format, 1j)
13291325
self.assertRaises(ValueError, '{0:o}'.format, 1.0)
13301326
self.assertRaises(ValueError, '{0:u}'.format, 1j)
@@ -1568,32 +1564,18 @@ def __int__(self):
15681564
self.assertEqual('%X' % letter_m, '6D')
15691565
self.assertEqual('%o' % letter_m, '155')
15701566
self.assertEqual('%c' % letter_m, 'm')
1571-
self.assertRaisesRegex(TypeError, '%x format: an integer is required, not float', operator.mod, '%x', 3.14)
1572-
self.assertRaisesRegex(TypeError, '%X format: an integer is required, not float', operator.mod, '%X', 2.11)
1567+
self.assertEqual('%x' % 3.14, '1.91eb851eb851fp+1')
1568+
self.assertEqual('%X' % 2.11, '1.0E147AE147AE1P+1')
15731569
self.assertRaisesRegex(TypeError, '%o format: an integer is required, not float', operator.mod, '%o', 1.79)
1574-
self.assertRaisesRegex(TypeError, '%x format: an integer is required, not PseudoFloat', operator.mod, '%x', pi)
1575-
self.assertRaisesRegex(TypeError, '%x format: an integer is required, not complex', operator.mod, '%x', 3j)
1576-
self.assertRaisesRegex(TypeError, '%X format: an integer is required, not complex', operator.mod, '%X', 2j)
1570+
self.assertRaisesRegex(TypeError, '%x format: an integer or float is required, not PseudoFloat', operator.mod, '%x', pi)
1571+
self.assertRaisesRegex(TypeError, '%x format: an integer or float is required, not complex', operator.mod, '%x', 3j)
1572+
self.assertRaisesRegex(TypeError, '%X format: an integer or float is required, not complex', operator.mod, '%X', 2j)
15771573
self.assertRaisesRegex(TypeError, '%o format: an integer is required, not complex', operator.mod, '%o', 1j)
15781574
self.assertRaisesRegex(TypeError, '%u format: a real number is required, not complex', operator.mod, '%u', 3j)
15791575
self.assertRaisesRegex(TypeError, '%i format: a real number is required, not complex', operator.mod, '%i', 2j)
15801576
self.assertRaisesRegex(TypeError, '%d format: a real number is required, not complex', operator.mod, '%d', 1j)
15811577
self.assertRaisesRegex(TypeError, '%c requires int or char', operator.mod, '%c', pi)
15821578

1583-
class RaisingNumber:
1584-
def __int__(self):
1585-
raise RuntimeError('int') # should not be `TypeError`
1586-
def __index__(self):
1587-
raise RuntimeError('index') # should not be `TypeError`
1588-
1589-
rn = RaisingNumber()
1590-
self.assertRaisesRegex(RuntimeError, 'int', operator.mod, '%d', rn)
1591-
self.assertRaisesRegex(RuntimeError, 'int', operator.mod, '%i', rn)
1592-
self.assertRaisesRegex(RuntimeError, 'int', operator.mod, '%u', rn)
1593-
self.assertRaisesRegex(RuntimeError, 'index', operator.mod, '%x', rn)
1594-
self.assertRaisesRegex(RuntimeError, 'index', operator.mod, '%X', rn)
1595-
self.assertRaisesRegex(RuntimeError, 'index', operator.mod, '%o', rn)
1596-
15971579
def test_formatting_with_enum(self):
15981580
# issue18780
15991581
import enum

0 commit comments

Comments
 (0)