Skip to content

Commit

Permalink
Generate pyo3_embed_python_source_code() when using `Codegen::modul…
Browse files Browse the repository at this point in the history
…e_from_str()`

Signed-off-by: Andrej Orsula <orsula.andrej@gmail.com>
  • Loading branch information
AndrejOrsula committed May 9, 2024
1 parent 0f83a07 commit 5a4b3ea
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 8 deletions.
2 changes: 1 addition & 1 deletion pyo3_bindgen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
//!
//! > As opposed to using build scripts, this approach does not offer the same level of customization via `pyo3_bindgen::Config`. Furthermore, the procedural macro is quite experimental and might not work in all cases.
//!
//! ```
//! ```ignore
//! use pyo3_bindgen::import_python;
//! import_python!("math");
//!
Expand Down
28 changes: 24 additions & 4 deletions pyo3_bindgen_engine/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::{
};
use itertools::Itertools;
use pyo3::prelude::*;
use rustc_hash::FxHashSet as HashSet;
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};

/// Engine for automatic generation of Rust FFI bindings to Python modules.
///
Expand Down Expand Up @@ -45,6 +45,8 @@ use rustc_hash::FxHashSet as HashSet;
pub struct Codegen {
cfg: Config,
modules: Vec<Module>,
/// Python source code included by [`Self::module_from_str()`] in the generated Rust bindings.
embedded_source_code: HashMap<String, String>,
}

impl Codegen {
Expand Down Expand Up @@ -82,15 +84,26 @@ impl Codegen {
}

/// Add a Python module from its source code and name to the list of modules for which to generate bindings.
pub fn module_from_str(self, source_code: &str, new_module_name: &str) -> Result<Self> {
///
/// # Note
///
/// When including a module in this way, the Python source code must be available also during runtime for
/// the underlying Python interpreter.
///
/// For convenience, you can call `module_name::pyo3_embed_python_source_code()` that is automatically
/// generated in the Rust bindings. This function must be called before attempting to use any functions
/// of classes from the module.
pub fn module_from_str(mut self, source_code: &str, module_name: &str) -> Result<Self> {
self.embedded_source_code
.insert(module_name.to_owned(), source_code.to_owned());
#[cfg(not(PyPy))]
pyo3::prepare_freethreaded_python();
pyo3::Python::with_gil(|py| {
let module = pyo3::types::PyModule::from_code_bound(
py,
source_code,
&format!("{new_module_name}/__init__.py"),
new_module_name,
&format!("{module_name}/__init__.py"),
module_name,
)?;
self.module(&module)
})
Expand Down Expand Up @@ -135,6 +148,13 @@ impl Codegen {
// Canonicalize the module tree
self.canonicalize();

// Embed the source code of the modules
self.modules.iter_mut().for_each(|module| {
if let Some(source_code) = self.embedded_source_code.get(&module.name.to_rs()) {
module.source_code = Some(source_code.clone());
}
});

// Generate the bindings for all modules
self.modules
.iter()
Expand Down
3 changes: 0 additions & 3 deletions pyo3_bindgen_engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,3 @@ use utils::result::Result;
pub use codegen::Codegen;
pub use config::Config;
pub use utils::{error::PyBindgenError, result::PyBindgenResult};

// TODO: Add struct for initialization of bindings from string of Python code https://github.com/AndrejOrsula/pyo3_bindgen/issues/21
// - It could be an extra single function in the source code that brings the self-contained Python code to the bindings
30 changes: 30 additions & 0 deletions pyo3_bindgen_engine/src/syntax/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub struct Module {
pub properties: Vec<Property>,
pub docstring: Option<String>,
pub is_package: bool,
pub source_code: Option<String>,
}

impl Module {
Expand Down Expand Up @@ -46,6 +47,7 @@ impl Module {
properties: Vec::default(),
docstring,
is_package: true,
source_code: None,

Check warning on line 50 in pyo3_bindgen_engine/src/syntax/module.rs

View check run for this annotation

Codecov / codecov/patch

pyo3_bindgen_engine/src/syntax/module.rs#L50

Added line #L50 was not covered by tests
})
}

Expand Down Expand Up @@ -309,6 +311,7 @@ impl Module {
properties,
docstring,
is_package,
source_code: None,
})
}

Expand Down Expand Up @@ -472,10 +475,37 @@ impl Module {
);
}

