summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSergey B Kirpichev <skirpichev@gmail.com>2024-01-15 15:04:17 (GMT)
committerGitHub <noreply@github.com>2024-01-15 15:04:17 (GMT)
commit0f2fa6150baf111a6c69d5d491c95c3c2ee60eaf (patch)
treee60136c4ac8da4e44cd3bdf4a702372be34a96d1
parentac10947ba79a15bfdaa3ca92c6864214648ab364 (diff)
downloadcpython-0f2fa6150baf111a6c69d5d491c95c3c2ee60eaf.zip
cpython-0f2fa6150baf111a6c69d5d491c95c3c2ee60eaf.tar.gz
cpython-0f2fa6150baf111a6c69d5d491c95c3c2ee60eaf.tar.bz2
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.
-rw-r--r--Doc/c-api/complex.rst18
-rw-r--r--Lib/test/test_capi/test_complex.py29
-rw-r--r--Misc/NEWS.d/next/Core and Builtins/2023-09-21-11-54-28.gh-issue-109598.CRidSy.rst3
-rw-r--r--Objects/complexobject.c33
4 files changed, 73 insertions, 10 deletions
diff --git a/Doc/c-api/complex.rst b/Doc/c-api/complex.rst
index e3fd001..5a04748 100644
--- a/Doc/c-api/complex.rst
+++ b/Doc/c-api/complex.rst
@@ -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)
diff --git a/Lib/test/test_capi/test_complex.py b/Lib/test/test_capi/test_complex.py
index d6fc1f0..a5b5955 100644
--- a/Lib/test/test_capi/test_complex.py
+++ b/Lib/test/test_capi/test_complex.py
@@ -77,8 +77,14 @@ class CAPIComplexTest(unittest.TestCase):
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)
@@ -104,11 +110,22 @@ class CAPIComplexTest(unittest.TestCase):
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)
diff --git a/Misc/NEWS.d/next/Core and Builtins/2023-09-21-11-54-28.gh-issue-109598.CRidSy.rst b/Misc/NEWS.d/next/Core and Builtins/2023-09-21-11-54-28.gh-issue-109598.CRidSy.rst
new file mode 100644
index 0000000..3eedc45
--- /dev/null
+++ b/Misc/NEWS.d/next/Core and Builtins/2023-09-21-11-54-28.gh-issue-109598.CRidSy.rst
@@ -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.
diff --git a/Objects/complexobject.c b/Objects/complexobject.c
index 0e96f54..d8b0e84 100644
--- a/Objects/complexobject.c
+++ b/Objects/complexobject.c
@@ -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 *