From ef01e95ae3659015c2ebe4ecdc048aadcda89930 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 30 May 2024 23:30:57 +0300 Subject: [PATCH] gh-109218: Deprecate weird cases in the complex() constructor (GH-119620) * 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 | 4 + Doc/whatsnew/3.14.rst | 4 + Lib/test/test_complex.py | 70 ++++++--- Lib/test/test_fractions.py | 5 +- ...-05-27-19-13-49.gh-issue-109218.-sdDg0.rst | 3 + Objects/complexobject.c | 139 +++++++++++++----- 6 files changed, 164 insertions(+), 61 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2024-05-27-19-13-49.gh-issue-109218.-sdDg0.rst diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst index c07b1043afe..7291461c69a 100644 --- a/Doc/library/functions.rst +++ b/Doc/library/functions.rst @@ -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) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 8c37825430c..d443cf9bc56 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -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 diff --git a/Lib/test/test_complex.py b/Lib/test/test_complex.py index f29b7d3ebd3..fb510ca9b70 100644 --- a/Lib/test/test_complex.py +++ b/Lib/test/test_complex.py @@ -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)) diff --git a/Lib/test/test_fractions.py b/Lib/test/test_fractions.py index 3648a8982a3..28607ee3700 100644 --- a/Lib/test/test_fractions.py +++ b/Lib/test/test_fractions.py @@ -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 index 00000000000..db762174a8c --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2024-05-27-19-13-49.gh-issue-109218.-sdDg0.rst @@ -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. diff --git a/Objects/complexobject.c b/Objects/complexobject.c index 17ee43725dd..59c84f1359b 100644 --- a/Objects/complexobject.c +++ b/Objects/complexobject.c @@ -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 */ };