Skip to content

Commit a5a3f3f

Browse files
authored
allow #[pymodule(...)] to accept all relevant #[pyo3(...)] options (#4330)
1 parent 6be8064 commit a5a3f3f

17 files changed

+125
-120
lines changed

examples/getitem/src/lib.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,7 @@ impl ExampleContainer {
7575
}
7676
}
7777

78-
#[pymodule]
79-
#[pyo3(name = "getitem")]
78+
#[pymodule(name = "getitem")]
8079
fn example(m: &Bound<'_, PyModule>) -> PyResult<()> {
8180
// ? -https://github.com/PyO3/maturin/issues/475
8281
m.add_class::<ExampleContainer>()?;

guide/src/module.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ fn double(x: usize) -> usize {
3131
x * 2
3232
}
3333

34-
#[pymodule]
35-
#[pyo3(name = "custom_name")]
34+
#[pymodule(name = "custom_name")]
3635
fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> {
3736
m.add_function(wrap_pyfunction!(double, m)?)
3837
}

newsfragments/4330.changed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`#[pymodule(...)]` now directly accepts all relevant `#[pyo3(...)]` options.

pyo3-macros-backend/src/module.rs

Lines changed: 64 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
33
use crate::{
44
attributes::{
5-
self, take_attributes, take_pyo3_options, CrateAttribute, ModuleAttribute, NameAttribute,
6-
SubmoduleAttribute,
5+
self, kw, take_attributes, take_pyo3_options, CrateAttribute, ModuleAttribute,
6+
NameAttribute, SubmoduleAttribute,
77
},
88
get_doc,
99
pyclass::PyClassPyO3Option,
@@ -16,7 +16,7 @@ use std::ffi::CString;
1616
use syn::{
1717
ext::IdentExt,
1818
parse::{Parse, ParseStream},
19-
parse_quote,
19+
parse_quote, parse_quote_spanned,
2020
punctuated::Punctuated,
2121
spanned::Spanned,
2222
token::Comma,
@@ -26,105 +26,89 @@ use syn::{
2626
#[derive(Default)]
2727
pub struct PyModuleOptions {
2828
krate: Option<CrateAttribute>,
29-
name: Option<syn::Ident>,
29+
name: Option<NameAttribute>,
3030
module: Option<ModuleAttribute>,
31-
is_submodule: bool,
31+
submodule: Option<kw::submodule>,
3232
}
3333

34-
impl PyModuleOptions {
35-
pub fn from_attrs(attrs: &mut Vec<syn::Attribute>) -> Result<Self> {
34+
impl Parse for PyModuleOptions {
35+
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
3636
let mut options: PyModuleOptions = Default::default();
3737

38-
for option in take_pyo3_options(attrs)? {
39-
match option {
40-
PyModulePyO3Option::Name(name) => options.set_name(name.value.0)?,
41-
PyModulePyO3Option::Crate(path) => options.set_crate(path)?,
42-
PyModulePyO3Option::Module(module) => options.set_module(module)?,
43-
PyModulePyO3Option::Submodule(submod) => options.set_submodule(submod)?,
44-
}
45-
}
38+
options.add_attributes(
39+
Punctuated::<PyModulePyO3Option, syn::Token![,]>::parse_terminated(input)?,
40+
)?;
4641

4742
Ok(options)
4843
}
44+
}
4945

50-
fn set_name(&mut self, name: syn::Ident) -> Result<()> {
51-
ensure_spanned!(
52-
self.name.is_none(),
53-
name.span() => "`name` may only be specified once"
54-
);
55-
56-
self.name = Some(name);
57-
Ok(())
58-
}
59-
60-
fn set_crate(&mut self, path: CrateAttribute) -> Result<()> {
61-
ensure_spanned!(
62-
self.krate.is_none(),
63-
path.span() => "`crate` may only be specified once"
64-
);
65-
66-
self.krate = Some(path);
67-
Ok(())
68-
}
69-
70-
fn set_module(&mut self, name: ModuleAttribute) -> Result<()> {
71-
ensure_spanned!(
72-
self.module.is_none(),
73-
name.span() => "`module` may only be specified once"
74-
);
75-
76-
self.module = Some(name);
77-
Ok(())
46+
impl PyModuleOptions {
47+
fn take_pyo3_options(&mut self, attrs: &mut Vec<syn::Attribute>) -> Result<()> {
48+
self.add_attributes(take_pyo3_options(attrs)?)
7849
}
7950

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 (it is implicitly always specified for nested modules)"
84-
);
85-
86-
self.is_submodule = true;
51+
fn add_attributes(
52+
&mut self,
53+
attrs: impl IntoIterator<Item = PyModulePyO3Option>,
54+
) -> Result<()> {
55+
macro_rules! set_option {
56+
($key:ident $(, $extra:literal)?) => {
57+
{
58+
ensure_spanned!(
59+
self.$key.is_none(),
60+
$key.span() => concat!("`", stringify!($key), "` may only be specified once" $(, $extra)?)
61+
);
62+
self.$key = Some($key);
63+
}
64+
};
65+
}
66+
for attr in attrs {
67+
match attr {
68+
PyModulePyO3Option::Crate(krate) => set_option!(krate),
69+
PyModulePyO3Option::Name(name) => set_option!(name),
70+
PyModulePyO3Option::Module(module) => set_option!(module),
71+
PyModulePyO3Option::Submodule(submodule) => set_option!(
72+
submodule,
73+
" (it is implicitly always specified for nested modules)"
74+
),
75+
}
76+
}
8777
Ok(())
8878
}
8979
}
9080

9181
pub fn pymodule_module_impl(
92-
mut module: syn::ItemMod,
93-
mut is_submodule: bool,
82+
module: &mut syn::ItemMod,
83+
mut options: PyModuleOptions,
9484
) -> Result<TokenStream> {
9585
let syn::ItemMod {
9686
attrs,
9787
vis,
9888
unsafety: _,
9989
ident,
100-
mod_token: _,
90+
mod_token,
10191
content,
10292
semi: _,
103-
} = &mut module;
93+
} = module;
10494
let items = if let Some((_, items)) = content {
10595
items
10696
} else {
107-
bail_spanned!(module.span() => "`#[pymodule]` can only be used on inline modules")
97+
bail_spanned!(mod_token.span() => "`#[pymodule]` can only be used on inline modules")
10898
};
109-
let options = PyModuleOptions::from_attrs(attrs)?;
99+
options.take_pyo3_options(attrs)?;
110100
let ctx = &Ctx::new(&options.krate, None);
111101
let Ctx { pyo3_path, .. } = ctx;
112102
let doc = get_doc(attrs, None, ctx);
113-
let name = options.name.unwrap_or_else(|| ident.unraw());
103+
let name = options
104+
.name
105+
.map_or_else(|| ident.unraw(), |name| name.value.0);
114106
let full_name = if let Some(module) = &options.module {
115107
format!("{}.{}", module.value.value(), name)
116108
} else {
117109
name.to_string()
118110
};
119111

120-
is_submodule = match (is_submodule, options.is_submodule) {
121-
(true, true) => {
122-
bail_spanned!(module.span() => "`submodule` may only be specified once (it is implicitly always specified for nested modules)")
123-
}
124-
(false, false) => false,
125-
(true, false) | (false, true) => true,
126-
};
127-
128112
let mut module_items = Vec::new();
129113
let mut module_items_cfg_attrs = Vec::new();
130114

@@ -280,7 +264,9 @@ pub fn pymodule_module_impl(
280264
)? {
281265
set_module_attribute(&mut item_mod.attrs, &full_name);
282266
}
283-
item_mod.attrs.push(parse_quote!(#[pyo3(submodule)]));
267+
item_mod
268+
.attrs
269+
.push(parse_quote_spanned!(item_mod.mod_token.span()=> #[pyo3(submodule)]));
284270
}
285271
}
286272
Item::ForeignMod(item) => {
@@ -358,10 +344,11 @@ pub fn pymodule_module_impl(
358344
)
359345
}
360346
}};
361-
let initialization = module_initialization(&name, ctx, module_def, is_submodule);
347+
let initialization = module_initialization(&name, ctx, module_def, options.submodule.is_some());
348+
362349
Ok(quote!(
363350
#(#attrs)*
364-
#vis mod #ident {
351+
#vis #mod_token #ident {
365352
#(#items)*
366353

367354
#initialization
@@ -381,13 +368,18 @@ pub fn pymodule_module_impl(
381368

382369
/// Generates the function that is called by the python interpreter to initialize the native
383370
/// module
384-
pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result<TokenStream> {
385-
let options = PyModuleOptions::from_attrs(&mut function.attrs)?;
386-
process_functions_in_module(&options, &mut function)?;
371+
pub fn pymodule_function_impl(
372+
function: &mut syn::ItemFn,
373+
mut options: PyModuleOptions,
374+
) -> Result<TokenStream> {
375+
options.take_pyo3_options(&mut function.attrs)?;
376+
process_functions_in_module(&options, function)?;
387377
let ctx = &Ctx::new(&options.krate, None);
388378
let Ctx { pyo3_path, .. } = ctx;
389379
let ident = &function.sig.ident;
390-
let name = options.name.unwrap_or_else(|| ident.unraw());
380+
let name = options
381+
.name
382+
.map_or_else(|| ident.unraw(), |name| name.value.0);
391383
let vis = &function.vis;
392384
let doc = get_doc(&function.attrs, None, ctx);
393385

@@ -402,7 +394,6 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result<TokenStream>
402394
.push(quote!(::std::convert::Into::into(#pyo3_path::impl_::pymethods::BoundRef(module))));
403395

404396
Ok(quote! {
405-
#function
406397
#[doc(hidden)]
407398
#vis mod #ident {
408399
#initialization

pyo3-macros/src/lib.rs

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
44
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
55
use proc_macro::TokenStream;
6-
use proc_macro2::{Span, TokenStream as TokenStream2};
6+
use proc_macro2::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,
10-
PyFunctionOptions,
10+
PyFunctionOptions, PyModuleOptions,
1111
};
1212
use quote::quote;
13-
use syn::{parse::Nothing, parse_macro_input, Item};
13+
use syn::{parse_macro_input, Item};
1414

1515
/// A proc macro used to implement Python modules.
1616
///
@@ -24,6 +24,9 @@ use syn::{parse::Nothing, parse_macro_input, Item};
2424
/// | Annotation | Description |
2525
/// | :- | :- |
2626
/// | `#[pyo3(name = "...")]` | Defines the name of the module in Python. |
27+
/// | `#[pyo3(submodule)]` | Skips adding a `PyInit_` FFI symbol to the compiled binary. |
28+
/// | `#[pyo3(module = "...")]` | Defines the Python `dotted.path` to the parent module for use in introspection. |
29+
/// | `#[pyo3(crate = "pyo3")]` | Defines the path to PyO3 to use code generated by the macro. |
2730
///
2831
/// For more on creating Python modules see the [module section of the guide][1].
2932
///
@@ -35,32 +38,29 @@ use syn::{parse::Nothing, parse_macro_input, Item};
3538
#[doc = concat!("[1]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/module.html")]
3639
#[proc_macro_attribute]
3740
pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream {
38-
match parse_macro_input!(input as Item) {
41+
let options = parse_macro_input!(args as PyModuleOptions);
42+
43+
let mut ast = parse_macro_input!(input as Item);
44+
let expanded = match &mut ast {
3945
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)
46+
match pymodule_module_impl(module, options) {
47+
// #[pymodule] on a module will rebuild the original ast, so we don't emit it here
48+
Ok(expanded) => return expanded.into(),
49+
Err(e) => Err(e),
50+
}
5751
}
52+
Item::Fn(function) => pymodule_function_impl(function, options),
5853
unsupported => Err(syn::Error::new_spanned(
5954
unsupported,
6055
"#[pymodule] only supports modules and functions.",
6156
)),
6257
}
63-
.unwrap_or_compile_error()
58+
.unwrap_or_compile_error();
59+
60+
quote!(
61+
#ast
62+
#expanded
63+
)
6464
.into()
6565
}
6666

tests/test_declarative_module.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,7 @@ create_exception!(
4949
"Some description."
5050
);
5151

52-
#[pymodule]
53-
#[pyo3(submodule)]
52+
#[pymodule(submodule)]
5453
mod external_submodule {}
5554

5655
/// A module written using declarative syntax.
@@ -144,8 +143,7 @@ mod declarative_submodule {
144143
use super::{double, double_value};
145144
}
146145

147-
#[pymodule]
148-
#[pyo3(name = "declarative_module_renamed")]
146+
#[pymodule(name = "declarative_module_renamed")]
149147
mod declarative_module2 {
150148
#[pymodule_export]
151149
use super::double;

tests/test_module.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,7 @@ fn test_module_with_explicit_py_arg() {
138138
});
139139
}
140140

141-
#[pymodule]
142-
#[pyo3(name = "other_name")]
141+
#[pymodule(name = "other_name")]
143142
fn some_name(m: &Bound<'_, PyModule>) -> PyResult<()> {
144143
m.add("other_name", "other_name")?;
145144
Ok(())

tests/ui/duplicate_pymodule_submodule.stderr

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@ error: `submodule` may only be specified once (it is implicitly always specified
44
4 | mod submod {}
55
| ^^^
66

7-
error[E0433]: failed to resolve: use of undeclared crate or module `submod`
8-
--> tests/ui/duplicate_pymodule_submodule.rs:4:6
7+
error[E0425]: cannot find value `_PYO3_DEF` in module `submod`
8+
--> tests/ui/duplicate_pymodule_submodule.rs:1:1
9+
|
10+
1 | #[pyo3::pymodule]
11+
| ^^^^^^^^^^^^^^^^^ not found in `submod`
12+
|
13+
= note: this error originates in the attribute macro `pyo3::pymodule` (in Nightly builds, run with -Z macro-backtrace for more info)
14+
help: consider importing this static
15+
|
16+
3 + use crate::mymodule::_PYO3_DEF;
917
|
10-
4 | mod submod {}
11-
| ^^^^^^ use of undeclared crate or module `submod`

tests/ui/empty.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// see invalid_pymodule_in_root.rs

tests/ui/invalid_pymodule_args.stderr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
error: unexpected token
1+
error: expected one of: `name`, `crate`, `module`, `submodule`
22
--> tests/ui/invalid_pymodule_args.rs:3:12
33
|
44
3 | #[pymodule(some_arg)]

tests/ui/invalid_pymodule_glob.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#![allow(unused_imports)]
2+
13
use pyo3::prelude::*;
24

35
#[pyfunction]

tests/ui/invalid_pymodule_glob.stderr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
error: #[pymodule] cannot import glob statements
2-
--> tests/ui/invalid_pymodule_glob.rs:11:16
2+
--> tests/ui/invalid_pymodule_glob.rs:13:16
33
|
4-
11 | use super::*;
4+
13 | use super::*;
55
| ^

0 commit comments

Comments
 (0)