]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-109218: Deprecate weird cases in the complex() constructor (GH-119620)
authorSerhiy Storchaka <storchaka@gmail.com>
Thu, 30 May 2024 20:30:57 +0000 (23:30 +0300)
committerGitHub <noreply@github.com>
Thu, 30 May 2024 20:30:57 +0000 (23:30 +0300)
* Passing a string as the "real" keyword argument is now an error;
  it should only be passed as a single positional argument.
* Passing a complex number as the "real" or "imag" argument is now deprecated;
  it should only be passed as a single positional argument.

Doc/library/functions.rst
Doc/whatsnew/3.14.rst
Lib/test/test_complex.py
Lib/test/test_fractions.py
Misc/NEWS.d/next/Core and Builtins/2024-05-27-19-13-49.gh-issue-109218.-sdDg0.rst [new file with mode: 0644]
Objects/complexobject.c

index c07b1043afe6270e1a6c716e33bcaa0745da3833..7291461c69acd2db5079700b2411bb53bde1afe5 100644 (file)
@@ -449,6 +449,10 @@ are always available.  They are listed here in alphabetical order.
       Falls back to :meth:`~object.__index__` if :meth:`~object.__complex__` and
       :meth:`~object.__float__` are not defined.
 
+   .. deprecated:: 3.14
+      Passing a complex number as the *real* or *imag* argument is now
+      deprecated; it should only be passed as a single positional argument.
+
 
 .. function:: delattr(object, name)
 
index 8c37825430c2cf2fd320d4a6b2cb9768fd22c2f6..d443cf9bc56b9850aa0ae75ecc0e1fbf62b821b8 100644 (file)
@@ -103,6 +103,10 @@ Optimizations
 Deprecated
 ==========
 
+* Passing a complex number as the *real* or *imag* argument in the
+  :func:`complex` constructor is now deprecated; it should only be passed
+  as a single positional argument.
+  (Contributed by Serhiy Storchaka in :gh:`109218`.)
 
 
 Removed
index f29b7d3ebd31abf4374cd397faed54dad992606f..fb510ca9b709027269ce1200ff7e876b8f3d61c4 100644 (file)
@@ -382,25 +382,53 @@ class ComplexTest(unittest.TestCase):
         check(complex(1.0, 10.0), 1.0, 10.0)
         check(complex(4.25, 0.5), 4.25, 0.5)
 
-        check(complex(4.25+0j, 0), 4.25, 0.0)
-        check(complex(ComplexSubclass(4.25+0j), 0), 4.25, 0.0)
-        check(complex(WithComplex(4.25+0j), 0), 4.25, 0.0)
-        check(complex(4.25j, 0), 0.0, 4.25)
-        check(complex(0j, 4.25), 0.0, 4.25)
-        check(complex(0, 4.25+0j), 0.0, 4.25)
-        check(complex(0, ComplexSubclass(4.25+0j)), 0.0, 4.25)
+        with self.assertWarnsRegex(DeprecationWarning,
+                "argument 'real' must be a real number, not complex"):
+            check(complex(4.25+0j, 0), 4.25, 0.0)
+        with self.assertWarnsRegex(DeprecationWarning,
+                "argument 'real' must be a real number, not .*ComplexSubclass"):
+            check(complex(ComplexSubclass(4.25+0j), 0), 4.25, 0.0)
+        with self.assertWarnsRegex(DeprecationWarning,
+                "argument 'real' must be a real number, not .*WithComplex"):
+            check(complex(WithComplex(4.25+0j), 0), 4.25, 0.0)
+        with self.assertWarnsRegex(DeprecationWarning,
+                "argument 'real' must be a real number, not complex"):
+            check(complex(4.25j, 0), 0.0, 4.25)
+        with self.assertWarnsRegex(DeprecationWarning,
+                "argument 'real' must be a real number, not complex"):
+            check(complex(0j, 4.25), 0.0, 4.25)
+        with self.assertWarnsRegex(DeprecationWarning,
+                "argument 'imag' must be a real number, not complex"):
+            check(complex(0, 4.25+0j), 0.0, 4.25)
+        with self.assertWarnsRegex(DeprecationWarning,
+                "argument 'imag' must be a real number, not .*ComplexSubclass"):
+            check(complex(0, ComplexSubclass(4.25+0j)), 0.0, 4.25)
         with self.assertRaisesRegex(TypeError,
-                "second argument must be a number, not 'WithComplex'"):
+                "argument 'imag' must be a real number, not .*WithComplex"):
             complex(0, WithComplex(4.25+0j))
