From 94bee45dee41876e88fe023b9163178d376355dc Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 15 Jul 2024 19:07:00 +0300 Subject: [PATCH] gh-84978: Add float.from_number() and complex.from_number() (GH-26827) They are alternate constructors which only accept numbers (including objects with special methods __float__, __complex__ and __index__), but not strings. --- Doc/library/functions.rst | 4 + Doc/library/stdtypes.rst | 36 ++++++++ Doc/whatsnew/3.14.rst | 4 + Lib/test/test_complex.py | 39 ++++++++ Lib/test/test_float.py | 89 +++++++++++++------ ...3-10-14-23-05-40.gh-issue-84978.Z0t6dg.rst | 1 + Objects/clinic/complexobject.c.h | 11 ++- Objects/clinic/floatobject.c.h | 11 ++- Objects/complexobject.c | 62 +++++++++---- Objects/floatobject.c | 31 +++++++ 10 files changed, 242 insertions(+), 46 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2023-10-14-23-05-40.gh-issue-84978.Z0t6dg.rst diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst index 17348dd907b..26ab086f914 100644 --- a/Doc/library/functions.rst +++ b/Doc/library/functions.rst @@ -438,6 +438,8 @@ are always available. They are listed here in alphabetical order. If one of arguments is a real number, only its real component is used in the above expressions. + See also :meth:`complex.from_number` which only accepts a single numeric argument. + If all arguments are omitted, returns ``0j``. The complex type is described in :ref:`typesnumeric`. @@ -788,6 +790,8 @@ are always available. They are listed here in alphabetical order. ``x.__float__()``. If :meth:`~object.__float__` is not defined then it falls back to :meth:`~object.__index__`. + See also :meth:`float.from_number` which only accepts a numeric argument. + If no argument is given, ``0.0`` is returned. The float type is described in :ref:`typesnumeric`. diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index d3f7cfb01d3..b74cd908d77 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -625,6 +625,23 @@ Additional Methods on Float The float type implements the :class:`numbers.Real` :term:`abstract base class`. float also has the following additional methods. +.. classmethod:: float.from_number(x) + + Class method to return a floating point number constructed from a number *x*. + + If the argument is an integer or a floating point number, a + floating point number with the same value (within Python's floating point + precision) is returned. If the argument is outside the range of a Python + float, an :exc:`OverflowError` will be raised. + + For a general Python object ``x``, ``float.from_number(x)`` delegates to + ``x.__float__()``. + If :meth:`~object.__float__` is not defined then it falls back + to :meth:`~object.__index__`. + + .. versionadded:: 3.14 + + .. method:: float.as_integer_ratio() Return a pair of integers whose ratio is exactly equal to the @@ -703,6 +720,25 @@ hexadecimal string representing the same number:: '0x1.d380000000000p+11' +Additional Methods on Complex +----------------------------- + +The :class:`!complex` type implements the :class:`numbers.Complex` +:term:`abstract base class`. +:class:`!complex` also has the following additional methods. + +.. classmethod:: complex.from_number(x) + + Class method to convert a number to a complex number. + + For a general Python object ``x``, ``complex.from_number(x)`` delegates to + ``x.__complex__()``. If :meth:`~object.__complex__` is not defined then it falls back + to :meth:`~object.__float__`. If :meth:`!__float__` is not defined then it falls back + to :meth:`~object.__index__`. + + .. versionadded:: 3.14 + + .. _numeric-hash: Hashing of numeric types diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index da9b45cd8e5..8f7b6ebd0af 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -75,6 +75,10 @@ New Features Other Language Changes ====================== +* Added class methods :meth:`float.from_number` and :meth:`complex.from_number` + to convert a number to :class:`float` or :class:`complex` type correspondingly. + They raise an error if the argument is a string. + (Contributed by Serhiy Storchaka in :gh:`84978`.) New Modules diff --git a/Lib/test/test_complex.py b/Lib/test/test_complex.py index 155240e30f1..e3a2205c43d 100644 --- a/Lib/test/test_complex.py +++ b/Lib/test/test_complex.py @@ -36,6 +36,16 @@ class WithFloat: class ComplexSubclass(complex): pass +class OtherComplexSubclass(complex): + pass + +class MyInt: + def __init__(self, value): + self.value = value + + def __int__(self): + return self.value + class WithComplex: def __init__(self, value): self.value = value @@ -675,6 +685,35 @@ class ComplexTest(unittest.TestCase): if not any(ch in lit for ch in 'xXoObB'): self.assertRaises(ValueError, complex, lit) + def test_from_number(self, cls=complex): + def eq(actual, expected): + self.assertEqual(actual, expected) + self.assertIs(type(actual), cls) + + eq(cls.from_number(3.14), 3.14+0j) + eq(cls.from_number(3.14j), 3.14j) + eq(cls.from_number(314), 314.0+0j) + eq(cls.from_number(OtherComplexSubclass(3.14, 2.72)), 3.14+2.72j) + eq(cls.from_number(WithComplex(3.14+2.72j)), 3.14+2.72j) + eq(cls.from_number(WithFloat(3.14)), 3.14+0j) + eq(cls.from_number(WithIndex(314)), 314.0+0j) + + cNAN = complex(NAN, NAN) + x = cls.from_number(cNAN) + self.assertTrue(x != x) + self.assertIs(type(x), cls) + if cls is complex: + self.assertIs(cls.from_number(cNAN), cNAN) + + self.assertRaises(TypeError, cls.from_number, '3.14') + self.assertRaises(TypeError, cls.from_number, b'3.14') + self.assertRaises(TypeError, cls.from_number, MyInt(314)) + self.assertRaises(TypeError, cls.from_number, {}) + self.assertRaises(TypeError, cls.from_number) + + def test_from_number_subclass(self): + self.test_from_number(ComplexSubclass) + def test_hash(self): for x in range(-30, 30): self.assertEqual(hash(x), hash(complex(x, 0))) diff --git a/Lib/test/test_float.py b/Lib/test/test_float.py index 756cf9bd771..36ba67ac741 100644 --- a/Lib/test/test_float.py +++ b/Lib/test/test_float.py @@ -32,6 +32,28 @@ class FloatSubclass(float): class OtherFloatSubclass(float): pass +class MyIndex: + def __init__(self, value): + self.value = value + + def __index__(self): + return self.value + +class MyInt: + def __init__(self, value): + self.value = value + + def __int__(self): + return self.value + +class FloatLike: + def __init__(self, value): + self.value = value + + def __float__(self): + return self.value + + class GeneralFloatCases(unittest.TestCase): def test_float(self): @@ -181,10 +203,6 @@ class GeneralFloatCases(unittest.TestCase): def test_floatconversion(self): # Make sure that calls to __float__() work properly - class Foo1(object): - def __float__(self): - return 42. - class Foo2(float): def __float__(self): return 42. @@ -206,45 +224,29 @@ class GeneralFloatCases(unittest.TestCase): def __float__(self): return float(str(self)) + 1 - self.assertEqual(float(Foo1()), 42.) + self.assertEqual(float(FloatLike(42.)), 42.) self.assertEqual(float(Foo2()), 42.) with self.assertWarns(DeprecationWarning): self.assertEqual(float(Foo3(21)), 42.) self.assertRaises(TypeError, float, Foo4(42)) self.assertEqual(float(FooStr('8')), 9.) - class Foo5: - def __float__(self): - return "" - self.assertRaises(TypeError, time.sleep, Foo5()) + self.assertRaises(TypeError, time.sleep, FloatLike("")) # Issue #24731 - class F: - def __float__(self): - return OtherFloatSubclass(42.) + f = FloatLike(OtherFloatSubclass(42.)) with self.assertWarns(DeprecationWarning): - self.assertEqual(float(F()), 42.) + self.assertEqual(float(f), 42.) with self.assertWarns(DeprecationWarning): - self.assertIs(type(float(F())), float) + self.assertIs(type(float(f)), float) with self.assertWarns(DeprecationWarning): - self.assertEqual(FloatSubclass(F()), 42.) + self.assertEqual(FloatSubclass(f), 42.) with self.assertWarns(DeprecationWarning): - self.assertIs(type(FloatSubclass(F())), FloatSubclass) - - class MyIndex: - def __init__(self, value): - self.value = value - def __index__(self): - return self.value + self.assertIs(type(FloatSubclass(f)), FloatSubclass) self.assertEqual(float(MyIndex(42)), 42.0) self.assertRaises(OverflowError, float, MyIndex(2**2000)) - - class MyInt: - def __int__(self): - return 42 - - self.assertRaises(TypeError, float, MyInt()) + self.assertRaises(TypeError, float, MyInt(42)) def test_keyword_args(self): with self.assertRaisesRegex(TypeError, 'keyword argument'): @@ -277,6 +279,37 @@ class GeneralFloatCases(unittest.TestCase): self.assertEqual(float(u), 2.5) self.assertEqual(u.newarg, 3) + def assertEqualAndType(self, actual, expected_value, expected_type): + self.assertEqual(actual, expected_value) + self.assertIs(type(actual), expected_type) + + def test_from_number(self, cls=float): + def eq(actual, expected): + self.assertEqual(actual, expected) + self.assertIs(type(actual), cls) + + eq(cls.from_number(3.14), 3.14) + eq(cls.from_number(314), 314.0) + eq(cls.from_number(OtherFloatSubclass(3.14)), 3.14) + eq(cls.from_number(FloatLike(3.14)), 3.14) + eq(cls.from_number(MyIndex(314)), 314.0) + + x = cls.from_number(NAN) + self.assertTrue(x != x) + self.assertIs(type(x), cls) + if cls is float: + self.assertIs(cls.from_number(NAN), NAN) + + self.assertRaises(TypeError, cls.from_number, '3.14') + self.assertRaises(TypeError, cls.from_number, b'3.14') + self.assertRaises(TypeError, cls.from_number, 3.14j) + self.assertRaises(TypeError, cls.from_number, MyInt(314)) + self.assertRaises(TypeError, cls.from_number, {}) + self.assertRaises(TypeError, cls.from_number) + + def test_from_number_subclass(self): + self.test_from_number(FloatSubclass) + def test_is_integer(self): self.assertFalse((1.1).is_integer()) self.assertTrue((1.).is_integer()) diff --git a/Misc/NEWS.d/next/Core and Builtins/2023-10-14-23-05-40.gh-issue-84978.Z0t6dg.rst b/Misc/NEWS.d/next/Core and Builtins/2023-10-14-23-05-40.gh-issue-84978.Z0t6dg.rst new file mode 100644 index 00000000000..b1f08288f92 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2023-10-14-23-05-40.gh-issue-84978.Z0t6dg.rst @@ -0,0 +1 @@ +Add class methods :meth:`float.from_number` and :meth:`complex.from_number`. diff --git a/Objects/clinic/complexobject.c.h b/Objects/clinic/complexobject.c.h index 46c3b352562..58fd4e26871 100644 --- a/Objects/clinic/complexobject.c.h +++ b/Objects/clinic/complexobject.c.h @@ -160,4 +160,13 @@ skip_optional_pos: exit: return return_value; } -/*[clinic end generated code: output=295ecfd71389d7fe input=a9049054013a1b77]*/ + +PyDoc_STRVAR(complex_from_number__doc__, +"from_number($type, number, /)\n" +"--\n" +"\n" +"Convert number to a complex floating-point number."); + +#define COMPLEX_FROM_NUMBER_METHODDEF \ + {"from_number", (PyCFunction)complex_from_number, METH_O|METH_CLASS, complex_from_number__doc__}, +/*[clinic end generated code: output=188438cc9ae167f7 input=a9049054013a1b77]*/ diff --git a/Objects/clinic/floatobject.c.h b/Objects/clinic/floatobject.c.h index 10f6149cc88..d20b314514a 100644 --- a/Objects/clinic/floatobject.c.h +++ b/Objects/clinic/floatobject.c.h @@ -227,6 +227,15 @@ exit: return return_value; } +PyDoc_STRVAR(float_from_number__doc__, +"from_number($type, number, /)\n" +"--\n" +"\n" +"Convert real number to a floating-point number."); + +#define FLOAT_FROM_NUMBER_METHODDEF \ + {"from_number", (PyCFunction)float_from_number, METH_O|METH_CLASS, float_from_number__doc__}, + PyDoc_STRVAR(float___getnewargs____doc__, "__getnewargs__($self, /)\n" "--\n" @@ -318,4 +327,4 @@ float___format__(PyObject *self, PyObject *arg) exit: return return_value; } -/*[clinic end generated code: output=c79743c8551c30d9 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=b9c8a1b6759ca073 input=a9049054013a1b77]*/ diff --git a/Objects/complexobject.c b/Objects/complexobject.c index 31897463dbe..7c8a6bd9dfc 100644 --- a/Objects/complexobject.c +++ b/Objects/complexobject.c @@ -757,22 +757,6 @@ complex___complex___impl(PyComplexObject *self) } -static PyMethodDef complex_methods[] = { - COMPLEX_CONJUGATE_METHODDEF - COMPLEX___COMPLEX___METHODDEF - COMPLEX___GETNEWARGS___METHODDEF - COMPLEX___FORMAT___METHODDEF - {NULL, NULL} /* sentinel */ -}; - -static PyMemberDef complex_members[] = { - {"real", Py_T_DOUBLE, offsetof(PyComplexObject, cval.real), Py_READONLY, - "the real part of a complex number"}, - {"imag", Py_T_DOUBLE, offsetof(PyComplexObject, cval.imag), Py_READONLY, - "the imaginary part of a complex number"}, - {0}, -}; - static PyObject * complex_from_string_inner(const char *s, Py_ssize_t len, void *type) { @@ -1142,6 +1126,52 @@ complex_new_impl(PyTypeObject *type, PyObject *r, PyObject *i) return complex_subtype_from_doubles(type, cr.real, ci.real); } +/*[clinic input] +@classmethod +complex.from_number + + number: object + / + +Convert number to a complex floating-point number. +[clinic start generated code]*/ + +static PyObject * +complex_from_number(PyTypeObject *type, PyObject *number) +/*[clinic end generated code: output=658a7a5fb0de074d input=3f8bdd3a2bc3facd]*/ +{ + if (PyComplex_CheckExact(number) && type == &PyComplex_Type) { + Py_INCREF(number); + return number; + } + Py_complex cv = PyComplex_AsCComplex(number); + if (cv.real == -1.0 && PyErr_Occurred()) { + return NULL; + } + PyObject *result = PyComplex_FromCComplex(cv); + if (type != &PyComplex_Type && result != NULL) { + Py_SETREF(result, PyObject_CallOneArg((PyObject *)type, result)); + } + return result; +} + +static PyMethodDef complex_methods[] = { + COMPLEX_FROM_NUMBER_METHODDEF + COMPLEX_CONJUGATE_METHODDEF + COMPLEX___COMPLEX___METHODDEF + COMPLEX___GETNEWARGS___METHODDEF + COMPLEX___FORMAT___METHODDEF + {NULL, NULL} /* sentinel */ +}; + +static PyMemberDef complex_members[] = { + {"real", Py_T_DOUBLE, offsetof(PyComplexObject, cval.real), Py_READONLY, + "the real part of a complex number"}, + {"imag", Py_T_DOUBLE, offsetof(PyComplexObject, cval.imag), Py_READONLY, + "the imaginary part of a complex number"}, + {0}, +}; + static PyNumberMethods complex_as_number = { (binaryfunc)complex_add, /* nb_add */ (binaryfunc)complex_sub, /* nb_subtract */ diff --git a/Objects/floatobject.c b/Objects/floatobject.c index 31f4145c123..9e1ef2a21b3 100644 --- a/Objects/floatobject.c +++ b/Objects/floatobject.c @@ -1669,6 +1669,36 @@ float_vectorcall(PyObject *type, PyObject * const*args, } +/*[clinic input] +@classmethod +float.from_number + + number: object + / + +Convert real number to a floating-point number. +[clinic start generated code]*/ + +static PyObject * +float_from_number(PyTypeObject *type, PyObject *number) +/*[clinic end generated code: output=bbcf05529fe907a3 input=1f8424d9bc11866a]*/ +{ + if (PyFloat_CheckExact(number) && type == &PyFloat_Type) { + Py_INCREF(number); + return number; + } + double x = PyFloat_AsDouble(number); + if (x == -1.0 && PyErr_Occurred()) { + return NULL; + } + PyObject *result = PyFloat_FromDouble(x); + if (type != &PyFloat_Type && result != NULL) { + Py_SETREF(result, PyObject_CallOneArg((PyObject *)type, result)); + } + return result; +} + + /*[clinic input] float.__getnewargs__ [clinic start generated code]*/ @@ -1782,6 +1812,7 @@ float___format___impl(PyObject *self, PyObject *format_spec) } static PyMethodDef float_methods[] = { + FLOAT_FROM_NUMBER_METHODDEF FLOAT_CONJUGATE_METHODDEF FLOAT___TRUNC___METHODDEF FLOAT___FLOOR___METHODDEF