From e73112f3f6b9a3cf01f1b292726279e45366660e Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Tue, 9 Jul 2024 08:15:12 -0400 Subject: [PATCH] Automatically treat nested modules as submodules (#4308) fixes #4286 --- guide/src/module.md | 4 ++-- newsfragments/4308.changed.md | 1 + pyo3-macros-backend/src/module.rs | 12 ++++++++++-- tests/test_compile_error.rs | 1 + tests/test_declarative_module.rs | 2 +- tests/ui/duplicate_pymodule_submodule.rs | 7 +++++++ tests/ui/duplicate_pymodule_submodule.stderr | 11 +++++++++++ 7 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 newsfragments/4308.changed.md create mode 100644 tests/ui/duplicate_pymodule_submodule.rs create mode 100644 tests/ui/duplicate_pymodule_submodule.stderr diff --git a/guide/src/module.md b/guide/src/module.md index a2cb8b37..ee5485e0 100644 --- a/guide/src/module.md +++ b/guide/src/module.md @@ -151,7 +151,6 @@ For nested modules, the name of the parent module is automatically added. In the following example, the `Unit` class will have for `module` `my_extension.submodule` because it is properly nested but the `Ext` class will have for `module` the default `builtins` because it not nested. -You can provide the `submodule` argument to `pymodule()` for modules that are not top-level modules. ```rust # mod declarative_module_module_attr_test { use pyo3::prelude::*; @@ -166,7 +165,7 @@ mod my_extension { #[pymodule_export] use super::Ext; - #[pymodule(submodule)] + #[pymodule] mod submodule { use super::*; // This is a submodule @@ -179,3 +178,4 @@ mod my_extension { ``` It is possible to customize the `module` value for a `#[pymodule]` with the `#[pyo3(module = "MY_MODULE")]` option. +You can provide the `submodule` argument to `pymodule()` for modules that are not top-level modules -- it is automatically set for modules nested inside of a `#[pymodule]`. diff --git a/newsfragments/4308.changed.md b/newsfragments/4308.changed.md new file mode 100644 index 00000000..6b5310cd --- /dev/null +++ b/newsfragments/4308.changed.md @@ -0,0 +1 @@ +Nested declarative `#[pymodule]` are automatically treated as submodules (no `PyInit_` entrypoint is created) diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 26e3816c..70941cf5 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -80,7 +80,7 @@ impl PyModuleOptions { fn set_submodule(&mut self, submod: SubmoduleAttribute) -> Result<()> { ensure_spanned!( !self.is_submodule, - submod.span() => "`submodule` may only be specified once" + submod.span() => "`submodule` may only be specified once (it is implicitly always specified for nested modules)" ); self.is_submodule = true; @@ -116,7 +116,14 @@ pub fn pymodule_module_impl( } else { name.to_string() }; - is_submodule = is_submodule || options.is_submodule; + + is_submodule = match (is_submodule, options.is_submodule) { + (true, true) => { + bail_spanned!(module.span() => "`submodule` may only be specified once (it is implicitly always specified for nested modules)") + } + (false, false) => false, + (true, false) | (false, true) => true, + }; let mut module_items = Vec::new(); let mut module_items_cfg_attrs = Vec::new(); @@ -273,6 +280,7 @@ pub fn pymodule_module_impl( )? { set_module_attribute(&mut item_mod.attrs, &full_name); } + item_mod.attrs.push(parse_quote!(#[pyo3(submodule)])); } } Item::ForeignMod(item) => { diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index bdfc4893..0d3fa045 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -66,4 +66,5 @@ fn test_compile_errors() { t.compile_fail("tests/ui/abi3_weakref.rs"); #[cfg(all(Py_LIMITED_API, not(Py_3_9)))] t.compile_fail("tests/ui/abi3_dict.rs"); + t.compile_fail("tests/ui/duplicate_pymodule_submodule.rs"); } diff --git a/tests/test_declarative_module.rs b/tests/test_declarative_module.rs index f62d5182..f0952438 100644 --- a/tests/test_declarative_module.rs +++ b/tests/test_declarative_module.rs @@ -115,7 +115,7 @@ mod declarative_module { } } - #[pymodule(submodule)] + #[pymodule] #[pyo3(module = "custom_root")] mod inner_custom_root { use super::*; diff --git a/tests/ui/duplicate_pymodule_submodule.rs b/tests/ui/duplicate_pymodule_submodule.rs new file mode 100644 index 00000000..774d3819 --- /dev/null +++ b/tests/ui/duplicate_pymodule_submodule.rs @@ -0,0 +1,7 @@ +#[pyo3::pymodule] +mod mymodule { + #[pyo3::pymodule(submodule)] + mod submod {} +} + +fn main() {} diff --git a/tests/ui/duplicate_pymodule_submodule.stderr b/tests/ui/duplicate_pymodule_submodule.stderr new file mode 100644 index 00000000..a16e9ac7 --- /dev/null +++ b/tests/ui/duplicate_pymodule_submodule.stderr @@ -0,0 +1,11 @@ +error: `submodule` may only be specified once (it is implicitly always specified for nested modules) + --> tests/ui/duplicate_pymodule_submodule.rs:4:2 + | +4 | mod submod {} + | ^^^ + +error[E0433]: failed to resolve: use of undeclared crate or module `submod` + --> tests/ui/duplicate_pymodule_submodule.rs:4:6 + | +4 | mod submod {} + | ^^^^^^ use of undeclared crate or module `submod`