-        check(complex(0.0, 4.25j), -4.25, 0.0)
-        check(complex(4.25+0j, 0j), 4.25, 0.0)
-        check(complex(4.25j, 0j), 0.0, 4.25)
-        check(complex(0j, 4.25+0j), 0.0, 4.25)
-        check(complex(0j, 4.25j), -4.25, 0.0)
+        with self.assertWarnsRegex(DeprecationWarning,
+                "argument 'imag' must be a real number, not complex"):
+            check(complex(0.0, 4.25j), -4.25, 0.0)
+        with self.assertWarnsRegex(DeprecationWarning,
+                "argument 'real' must be a real number, not complex"):
+            check(complex(4.25+0j, 0j), 4.25, 0.0)
+        with self.assertWarnsRegex(DeprecationWarning,
+                "argument 'real' must be a real number, not complex"):
+            check(complex(4.25j, 0j), 0.0, 4.25)
+        with self.assertWarnsRegex(DeprecationWarning,
+                "argument 'real' must be a real number, not complex"):
+            check(complex(0j, 4.25+0j), 0.0, 4.25)
+        with self.assertWarnsRegex(DeprecationWarning,
+                "argument 'real' must be a real number, not complex"):
+            check(complex(0j, 4.25j), -4.25, 0.0)
 
         check(complex(real=4.25), 4.25, 0.0)
-        check(complex(real=4.25+0j), 4.25, 0.0)
-        check(complex(real=4.25+1.5j), 4.25, 1.5)
+        with self.assertWarnsRegex(DeprecationWarning,
+                "argument 'real' must be a real number, not complex"):
+            check(complex(real=4.25+0j), 4.25, 0.0)
+        with self.assertWarnsRegex(DeprecationWarning,
+                "argument 'real' must be a real number, not complex"):
+            check(complex(real=4.25+1.5j), 4.25, 1.5)
         check(complex(imag=1.5), 0.0, 1.5)
         check(complex(real=4.25, imag=1.5), 4.25, 1.5)
         check(complex(4.25, imag=1.5), 4.25, 1.5)
@@ -420,22 +448,22 @@ class ComplexTest(unittest.TestCase):
         del c, c2
 
         self.assertRaisesRegex(TypeError,
-            "first argument must be a string or a number, not 'dict'",
+            "argument must be a string or a number, not dict",
             complex, {})
         self.assertRaisesRegex(TypeError,
-            "first argument must be a string or a number, not 'NoneType'",
+            "argument must be a string or a number, not NoneType",
             complex, None)
         self.assertRaisesRegex(TypeError,
-            "first argument must be a string or a number, not 'dict'",
+            "argument 'real' must be a real number, not dict",
             complex, {1:2}, 0)
         self.assertRaisesRegex(TypeError,
-            "can't take second arg if first is a string",
+            "argument 'real' must be a real number, not str",
             complex, '1', 0)
         self.assertRaisesRegex(TypeError,
-            "second argument must be a number, not 'dict'",
+            "argument 'imag' must be a real number, not dict",
             complex, 0, {1:2})
         self.assertRaisesRegex(TypeError,
-                "second arg can't be a string",
+            "argument 'imag' must be a real number, not str",
             complex, 0, '1')
 
         self.assertRaises(TypeError, complex, WithComplex(1.5))
index 3648a8982a37e05215dfe629073ae8b976d03f5a..28607ee37000f988f53bb5af89ecaad84b472197 100644 (file)
@@ -806,7 +806,10 @@ class FractionTest(unittest.TestCase):
         self.assertTypedEquals(F(3, 2) * Polar(4, 2), Polar(F(6, 1), 2))
         self.assertTypedEquals(F(3, 2) * Polar(4.0, 2), Polar(6.0, 2))
         self.assertTypedEquals(F(3, 2) * Rect(4, 3), Rect(F(6, 1), F(9, 2)))
-        self.assertTypedEquals(F(3, 2) * RectComplex(4, 3), RectComplex(6.0+0j, 4.5+0j))
+        with self.assertWarnsRegex(DeprecationWarning,
+                "argument 'real' must be a real number, not complex"):
+            self.assertTypedEquals(F(3, 2) * RectComplex(4, 3),
+                                   RectComplex(6.0+0j, 4.5+0j))
         self.assertRaises(TypeError, operator.mul, Polar(4, 2), F(3, 2))
         self.assertTypedEquals(Rect(4, 3) * F(3, 2), 6.0 + 4.5j)
         self.assertEqual(F(3, 2) * SymbolicComplex('X'), SymbolicComplex('3/2 * X'))
