Skip to content

Commit 64b06ea

Browse files
committed
Change add_submodule() to take &PyModule.
The C-exported wrapper generated through `#[pymodule]` is only required for the top-level module.
1 parent 06cd7c7 commit 64b06ea

File tree

4 files changed

+50
-23
lines changed

4 files changed

+50
-23
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
3333
- 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)
3434
- Change `#[pyproto]` to return NotImplemented for operators for which Python can try a reversed operation. #[1072](https://github.com/PyO3/pyo3/pull/1072)
3535
- `PyModule::add` now uses `IntoPy<PyObject>` instead of `ToPyObject`. #[1124](https://github.com/PyO3/pyo3/pull/1124)
36+
- Add nested modules as `&PyModule` instead of using the wrapper generated by `#[pymodule]`. [#1143](https://github.com/PyO3/pyo3/pull/1143)
3637

3738
### Removed
3839
- Remove `PyString::as_bytes`. [#1023](https://github.com/PyO3/pyo3/pull/1023)

guide/src/module.md

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,22 @@ fn sum_as_string(a: i64, b: i64) -> String {
3232
# fn main() {}
3333
```
3434

35-
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.
35+
The `#[pymodule]` procedural macro attribute takes care of exporting the initialization function of your
36+
module to Python. It can take as an argument the name of your module, which must be the name of the `.so`
37+
or `.pyd` file; the default is the Rust function's name.
3638

37-
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:
39+
If the name of the module (the default being the function name) does not match the name of the `.so` or
40+
`.pyd` file, you will get an import error in Python with the following message:
3841
`ImportError: dynamic module does not define module export function (PyInit_name_of_your_module)`
3942

40-
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).
43+
To import the module, either copy the shared library as described in [the README](https://github.com/PyO3/pyo3)
44+
or use a tool, e.g. `maturin develop` with [maturin](https://github.com/PyO3/maturin) or
45+
`python setup.py develop` with [setuptools-rust](https://github.com/PyO3/setuptools-rust).
4146

4247
## Documentation
4348

44-
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.
49+
The [Rust doc comments](https://doc.rust-lang.org/stable/book/first-edition/comments.html) of the module
50+
initialization function will be applied automatically as the Python docstring of your module.
4551

4652
```python
4753
import rust2py
@@ -53,7 +59,8 @@ Which means that the above Python code will print `This module is implemented in
5359

5460
## Modules as objects
5561

56-
In Python, modules are first class objects. This means that you can store them as values or add them to dicts or other modules:
62+
In Python, modules are first class objects. This means that you can store them as values or add them to
63+
dicts or other modules:
5764

5865
```rust
5966
use pyo3::prelude::*;
@@ -65,15 +72,16 @@ fn subfunction() -> String {
6572
"Subfunction".to_string()
6673
}
6774

68-
#[pymodule]
69-
fn submodule(_py: Python, module: &PyModule) -> PyResult<()> {
75+
fn init_submodule(module: &PyModule) -> PyResult<()> {
7076
module.add_function(wrap_pyfunction!(subfunction))?;
7177
Ok(())
7278
}
7379

7480
#[pymodule]
75-
fn supermodule(_py: Python, module: &PyModule) -> PyResult<()> {
76-
module.add_submodule(wrap_pymodule!(submodule))?;
81+
fn supermodule(py: Python, module: &PyModule) -> PyResult<()> {
82+
let submod = PyModule::new(py, "submodule")?;
83+
init_submodule(submod)?;
84+
module.add_submodule(submod)?;
7785
Ok(())
7886
}
7987

@@ -86,3 +94,5 @@ fn supermodule(_py: Python, module: &PyModule) -> PyResult<()> {
8694
```
8795

8896
This way, you can create a module hierarchy within a single extension module.
97+
98+
It is not necessary to add `#[pymodule]` on nested modules, this is only required on the top-level module.

src/types/module.rs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -228,20 +228,19 @@ impl PyModule {
228228
///
229229
/// ```rust
230230
/// use pyo3::prelude::*;
231-
/// #[pymodule]
232-
/// fn utils(_py: Python, _module: &PyModule) -> PyResult<()> {
233-
/// Ok(())
231+
///
232+
/// fn init_utils(module: &PyModule) -> PyResult<()> {
233+
/// module.add("super_useful_constant", "important")
234234
/// }
235235
/// #[pymodule]
236-
/// fn top_level(_py: Python, module: &PyModule) -> PyResult<()> {
237-
/// module.add_submodule(pyo3::wrap_pymodule!(utils))
236+
/// fn top_level(py: Python, module: &PyModule) -> PyResult<()> {
237+
/// let utils = PyModule::new(py, "utils")?;
238+
/// init_utils(utils)?;
239+
/// module.add_submodule(utils)
238240
/// }
239241
/// ```
240-
pub fn add_submodule<'a>(&'a self, wrapper: &impl Fn(Python<'a>) -> PyObject) -> PyResult<()> {
241-
let py = self.py();
242-
let module = wrapper(py);
243-
let name = module.getattr(py, "__name__")?;
244-
let name = name.extract(py)?;
242+
pub fn add_submodule(&self, module: &PyModule) -> PyResult<()> {
243+
let name = module.name()?;
245244
self.add(name, module)
246245
}
247246

tests/test_module.rs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,15 @@ fn subfunction() -> String {
218218
"Subfunction".to_string()
219219
}
220220

221+
fn submodule(module: &PyModule) -> PyResult<()> {
222+
use pyo3::wrap_pyfunction;
223+
224+
module.add_function(wrap_pyfunction!(subfunction))?;
225+
Ok(())
226+
}
227+
221228
#[pymodule]
222-
fn submodule(_py: Python, module: &PyModule) -> PyResult<()> {
229+
fn submodule_with_init_fn(_py: Python, module: &PyModule) -> PyResult<()> {
223230
use pyo3::wrap_pyfunction;
224231

225232
module.add_function(wrap_pyfunction!(subfunction))?;
@@ -232,11 +239,16 @@ fn superfunction() -> String {
232239
}
233240

234241
#[pymodule]
235-
fn supermodule(_py: Python, module: &PyModule) -> PyResult<()> {
236-
use pyo3::{wrap_pyfunction, wrap_pymodule};
242+
fn supermodule(py: Python, module: &PyModule) -> PyResult<()> {
243+
use pyo3::wrap_pyfunction;
237244

238245
module.add_function(wrap_pyfunction!(superfunction))?;
239-
module.add_submodule(wrap_pymodule!(submodule))?;
246+
let module_to_add = PyModule::new(py, "submodule")?;
247+
submodule(module_to_add)?;
248+
module.add_submodule(module_to_add)?;
249+
let module_to_add = PyModule::new(py, "submodule_with_init_fn")?;
250+
submodule_with_init_fn(py, module_to_add)?;
251+
module.add_submodule(module_to_add)?;
240252
Ok(())
241253
}
242254

@@ -258,6 +270,11 @@ fn test_module_nesting() {
258270
supermodule,
259271
"supermodule.submodule.subfunction() == 'Subfunction'"
260272
);
273+
py_assert!(
274+
py,
275+
supermodule,
276+
"supermodule.submodule_with_init_fn.subfunction() == 'Subfunction'"
277+
);
261278
}
262279

263280
// Test that argument parsing specification works for pyfunctions

0 commit comments

Comments
 (0)