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.
This commit is contained in:
Serhiy Storchaka 2024-05-30 23:30:57 +03:00 committed by GitHub
parent deda85717b
commit ef01e95ae3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 164 additions and 61 deletions

View 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 Falls back to :meth:`~object.__index__` if :meth:`~object.__complex__` and
:meth:`~object.__float__` are not defined. :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) .. function:: delattr(object, name)

View File

@ -103,6 +103,10 @@ Optimizations
Deprecated 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 Removed

View File

@ -382,25 +382,53 @@ class ComplexTest(unittest.TestCase):
check(complex(1.0, 10.0), 1.0, 10.0) check(complex(1.0, 10.0), 1.0, 10.0)
check(complex(4.25, 0.5), 4.25, 0.5) check(complex(4.25, 0.5), 4.25, 0.5)
check(complex(4.25+0j, 0), 4.25, 0.0) with self.assertWarnsRegex(DeprecationWarning,
check(complex(ComplexSubclass(4.25+0j), 0), 4.25, 0.0) "argument 'real' must be a real number, not complex"):
check(complex(WithComplex(4.25+0j), 0), 4.25, 0.0) check(complex(4.25+0j, 0), 4.25, 0.0)
check(complex(4.25j, 0), 0.0, 4.25) with self.assertWarnsRegex(DeprecationWarning,
check(complex(0j, 4.25), 0.0, 4.25) "argument 'real' must be a real number, not .*ComplexSubclass"):
check(complex(0, 4.25+0j), 0.0, 4.25) check(complex(ComplexSubclass(4.25+0j), 0), 4.25, 0.0)
check(complex(0, ComplexSubclass(4.25+0j)), 0.0, 4.25) 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, 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)) complex(0, WithComplex(4.25+0j))
check(complex(0.0, 4.25j), -4.25, 0.0) with self.assertWarnsRegex(DeprecationWarning,
check(complex(4.25+0j, 0j), 4.25, 0.0) "argument 'imag' must be a real number, not complex"):
check(complex(4.25j, 0j), 0.0, 4.25) check(complex(0.0, 4.25j), -4.25, 0.0)
check(complex(0j, 4.25+0j), 0.0, 4.25) with self.assertWarnsRegex(DeprecationWarning,
check(complex(0j, 4.25j), -4.25, 0.0) "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), 4.25, 0.0)
check(complex(real=4.25+0j), 4.25, 0.0) with self.assertWarnsRegex(DeprecationWarning,
check(complex(real=4.25+1.5j), 4.25, 1.5) "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(imag=1.5), 0.0, 1.5)
check(complex(real=4.25, imag=1.5), 4.25, 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) check(complex(4.25, imag=1.5), 4.25, 1.5)
@ -420,22 +448,22 @@ class ComplexTest(unittest.TestCase):
del c, c2 del c, c2
self.assertRaisesRegex(TypeError, 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, {}) complex, {})
self.assertRaisesRegex(TypeError, 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) complex, None)
self.assertRaisesRegex(TypeError, 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) complex, {1:2}, 0)
self.assertRaisesRegex(TypeError, 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) complex, '1', 0)
self.assertRaisesRegex(TypeError, self.assertRaisesRegex(TypeError,
"second argument must be a number, not 'dict'", "argument 'imag' must be a real number, not dict",
complex, 0, {1:2}) complex, 0, {1:2})
self.assertRaisesRegex(TypeError, self.assertRaisesRegex(TypeError,
"second arg can't be a string", "argument 'imag' must be a real number, not str",
complex, 0, '1') complex, 0, '1')
self.assertRaises(TypeError, complex, WithComplex(1.5)) self.assertRaises(TypeError, complex, WithComplex(1.5))

View 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, 2), Polar(F(6, 1), 2))
self.assertTypedEquals(F(3, 2) * Polar(4.0, 2), Polar(6.0, 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) * 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.assertRaises(TypeError, operator.mul, Polar(4, 2), F(3, 2))
self.assertTypedEquals(Rect(4, 3) * F(3, 2), 6.0 + 4.5j) self.assertTypedEquals(Rect(4, 3) * F(3, 2), 6.0 + 4.5j)
self.assertEqual(F(3, 2) * SymbolicComplex('X'), SymbolicComplex('3/2 * X')) self.assertEqual(F(3, 2) * SymbolicComplex('X'), SymbolicComplex('3/2 * X'))

View File

@ -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.

View File

@ -894,8 +894,8 @@ complex_subtype_from_string(PyTypeObject *type, PyObject *v)
} }
else { else {
PyErr_Format(PyExc_TypeError, PyErr_Format(PyExc_TypeError,
"complex() argument must be a string or a number, not '%.200s'", "complex() argument must be a string or a number, not %T",
Py_TYPE(v)->tp_name); v);
return NULL; return NULL;
} }
@ -905,6 +905,77 @@ complex_subtype_from_string(PyTypeObject *type, PyObject *v)
return result; 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] /*[clinic input]
@classmethod @classmethod
complex.__new__ as complex_new complex.__new__ as complex_new
@ -933,32 +1004,10 @@ complex_new_impl(PyTypeObject *type, PyObject *r, PyObject *i)
if (r == NULL) { if (r == NULL) {
r = _PyLong_GetZero(); r = _PyLong_GetZero();
} }
PyObject *orig_r = r;
/* Special-case for a single argument when type(arg) is complex. */ /* DEPRECATED: The call of try_complex_special_method() for the "real"
if (PyComplex_CheckExact(r) && i == NULL && * part will be dropped after the end of the deprecation period. */
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;
}
tmp = try_complex_special_method(r); tmp = try_complex_special_method(r);
if (tmp) { if (tmp) {
r = 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))) (nbr->nb_float == NULL && nbr->nb_index == NULL && !PyComplex_Check(r)))
{ {
PyErr_Format(PyExc_TypeError, PyErr_Format(PyExc_TypeError,
"complex() first argument must be a string or a number, " "complex() argument 'real' must be a real number, not %T",
"not '%.200s'", r);
Py_TYPE(r)->tp_name);
if (own_r) { if (own_r) {
Py_DECREF(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))) (nbi->nb_float == NULL && nbi->nb_index == NULL && !PyComplex_Check(i)))
{ {
PyErr_Format(PyExc_TypeError, PyErr_Format(PyExc_TypeError,
"complex() second argument must be a number, " "complex() argument 'imag' must be a real number, not %T",
"not '%.200s'", i);
Py_TYPE(i)->tp_name);
if (own_r) { if (own_r) {
Py_DECREF(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 both be treated as numbers, and the constructor should return a
complex number equal to (real + imag*1j). complex number equal to (real + imag*1j).
The following is DEPRECATED:
Note that we do NOT assume the input to already be in canonical Note that we do NOT assume the input to already be in canonical
form; the "real" and "imag" parts might themselves be complex form; the "real" and "imag" parts might themselves be complex
numbers, which slightly complicates the code below. */ 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 = ((PyComplexObject*)r)->cval;
cr_is_complex = 1; cr_is_complex = 1;
if (own_r) { if (own_r) {
/* r was a newly created complex number, rather
than the original "real" argument. */
Py_DECREF(r); 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 { else {
/* The "real" part really is entirely real, and contributes /* The "real" part really is entirely real, and contributes
nothing in the imaginary direction. nothing in the imaginary direction.
Just treat it as a double. */ Just treat it as a double. */
tmp = PyNumber_Float(r); tmp = PyNumber_Float(r);
if (own_r) { assert(!own_r);
/* r was a newly created complex number, rather
than the original "real" argument. */
Py_DECREF(r);
}
if (tmp == NULL) if (tmp == NULL)
return NULL; return NULL;
assert(PyFloat_Check(tmp)); assert(PyFloat_Check(tmp));
@ -1035,6 +1091,11 @@ complex_new_impl(PyTypeObject *type, PyObject *r, PyObject *i)
ci.real = cr.imag; ci.real = cr.imag;
} }
else if (PyComplex_Check(i)) { 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 = ((PyComplexObject*)i)->cval;
ci_is_complex = 1; ci_is_complex = 1;
} else { } else {
@ -1134,6 +1195,6 @@ PyTypeObject PyComplex_Type = {
0, /* tp_dictoffset */ 0, /* tp_dictoffset */
0, /* tp_init */ 0, /* tp_init */
PyType_GenericAlloc, /* tp_alloc */ PyType_GenericAlloc, /* tp_alloc */
complex_new, /* tp_new */ actual_complex_new, /* tp_new */
PyObject_Del, /* tp_free */ PyObject_Del, /* tp_free */
}; };