Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Development towards 0.2.0 #2

Merged
merged 6 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ license = "MIT OR Apache-2.0"
readme = "README.md"
repository = "https://github.com/AndrejOrsula/pyo3_bindgen"
rust-version = "1.70"
version = "0.1.1"
version = "0.2.0"

[workspace.dependencies]
pyo3_bindgen = { path = "pyo3_bindgen", version = "0.1.1" }
pyo3_bindgen_engine = { path = "pyo3_bindgen_engine", version = "0.1.1" }
pyo3_bindgen_macros = { path = "pyo3_bindgen_macros", version = "0.1.1" }
pyo3_bindgen = { path = "pyo3_bindgen", version = "0.2.0" }
pyo3_bindgen_engine = { path = "pyo3_bindgen_engine", version = "0.2.0" }
pyo3_bindgen_macros = { path = "pyo3_bindgen_macros", version = "0.2.0" }

assert_cmd = { version = "2" }
clap = { version = "4.4", features = ["derive"] }
Expand Down
29 changes: 8 additions & 21 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,26 +148,20 @@ 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

This project is in early development, and as such, the API of the generated bindings is not yet stable.

- Not all Python types are mapped to their Rust equivalents yet. Especially the support for mapping types of module-wide classes for which bindings are generated is also still missing. For this reason, a lot of boilerplate might be currently required when using the generated bindings (e.g. `let typed_value: target_module::Class = any_value.extract()?;`).
- The binding generation is primarily designed to be used inside build scripts or via procedural macros. Therefore, the performance of the codegen process is [benchmarked](./pyo3_bindgen_engine/benches/bindgen.rs) to understand the potential impact on build times. Surprisingly, even the initial unoptimized version of the engine is able to process the entire `numpy` 1.26.3 in ~300 ms on a *modern* laptop while generating 166k lines of formatted Rust code (line count includes documentation). Adding more features might increase this time, but there is also plenty of room for optimization in the current naive implementation.
- Not all Python types are mapped to their Rust equivalents yet. For this reason, some additional typecasting might be currently required when using the generated bindings (e.g. `let typed_value: target_module::Class = any_value.extract()?;`).
- The binding generation is primarily designed to be used inside build scripts or via procedural macros. Therefore, the performance of the codegen process is [benchmarked](./pyo3_bindgen_engine/benches/bindgen.rs) to understand the potential impact on build times. Although there is currently plenty of room for optimization in the current naive implementation, even the largest modules are processed in less than a second on a *modern* laptop.
- The generation of bindings should never panic as long as the target Python module can be successfully imported. If it does, it is a bug resulting from an unexpected edge-case Python module structure or an unforeseen combination of enabled PyO3 features.
- However, the generated bindings might not directly compile in some specific cases. Currently, there are two known issue; bindings will contain duplicate function definitions if present in the original code, and function parameters might use the same name as a class defined in the same scope (allowed in Python but not in Rust). If you encounter any other issues, consider manually rewriting the problematic parts of the bindings.
- Although implemented, the procedural macros might not work in all cases - especially when some PyO3 features are enabled. In most cases, PyO3 fails to import the target Python module when used from within a `proc_macro` crate. Therefore, it is recommended to use build scripts instead for now.
- The code will be refactored and cleaned up in the upcoming releases. The current implementation is a result of a very quick prototype that was built to test the feasibility of the idea.
- The code will be refactored and cleaned up in the upcoming releases. The current implementation is a result of a very quick prototype that was built to test the feasibility of the idea. For example, configurability of the generated bindings is planned (e.g. allowlist/ignorelist of attributes). Furthermore, automatic generation of dependent Python modules will be considered in order to provide a more complete typing experience.

Please [report](https://github.com/AndrejOrsula/pyo3_bindgen/issues/new) any issues that you might encounter. Contributions are more than welcome! If you are looking for a place to start, consider searching for `TODO` comments in the codebase.

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
25 changes: 24 additions & 1 deletion pyo3_bindgen_engine/src/bindgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ pub use class::bind_class;
pub use function::bind_function;
pub use module::{bind_module, bind_reexport};

// TODO: Refactor everything into a large configurable struct that keeps track of all the
// important information needed to properly generate the bindings
// - Use builder pattern for the configuration of the struct
// - Keep track of all the types/classes that have been generated
// - Keep track of all imports to understand where each type is coming from
// - Keep track of all the external types that are used as parameters/return types and consider generating bindings for them as well

// TODO: Ensure there are no duplicate entries in the generated code

/// 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 +84,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
30 changes: 15 additions & 15 deletions pyo3_bindgen_engine/src/bindgen/attribute.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
use crate::types::map_attr_type;
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 + Default>(
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 = map_attr_type(getter_type, true)?;
let setter_type = map_attr_type(setter_type, false)?;
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
Loading