Skip to content

Commit 54d7892

Browse files
committed
+1
1 parent ae9e54c commit 54d7892

12 files changed

+103
-37
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: 18 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,19 @@ 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 style
2481+
``[±]0xh.[hhhp±d``, where there is one hexadecimal digit before the
2482+
decimal-point character and the number of hexadecimal digits after it is
2483+
equal to the precision; if the precision is missing, then the precision is
2484+
sufficient for an exact representation of the value. If the precision is
2485+
zero and the alternate form is not specified, no decimal-point character
2486+
appears. The exponent ``d`` is written in decimal, it always contains at
2487+
least one digit, and it gives the power of 2 by which to multiply the
2488+
coefficient.
24782489

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

2519+
.. versionchanged:: 3.13
2520+
Support ``'x'`` and ``'X'`` format types for :class:`float`.
2521+
25082522

25092523
.. index::
25102524
single: buffer protocol; binary sequence types

Lib/test/test_float.py

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

703+
# hexadecimal format
704+
self.assertEqual(format(float.fromhex('0x0.0030p+0'), 'x'), '0x1.8p-11')
705+
self.assertEqual(format(float.fromhex('0x0.0040p+0'), 'x'), '0x1p-10')
706+
self.assertEqual(format(float.fromhex('0x0.0040p+0'), '>10x'), ' 0x1p-10')
707+
self.assertEqual(format(float.fromhex('0x0.0040p+0'), '>#10x'), ' 0x1.p-10')
708+
self.assertEqual(format(float.fromhex('0x0.0040p+0'), '<10x'), '0x1p-10 ')
709+
self.assertEqual(format(float.fromhex('0x0.0040p+0'), '<#10x'), '0x1.p-10 ')
710+
703711
# conversion to string should fail
704712
self.assertRaises(ValueError, format, 3.0, "s")
705713

706-
# confirm format options expected to fail on floats, such as integer
707-
# presentation types
714+
# confirm format options expected to fail on floats, such as some
715+
# integer presentation types
708716
for format_spec in 'sbcdo':
709717
self.assertRaises(ValueError, format, 0.0, format_spec)
710718
self.assertRaises(ValueError, format, 1.0, format_spec)

Lib/test/test_format.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,13 @@ 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, '0x1.91eb851eb851fp+1')
274+
testcommon("%X", 3.14, '0X1.91EB851EB851FP+1')
275+
testcommon("%+.3x", 3.14, '+0x1.91fp+1')
276+
testcommon("%x", -0.5, '-0x1p-1')
277+
testcommon("%#x", -0.5, '-0x1.p-1')
278+
272279
if verbose:
273280
print('Testing exceptions')
274281
test_exc_common('%', (), ValueError, "incomplete format")
@@ -279,9 +286,9 @@ def test_common_format(self):
279286
test_exc_common('%d', b'1', TypeError,
280287
"%d format: a real number is required, not bytes")
281288
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")
289+
"%x format: an integer or float is required, not str")
290+
test_exc_common('%i', 1j, TypeError,
291+
"%i format: a real number is required, not complex")
285292

286293
def test_str_format(self):
287294
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 = 0x1.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_str.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1564,12 +1564,12 @@ def __int__(self):
15641564
self.assertEqual('%X' % letter_m, '6D')
15651565
self.assertEqual('%o' % letter_m, '155')
15661566
self.assertEqual('%c' % letter_m, 'm')
1567-
self.assertRaisesRegex(TypeError, '%x format: an integer is required, not float', operator.mod, '%x', 3.14)
1568-
self.assertRaisesRegex(TypeError, '%X format: an integer is required, not float', operator.mod, '%X', 2.11)
1567+
self.assertEqual('%x' % 3.14, '0x1.91eb851eb851fp+1')
1568+
self.assertEqual('%X' % 2.11, '0X1.0E147AE147AE1P+1')
15691569
self.assertRaisesRegex(TypeError, '%o format: an integer is required, not float', operator.mod, '%o', 1.79)
1570-
self.assertRaisesRegex(TypeError, '%x format: an integer is required, not PseudoFloat', operator.mod, '%x', pi)
1571-
self.assertRaisesRegex(TypeError, '%x format: an integer is required, not complex', operator.mod, '%x', 3j)
1572-
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)
15731573
self.assertRaisesRegex(TypeError, '%o format: an integer is required, not complex', operator.mod, '%o', 1j)
15741574
self.assertRaisesRegex(TypeError, '%u format: a real number is required, not complex', operator.mod, '%u', 3j)
15751575
self.assertRaisesRegex(TypeError, '%i format: a real number is required, not complex', operator.mod, '%i', 2j)

