Skip to content

Commit 0e07b65

Browse files
committed
gh-109598: make PyComplex_RealAsDouble/ImagAsDouble use __complex__
* 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. * Full test coverage for changed functions.
1 parent 9ccf054 commit 0e07b65

File tree

11 files changed

+182
-5
lines changed

11 files changed

+182
-5
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_complex.py

+33
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
from math import atan2, isnan, copysign
99
import operator
1010

11+
try:
12+
import _testcapi
13+
except ImportError:
14+
_testcapi = None
15+
1116
INF = float("inf")
1217
NAN = float("nan")
1318
# These tests ensure that complex math does the right thing
@@ -791,6 +796,34 @@ def test_format(self):
791796
self.assertEqual(format(complex(INF, 1), 'F'), 'INF+1.000000j')
792797
self.assertEqual(format(complex(INF, -1), 'F'), 'INF-1.000000j')
793798

799+
@unittest.skipIf(_testcapi is None, 'needs _testcapi')
800+
def test_PyComplex_RealAsDouble(self):
801+
from test.test_capi.test_getargs import BadComplex, Complex
802+
803+
f = _testcapi.complex_real_as_double
804+
805+
self.assertFloatsAreIdentical(f(1+2j), 1.0)
806+
self.assertFloatsAreIdentical(f(1), 1.0)
807+
self.assertFloatsAreIdentical(f(-1), -1.0)
808+
self.assertFloatsAreIdentical(f(Complex()), 4.25)
809+
810+
self.assertRaises(TypeError, f, None)
811+
self.assertRaises(TypeError, f, BadComplex())
812+
813+
@unittest.skipIf(_testcapi is None, 'needs _testcapi')
814+
def test_PyComplex_ImagAsDouble(self):
815+
from test.test_capi.test_getargs import BadComplex, Complex
816+
817+
f = _testcapi.complex_imag_as_double
818+
819+
self.assertFloatsAreIdentical(f(1+2j), 2.0)
820+
self.assertFloatsAreIdentical(f(1), 0.0)
821+
self.assertFloatsAreIdentical(f(-1), 0.0)
822+
self.assertFloatsAreIdentical(f(Complex()), 0.5)
823+
824+
self.assertRaises(TypeError, f, None)
825+
self.assertRaises(TypeError, f, BadComplex())
826+
794827

795828
if __name__ == "__main__":
796829
unittest.main()
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.

Modules/Setup.stdlib.in

+1-1
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@
159159
@MODULE__XXTESTFUZZ_TRUE@_xxtestfuzz _xxtestfuzz/_xxtestfuzz.c _xxtestfuzz/fuzzer.c
160160
@MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c
161161
@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c
162-
@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/vectorcall_limited.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/pyos.c _testcapi/immortal.c _testcapi/heaptype_relative.c _testcapi/gc.c
162+
@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/vectorcall_limited.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/pyos.c _testcapi/immortal.c _testcapi/heaptype_relative.c _testcapi/gc.c _testcapi/complex.c
163163
@MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c
164164
@MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c
165165

Modules/_testcapi/clinic/complex.c.h

+22
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/_testcapi/complex.c

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#include "parts.h"
2+
#include "clinic/complex.c.h"
3+
4+
5+
/*[clinic input]
6+
module _testcapi
7+
[clinic start generated code]*/
8+
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=6361033e795369fc]*/
9+
10+
/*[clinic input]
11+
_testcapi.complex_real_as_double
12+
13+
op: object
14+
/
15+
16+
Test PyComplex_RealAsDouble().
17+
[clinic start generated code]*/
18+
19+
static PyObject *
20+
_testcapi_complex_real_as_double(PyObject *module, PyObject *op)
21+
/*[clinic end generated code: output=91427770a4583095 input=7ffd3ffd53495b3d]*/
22+
{
23+
double real = PyComplex_RealAsDouble(op);
24+
25+
if (real == -1. && PyErr_Occurred()) {
26+
return NULL;
27+
}
28+
29+
return PyFloat_FromDouble(real);
30+
}
31+
32+
/*[clinic input]
33+
_testcapi.complex_imag_as_double
34+
35+
op: object
36+
/
37+
38+
Test PyComplex_ImagAsDouble().
39+
[clinic start generated code]*/
40+
41+
static PyObject *
42+
_testcapi_complex_imag_as_double(PyObject *module, PyObject *op)
43+
/*[clinic end generated code: output=f66f10a3d47beec4 input=e2b4b00761e141ea]*/
44+
{
45+
double imag = PyComplex_ImagAsDouble(op);
46+
47+
if (imag == -1. && PyErr_Occurred()) {
48+
return NULL;
49+
}
50+
51+
return PyFloat_FromDouble(imag);
52+
}
53+
54+
static PyMethodDef test_methods[] = {
55+
_TESTCAPI_COMPLEX_REAL_AS_DOUBLE_METHODDEF
56+
_TESTCAPI_COMPLEX_IMAG_AS_DOUBLE_METHODDEF
57+
{NULL},
58+
};
59+
60+
int
61+
_PyTestCapi_Init_Complex(PyObject *mod)
62+
{
63+
if (PyModule_AddFunctions(mod, test_methods) < 0) {
64+
return -1;
65+
}
66+
67+
return 0;
68+
}

Modules/_testcapi/parts.h

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ int _PyTestCapi_Init_PyAtomic(PyObject *module);
2626
int _PyTestCapi_Init_PyOS(PyObject *module);
2727
int _PyTestCapi_Init_Immortal(PyObject *module);
2828
int _PyTestCapi_Init_GC(PyObject *mod);
29+
int _PyTestCapi_Init_Complex(PyObject *mod);
2930

3031
int _PyTestCapi_Init_VectorcallLimited(PyObject *module);
3132
int _PyTestCapi_Init_HeaptypeRelative(PyObject *module);

Modules/_testcapimodule.c

+3
Original file line numberDiff line numberDiff line change
@@ -4024,6 +4024,9 @@ PyInit__testcapi(void)
40244024
if (_PyTestCapi_Init_HeaptypeRelative(m) < 0) {
40254025
return NULL;
40264026
}
4027+
if (_PyTestCapi_Init_Complex(m) < 0) {
4028+
return NULL;
4029+
}
40274030

40284031
PyState_AddModule(m, &_testcapimodule);
40294032
return m;

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 *

PCbuild/_testcapi.vcxproj

+1
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
<ClCompile Include="..\Modules\_testcapi\pyos.c" />
117117
<ClCompile Include="..\Modules\_testcapi\immortal.c" />
118118
<ClCompile Include="..\Modules\_testcapi\gc.c" />
119+
<ClCompile Include="..\Modules\_testcapi\complex.c" />
119120
</ItemGroup>
120121
<ItemGroup>
121122
<ResourceCompile Include="..\PC\python_nt.rc" />

PCbuild/_testcapi.vcxproj.filters

+3
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@
7575
<ClCompile Include="..\Modules\_testcapi\gc.c">
7676
<Filter>Source Files</Filter>
7777
</ClCompile>
78+
<ClCompile Include="..\Modules\_testcapi\complex.c">
79+
<Filter>Source Files</Filter>
80+
</ClCompile>
7881
</ItemGroup>
7982
<ItemGroup>
8083
<ResourceCompile Include="..\PC\python_nt.rc">

0 commit comments

Comments
 (0)