Skip to content

Commit

Permalink
Add typy mapping for generated class bindings
Browse files Browse the repository at this point in the history
Signed-off-by: Andrej Orsula <orsula.andrej@gmail.com>
  • Loading branch information
AndrejOrsula committed Jan 22, 2024
1 parent ce75fc6 commit a096f39
Show file tree
Hide file tree
Showing 11 changed files with 920 additions and 387 deletions.
22 changes: 4 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,15 +115,8 @@ fn main() {
Afterwards, include the generated bindings anywhere in your crate.

```rs
#[allow(
clippy::all,
non_camel_case_types,
non_snake_case,
non_upper_case_globals
)]
pub mod target_module {
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
}
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
pub use target_module::*;
```

### <a href="#-option-2-cli-tool"><img src="https://www.svgrepo.com/show/353478/bash-icon.svg" width="16" height="16"></a> Option 2: CLI tool
Expand Down Expand Up @@ -155,15 +148,8 @@ pyo3_bindgen = { version = "0.1", features = ["macros"] }
Then, you can call the `import_python!` macro anywhere in your crate.

```rs
#[allow(
clippy::all,
non_camel_case_types,
non_snake_case,
non_upper_case_globals
)]
pub mod target_module {
pyo3_bindgen::import_python!("target_module");
}
pyo3_bindgen::import_python!("target_module");
pub use target_module::*;
```

## Status
Expand Down
22 changes: 4 additions & 18 deletions pyo3_bindgen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,8 @@
//! Afterwards, include the generated bindings anywhere in your crate.
//!
//! ```rs
//! #[allow(
//! clippy::all,
//! non_camel_case_types,
//! non_snake_case,
//! non_upper_case_globals
//! )]
//! pub mod target_module {
//! include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
//! }
//! include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
//! pub use target_module::*;
//! ```
//!
//! ### <a href="#-option-2-cli-tool"><img src="https://www.svgrepo.com/show/353478/bash-icon.svg" width="16" height="16"></a> Option 2: CLI tool
Expand Down Expand Up @@ -72,15 +65,8 @@
//! Then, you can call the `import_python!` macro anywhere in your crate.
//!
//! ```rs
//! #[allow(
//! clippy::all,
//! non_camel_case_types,
//! non_snake_case,
//! non_upper_case_globals
//! )]
//! pub mod target_module {
//! pyo3_bindgen::import_python!("target_module");
//! }
//! pyo3_bindgen::import_python!("target_module");
//! pub use target_module::*;
//! ```
pub use pyo3_bindgen_engine::{
Expand Down
20 changes: 19 additions & 1 deletion pyo3_bindgen_engine/src/bindgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ pub use class::bind_class;
pub use function::bind_function;
pub use module::{bind_module, bind_reexport};

// TODO: Ensure there are no duplicate entries in the generated code
// TODO: Refactor everything into a large configurable struct that keeps track of all the
// important information needed to properly generate the bindings

/// Generate Rust bindings to a Python module specified by its name. Generating bindings to
/// submodules such as `os.path` is also supported as long as the module can be directly imported
/// from the Python interpreter via `import os.path`.
Expand Down Expand Up @@ -75,7 +79,21 @@ pub fn generate_bindings_for_module(
py: pyo3::Python,
module: &pyo3::types::PyModule,
) -> Result<proc_macro2::TokenStream, pyo3::PyErr> {
bind_module(py, module, module, &mut std::collections::HashSet::new())
let all_types = module::collect_types_of_module(
py,
module,
module,
&mut std::collections::HashSet::new(),
&mut std::collections::HashSet::default(),
)?;

bind_module(
py,
module,
module,
&mut std::collections::HashSet::new(),
&all_types,
)
}

/// Generate Rust bindings to a Python module specified by its `source_code`. The module will be
Expand Down
28 changes: 14 additions & 14 deletions pyo3_bindgen_engine/src/bindgen/attribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ use crate::types::Type;

/// Generate Rust bindings to a Python attribute. The attribute can be a standalone
/// attribute or a property of a class.
pub fn bind_attribute(
pub fn bind_attribute<S: ::std::hash::BuildHasher>(
py: pyo3::Python,
module_name: Option<&str>,
module_name: &str,
is_class: bool,
name: &str,
attr: &pyo3::PyAny,
attr_type: &pyo3::PyAny,
all_types: &std::collections::HashSet<String, S>,
) -> Result<proc_macro2::TokenStream, pyo3::PyErr> {
let mut token_stream = proc_macro2::TokenStream::new();

Expand Down Expand Up @@ -72,29 +74,29 @@ pub fn bind_attribute(
};
let setter_ident = quote::format_ident!("set_{}", name);

let getter_type = Type::try_from(getter_type)?.into_rs_owned();
let setter_type = Type::try_from(setter_type)?.into_rs_borrowed();
let getter_type = Type::try_from(getter_type)?.into_rs_owned(module_name, all_types);
let setter_type = Type::try_from(setter_type)?.into_rs_borrowed(module_name, all_types);

if let Some(module_name) = module_name {
if is_class {
token_stream.extend(quote::quote! {
#[doc = #getter_doc]
pub fn #getter_ident<'py>(
&'py self,
py: ::pyo3::marker::Python<'py>,
) -> ::pyo3::PyResult<#getter_type> {
py.import(::pyo3::intern!(py, #module_name))?
.getattr(::pyo3::intern!(py, #name))?
self.getattr(::pyo3::intern!(py, #name))?
.extract()
}
});
if has_setter {
token_stream.extend(quote::quote! {
#[doc = #setter_doc]
pub fn #setter_ident<'py>(
&'py self,
py: ::pyo3::marker::Python<'py>,
value: #setter_type,
) -> ::pyo3::PyResult<()> {
py.import(::pyo3::intern!(py, #module_name))?
.setattr(::pyo3::intern!(py, #name), value)?;
self.setattr(::pyo3::intern!(py, #name), value)?;
Ok(())
}
});
Expand All @@ -103,10 +105,9 @@ pub fn bind_attribute(
token_stream.extend(quote::quote! {
#[doc = #getter_doc]
pub fn #getter_ident<'py>(
&'py self,
py: ::pyo3::marker::Python<'py>,
) -> ::pyo3::PyResult<#getter_type> {
self.as_ref(py)
py.import(::pyo3::intern!(py, #module_name))?
.getattr(::pyo3::intern!(py, #name))?
.extract()
}
Expand All @@ -115,12 +116,11 @@ pub fn bind_attribute(
token_stream.extend(quote::quote! {
#[doc = #setter_doc]
pub fn #setter_ident<'py>(
&'py mut self,
py: ::pyo3::marker::Python<'py>,
value: #setter_type,
) -> ::pyo3::PyResult<()> {
self.as_ref(py)
.setattr(::pyo3::intern!(py, #name), value)?;
py.import(::pyo3::intern!(py, #module_name))?
.setattr(::pyo3::intern!(py, #name), value)?;
Ok(())
}
});
Expand Down
155 changes: 104 additions & 51 deletions pyo3_bindgen_engine/src/bindgen/class.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
use crate::bindgen::{bind_attribute, bind_function};

// TODO: Look into pyo3::pyobject_native_type_named pyo3::pyobject_native_type_extract macros
// Just look into how one of the native Pyo3 types is implemented and copy that

/// Generate Rust bindings to a Python class with all its methods and attributes (properties).
/// This function will call itself recursively to generate bindings to all nested classes.
pub fn bind_class(
pub fn bind_class<S: ::std::hash::BuildHasher>(
py: pyo3::Python,
root_module: &pyo3::types::PyModule,
class: &pyo3::types::PyType,
all_types: &std::collections::HashSet<String, S>,
) -> Result<proc_macro2::TokenStream, pyo3::PyErr> {
let inspect = py.import("inspect")?;

// Extract the names of the modules
let root_module_name = root_module.name()?;
let full_class_name = class.name()?;
let class_name: &str = full_class_name.split('.').last().unwrap();
let class_full_name = class.name()?;
let class_name = class_full_name.split('.').last().unwrap();
let class_module_name = format!(
"{}{}{}",
class.getattr("__module__")?,
if class_full_name.contains('.') {
"."

Check warning on line 21 in pyo3_bindgen_engine/src/bindgen/class.rs

View check run for this annotation

Codecov / codecov/patch

pyo3_bindgen_engine/src/bindgen/class.rs#L21

Added line #L21 was not covered by tests
} else {
""
},
class_full_name.trim_end_matches(&format!(".{class_name}"))
);

// Create the Rust class identifier (raw string if it is a keyword)
let class_ident = if syn::parse_str::<syn::Ident>(class_name).is_ok() {
Expand Down Expand Up @@ -107,7 +115,7 @@ pub fn bind_class(
.getattr("__module__")
.unwrap_or(pyo3::types::PyString::new(py, ""))
.to_string()
.ne(full_class_name);
.ne(&class_module_name);

let is_class = attr_type
.is_subclass_of::<pyo3::types::PyType>()
Expand All @@ -128,21 +136,52 @@ pub fn bind_class(
debug_assert!(![is_class, is_function].iter().all(|&v| v));

if is_class && !is_reexport {
impl_token_stream.extend(bind_class(py, root_module, attr.downcast().unwrap()));
impl_token_stream.extend(bind_class(
py,
root_module,
attr.downcast().unwrap(),
all_types,
));

Check warning on line 144 in pyo3_bindgen_engine/src/bindgen/class.rs

View check run for this annotation

Codecov / codecov/patch

pyo3_bindgen_engine/src/bindgen/class.rs#L139-L144

Added lines #L139 - L144 were not covered by tests
} else if is_function {
fn_names.push(name.to_string());
impl_token_stream.extend(bind_function(py, "", name, attr));
impl_token_stream.extend(bind_function(
py,
&class_module_name,
name,
attr,
all_types,
));
} else if !name.starts_with('_') {
impl_token_stream.extend(bind_attribute(py, None, name, attr, attr_type));
impl_token_stream.extend(bind_attribute(
py,
&class_module_name,
true,
name,
attr,
attr_type,
all_types,
));
}
});

// Add new and call aliases (currently a reimplemented versions of the function)
if fn_names.contains(&"__init__".to_string()) && !fn_names.contains(&"new".to_string()) {
impl_token_stream.extend(bind_function(py, "", "new", class.getattr("__init__")?));
impl_token_stream.extend(bind_function(
py,
&class_module_name,
"new",
class.getattr("__init__")?,
all_types,
));
}
if fn_names.contains(&"__call__".to_string()) && !fn_names.contains(&"call".to_string()) {
impl_token_stream.extend(bind_function(py, "", "call", class.getattr("__call__")?));
impl_token_stream.extend(bind_function(
py,
&class_module_name,
"call",
class.getattr("__call__")?,
all_types,

Check warning on line 183 in pyo3_bindgen_engine/src/bindgen/class.rs

View check run for this annotation

Codecov / codecov/patch

pyo3_bindgen_engine/src/bindgen/class.rs#L178-L183

Added lines #L178 - L183 were not covered by tests
));
}

let mut doc = class.getattr("__doc__")?.to_string();
Expand All @@ -153,48 +192,62 @@ pub fn bind_class(
Ok(quote::quote! {
#[doc = #doc]
#[repr(transparent)]
#[derive(Clone, Debug)]
pub struct #class_ident(pub ::pyo3::PyObject);
#[automatically_derived]
impl ::std::ops::Deref for #class_ident {
type Target = ::pyo3::PyObject;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[automatically_derived]
impl ::std::ops::DerefMut for #class_ident {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[automatically_derived]
impl<'py> ::pyo3::FromPyObject<'py> for #class_ident {
fn extract(value: &'py ::pyo3::PyAny) -> ::pyo3::PyResult<Self> {
Ok(Self(value.into()))
}
}
#[automatically_derived]
impl ::pyo3::ToPyObject for #class_ident {
fn to_object<'py>(&'py self, py: ::pyo3::Python<'py>) -> ::pyo3::PyObject {
self.as_ref(py).to_object(py)
}
}
#[automatically_derived]
impl From<::pyo3::PyObject> for #class_ident {
fn from(value: ::pyo3::PyObject) -> Self {
Self(value)
}
}
#[automatically_derived]
impl<'py> From<&'py ::pyo3::PyAny> for #class_ident {
fn from(value: &'py ::pyo3::PyAny) -> Self {
Self(value.into())
}
}
pub struct #class_ident(::pyo3::PyAny);
// Note: Using these macros is probably not the best idea, but it makes possible wrapping around ::pyo3::PyAny instead of ::pyo3::PyObject, which improves usability
::pyo3::pyobject_native_type_named!(#class_ident);
::pyo3::pyobject_native_type_info!(#class_ident, ::pyo3::pyobject_native_static_type_object!(::pyo3::ffi::PyBaseObject_Type), ::std::option::Option::Some(#class_module_name));
::pyo3::pyobject_native_type_extract!(#class_ident);
#[automatically_derived]
impl #class_ident {
#impl_token_stream
}
})

// Ok(quote::quote! {
// #[doc = #doc]
// #[repr(transparent)]
// #[derive(Clone, Debug)]
// pub struct #class_ident(pub ::pyo3::PyObject);
// #[automatically_derived]
// impl ::std::ops::Deref for #class_ident {
// type Target = ::pyo3::PyObject;
// fn deref(&self) -> &Self::Target {
// &self.0
// }
// }
// #[automatically_derived]
// impl ::std::ops::DerefMut for #class_ident {
// fn deref_mut(&mut self) -> &mut Self::Target {
// &mut self.0
// }
// }
// #[automatically_derived]
// impl<'py> ::pyo3::FromPyObject<'py> for #class_ident {
// fn extract(value: &'py ::pyo3::PyAny) -> ::pyo3::PyResult<Self> {
// Ok(Self(value.into()))
// }
// }
// #[automatically_derived]
// impl ::pyo3::ToPyObject for #class_ident {
// fn to_object<'py>(&'py self, py: ::pyo3::Python<'py>) -> ::pyo3::PyObject {
// self.as_ref(py).to_object(py)
// }
// }
// #[automatically_derived]
// impl From<::pyo3::PyObject> for #class_ident {
// fn from(value: ::pyo3::PyObject) -> Self {
// Self(value)
// }
// }
// #[automatically_derived]
// impl<'py> From<&'py ::pyo3::PyAny> for #class_ident {
// fn from(value: &'py ::pyo3::PyAny) -> Self {
// Self(value.into())
// }
// }
// #[automatically_derived]
// impl #class_ident {
// #impl_token_stream
// }
// })
}
Loading

0 comments on commit a096f39

Please sign in to comment.