Skip to content

Commit 0f2fa61

Browse files
authored
gh-109598: make PyComplex_RealAsDouble/ImagAsDouble use __complex__ (GH-109647)
`PyComplex_RealAsDouble()`/`PyComplex_ImagAsDouble` now try to convert an object to a `complex` instance using its `__complex__()` method before falling back to the ``__float__()`` method. PyComplex_ImagAsDouble() also will not silently return 0.0 for non-complex types anymore. Instead we try to call PyFloat_AsDouble() and return 0.0 only if this call is successful.
1 parent ac10947 commit 0f2fa61

File tree

4 files changed

+73
-10
lines changed

4 files changed

+73
-10
lines changed

Doc/c-api/complex.rst

+18
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,29 @@ Complex Numbers as Python Objects
117117
118118
Return the real part of *op* as a C :c:expr:`double`.
119119
120+
If *op* is not a Python complex number object but has a
121+
:meth:`~object.__complex__` method, this method will first be called to
122+
convert *op* to a Python complex number object. If :meth:`!__complex__` is
123+
not defined then it falls back to call :c:func:`PyFloat_AsDouble` and
124+
returns its result. Upon failure, this method returns ``-1.0``, so one
125+
should call :c:func:`PyErr_Occurred` to check for errors.
126+
127+
.. versionchanged:: 3.13
128+
Use :meth:`~object.__complex__` if available.
120129
121130
.. c:function:: double PyComplex_ImagAsDouble(PyObject *op)
122131
123132
Return the imaginary part of *op* as a C :c:expr:`double`.
124133
134+
If *op* is not a Python complex number object but has a
135+
:meth:`~object.__complex__` method, this method will first be called to
136+
convert *op* to a Python complex number object. If :meth:`!__complex__` is
137+
not defined then it falls back to call :c:func:`PyFloat_AsDouble` and
138+
returns ``0.0`` on success. Upon failure, this method returns ``-1.0``, so
139+
one should call :c:func:`PyErr_Occurred` to check for errors.
140+
141+
.. versionchanged:: 3.13
142+
Use :meth:`~object.__complex__` if available.
125143
126144
.. c:function:: Py_complex PyComplex_AsCComplex(PyObject *op)
127145

Lib/test/test_capi/test_complex.py

+23-6
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,14 @@ def test_realasdouble(self):
7777
self.assertEqual(realasdouble(FloatSubclass(4.25)), 4.25)
7878

7979
# Test types with __complex__ dunder method
80-
# Function doesn't support classes with __complex__ dunder, see #109598
81-
self.assertRaises(TypeError, realasdouble, Complex())
80+
self.assertEqual(realasdouble(Complex()), 4.25)
81+
self.assertRaises(TypeError, realasdouble, BadComplex())
82+
with self.assertWarns(DeprecationWarning):
83+
self.assertEqual(realasdouble(BadComplex2()), 4.25)
84+
with warnings.catch_warnings():
85+
warnings.simplefilter("error", DeprecationWarning)
86+
self.assertRaises(DeprecationWarning, realasdouble, BadComplex2())
87+
self.assertRaises(RuntimeError, realasdouble, BadComplex3())
8288

8389
# Test types with __float__ dunder method
8490
self.assertEqual(realasdouble(Float()), 4.25)
@@ -104,11 +110,22 @@ def test_imagasdouble(self):
104110
self.assertEqual(imagasdouble(FloatSubclass(4.25)), 0.0)
105111

106112
# Test types with __complex__ dunder method
107-
# Function doesn't support classes with __complex__ dunder, see #109598
108-
self.assertEqual(imagasdouble(Complex()), 0.0)
113+
self.assertEqual(imagasdouble(Complex()), 0.5)
114+
self.assertRaises(TypeError, imagasdouble, BadComplex())
115+
with self.assertWarns(DeprecationWarning):
116+
self.assertEqual(imagasdouble(BadComplex2()), 0.5)
117+
with warnings.catch_warnings():
118+
warnings.simplefilter("error", DeprecationWarning)
119+
self.assertRaises(DeprecationWarning, imagasdouble, BadComplex2())
120+
self.assertRaises(RuntimeError, imagasdouble, BadComplex3())
121+
122+
# Test types with __float__ dunder method
123+
self.assertEqual(imagasdouble(Float()), 0.0)
124+
self.assertRaises(TypeError, imagasdouble, BadFloat())
125+
with self.assertWarns(DeprecationWarning):
126+
self.assertEqual(imagasdouble(BadFloat2()), 0.0)
109127

110-
# Function returns 0.0 anyway, see #109598
111-
self.assertEqual(imagasdouble(object()), 0.0)
128+
self.assertRaises(TypeError, imagasdouble, object())
112129

113130
# CRASHES imagasdouble(NULL)
114131

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:c:func:`PyComplex_RealAsDouble`/:c:func:`PyComplex_ImagAsDouble` now tries to
2+
convert an object to a :class:`complex` instance using its ``__complex__()`` method
3+
before falling back to the ``__float__()`` method. Patch by Sergey B Kirpichev.

Objects/complexobject.c

+29-4
Original file line numberDiff line numberDiff line change
@@ -256,26 +256,51 @@ PyComplex_FromDoubles(double real, double imag)
256256
return PyComplex_FromCComplex(c);
257257
}
258258

259+
static PyObject * try_complex_special_method(PyObject *);
260+
259261
double
260262
PyComplex_RealAsDouble(PyObject *op)
261263
{
264+
double real = -1.0;
265+
262266
if (PyComplex_Check(op)) {
263-
return ((PyComplexObject *)op)->cval.real;
267+
real = ((PyComplexObject *)op)->cval.real;
264268
}
265269
else {
266-
return PyFloat_AsDouble(op);
270+
PyObject* newop = try_complex_special_method(op);
271+
if (newop) {
272+
real = ((PyComplexObject *)newop)->cval.real;
273+
Py_DECREF(newop);
274+
} else if (!PyErr_Occurred()) {
275+
real = PyFloat_AsDouble(op);
276+
}
267277
}
278+
279+
return real;
268280
}
269281

270282
double
271283
PyComplex_ImagAsDouble(PyObject *op)
272284
{
285+
double imag = -1.0;
286+
273287
if (PyComplex_Check(op)) {
274-
return ((PyComplexObject *)op)->cval.imag;
288+
imag = ((PyComplexObject *)op)->cval.imag;
275289
}
276290
else {
277-
return 0.0;
291+
PyObject* newop = try_complex_special_method(op);
292+
if (newop) {
293+
imag = ((PyComplexObject *)newop)->cval.imag;
294+
Py_DECREF(newop);
295+
} else if (!PyErr_Occurred()) {
296+
PyFloat_AsDouble(op);
297+
if (!PyErr_Occurred()) {
298+
imag = 0.0;
299+
}
300+
}
278301
}
302+
303+
return imag;
279304
}
280305

281306
static PyObject *

0 commit comments

Comments
 (0)