Lib/test/test_types.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -525,8 +525,8 @@ def test(f, format_spec, result):
525525
self.assertRaises(TypeError, 3.0.__format__, None)
526526
self.assertRaises(TypeError, 3.0.__format__, 0)
527527

528-
# confirm format options expected to fail on floats, such as integer
529-
# presentation types
528+
# confirm format options expected to fail on floats, such as some
529+
# integer presentation types
530530
for format_spec in 'sbcdo':
531531
self.assertRaises(ValueError, format, 0.0, format_spec)
532532
self.assertRaises(ValueError, format, 1.0, format_spec)

Objects/bytesobject.c

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -413,12 +413,15 @@ formatfloat(PyObject *v, int flags, int prec, int type,
413413
return NULL;
414414
}
415415

416-
if (prec < 0)
416+
if (prec < 0 && type != 'x' && type != 'X')
417417
prec = 6;
418418

419419
if (flags & F_ALT) {
420420
dtoa_flags |= Py_DTSF_ALT;
421421
}
422+
if (flags & F_SIGN) {
423+
dtoa_flags |= Py_DTSF_SIGN;
424+
}
422425
p = PyOS_double_to_string(x, type, prec, dtoa_flags, NULL);
423426

424427
if (p == NULL)
@@ -463,13 +466,30 @@ formatlong(PyObject *v, int flags, int prec, int type)
463466
Py_DECREF(iobj);
464467
return result;
465468
}
466-
if (!PyErr_ExceptionMatches(PyExc_TypeError))
469+
if (PyErr_ExceptionMatches(PyExc_TypeError)) {
470+
if (type == 'x' || type == 'X') {
471+
PyErr_Clear();
472+
473+
PyObject *fobj = PyNumber_Float(v);
474+
if (!fobj) {
475+
goto wrongtype;
476+
}
477+
formatfloat(fobj, flags, prec, type, &result, NULL, NULL);
478+
if (!result) {
479+
return NULL;
480+
}
481+
return PyUnicode_FromEncodedObject(result, NULL, NULL);
482+
}
483+
}
484+
else {
467485
return NULL;
486+
}
468487
}
488+
wrongtype:
469489
PyErr_Format(PyExc_TypeError,
470490
"%%%c format: %s is required, not %.200s", type,
471-
(type == 'o' || type == 'x' || type == 'X') ? "an integer"
472-
: "a real number",
491+
(type == 'x' || type == 'X') ?
492+
"an integer or float" : type == 'o' ? "an integer": "a real number",
473493
Py_TYPE(v)->tp_name);
474494
return NULL;
475495
}

Objects/floatobject.c

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1225,13 +1225,19 @@ static PyObject *
12251225
float_hex_impl(PyObject *self)
12261226
/*[clinic end generated code: output=0ebc9836e4d302d4 input=bec1271a33d47e67]*/
12271227
{
1228-
double x;
1229-
1230-
CONVERT_TO_DOUBLE(self, x);
1231-
1228+
PyObject *result = NULL;
1229+
double x = PyFloat_AS_DOUBLE(self);
12321230
char *buf = PyOS_double_to_string(x, 'x', -1, 0, NULL);
12331231

1234-
return PyUnicode_FromString(buf);
1232+
if (buf) {
1233+
result = PyUnicode_FromString(buf);
1234+
PyMem_Free(buf);
1235+
}
1236+
else {
1237+
PyErr_NoMemory();
1238+
}
1239+
1240+
return result;
12351241
}
12361242

