From 64b06ea9ec1b0f1c1c58f2cd59d923cb9de4a4f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20P=C3=BCtz?= Date: Sat, 5 Sep 2020 15:54:03 +0200 Subject: [PATCH] Change `add_submodule()` to take `&PyModule`. The C-exported wrapper generated through `#[pymodule]` is only required for the top-level module. --- CHANGELOG.md | 1 + guide/src/module.md | 28 +++++++++++++++++++--------- src/types/module.rs | 19 +++++++++---------- tests/test_module.rs | 25 +++++++++++++++++++++---- 4 files changed, 50 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd649b91..962ba01e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Implement `Send + Sync` for `PyErr`. `PyErr::new`, `PyErr::from_type`, `PyException::py_err` and `PyException::into` have had these bounds added to their arguments. [#1067](https://github.com/PyO3/pyo3/pull/1067) - Change `#[pyproto]` to return NotImplemented for operators for which Python can try a reversed operation. #[1072](https://github.com/PyO3/pyo3/pull/1072) - `PyModule::add` now uses `IntoPy` instead of `ToPyObject`. #[1124](https://github.com/PyO3/pyo3/pull/1124) +- Add nested modules as `&PyModule` instead of using the wrapper generated by `#[pymodule]`. [#1143](https://github.com/PyO3/pyo3/pull/1143) ### Removed - Remove `PyString::as_bytes`. [#1023](https://github.com/PyO3/pyo3/pull/1023) diff --git a/guide/src/module.md b/guide/src/module.md index a458eadd..6b1d4581 100644 --- a/guide/src/module.md +++ b/guide/src/module.md @@ -32,16 +32,22 @@ fn sum_as_string(a: i64, b: i64) -> String { # fn main() {} ``` -The `#[pymodule]` procedural macro attribute takes care of exporting the initialization function of your module to Python. It can take as an argument the name of your module, which must be the name of the `.so` or `.pyd` file; the default is the Rust function's name. +The `#[pymodule]` procedural macro attribute takes care of exporting the initialization function of your +module to Python. It can take as an argument the name of your module, which must be the name of the `.so` +or `.pyd` file; the default is the Rust function's name. -If the name of the module (the default being the function name) does not match the name of the `.so` or `.pyd` file, you will get an import error in Python with the following message: +If the name of the module (the default being the function name) does not match the name of the `.so` or +`.pyd` file, you will get an import error in Python with the following message: `ImportError: dynamic module does not define module export function (PyInit_name_of_your_module)` -To import the module, either copy the shared library as described in [the README](https://github.com/PyO3/pyo3) or use a tool, e.g. `maturin develop` with [maturin](https://github.com/PyO3/maturin) or `python setup.py develop` with [setuptools-rust](https://github.com/PyO3/setuptools-rust). +To import the module, either copy the shared library as described in [the README](https://github.com/PyO3/pyo3) +or use a tool, e.g. `maturin develop` with [maturin](https://github.com/PyO3/maturin) 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 docstring of your module. +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 docstring of your module. ```python import rust2py @@ -53,7 +59,8 @@ Which means that the above Python code will print `This module is implemented in ## Modules as objects -In Python, modules are first class objects. This means that you can store them as values or add them to dicts or other modules: +In Python, modules are first class objects. This means that you can store them as values or add them to +dicts or other modules: ```rust use pyo3::prelude::*; @@ -65,15 +72,16 @@ fn subfunction() -> String { "Subfunction".to_string() } -#[pymodule] -fn submodule(_py: Python, module: &PyModule) -> PyResult<()> { +fn init_submodule(module: &PyModule) -> PyResult<()> { module.add_function(wrap_pyfunction!(subfunction))?; Ok(()) } #[pymodule] -fn supermodule(_py: Python, module: &PyModule) -> PyResult<()> { - module.add_submodule(wrap_pymodule!(submodule))?; +fn supermodule(py: Python, module: &PyModule) -> PyResult<()> { + let submod = PyModule::new(py, "submodule")?; + init_submodule(submod)?; + module.add_submodule(submod)?; Ok(()) } @@ -86,3 +94,5 @@ fn supermodule(_py: Python, module: &PyModule) -> PyResult<()> { ``` This way, you can create a module hierarchy within a single extension module. + +It is not necessary to add `#[pymodule]` on nested modules, this is only required on the top-level module. \ No newline at end of file diff --git a/src/types/module.rs b/src/types/module.rs index 437085f5..0e480010 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -228,20 +228,19 @@ impl PyModule { /// /// ```rust /// use pyo3::prelude::*; - /// #[pymodule] - /// fn utils(_py: Python, _module: &PyModule) -> PyResult<()> { - /// Ok(()) + /// + /// fn init_utils(module: &PyModule) -> PyResult<()> { + /// module.add("super_useful_constant", "important") /// } /// #[pymodule] - /// fn top_level(_py: Python, module: &PyModule) -> PyResult<()> { - /// module.add_submodule(pyo3::wrap_pymodule!(utils)) + /// fn top_level(py: Python, module: &PyModule) -> PyResult<()> { + /// let utils = PyModule::new(py, "utils")?; + /// init_utils(utils)?; + /// module.add_submodule(utils) /// } /// ``` - pub fn add_submodule<'a>(&'a self, wrapper: &impl Fn(Python<'a>) -> PyObject) -> PyResult<()> { - let py = self.py(); - let module = wrapper(py); - let name = module.getattr(py, "__name__")?; - let name = name.extract(py)?; + pub fn add_submodule(&self, module: &PyModule) -> PyResult<()> { + let name = module.name()?; self.add(name, module) } diff --git a/tests/test_module.rs b/tests/test_module.rs index 037c0217..7c278bdc 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -218,8 +218,15 @@ fn subfunction() -> String { "Subfunction".to_string() } +fn submodule(module: &PyModule) -> PyResult<()> { + use pyo3::wrap_pyfunction; + + module.add_function(wrap_pyfunction!(subfunction))?; + Ok(()) +} + #[pymodule] -fn submodule(_py: Python, module: &PyModule) -> PyResult<()> { +fn submodule_with_init_fn(_py: Python, module: &PyModule) -> PyResult<()> { use pyo3::wrap_pyfunction; module.add_function(wrap_pyfunction!(subfunction))?; @@ -232,11 +239,16 @@ fn superfunction() -> String { } #[pymodule] -fn supermodule(_py: Python, module: &PyModule) -> PyResult<()> { - use pyo3::{wrap_pyfunction, wrap_pymodule}; +fn supermodule(py: Python, module: &PyModule) -> PyResult<()> { + use pyo3::wrap_pyfunction; module.add_function(wrap_pyfunction!(superfunction))?; - module.add_submodule(wrap_pymodule!(submodule))?; + let module_to_add = PyModule::new(py, "submodule")?; + submodule(module_to_add)?; + module.add_submodule(module_to_add)?; + let module_to_add = PyModule::new(py, "submodule_with_init_fn")?; + submodule_with_init_fn(py, module_to_add)?; + module.add_submodule(module_to_add)?; Ok(()) } @@ -258,6 +270,11 @@ fn test_module_nesting() { supermodule, "supermodule.submodule.subfunction() == 'Subfunction'" ); + py_assert!( + py, + supermodule, + "supermodule.submodule_with_init_fn.subfunction() == 'Subfunction'" + ); } // Test that argument parsing specification works for pyfunctions