diff --git a/pyo3_bindgen/src/lib.rs b/pyo3_bindgen/src/lib.rs index 7e1132f..b0aa3c9 100644 --- a/pyo3_bindgen/src/lib.rs +++ b/pyo3_bindgen/src/lib.rs @@ -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"); //! diff --git a/pyo3_bindgen_engine/src/codegen.rs b/pyo3_bindgen_engine/src/codegen.rs index 04e9f82..32bf8e0 100644 --- a/pyo3_bindgen_engine/src/codegen.rs +++ b/pyo3_bindgen_engine/src/codegen.rs @@ -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. /// @@ -45,6 +45,8 @@ use rustc_hash::FxHashSet as HashSet; pub struct Codegen { cfg: Config, modules: Vec, + /// Python source code included by [`Self::module_from_str()`] in the generated Rust bindings. + embedded_source_code: HashMap, } impl Codegen { @@ -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 { + /// + /// # 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.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) }) @@ -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() diff --git a/pyo3_bindgen_engine/src/lib.rs b/pyo3_bindgen_engine/src/lib.rs index 393a7f3..4f7f3ce 100644 --- a/pyo3_bindgen_engine/src/lib.rs +++ b/pyo3_bindgen_engine/src/lib.rs @@ -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 diff --git a/pyo3_bindgen_engine/src/syntax/module.rs b/pyo3_bindgen_engine/src/syntax/module.rs index 6e1d262..11ea6bc 100644 --- a/pyo3_bindgen_engine/src/syntax/module.rs +++ b/pyo3_bindgen_engine/src/syntax/module.rs @@ -19,6 +19,7 @@ pub struct Module { pub properties: Vec, pub docstring: Option, pub is_package: bool, + pub source_code: Option, } impl Module { @@ -46,6 +47,7 @@ impl Module { properties: Vec::default(), docstring, is_package: true, + source_code: None, }) } @@ -309,6 +311,7 @@ impl Module { properties, docstring, is_package, + source_code: None, }) } @@ -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 } }); diff --git a/pyo3_bindgen_engine/tests/bindgen.rs b/pyo3_bindgen_engine/tests/bindgen.rs index aa49275..1e36912 100644 --- a/pyo3_bindgen_engine/tests/bindgen.rs +++ b/pyo3_bindgen_engine/tests/bindgen.rs @@ -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 { ::pyo3::types::PyAnyMethods::extract( &::pyo3::types::PyAnyMethods::getattr( @@ -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>, @@ -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);