diff --git a/Misc/NEWS.d/next/Core and Builtins/2024-05-27-19-13-49.gh-issue-109218.-sdDg0.rst b/Misc/NEWS.d/next/Core and Builtins/2024-05-27-19-13-49.gh-issue-109218.-sdDg0.rst
new file mode 100644 (file)
index 0000000..db76217
--- /dev/null
@@ -0,0 +1,3 @@
+:func:`complex` accepts now a string only as a positional argument. Passing
+a complex number as the "real" or "imag" argument is deprecated; it should
+only be passed as a single positional argument.
index 17ee43725dd70d28b9dc1417b7bdeac9bfe71dfc..59c84f1359b96640da6dfbe7088899fbf3e8603c 100644 (file)
@@ -894,8 +894,8 @@ complex_subtype_from_string(PyTypeObject *type, PyObject *v)
     }
     else {
         PyErr_Format(PyExc_TypeError,
-            "complex() argument must be a string or a number, not '%.200s'",
-            Py_TYPE(v)->tp_name);
+            "complex() argument must be a string or a number, not %T",
+            v);
         return NULL;
     }
 
@@ -905,6 +905,77 @@ complex_subtype_from_string(PyTypeObject *type, PyObject *v)
     return result;
 }
 
+/* The constructor should only accept a string as a positional argument,
+ * not as by the 'real' keyword.  But Argument Clinic does not allow
+ * to distinguish between argument passed positionally and by keyword.
+ * So the constructor must be split into two parts: actual_complex_new()
+ * handles the case of no arguments and one positional argument, and calls
+ * complex_new(), implemented with Argument Clinic, to handle the remaining
+ * cases: 'real' and 'imag' arguments.  This separation is well suited
+ * for different constructor roles: convering a string or number to a complex
+ * number and constructing a complex number from real and imaginary parts.
+ */
+static PyObject *
+actual_complex_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
+{
+    PyObject *res = NULL;
+    PyNumberMethods *nbr;
+
+    if (PyTuple_GET_SIZE(args) > 1 || (kwargs != NULL && PyDict_GET_SIZE(kwargs))) {
+        return complex_new(type, args, kwargs);
+    }
+    if (!PyTuple_GET_SIZE(args)) {
+        return complex_subtype_from_doubles(type, 0, 0);
+    }
+
+    PyObject *arg = PyTuple_GET_ITEM(args, 0);
+    /* Special-case for a single argument when type(arg) is complex. */
+    if (PyComplex_CheckExact(arg) && type == &PyComplex_Type) {
+        /* Note that we can't know whether it's safe to return
+           a complex *subclass* instance as-is, hence the restriction
+           to exact complexes here.  If either the input or the
+           output is a complex subclass, it will be handled below
+           as a non-orthogonal vector.  */
+        return Py_NewRef(arg);
+    }
+    if (PyUnicode_Check(arg)) {
+        return complex_subtype_from_string(type, arg);
+    }
+    PyObject *tmp = try_complex_special_method(arg);
+    if (tmp) {
+        Py_complex c = ((PyComplexObject*)tmp)->cval;
+        res = complex_subtype_from_doubles(type, c.real, c.imag);
+        Py_DECREF(tmp);
+    }
+    else if (PyErr_Occurred()) {
+        return NULL;
+    }
+    else if (PyComplex_Check(arg)) {
+        /* Note that if arg is of a complex subtype, we're only
+           retaining its real & imag parts here, and the return
+           value is (properly) of the builtin complex type. */
+        Py_complex c = ((PyComplexObject*)arg)->cval;
+        res = complex_subtype_from_doubles(type, c.real, c.imag);
+    }
+    else if ((nbr = Py_TYPE(arg)->tp_as_number) != NULL &&
+             (nbr->nb_float != NULL || nbr->nb_index != NULL))
+    {
+        /* The argument really is entirely real, and contributes
+           nothing in the imaginary direction.
+           Just treat it as a double. */
+        double r = PyFloat_AsDouble(arg);
+        if (r != -1.0 || !PyErr_Occurred()) {
+            res = complex_subtype_from_doubles(type, r, 0);
+        }
+    }
+    else {
+        PyErr_Format(PyExc_TypeError,
+                     "complex() argument must be a string or a number, not %T",
+                     arg);
+    }
+    return res;
+}
+
 /*[clinic input]
 @classmethod
 complex.__new__ as complex_new
@@ -933,32 +1004,10 @@ complex_new_impl(PyTypeObject *type, PyObject *r, PyObject *i)
     if (r == NULL) {
         r = _PyLong_GetZero();
     }
+    PyObject *orig_r = r;
 
-    /* Special-case for a single argument when type(arg) is complex. */
-    if (PyComplex_CheckExact(r) && i == NULL &&
-        type == &PyComplex_Type) {
-        /* Note that we can't know whether it's safe to return
-           a complex *subclass* instance as-is, hence the restriction
-           to exact complexes here.  If either the input or the
-           output is a complex subclass, it will be handled below
-           as a non-orthogonal vector.  */
-        return Py_NewRef(r);
-    }
-    if (PyUnicode_Check(r)) {
-        if (i != NULL) {
-            PyErr_SetString(PyExc_TypeError,
-                            "complex() can't take second arg"
-                            " if first is a string");
-            return NULL;
-        }
-        return complex_subtype_from_string(type, r);
-    }
-    if (i != NULL && PyUnicode_Check(i)) {
-        PyErr_SetString(PyExc_TypeError,
-                        "complex() second arg can't be a string");
-        return NULL;
-    }
-
+    /* DEPRECATED: The call of try_complex_special_method() for the "real"
+     * part will be dropped after the end of the deprecation period. */
     tmp = try_complex_special_method(r);
     if (tmp) {
         r = tmp;
@@ -973,9 +1022,8 @@ complex_new_impl(PyTypeObject *type, PyObject *r, PyObject *i)
         (nbr->nb_float == NULL && nbr->nb_index == NULL && !PyComplex_Check(r)))
     {
         PyErr_Format(PyExc_TypeError,
-                     "complex() first argument must be a string or a number, "
-                     "not '%.200s'",
-                     Py_TYPE(r)->tp_name);
+                     "complex() argument 'real' must be a real number, not %T",
+                     r);
         if (own_r) {
             Py_DECREF(r);
         }
