]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-84978: Add float.from_number() and complex.from_number() (GH-26827)
authorSerhiy Storchaka <storchaka@gmail.com>
Mon, 15 Jul 2024 16:07:00 +0000 (19:07 +0300)
committerGitHub <noreply@github.com>
Mon, 15 Jul 2024 16:07:00 +0000 (16:07 +0000)
They are alternate constructors which only accept numbers
(including objects with special methods __float__, __complex__
and __index__), but not strings.

Doc/library/functions.rst
Doc/library/stdtypes.rst
Doc/whatsnew/3.14.rst
Lib/test/test_complex.py
Lib/test/test_float.py
Misc/NEWS.d/next/Core and Builtins/2023-10-14-23-05-40.gh-issue-84978.Z0t6dg.rst [new file with mode: 0644]
Objects/clinic/complexobject.c.h
Objects/clinic/floatobject.c.h
Objects/complexobject.c
Objects/floatobject.c

index 17348dd907bf67c49c7ca805b839956e8663e8dd..26ab086f914d7d8b8f6dd29a937619c98a082ee8 100644 (file)
@@ -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`.
index d3f7cfb01d3c2122305a4961d189a547f4ae8a08..b74cd908d7741b0bc42204a542023b4096d6242b 100644 (file)
@@ -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
index da9b45cd8e58b38325f0b1107e024c62e59f842e..8f7b6ebd0af31662c1d8b1f0093204b8f39a65b3 100644 (file)
@@ -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
index 155240e30f1ad1f954d91fd02d9dda3c249aba44..e3a2205c43d747f21c3bf64072aa6878cd3414bb 100644 (file)
@@ -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)))
index 756cf9bd7719c0122792e79a005f2f4cff8ed009..36ba67ac7415e7a6ca4a6f22b3ea4617782a4e83 100644 (file)
@@ -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 (file)
index 0000000..b1f0828
--- /dev/null
@@ -0,0 +1 @@
+Add class methods :meth:`float.from_number` and :meth:`complex.from_number`.
index 46c3b352562445374daa87452d2d83cd2982b8cf..58fd4e26871b4dd2d0bd73923446bbca884f77f0 100644 (file)
@@ -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]*/
index 10f6149cc88c22fe34f69ea1366ab0e9cec9f109..d20b314514a6f21925bfd8139ca0b8a2c6d6dbff 100644 (file)
@@ -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]*/
index 31897463dbe689737aa3fc6baa5b81cd13a20b3c..7c8a6bd9dfcd3fc289cfce25b543e26985389f50 100644 (file)
@@ -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 */
index 31f4145c123334944c67af2318bbe96728c2a9ec..9e1ef2a21b35c46d3c89337da94afff4d6405069 100644 (file)
@@ -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