-
Notifications
You must be signed in to change notification settings - Fork 868
Description
Bug Description
Classes that are not complex
but define __complex__
convert to num-complex
types successfully when the full C API is in use, but fail with varying TypeError
s when the limited API is in use.
Steps to Reproduce
This is a MWE PyO3 module (strictly the return value of noop
is irrelevant, but I left it here for clarity):
use pyo3::prelude::*;
use num_complex::Complex64;
#[pyfunction]
fn noop(a: Complex64) -> Complex64 {
a
}
#[pymodule]
fn core(_py: pyo3::Python<'_>, module: &PyModule) -> PyResult<()> {
module.add_function(wrap_pyfunction!(noop, module)?)?;
Ok(())
}
In Python space, if I compile against the full API, classes that define __complex__
will successfully convert:
from pyo3_test import noop
class A:
def __complex__(self):
return 1j
noop(A()) # returns 1j
If I rebuild the extension in limited-API mode, I instead get a type error:
TypeError Traceback (most recent call last)
Cell In[1], line 7
4 def __complex__(self):
5 return 1j
----> 7 noop(A())
TypeError: argument 'a': must be real number, not A
Backtrace
No response
Your operating system and version
macOS 13.3.1
Your Python version (python --version
)
Python 3.10.6
Your Rust version (rustc --version
)
rustc 1.67.1 (d5a82bbd2 2023-02-07)
Your PyO3 version
0.18.3
How did you install python? Did you use a virtualenv?
From source, in a venv.
Additional Info
The relevant conversions are here:
pyo3/src/conversions/num_complex.rs
Lines 139 to 166 in 3ec966d
#[cfg_attr(docsrs, doc(cfg(feature = "num-complex")))] | |
impl<'source> FromPyObject<'source> for Complex<$float> { | |
fn extract(obj: &'source PyAny) -> PyResult<Complex<$float>> { | |
#[cfg(not(any(Py_LIMITED_API, PyPy)))] | |
unsafe { | |
let val = ffi::PyComplex_AsCComplex(obj.as_ptr()); | |
if val.real == -1.0 { | |
if let Some(err) = PyErr::take(obj.py()) { | |
return Err(err); | |
} | |
} | |
Ok(Complex::new(val.real as $float, val.imag as $float)) | |
} | |
#[cfg(any(Py_LIMITED_API, PyPy))] | |
unsafe { | |
let ptr = obj.as_ptr(); | |
let real = ffi::PyComplex_RealAsDouble(ptr); | |
if real == -1.0 { | |
if let Some(err) = PyErr::take(obj.py()) { | |
return Err(err); | |
} | |
} | |
let imag = ffi::PyComplex_ImagAsDouble(ptr); | |
Ok(Complex::new(real as $float, imag as $float)) | |
} | |
} | |
} |
PyComplex_AsCComplex
uses the full __complex__
conversion machinery in CPython, but PyComplex_RealAsDouble
only tests if the object is already of type complex
, and if not, assumes it's real and uses the __float__
conversion machinery, hence the "must be a real number" errors above.
Could we modify the abi3 conversion to do the full __complex__
conversion before the rest of its logic if PyComplex_Check
is false? I'd be happy to make a PR.