12371243
/* Convert a hexadecimal string to a float. */

Objects/unicodeobject.c

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13710,11 +13710,13 @@ formatfloat(PyObject *v, struct unicode_format_arg_t *arg,
1371013710
return -1;
1371113711

1371213712
prec = arg->prec;
13713-
if (prec < 0)
13713+
if (prec < 0 && arg->ch != 'x' && arg->ch != 'X')
1371413714
prec = 6;
1371513715

1371613716
if (arg->flags & F_ALT)
1371713717
dtoa_flags |= Py_DTSF_ALT;
13718+
if (arg->flags & F_SIGN)
13719+
dtoa_flags |= Py_DTSF_SIGN;
1371813720
p = PyOS_double_to_string(x, arg->ch, prec, dtoa_flags, NULL);
1371913721
if (p == NULL)
1372013722
return -1;
@@ -13902,11 +13904,12 @@ mainformatlong(PyObject *v,
1390213904
if (PyErr_ExceptionMatches(PyExc_TypeError)) {
1390313905
if (type == 'x' || type == 'X') {
1390413906
PyErr_Clear();
13905-
if (PyFloat_Check(v)) {
13906-
if (formatfloat(v, arg, NULL, writer) == -1)
13907-
return -1;
13908-
return 1;
13907+
13908+
PyObject *fobj = PyNumber_Float(v);
13909+
if (!fobj) {
13910+
goto wrongtype;
1390913911
}
13912+
return formatfloat(fobj, arg, p_output, NULL);
1391013913
}
1391113914
goto wrongtype;
1391213915
}
@@ -13964,10 +13967,15 @@ mainformatlong(PyObject *v,
1396413967
switch(type)
1396513968
{
1396613969
case 'o':
13970+
PyErr_Format(PyExc_TypeError,
13971+
"%%%c format: an integer is required, "
13972+
"not %.200s",
13973+
type, Py_TYPE(v)->tp_name);
13974+
break;
1396713975
case 'x':
1396813976
case 'X':
1396913977
PyErr_Format(PyExc_TypeError,
13970-
"%%%c format: an integer is required, "
13978+
"%%%c format: an integer or float is required, "
1397113979
"not %.200s",
1397213980
type, Py_TYPE(v)->tp_name);
1397313981
break;

Python/formatter_unicode.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1282,7 +1282,7 @@ format_complex_internal(PyObject *value,
12821282
format the result. We take care of that later. */
12831283
type = 'g';
12841284

1285-
if (precision < 0 && (type != 'x' || type != 'X'))
1285+
if (precision < 0 && type != 'x' && type != 'X')
12861286
precision = default_precision;
12871287
else if (type == 'r')
12881288
type = 'g';

Python/pystrtod.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -958,7 +958,7 @@ _Py_dg_dtoa_hex(double x, int precision, int always_add_sign,
958958
int e, shift, i, si;
959959
double m = frexp(fabs(x), &e);
960960

961-
if (m || e) {
961+
if (m) {
962962
/* normalization XXX: valid after rounding? */
963963
shift = 1 - Py_MAX(DBL_MIN_EXP - e, 0);
964964
m = ldexp(m, shift);
@@ -1342,6 +1342,7 @@ char * PyOS_double_to_string(double val,
13421342
/* Validate format_code, and map upper and lower case. Compute the
13431343
mode and make any adjustments as needed. */
13441344
switch (format_code) {
1345+
/* hexadecimal floats */
13451346
case 'X':
13461347
float_strings = uc_float_strings;
13471348
/* Fall through. */

0 commit comments

Comments
 (0)