From 863ffb161f5041df561b8299e1c99e9eac4b9db4 Mon Sep 17 00:00:00 2001 From: konstin Date: Mon, 12 Nov 2018 22:25:45 +0100 Subject: [PATCH] Add wrap_module macro --- CHANGELOG.md | 7 +++ README.md | 2 +- examples/rustapi_module/src/datetime.rs | 28 ++++++------ examples/rustapi_module/src/othermod.rs | 2 +- examples/word-count/src/lib.rs | 2 +- guide/src/function.md | 4 +- guide/src/module.md | 43 +++++++++++++---- guide/src/overview.md | 2 +- pyo3-derive-backend/src/module.rs | 6 +-- src/lib.rs | 21 ++++++++- src/types/module.rs | 12 +++-- tests/test_module.rs | 61 +++++++++++++++++++++---- 12 files changed, 145 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2ea9293..70c8d83e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Added + + * Added a `wrap_module!` macro similar to the existing `wrap_function!` macro. Only available on python 3 + +### Changed + * Renamed `add_function` to `add_wrapped` as it now also supports modules. + ### Removed * `PyToken` was removed due to unsoundness (See [#94](https://github.com/PyO3/pyo3/issues/94)). diff --git a/README.md b/README.md index 6663fed7..d4124f5a 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ fn sum_as_string(a: usize, b: usize) -> PyResult { /// This module is a python module implemented in Rust. #[pymodinit] fn string_sum(py: Python, m: &PyModule) -> PyResult<()> { - m.add_function(wrap_function!(sum_as_string))?; + m.add_wrapped(wrap_function!(sum_as_string))?; Ok(()) } diff --git a/examples/rustapi_module/src/datetime.rs b/examples/rustapi_module/src/datetime.rs index 98cf376c..bf055f56 100644 --- a/examples/rustapi_module/src/datetime.rs +++ b/examples/rustapi_module/src/datetime.rs @@ -204,26 +204,26 @@ impl TzClass { #[pymodinit] fn datetime(_py: Python, m: &PyModule) -> PyResult<()> { - m.add_function(wrap_function!(make_date))?; - m.add_function(wrap_function!(get_date_tuple))?; - m.add_function(wrap_function!(date_from_timestamp))?; - m.add_function(wrap_function!(make_time))?; - m.add_function(wrap_function!(get_time_tuple))?; - m.add_function(wrap_function!(make_delta))?; - m.add_function(wrap_function!(get_delta_tuple))?; - m.add_function(wrap_function!(make_datetime))?; - m.add_function(wrap_function!(get_datetime_tuple))?; - m.add_function(wrap_function!(datetime_from_timestamp))?; + m.add_wrapped(wrap_function!(make_date))?; + m.add_wrapped(wrap_function!(get_date_tuple))?; + m.add_wrapped(wrap_function!(date_from_timestamp))?; + m.add_wrapped(wrap_function!(make_time))?; + m.add_wrapped(wrap_function!(get_time_tuple))?; + m.add_wrapped(wrap_function!(make_delta))?; + m.add_wrapped(wrap_function!(get_delta_tuple))?; + m.add_wrapped(wrap_function!(make_datetime))?; + m.add_wrapped(wrap_function!(get_datetime_tuple))?; + m.add_wrapped(wrap_function!(datetime_from_timestamp))?; // Python 3.6+ functions #[cfg(Py_3_6)] { - m.add_function(wrap_function!(time_with_fold))?; - m.add_function(wrap_function!(get_time_tuple_fold))?; - m.add_function(wrap_function!(get_datetime_tuple_fold))?; + m.add_wrapped(wrap_function!(time_with_fold))?; + m.add_wrapped(wrap_function!(get_time_tuple_fold))?; + m.add_wrapped(wrap_function!(get_datetime_tuple_fold))?; } - m.add_function(wrap_function!(issue_219))?; + m.add_wrapped(wrap_function!(issue_219))?; m.add_class::()?; Ok(()) diff --git a/examples/rustapi_module/src/othermod.rs b/examples/rustapi_module/src/othermod.rs index d42a58d4..6a84b0c1 100644 --- a/examples/rustapi_module/src/othermod.rs +++ b/examples/rustapi_module/src/othermod.rs @@ -30,7 +30,7 @@ fn double(x: i32) -> i32 { #[pymodinit] fn othermod(_py: Python, m: &PyModule) -> PyResult<()> { - m.add_function(wrap_function!(double))?; + m.add_wrapped(wrap_function!(double))?; m.add_class::()?; m.add("USIZE_MIN", usize::min_value())?; diff --git a/examples/word-count/src/lib.rs b/examples/word-count/src/lib.rs index 05ee9956..9ed82115 100644 --- a/examples/word-count/src/lib.rs +++ b/examples/word-count/src/lib.rs @@ -80,7 +80,7 @@ fn count_line(line: &str, needle: &str) -> usize { #[pymodinit] fn word_count(_py: Python, m: &PyModule) -> PyResult<()> { - m.add_function(wrap_function!(count_line))?; + m.add_wrapped(wrap_function!(count_line))?; m.add_class::()?; Ok(()) diff --git a/guide/src/function.md b/guide/src/function.md index aa9dee72..c061f40f 100644 --- a/guide/src/function.md +++ b/guide/src/function.md @@ -29,7 +29,7 @@ fn rust2py(py: Python, m: &PyModule) -> PyResult<()> { ``` The other is annotating a function with `#[py::function]` and then adding it -to the module using the `add_function_to_module!` macro, which takes the module +to the module using the `add_wrapped_to_module!` macro, which takes the module as first parameter, the function name as second and an instance of `Python` as third. @@ -47,7 +47,7 @@ fn double(x: usize) -> usize { #[pymodinit] fn module_with_functions(py: Python, m: &PyModule) -> PyResult<()> { - m.add_function(wrap_function!(double)).unwrap(); + m.add_wrapped(wrap_function!(double)).unwrap(); Ok(()) } diff --git a/guide/src/module.md b/guide/src/module.md index 4972eda6..3863a5f8 100644 --- a/guide/src/module.md +++ b/guide/src/module.md @@ -8,8 +8,6 @@ As shown in the Getting Started chapter, you can create a module as follows: extern crate pyo3; use pyo3::{PyResult, Python, PyModule}; - - // add bindings to the generated python module // N.B: names: "librust2py" must be the name of the `.so` or `.pyd` file /// This module is implemented in Rust. @@ -36,7 +34,11 @@ fn sum_as_string(a:i64, b:i64) -> String { # fn main() {} ``` -The `#[pymodinit}` procedural macro attribute takes care of exporting the initialization function of your module to Python. It takes one argument as the name of your module, it must be the name of the `.so` or `.pyd` file. +The `#[pymodinit]` procedural macro attribute takes care of exporting the initialization function of your module to Python. It takes one argument as the name of your module, which must be the name of the `.so` or `.pyd` file. + +To import the module, either copy the shared library as described in [Get Started](./overview.md) or use a tool, e.g. `pyo3-pack develop` with [pyo3-pack](https://github.com/PyO3/pyo3-pack) or `python setup.py develop` with [setuptools-rust](https://github.com/PyO3/setuptools-rust). + +## Documentation The [Rust doc comments](https://doc.rust-lang.org/stable/book/first-edition/comments.html) of the module initialization function will be applied automatically as the Python doc string of your module. @@ -48,9 +50,34 @@ print(rust2py.__doc__) Which means that the above Python code will print `This module is implemented in Rust.`. -> On macOS, you will need to rename the output from `*.dylib` to `*.so`. -> -> On Windows, you will need to rename the output from `*.dll` to `*.pyd`. +## Modules as objects -For `setup.py` integration, You can use [setuptools-rust](https://github.com/PyO3/setuptools-rust), -learn more about it in [Distribution](./distribution.html). +In python, modules are first class objects. This means can store them as values or add them to dicts or other modules: + +```rust +#[pyfunction] +fn subfunction() -> String { + "Subfunction".to_string() +} + +#[pymodule] +fn submodule(_py: Python, module: &PyModule) -> PyResult<()> { + module.add_wrapped(wrap_function!(subfunction))?; + Ok(()) +} + +#[pymodule] +fn supermodule(_py: Python, module: &PyModule) -> PyResult<()> { + module.add_wrapped(wrap_module!(submodule))?; + Ok(()) +} + +fn nested_call() { + let gil = GILGuard::acquire(); + let py = gil.python(); + let supermodule = wrap_module!(supermodule)(py); + ctx.set_item("supermodule", supermodule); + + py.run("assert supermodule.submodule.subfuntion() == 'Subfunction'", None, Some(&ctx)).unwrap(); +} +``` diff --git a/guide/src/overview.md b/guide/src/overview.md index 61957e65..fbdf3520 100644 --- a/guide/src/overview.md +++ b/guide/src/overview.md @@ -49,7 +49,7 @@ fn sum_as_string(a: usize, b: usize) -> PyResult { /// This module is a python moudle implemented in Rust. #[pymodinit] fn rust_py(py: Python, m: &PyModule) -> PyResult<()> { - m.add_function(wrap_function!(sum_as_string))?; + m.add_wrapped(wrap_function!(sum_as_string))?; Ok(()) } diff --git a/pyo3-derive-backend/src/module.rs b/pyo3-derive-backend/src/module.rs index 53965382..e5003313 100644 --- a/pyo3-derive-backend/src/module.rs +++ b/pyo3-derive-backend/src/module.rs @@ -12,7 +12,7 @@ use proc_macro2::{Span, TokenStream}; /// Generates the function that is called by the python interpreter to initialize the native /// module pub fn py3_init(fnname: &syn::Ident, name: &syn::Ident, doc: syn::Lit) -> TokenStream { - let cb_name: syn::Ident = syn::parse_str(&format!("PyInit_{}", name)).unwrap(); + let cb_name = syn::Ident::new(&format!("PyInit_{}", name), Span::call_site()); quote! { #[no_mangle] @@ -26,7 +26,7 @@ pub fn py3_init(fnname: &syn::Ident, name: &syn::Ident, doc: syn::Lit) -> TokenS } pub fn py2_init(fnname: &syn::Ident, name: &syn::Ident, doc: syn::Lit) -> TokenStream { - let cb_name: syn::Ident = syn::parse_str(&format!("init{}", name)).unwrap(); + let cb_name = syn::Ident::new(&format!("init{}", name), Span::call_site()); quote! { #[no_mangle] @@ -51,7 +51,7 @@ pub fn process_functions_in_module(func: &mut syn::ItemFn) { let item: syn::ItemFn = parse_quote!{ fn block_wrapper() { #function_to_python - #module_name.add_function(&#function_wrapper_ident)?; + #module_name.add_wrapped(&#function_wrapper_ident)?; } }; stmts.extend(item.block.stmts.into_iter()); diff --git a/src/lib.rs b/src/lib.rs index 792ecc2c..d9790b49 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -201,7 +201,7 @@ pub mod proc_macro { /// Returns a function that takes a [Python] instance and returns a python function. /// -/// Use this together with `#[function]` and [types::PyModule::add_function]. +/// Use this together with `#[pyfunction]` and [types::PyModule::add_wrapped]. #[macro_export] macro_rules! wrap_function { ($function_name:ident) => {{ @@ -218,3 +218,22 @@ macro_rules! wrap_function { } }}; } + +/// Returns a function that takes a [Python] instance and returns a python module. +/// +/// Use this together with `#[pymodule]` and [types::PyModule::add_wrapped]. +#[cfg(Py_3)] +#[macro_export] +macro_rules! wrap_module { + ($module_name:ident) => {{ + use $crate::mashup::*; + + mashup! { + m["method"] = PyInit_ $module_name; + } + + m! { + &|py| unsafe { crate::PyObject::from_owned_ptr(py, "method"()) } + } + }}; +} diff --git a/src/types/module.rs b/src/types/module.rs index 388cd5e7..29f8f92a 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -169,12 +169,14 @@ impl PyModule { self.setattr(T::NAME, ty) } - /// Adds a function to a module, using the functions __name__ as name. + /// Adds a function or a (sub)module to a module, using the functions __name__ as name. /// - /// Use this together with the`#[pyfunction]` and [wrap_function!] macro. + /// Use this together with the`#[pyfunction]` and [wrap_function!] or `#[pymodule]` and + /// [wrap_module!]. /// /// ```rust,ignore - /// m.add_function(wrap_function!(double)); + /// m.add_wrapped(wrap_function!(double)); + /// m.add_wrapped(wrap_module!(utils)); /// ``` /// /// You can also add a function with a custom name using [add](PyModule::add): @@ -182,11 +184,11 @@ impl PyModule { /// ```rust,ignore /// m.add("also_double", wrap_function!(double)(py)); /// ``` - pub fn add_function(&self, wrapper: &Fn(Python) -> PyObject) -> PyResult<()> { + pub fn add_wrapped(&self, wrapper: &Fn(Python) -> PyObject) -> PyResult<()> { let function = wrapper(self.py()); let name = function .getattr(self.py(), "__name__") - .expect("A function must have a __name__"); + .expect("A function or module must have a __name__"); self.add(name.extract(self.py()).unwrap(), function) } } diff --git a/tests/test_module.rs b/tests/test_module.rs index f70ce9ae..cc541687 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -40,22 +40,22 @@ fn module_with_functions(py: Python, m: &PyModule) -> PyResult<()> { m.add("foo", "bar").unwrap(); - m.add_function(wrap_function!(double)).unwrap(); + m.add_wrapped(wrap_function!(double)).unwrap(); m.add("also_double", wrap_function!(double)(py)).unwrap(); Ok(()) } #[test] -#[cfg(Py_3)] fn test_module_with_functions() { let gil = Python::acquire_gil(); let py = gil.python(); let d = PyDict::new(py); - d.set_item("module_with_functions", unsafe { - PyObject::from_owned_ptr(py, PyInit_module_with_functions()) - }) + d.set_item( + "module_with_functions", + wrap_module!(module_with_functions)(py), + ) .unwrap(); let run = |code| py.run(code, None, Some(d)).unwrap(); @@ -131,16 +131,61 @@ fn r#move() -> usize { #[pymodinit] fn raw_ident_module(_py: Python, module: &PyModule) -> PyResult<()> { - module.add_function(wrap_function!(r#move)) + module.add_wrapped(wrap_function!(r#move)) } #[test] -#[cfg(Py_3)] fn test_raw_idents() { let gil = Python::acquire_gil(); let py = gil.python(); - let module = unsafe { PyObject::from_owned_ptr(py, PyInit_raw_ident_module()) }; + let module = wrap_module!(raw_ident_module)(py); py_assert!(py, module, "module.move() == 42"); } + +#[pyfunction] +#[cfg(Py_3)] +fn subfunction() -> String { + "Subfunction".to_string() +} + +#[pymodinit] +#[cfg(Py_3)] +fn submodule(_py: Python, module: &PyModule) -> PyResult<()> { + module.add_wrapped(wrap_function!(subfunction))?; + Ok(()) +} + +#[pyfunction] +#[cfg(Py_3)] +fn superfunction() -> String { + "Superfunction".to_string() +} + +#[pymodinit] +#[cfg(Py_3)] +fn supermodule(_py: Python, module: &PyModule) -> PyResult<()> { + module.add_wrapped(wrap_function!(superfunction))?; + module.add_wrapped(wrap_module!(submodule))?; + Ok(()) +} + +#[test] +#[cfg(Py_3)] +fn test_module_nesting() { + let gil = GILGuard::acquire(); + let py = gil.python(); + let supermodule = wrap_module!(supermodule)(py); + + py_assert!( + py, + supermodule, + "supermodule.superfunction() == 'Superfunction'" + ); + py_assert!( + py, + supermodule, + "supermodule.submodule.subfunction() == 'Subfunction'" + ); +}