Skip to content

Inconsistent custom __complex__ conversions with abi3 #3164

@jakelishman

Description

@jakelishman

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 TypeErrors 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:

#[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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions