Skip to content

Commit

Permalink
pythongh-109598: make PyComplex_RealAsDouble/ImagAsDouble use __compl…
Browse files Browse the repository at this point in the history
…ex__ (pythonGH-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.
  • Loading branch information
skirpichev authored and Glyphack committed Jan 27, 2024
1 parent 80f89ad commit edd44dc
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 10 deletions.
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

0 comments on commit edd44dc

Please sign in to comment.