// Embed the source code if the module was parsed directly from source code
let embed_source_code_fn = if let Some(source_code) = &self.source_code {
let module_name = self.name.to_rs();
let file_name = format!("{module_name}/__init__.py");
quote::quote! {
pub fn pyo3_embed_python_source_code<'py>(py: ::pyo3::marker::Python<'py>) -> ::pyo3::PyResult<()> {
const SOURCE_CODE: &str = #source_code;
pyo3::types::PyAnyMethods::set_item(
&pyo3::types::PyAnyMethods::getattr(
py.import_bound(pyo3::intern!(py, "sys"))?.as_any(),
pyo3::intern!(py, "modules"),
)?,
#module_name,
pyo3::types::PyModule::from_code_bound(
py,
SOURCE_CODE,
#file_name,
#module_name,
)?,
)
}
}
} else {
proc_macro2::TokenStream::new()
};

// Finalize the module with its content
let module_ident: syn::Ident = self.name.name().try_into()?;
output.extend(quote::quote! {
pub mod #module_ident {
#embed_source_code_fn
#module_content
}
});
Expand Down
54 changes: 54 additions & 0 deletions pyo3_bindgen_engine/tests/bindgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,24 @@ test_bindgen! {
unused
)]
pub mod mod_bindgen_property {
pub fn pyo3_embed_python_source_code<'py>(
py: ::pyo3::marker::Python<'py>,
) -> ::pyo3::PyResult<()> {
const SOURCE_CODE: &str = "my_property: float = 0.42\n";
pyo3::types::PyAnyMethods::set_item(
&pyo3::types::PyAnyMethods::getattr(
py.import_bound(pyo3::intern!(py, "sys"))?.as_any(),
pyo3::intern!(py, "modules"),
)?,
"mod_bindgen_property",
pyo3::types::PyModule::from_code_bound(
py,
SOURCE_CODE,
"mod_bindgen_property/__init__.py",
"mod_bindgen_property",
)?,
)
}
pub fn my_property<'py>(py: ::pyo3::marker::Python<'py>) -> ::pyo3::PyResult<f64> {
::pyo3::types::PyAnyMethods::extract(
&::pyo3::types::PyAnyMethods::getattr(
Expand Down Expand Up @@ -93,6 +111,24 @@ test_bindgen! {
unused
)]
pub mod mod_bindgen_function {
pub fn pyo3_embed_python_source_code<'py>(
py: ::pyo3::marker::Python<'py>,
) -> ::pyo3::PyResult<()> {
const SOURCE_CODE: &str = "def my_function(my_arg1: str) -> int:\n \"\"\"My docstring for `my_function`\"\"\"\n ...\n";
pyo3::types::PyAnyMethods::set_item(
&pyo3::types::PyAnyMethods::getattr(
py.import_bound(pyo3::intern!(py, "sys"))?.as_any(),
pyo3::intern!(py, "modules"),
)?,
"mod_bindgen_function",
pyo3::types::PyModule::from_code_bound(
py,
SOURCE_CODE,
"mod_bindgen_function/__init__.py",
"mod_bindgen_function",
)?,
)
}
/// My docstring for `my_function`
pub fn my_function<'py>(
py: ::pyo3::marker::Python<'py>,
Expand Down Expand Up @@ -151,6 +187,24 @@ test_bindgen! {
unused
)]
pub mod mod_bindgen_class {
pub fn pyo3_embed_python_source_code<'py>(
py: ::pyo3::marker::Python<'py>,
) -> ::pyo3::PyResult<()> {
const SOURCE_CODE: &str = "from typing import Dict, Optional\nclass MyClass:\n \"\"\"My docstring for `MyClass`\"\"\"\n def __init__(self, my_arg1: str, my_arg2: Optional[int] = None):\n \"\"\"My docstring for __init__\"\"\"\n ...\n def my_method(self, my_arg1: Dict[str, int], **kwargs):\n \"\"\"My docstring for `my_method`\"\"\"\n ...\n @property\n def my_property(self) -> int:\n ...\n @my_property.setter\n def my_property(self, value: int):\n ...\n\ndef my_function_with_class_param(my_arg1: MyClass):\n ...\n\ndef my_function_with_class_return() -> MyClass:\n ...\n";
pyo3::types::PyAnyMethods::set_item(
&pyo3::types::PyAnyMethods::getattr(
py.import_bound(pyo3::intern!(py, "sys"))?.as_any(),
pyo3::intern!(py, "modules"),
)?,
"mod_bindgen_class",
pyo3::types::PyModule::from_code_bound(
py,
SOURCE_CODE,
"mod_bindgen_class/__init__.py",
"mod_bindgen_class",
)?,
)
}
/// My docstring for `MyClass`
#[repr(transparent)]
pub struct MyClass(::pyo3::PyAny);
Expand Down

0 comments on commit 5a4b3ea

Please sign in to comment.