Skip to content

gh-109598: make PyComplex_RealAsDouble/ImagAsDouble use __complex__ #109647

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jan 15, 2024
18 changes: 18 additions & 0 deletions Doc/c-api/complex.rst
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,29 @@ Complex Numbers as Python Objects

Return the real part of *op* as a C :c:expr:`double`.

If *op* is not a Python complex number object but has a
:meth:`~object.__complex__` method, this method will first be called to
convert *op* to a Python complex number object. If :meth:`!__complex__` is
not defined then it falls back to call :c:func:`PyFloat_AsDouble` and
returns its result. Upon failure, this method returns ``-1.0``, so one
should call :c:func:`PyErr_Occurred` to check for errors.

.. versionchanged:: 3.13
Use :meth:`~object.__complex__` if available.

.. c:function:: double PyComplex_ImagAsDouble(PyObject *op)

Return the imaginary part of *op* as a C :c:expr:`double`.

If *op* is not a Python complex number object but has a
:meth:`~object.__complex__` method, this method will first be called to
convert *op* to a Python complex number object. If :meth:`!__complex__` is
not defined then it falls back to call :c:func:`PyFloat_AsDouble` and
returns ``0.0`` on success. Upon failure, this method returns ``-1.0``, so
one should call :c:func:`PyErr_Occurred` to check for errors.

.. versionchanged:: 3.13
Use :meth:`~object.__complex__` if available.

.. c:function:: Py_complex PyComplex_AsCComplex(PyObject *op)

Expand Down
29 changes: 23 additions & 6 deletions Lib/test/test_capi/test_complex.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,14 @@ def test_realasdouble(self):
self.assertEqual(realasdouble(FloatSubclass(4.25)), 4.25)

# Test types with __complex__ dunder method
# Function doesn't support classes with __complex__ dunder, see #109598
self.assertRaises(TypeError, realasdouble, Complex())
self.assertEqual(realasdouble(Complex()), 4.25)
self.assertRaises(TypeError, realasdouble, BadComplex())
with self.assertWarns(DeprecationWarning):
self.assertEqual(realasdouble(BadComplex2()), 4.25)
with warnings.catch_warnings():
warnings.simplefilter("error", DeprecationWarning)
self.assertRaises(DeprecationWarning, realasdouble, BadComplex2())
self.assertRaises(RuntimeError, realasdouble, BadComplex3())

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

# Test types with __complex__ dunder method
# Function doesn't support classes with __complex__ dunder, see #109598
self.assertEqual(imagasdouble(Complex()), 0.0)
self.assertEqual(imagasdouble(Complex()), 0.5)
self.assertRaises(TypeError, imagasdouble, BadComplex())
with self.assertWarns(DeprecationWarning):
self.assertEqual(imagasdouble(BadComplex2()), 0.5)
with warnings.catch_warnings():
warnings.simplefilter("error", DeprecationWarning)
self.assertRaises(DeprecationWarning, imagasdouble, BadComplex2())
self.assertRaises(RuntimeError, imagasdouble, BadComplex3())

# Test types with __float__ dunder method
self.assertEqual(imagasdouble(Float()), 0.0)
self.assertRaises(TypeError, imagasdouble, BadFloat())
with self.assertWarns(DeprecationWarning):
self.assertEqual(imagasdouble(BadFloat2()), 0.0)

# Function returns 0.0 anyway, see #109598
self.assertEqual(imagasdouble(object()), 0.0)
self.assertRaises(TypeError, imagasdouble, object())

# CRASHES imagasdouble(NULL)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:c:func:`PyComplex_RealAsDouble`/:c:func:`PyComplex_ImagAsDouble` now tries to
convert an object to a :class:`complex` instance using its ``__complex__()`` method
before falling back to the ``__float__()`` method. Patch by Sergey B Kirpichev.
33 changes: 29 additions & 4 deletions Objects/complexobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -256,26 +256,51 @@ PyComplex_FromDoubles(double real, double imag)
return PyComplex_FromCComplex(c);
}

static PyObject * try_complex_special_method(PyObject *);

double
PyComplex_RealAsDouble(PyObject *op)
{
double real = -1.0;

if (PyComplex_Check(op)) {
return ((PyComplexObject *)op)->cval.real;
real = ((PyComplexObject *)op)->cval.real;
}
else {
return PyFloat_AsDouble(op);
PyObject* newop = try_complex_special_method(op);
if (newop) {
real = ((PyComplexObject *)newop)->cval.real;
Py_DECREF(newop);
} else if (!PyErr_Occurred()) {
real = PyFloat_AsDouble(op);
}
}

return real;
}

double
PyComplex_ImagAsDouble(PyObject *op)
{
double imag = -1.0;

if (PyComplex_Check(op)) {
return ((PyComplexObject *)op)->cval.imag;
imag = ((PyComplexObject *)op)->cval.imag;
}
else {
return 0.0;
PyObject* newop = try_complex_special_method(op);
if (newop) {
imag = ((PyComplexObject *)newop)->cval.imag;
Py_DECREF(newop);
} else if (!PyErr_Occurred()) {
PyFloat_AsDouble(op);
if (!PyErr_Occurred()) {
imag = 0.0;
}
}
}

return imag;
}

static PyObject *
Expand Down