diff --git a/Doc/c-api/long.rst b/Doc/c-api/long.rst index a0e111af5996d7..9e89056dabf1a5 100644 --- a/Doc/c-api/long.rst +++ b/Doc/c-api/long.rst @@ -507,6 +507,22 @@ distinguished from a number. Use :c:func:`PyErr_Occurred` to disambiguate. .. versionadded:: 3.14 +.. c:function:: Py_ssize_t PyLong_GetNumBits(PyObject *obj) + + Get the number of bits of an integer. + + * Return the number of bits on success: greater than or equal to zero. + * Set an exception and return ``-1`` on error. + * Set an :exc:`OverflowError` exception, and return ``-1`` if the number + of bits doesn't fit into :c:type:`Py_ssize_t`. + + Calling the :py:meth:`int.bit_length` method should be preferred to support + integers larger than :c:type:`Py_ssize_t` bits and to avoid + :exc:`!OverflowError`. + + .. versionadded:: 3.14 + + .. c:function:: int PyUnstable_Long_IsCompact(const PyLongObject* op) Return 1 if *op* is compact, 0 otherwise. diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index b2dd80b64a691a..3396c7eb402460 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -258,6 +258,9 @@ New Features * Add :c:func:`PyLong_GetSign` function to get the sign of :class:`int` objects. (Contributed by Sergey B Kirpichev in :gh:`116560`.) +* Add :c:func:`PyLong_GetNumBits` function to get the number of bits of an + integer. (Contributed by Victor Stinner in :gh:`119714`.) + Porting to Python 3.14 ---------------------- diff --git a/Include/cpython/longobject.h b/Include/cpython/longobject.h index 19a6722d07734a..0fe35663a06b3f 100644 --- a/Include/cpython/longobject.h +++ b/Include/cpython/longobject.h @@ -115,3 +115,6 @@ PyAPI_FUNC(int) _PyLong_AsByteArray(PyLongObject* v, /* For use by the gcd function in mathmodule.c */ PyAPI_FUNC(PyObject *) _PyLong_GCD(PyObject *, PyObject *); + +// Get the number of bits of an integer. +PyAPI_FUNC(Py_ssize_t) PyLong_GetNumBits(PyObject *obj); diff --git a/Lib/test/test_capi/test_long.py b/Lib/test/test_capi/test_long.py index 06a29b5a0505b4..18754802fef46e 100644 --- a/Lib/test/test_capi/test_long.py +++ b/Lib/test/test_capi/test_long.py @@ -1,6 +1,7 @@ import unittest import sys import test.support as support +from test.support import bigmemtest from test.support import import_helper @@ -737,6 +738,32 @@ def test_long_getsign(self): # CRASHES getsign(NULL) + def test_long_getnumbits(self): + numbits = _testcapi.pylong_getnumbits + self.assertEqual(numbits(0), 0) + self.assertEqual(numbits(7), 3) + self.assertEqual(numbits(-37), 6) + self.assertEqual(numbits(684_643_661_981), 40) + self.assertEqual(numbits(2**123-1), 123) + + for invalid_type in (1.0, 1j, "abc"): + with self.subTest(invalid_type=invalid_type): + with self.assertRaises(TypeError): + numbits(invalid_type) + + # CRASHES numbits(NULL) + + # The test is always skipped on 64-bit platforms, it requires way too much + # memory. It's only run on 32-bit platforms. + @bigmemtest(size=(_testcapi.PY_SSIZE_T_MAX // sys.int_info.bits_per_digit + * sys.int_info.sizeof_digit) + sys.getsizeof(123), + memuse=1, dry_run=False) + def test_long_getnumbits_overflow(self, size): + numbits = _testcapi.pylong_getnumbits + big_int = 1 << _testcapi.PY_SSIZE_T_MAX + with self.assertRaises(OverflowError): + numbits(big_int) + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/C API/2024-05-29-11-25-26.gh-issue-119714.We9eUg.rst b/Misc/NEWS.d/next/C API/2024-05-29-11-25-26.gh-issue-119714.We9eUg.rst new file mode 100644 index 00000000000000..12cc5aa184c075 --- /dev/null +++ b/Misc/NEWS.d/next/C API/2024-05-29-11-25-26.gh-issue-119714.We9eUg.rst @@ -0,0 +1,2 @@ +Add :c:func:`PyLong_GetNumBits` function to get the number of bits of an +integer. Patch by Victor Stinner. diff --git a/Modules/_testcapi/long.c b/Modules/_testcapi/long.c index 2b5e85d5707522..8ddd0d952e94e2 100644 --- a/Modules/_testcapi/long.c +++ b/Modules/_testcapi/long.c @@ -117,6 +117,18 @@ pylong_aspid(PyObject *module, PyObject *arg) } +static PyObject * +pylong_getnumbits(PyObject *module, PyObject *arg) +{ + NULLABLE(arg); + Py_ssize_t bits = PyLong_GetNumBits(arg); + if (bits < 0) { + return NULL; + } + return PyLong_FromSsize_t(bits); +} + + static PyMethodDef test_methods[] = { _TESTCAPI_CALL_LONG_COMPACT_API_METHODDEF {"pylong_fromunicodeobject", pylong_fromunicodeobject, METH_VARARGS}, @@ -124,6 +136,7 @@ static PyMethodDef test_methods[] = { {"pylong_fromnativebytes", pylong_fromnativebytes, METH_VARARGS}, {"pylong_getsign", pylong_getsign, METH_O}, {"pylong_aspid", pylong_aspid, METH_O}, + {"pylong_getnumbits", pylong_getnumbits, METH_O}, {NULL}, }; diff --git a/Objects/longobject.c b/Objects/longobject.c index 054689471e7aa9..3a05aceffcfbbf 100644 --- a/Objects/longobject.c +++ b/Objects/longobject.c @@ -792,28 +792,32 @@ bit_length_digit(digit x) return _Py_bit_length((unsigned long)x); } + size_t _PyLong_NumBits(PyObject *vv) { PyLongObject *v = (PyLongObject *)vv; - size_t result = 0; - Py_ssize_t ndigits; - int msd_bits; - assert(v != NULL); assert(PyLong_Check(v)); - ndigits = _PyLong_DigitCount(v); + + Py_ssize_t ndigits = _PyLong_DigitCount(v); assert(ndigits == 0 || v->long_value.ob_digit[ndigits - 1] != 0); - if (ndigits > 0) { - digit msd = v->long_value.ob_digit[ndigits - 1]; - if ((size_t)(ndigits - 1) > SIZE_MAX / (size_t)PyLong_SHIFT) - goto Overflow; - result = (size_t)(ndigits - 1) * (size_t)PyLong_SHIFT; - msd_bits = bit_length_digit(msd); - if (SIZE_MAX - msd_bits < result) - goto Overflow; - result += msd_bits; + if (ndigits == 0) { + return 0; + } + assert(ndigits > 0); + + digit msd = v->long_value.ob_digit[ndigits - 1]; + if ((size_t)(ndigits - 1) > SIZE_MAX / (size_t)PyLong_SHIFT) { + goto Overflow; + } + size_t result = (size_t)(ndigits - 1) * (size_t)PyLong_SHIFT; + + int msd_bits = bit_length_digit(msd); + if (SIZE_MAX - msd_bits < result) { + goto Overflow; } + result += msd_bits; return result; Overflow: @@ -822,6 +826,27 @@ _PyLong_NumBits(PyObject *vv) return (size_t)-1; } + +Py_ssize_t +PyLong_GetNumBits(PyObject *obj) +{ + if (!PyLong_Check(obj)) { + PyErr_Format(PyExc_TypeError, "expect int, got %T", obj); + return -1; + } + size_t bits = _PyLong_NumBits(obj); + if (bits == (size_t)-1) { + return -1; + } + if (bits > (size_t)PY_SSIZE_T_MAX) { + PyErr_SetString(PyExc_OverflowError, + "int has too many bits to express in Py_ssize_t"); + return -1; + } + return (Py_ssize_t)bits; +} + + PyObject * _PyLong_FromByteArray(const unsigned char* bytes, size_t n, int little_endian, int is_signed)