pyo3 icon indicating copy to clipboard operation
pyo3 copied to clipboard

Unable to implement FromPyObject for #[pyclass] with Clone

Open datapythonista opened this issue 1 year ago • 4 comments

Bug Description

Seems intentional, but a #[pyclass] that implements Clone automatically seems to implement FromPyObject, which then can not be implemented again for my class.

I have a struct (Foo in the example) that I want to use as a #[pyfunction] argument and be able to receive many different Python types that will be converted in different ways to an instance of Foo. In my real case this is an expression in DataFusion which can be many custom classes or Python types. Things seem to work as expected, being able to implement the casting from the different types in FromPyObject. Except that my class needs to implement Clone, and then I don't seem to have a way to define how objects are converted to Foo.

Steps to Reproduce

use pyo3::prelude::*;      
      
#[derive(Debug, Clone)]  // Removing Clone here, everything works as expected
#[pyclass]      
struct Foo {    
    pub value: i64,      
}       
      
// If I remove this it compiles, but calling in Python: `>>> recive_from_int(42)`
// causes this error: `TypeError: argument 'foo': 'int' object cannot be converted to 'Foo'`
impl<'py> FromPyObject<'py> for Foo {    
    fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult<Self> {    
        let value: i64 = obj.extract()?;    
        Ok(Foo { value })    
    }       
}       
      
#[pyfunction]      
fn receive_foo_from_int(foo: Foo) -> PyResult<i64> {    
    Ok(foo.value)                                                              
}      
      
#[pymodule]      
fn auto_cast(m: &Bound<'_, PyModule>) -> PyResult<()> {      
    m.add_function(wrap_pyfunction!(receive_foo_from_int, m)?)?;    
    Ok(())      
}

Backtrace

$ maturin develop
🔗 Found pyo3 bindings
🐍 Found CPython 3.12 at /home/mgarcia/.miniforge3/envs/data/bin/python
📡 Using build options features from pyproject.toml
   Compiling auto_cast v0.1.0 (/home/mgarcia/src/auto_cast)
error[E0119]: conflicting implementations of trait `pyo3::FromPyObject<'_>` for type `Foo`
 --> src/lib.rs:9:1
  |
9 | impl<'py> FromPyObject<'py> for Foo {
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: conflicting implementation in crate `pyo3`:
          - impl<T> pyo3::FromPyObject<'_> for T
            where T: PyClass, T: Clone;

For more information about this error, try `rustc --explain E0119`.
error: could not compile `auto_cast` (lib) due to 1 previous error
💥 maturin failed
  Caused by: Failed to build a native library through cargo
  Caused by: Cargo build finished with "exit status: 101": `env -u CARGO PYO3_ENVIRONMENT_SIGNATURE="cpython-3.12-64bit" PYO3_PYTHON="/home/mgarcia/.miniforge3/envs/data/bin/python" PYTHON_SYS_EXECUTABLE="/home/mgarcia/.miniforge3/envs/data/bin/python" "cargo" "rustc" "--features" "pyo3/extension-module" "--message-format" "json-render-diagnostics" "--manifest-path" "/home/mgarcia/src/auto_cast/Cargo.toml" "--lib"`


### Your operating system and version

Linux 6.9.5-arch1-1

### Your Python version (`python --version`)

Python 3.12.2

### Your Rust version (`rustc --version`)

rustc 1.81.0-nightly (bcf94dec5 2024-06-23)

### Your PyO3 version

0.21.1

### How did you install python? Did you use a virtualenv?

mamba / conda-forge

### Additional Info

_No response_

datapythonista avatar Jul 11 '24 12:07 datapythonista

Did you ever find a solution to this problem?

marcpabst avatar Sep 01 '24 18:09 marcpabst

Did you ever find a solution to this problem?

Unfortunately I couldn't find any workaround.

datapythonista avatar Sep 01 '24 22:09 datapythonista

I ended up creating an IntoX(X) wrapper and implementing FromPyObject and Into<X> for it. Seems to work well and feels quite elegant.

marcpabst avatar Sep 01 '24 22:09 marcpabst

The root cause here is we have a generic blanket impl FromPyObject for T: PyClass + Clone.

I think that blanket has caused unwelcome problems a few times now and creates subtle bugs, so I'm thinking we should consider removing. Just need to time it well around other breakages.

davidhewitt avatar Sep 03 '24 06:09 davidhewitt

A slight variation that lead me to this issue:

#[derive(Clone, Debug)
#[cfg_attr(feature = "pyo3", derive(IntoPyObject, FromPyObject)]
pub struct Config {
    pub client_id: String,
    #[cfg_attr(feature = "pyo3", pyo3(from_py_with = crate::extract_url, into_py_with = crate::convert_url_into_py))]
    pub url: Url,
}

That is working nicely, but I want to attach some pymethods, so I need to turn my Config struct into a pyclass, and get the error that FromPyObject is defined multiple times. But if I remove it, I now get the error that from_py_with is not allowed without deriving FromPyObject.

Is there a way to make that work in the meantime ?

vsiles avatar Sep 10 '25 06:09 vsiles

We are introducing #[pyclass(skip_from_py_object)] in PyO3 0.27 (releasing very soon) to remove the built-in FromPyObject implementation. For further updates follow #5419

davidhewitt avatar Oct 15 '25 20:10 davidhewitt