@@ -987,9 +1035,8 @@ complex_new_impl(PyTypeObject *type, PyObject *r, PyObject *i)
             (nbi->nb_float == NULL && nbi->nb_index == NULL && !PyComplex_Check(i)))
         {
             PyErr_Format(PyExc_TypeError,
-                         "complex() second argument must be a number, "
-                         "not '%.200s'",
-                         Py_TYPE(i)->tp_name);
+                         "complex() argument 'imag' must be a real number, not %T",
+                         i);
             if (own_r) {
                 Py_DECREF(r);
             }
@@ -1001,6 +1048,7 @@ complex_new_impl(PyTypeObject *type, PyObject *r, PyObject *i)
        both be treated as numbers, and the constructor should return a
        complex number equal to (real + imag*1j).
 
+       The following is DEPRECATED:
        Note that we do NOT assume the input to already be in canonical
        form; the "real" and "imag" parts might themselves be complex
        numbers, which slightly complicates the code below. */
@@ -1011,19 +1059,27 @@ complex_new_impl(PyTypeObject *type, PyObject *r, PyObject *i)
         cr = ((PyComplexObject*)r)->cval;
         cr_is_complex = 1;
         if (own_r) {
+            /* r was a newly created complex number, rather
+               than the original "real" argument. */
             Py_DECREF(r);
         }
+        nbr = Py_TYPE(orig_r)->tp_as_number;
+        if (nbr == NULL ||
+            (nbr->nb_float == NULL && nbr->nb_index == NULL))
+        {
+            if (PyErr_WarnFormat(PyExc_DeprecationWarning, 1,
+                    "complex() argument 'real' must be a real number, not %T",
+                    orig_r)) {
+                return NULL;
+            }
+        }
     }
     else {
         /* The "real" part really is entirely real, and contributes
            nothing in the imaginary direction.
            Just treat it as a double. */
         tmp = PyNumber_Float(r);
-        if (own_r) {
-            /* r was a newly created complex number, rather
-               than the original "real" argument. */
-            Py_DECREF(r);
-        }
+        assert(!own_r);
         if (tmp == NULL)
             return NULL;
         assert(PyFloat_Check(tmp));
@@ -1035,6 +1091,11 @@ complex_new_impl(PyTypeObject *type, PyObject *r, PyObject *i)
         ci.real = cr.imag;
     }
     else if (PyComplex_Check(i)) {
+        if (PyErr_WarnFormat(PyExc_DeprecationWarning, 1,
+                "complex() argument 'imag' must be a real number, not %T",
+                i)) {
+            return NULL;
+        }
         ci = ((PyComplexObject*)i)->cval;
         ci_is_complex = 1;
     } else {
@@ -1134,6 +1195,6 @@ PyTypeObject PyComplex_Type = {
     0,                                          /* tp_dictoffset */
     0,                                          /* tp_init */
     PyType_GenericAlloc,                        /* tp_alloc */
-    complex_new,                                /* tp_new */
+    actual_complex_new,                         /* tp_new */
     PyObject_Del,                               /* tp_free */
 };