Skip to content

Commit db2d069

Browse files
committed
refs PyO3#4286 -- allow setting submodule on declarative pymodules
1 parent f3603a0 commit db2d069

File tree

6 files changed

+77
-19
lines changed

6 files changed

+77
-19
lines changed

guide/src/module.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ The `#[pymodule]` macro automatically sets the `module` attribute of the `#[pycl
154154
For nested modules, the name of the parent module is automatically added.
155155
In the following example, the `Unit` class will have for `module` `my_extension.submodule` because it is properly nested
156156
but the `Ext` class will have for `module` the default `builtins` because it not nested.
157+
158+
You can provide the `submodule` argument to `pymodule()` for modules that are not top-level modules.
157159
```rust
158160
# mod declarative_module_module_attr_test {
159161
use pyo3::prelude::*;
@@ -168,7 +170,7 @@ mod my_extension {
168170
#[pymodule_export]
169171
use super::Ext;
170172

171-
#[pymodule]
173+
#[pymodule(submodule)]
172174
mod submodule {
173175
use super::*;
174176
// This is a submodule

newsfragments/4301.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
allow setting `submodule` on declarative `#[pymodule]`s

pyo3-macros-backend/src/attributes.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ pub mod kw {
3737
syn::custom_keyword!(set_all);
3838
syn::custom_keyword!(signature);
3939
syn::custom_keyword!(subclass);
40+
syn::custom_keyword!(submodule);
4041
syn::custom_keyword!(text_signature);
4142
syn::custom_keyword!(transparent);
4243
syn::custom_keyword!(unsendable);
@@ -178,6 +179,7 @@ pub type ModuleAttribute = KeywordAttribute<kw::module, LitStr>;
178179
pub type NameAttribute = KeywordAttribute<kw::name, NameLitStr>;
179180
pub type RenameAllAttribute = KeywordAttribute<kw::rename_all, RenamingRuleLitStr>;
180181
pub type TextSignatureAttribute = KeywordAttribute<kw::text_signature, TextSignatureAttributeValue>;
182+
pub type SubmoduleAttribute = kw::submodule;
181183

182184
impl<K: Parse + std::fmt::Debug, V: Parse> Parse for KeywordAttribute<K, V> {
183185
fn parse(input: ParseStream<'_>) -> Result<Self> {

pyo3-macros-backend/src/module.rs

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use crate::{
44
attributes::{
55
self, take_attributes, take_pyo3_options, CrateAttribute, ModuleAttribute, NameAttribute,
6+
SubmoduleAttribute,
67
},
78
get_doc,
89
pyclass::PyClassPyO3Option,
@@ -27,6 +28,7 @@ pub struct PyModuleOptions {
2728
krate: Option<CrateAttribute>,
2829
name: Option<syn::Ident>,
2930
module: Option<ModuleAttribute>,
31+
is_submodule: bool,
3032
}
3133

3234
impl PyModuleOptions {
@@ -38,6 +40,7 @@ impl PyModuleOptions {
3840
PyModulePyO3Option::Name(name) => options.set_name(name.value.0)?,
3941
PyModulePyO3Option::Crate(path) => options.set_crate(path)?,
4042
PyModulePyO3Option::Module(module) => options.set_module(module)?,
43+
PyModulePyO3Option::Submodule(submod) => options.set_submodule(submod)?,
4144
}
4245
}
4346

@@ -73,9 +76,22 @@ impl PyModuleOptions {
7376
self.module = Some(name);
7477
Ok(())
7578
}
79+
80+
fn set_submodule(&mut self, submod: SubmoduleAttribute) -> Result<()> {
81+
ensure_spanned!(
82+
!self.is_submodule,
83+
submod.span() => "`submodule` may only be specified once"
84+
);
85+
86+
self.is_submodule = true;
87+
Ok(())
88+
}
7689
}
7790

78-
pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result<TokenStream> {
91+
pub fn pymodule_module_impl(
92+
mut module: syn::ItemMod,
93+
mut is_submodule: bool,
94+
) -> Result<TokenStream> {
7995
let syn::ItemMod {
8096
attrs,
8197
vis,
@@ -100,6 +116,7 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result<TokenStream> {
100116
} else {
101117
name.to_string()
102118
};
119+
is_submodule = is_submodule || options.is_submodule;
103120

104121
let mut module_items = Vec::new();
105122
let mut module_items_cfg_attrs = Vec::new();
@@ -297,7 +314,7 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result<TokenStream> {
297314
)
298315
}
299316
}};
300-
let initialization = module_initialization(&name, ctx, module_def);
317+
let initialization = module_initialization(&name, ctx, module_def, is_submodule);
301318
Ok(quote!(
302319
#(#attrs)*
303320
#vis mod #ident {
@@ -331,7 +348,7 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result<TokenStream>
331348
let vis = &function.vis;
332349
let doc = get_doc(&function.attrs, None, ctx);
333350

334-
let initialization = module_initialization(&name, ctx, quote! { MakeDef::make_def() });
351+
let initialization = module_initialization(&name, ctx, quote! { MakeDef::make_def() }, false);
335352

336353
// Module function called with optional Python<'_> marker as first arg, followed by the module.
337354
let mut module_args = Vec::new();
@@ -396,28 +413,37 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result<TokenStream>
396413
})
397414
}
398415

399-
fn module_initialization(name: &syn::Ident, ctx: &Ctx, module_def: TokenStream) -> TokenStream {
416+
fn module_initialization(
417+
name: &syn::Ident,
418+
ctx: &Ctx,
419+
module_def: TokenStream,
420+
is_submodule: bool,
421+
) -> TokenStream {
400422
let Ctx { pyo3_path, .. } = ctx;
401423
let pyinit_symbol = format!("PyInit_{}", name);
402424
let name = name.to_string();
403425
let pyo3_name = LitCStr::new(CString::new(name).unwrap(), Span::call_site(), ctx);
404426

405-
quote! {
427+
let mut result = quote! {
406428
#[doc(hidden)]
407429
pub const __PYO3_NAME: &'static ::std::ffi::CStr = #pyo3_name;
408430

409431
pub(super) struct MakeDef;
410432
#[doc(hidden)]
411433
pub static _PYO3_DEF: #pyo3_path::impl_::pymodule::ModuleDef = #module_def;
412-
413-
/// This autogenerated function is called by the python interpreter when importing
414-
/// the module.
415-
#[doc(hidden)]
416-
#[export_name = #pyinit_symbol]
417-
pub unsafe extern "C" fn __pyo3_init() -> *mut #pyo3_path::ffi::PyObject {
418-
#pyo3_path::impl_::trampoline::module_init(|py| _PYO3_DEF.make_module(py))
419-
}
434+
};
435+
if !is_submodule {
436+
result.extend(quote! {
437+
/// This autogenerated function is called by the python interpreter when importing
438+
/// the module.
439+
#[doc(hidden)]
440+
#[export_name = #pyinit_symbol]
441+
pub unsafe extern "C" fn __pyo3_init() -> *mut #pyo3_path::ffi::PyObject {
442+
#pyo3_path::impl_::trampoline::module_init(|py| _PYO3_DEF.make_module(py))
443+
}
444+
});
420445
}
446+
result
421447
}
422448

423449
/// Finds and takes care of the #[pyfn(...)] in `#[pymodule]`
@@ -557,6 +583,7 @@ fn has_pyo3_module_declared<T: Parse>(
557583
}
558584

559585
enum PyModulePyO3Option {
586+
Submodule(SubmoduleAttribute),
560587
Crate(CrateAttribute),
561588
Name(NameAttribute),
562589
Module(ModuleAttribute),
@@ -571,6 +598,8 @@ impl Parse for PyModulePyO3Option {
571598
input.parse().map(PyModulePyO3Option::Crate)
572599
} else if lookahead.peek(attributes::kw::module) {
573600
input.parse().map(PyModulePyO3Option::Module)
601+
} else if lookahead.peek(attributes::kw::submodule) {
602+
input.parse().map(PyModulePyO3Option::Submodule)
574603
} else {
575604
Err(lookahead.error())
576605
}

pyo3-macros/src/lib.rs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
44
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
55
use proc_macro::TokenStream;
6-
use proc_macro2::TokenStream as TokenStream2;
6+
use proc_macro2::{Span, TokenStream as TokenStream2};
77
use pyo3_macros_backend::{
88
build_derive_from_pyobject, build_py_class, build_py_enum, build_py_function, build_py_methods,
99
pymodule_function_impl, pymodule_module_impl, PyClassArgs, PyClassMethodsType,
@@ -35,10 +35,26 @@ use syn::{parse::Nothing, parse_macro_input, Item};
3535
/// [1]: https://pyo3.rs/latest/module.html
3636
#[proc_macro_attribute]
3737
pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream {
38-
parse_macro_input!(args as Nothing);
3938
match parse_macro_input!(input as Item) {
40-
Item::Mod(module) => pymodule_module_impl(module),
41-
Item::Fn(function) => pymodule_function_impl(function),
39+
Item::Mod(module) => {
40+
let is_submodule = match parse_macro_input!(args as Option<syn::Ident>) {
41+
Some(i) if i == "submodule" => true,
42+
Some(_) => {
43+
return syn::Error::new(
44+
Span::call_site(),
45+
"#[pymodule] only accepts submodule as an argument",
46+
)
47+
.into_compile_error()
48+
.into();
49+
}
50+
None => false,
51+
};
52+
pymodule_module_impl(module, is_submodule)
53+
}
54+
Item::Fn(function) => {
55+
parse_macro_input!(args as Nothing);
56+
pymodule_function_impl(function)
57+
}
4258
unsupported => Err(syn::Error::new_spanned(
4359
unsupported,
4460
"#[pymodule] only supports modules and functions.",

tests/test_declarative_module.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ create_exception!(
4949
"Some description."
5050
);
5151

52+
#[pymodule]
53+
#[pyo3(submodule)]
54+
mod external_submodule {}
55+
5256
/// A module written using declarative syntax.
5357
#[pymodule]
5458
mod declarative_module {
@@ -70,6 +74,9 @@ mod declarative_module {
7074
#[pymodule_export]
7175
use super::some_module::SomeException;
7276

77+
#[pymodule_export]
78+
use super::external_submodule;
79+
7380
#[pymodule]
7481
mod inner {
7582
use super::*;
@@ -108,7 +115,7 @@ mod declarative_module {
108115
}
109116
}
110117

111-
#[pymodule]
118+
#[pymodule(submodule)]
112119
#[pyo3(module = "custom_root")]
113120
mod inner_custom_root {
114121
use super::*;
@@ -174,6 +181,7 @@ fn test_declarative_module() {
174181
py_assert!(py, m, "hasattr(m, 'LocatedClass')");
175182
py_assert!(py, m, "isinstance(m.inner.Struct(), m.inner.Struct)");
176183
py_assert!(py, m, "isinstance(m.inner.Enum.A, m.inner.Enum)");
184+
py_assert!(py, m, "hasattr(m, 'external_submodule')")
177185
})
178186
}
179187

0 commit comments

Comments
 (0)