Skip to content

Commit e0e3981

Browse files
Tptdavidhewittbirkenfeld
authored
#[pymodule] mod some_module { ... } v3 (#3815)
* #[pymodule] mod some_module { ... } v3 Based on #2367 and #3294 Allows to export classes, native classes, functions and submodules and provide an init function See test/test_module.rs for an example Future work: - update examples, README and guide - investigate having #[pyclass] and #[pyfunction] directly in the #[pymodule] Co-authored-by: David Hewitt <mail@davidhewitt.dev> Co-authored-by: Georg Brandl <georg@python.org> * tests: group exported imports * Consolidate pymodule macro code to avoid duplicates * Makes pymodule_init take Bound<'_, PyModule> * Renames #[pyo3] to #[pymodule_export] * Gates #[pymodule] mod behind the experimental-declarative-modules feature * Properly fails on functions inside of declarative modules --------- Co-authored-by: David Hewitt <mail@davidhewitt.dev> Co-authored-by: Georg Brandl <georg@python.org>
1 parent c06bb8f commit e0e3981

20 files changed

+458
-71
lines changed

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ default = ["macros"]
6969
# and IntoPy traits
7070
experimental-inspect = []
7171

72+
# Enables annotating Rust inline modules with #[pymodule] to build Python modules declaratively
73+
experimental-declarative-modules = ["pyo3-macros/experimental-declarative-modules", "macros"]
74+
7275
# Enables macros: #[pyclass], #[pymodule], #[pyfunction] etc.
7376
macros = ["pyo3-macros", "indoc", "unindent"]
7477

@@ -114,6 +117,7 @@ full = [
114117
"chrono-tz",
115118
"either",
116119
"experimental-inspect",
120+
"experimental-declarative-modules",
117121
"eyre",
118122
"hashbrown",
119123
"indexmap",

newsfragments/3815.added.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
The ability to create Python modules with a Rust `mod` block
2+
behind the `experimental-declarative-modules` feature.

pyo3-macros-backend/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ mod pymethod;
2222
mod quotes;
2323

2424
pub use frompyobject::build_derive_from_pyobject;
25-
pub use module::{process_functions_in_module, pymodule_impl, PyModuleOptions};
25+
pub use module::{pymodule_function_impl, pymodule_module_impl, PyModuleOptions};
2626
pub use pyclass::{build_py_class, build_py_enum, PyClassArgs};
2727
pub use pyfunction::{build_py_function, PyFunctionOptions};
2828
pub use pyimpl::{build_py_methods, PyClassMethodsType};

pyo3-macros-backend/src/module.rs

Lines changed: 186 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
33
use crate::{
44
attributes::{self, take_attributes, take_pyo3_options, CrateAttribute, NameAttribute},
5+
get_doc,
56
pyfunction::{impl_wrap_pyfunction, PyFunctionOptions},
6-
utils::{get_pyo3_crate, PythonDoc},
7+
utils::get_pyo3_crate,
78
};
89
use proc_macro2::TokenStream;
910
use quote::quote;
@@ -12,7 +13,7 @@ use syn::{
1213
parse::{Parse, ParseStream},
1314
spanned::Spanned,
1415
token::Comma,
15-
Ident, Path, Result, Visibility,
16+
Item, Path, Result,
1617
};
1718

1819
#[derive(Default)]
@@ -56,33 +57,154 @@ impl PyModuleOptions {
5657
}
5758
}
5859

60+
pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result<TokenStream> {
61+
let syn::ItemMod {
62+
attrs,
63+
vis,
64+
unsafety: _,
65+
ident,
66+
mod_token: _,
67+
content,
68+
semi: _,
69+
} = &mut module;
70+
let items = if let Some((_, items)) = content {
71+
items
72+
} else {
73+
bail_spanned!(module.span() => "`#[pymodule]` can only be used on inline modules")
74+
};
75+
let options = PyModuleOptions::from_attrs(attrs)?;
76+
let krate = get_pyo3_crate(&options.krate);
77+
let doc = get_doc(attrs, None);
78+
79+
let mut module_items = Vec::new();
80+
let mut module_items_cfg_attrs = Vec::new();
81+
82+
fn extract_use_items(
83+
source: &syn::UseTree,
84+
cfg_attrs: &[syn::Attribute],
85+
target_items: &mut Vec<syn::Ident>,
86+
target_cfg_attrs: &mut Vec<Vec<syn::Attribute>>,
87+
) -> Result<()> {
88+
match source {
89+
syn::UseTree::Name(name) => {
90+
target_items.push(name.ident.clone());
91+
target_cfg_attrs.push(cfg_attrs.to_vec());
92+
}
93+
syn::UseTree::Path(path) => {
94+
extract_use_items(&path.tree, cfg_attrs, target_items, target_cfg_attrs)?
95+
}
96+
syn::UseTree::Group(group) => {
97+
for tree in &group.items {
98+
extract_use_items(tree, cfg_attrs, target_items, target_cfg_attrs)?
99+
}
100+
}
101+
syn::UseTree::Glob(glob) => {
102+
bail_spanned!(glob.span() => "#[pymodule] cannot import glob statements")
103+
}
104+
syn::UseTree::Rename(rename) => {
105+
target_items.push(rename.rename.clone());
106+
target_cfg_attrs.push(cfg_attrs.to_vec());
107+
}
108+
}
109+
Ok(())
110+
}
111+
112+
let mut pymodule_init = None;
113+
114+
for item in &mut *items {
115+
match item {
116+
Item::Use(item_use) => {
117+
let mut is_pyo3 = false;
118+
item_use.attrs.retain(|attr| {
119+
let found = attr.path().is_ident("pymodule_export");
120+
is_pyo3 |= found;
121+
!found
122+
});
123+
if is_pyo3 {
124+
let cfg_attrs = item_use
125+
.attrs
126+
.iter()
127+
.filter(|attr| attr.path().is_ident("cfg"))
128+
.cloned()
129+
.collect::<Vec<_>>();
130+
extract_use_items(
131+
&item_use.tree,
132+
&cfg_attrs,
133+
&mut module_items,
134+
&mut module_items_cfg_attrs,
135+
)?;
136+
}
137+
}
138+
Item::Fn(item_fn) => {
139+
let mut is_module_init = false;
140+
item_fn.attrs.retain(|attr| {
141+
let found = attr.path().is_ident("pymodule_init");
142+
is_module_init |= found;
143+
!found
144+
});
145+
if is_module_init {
146+
ensure_spanned!(pymodule_init.is_none(), item_fn.span() => "only one pymodule_init may be specified");
147+
let ident = &item_fn.sig.ident;
148+
pymodule_init = Some(quote! { #ident(module)?; });
149+
} else {
150+
bail_spanned!(item.span() => "only 'use' statements and and pymodule_init functions are allowed in #[pymodule]")
151+
}
152+
}
153+
item => {
154+
bail_spanned!(item.span() => "only 'use' statements and and pymodule_init functions are allowed in #[pymodule]")
155+
}
156+
}
157+
}
158+
159+
let initialization = module_initialization(options, ident);
160+
Ok(quote!(
161+
#vis mod #ident {
162+
#(#items)*
163+
164+
#initialization
165+
166+
impl MakeDef {
167+
const fn make_def() -> #krate::impl_::pymodule::ModuleDef {
168+
use #krate::impl_::pymodule as impl_;
169+
const INITIALIZER: impl_::ModuleInitializer = impl_::ModuleInitializer(__pyo3_pymodule);
170+
unsafe {
171+
impl_::ModuleDef::new(
172+
__PYO3_NAME,
173+
#doc,
174+
INITIALIZER
175+
)
176+
}
177+
}
178+
}
179+
180+
fn __pyo3_pymodule(module: &#krate::Bound<'_, #krate::types::PyModule>) -> #krate::PyResult<()> {
181+
use #krate::impl_::pymodule::PyAddToModule;
182+
#(
183+
#(#module_items_cfg_attrs)*
184+
#module_items::add_to_module(module)?;
185+
)*
186+
#pymodule_init
187+
Ok(())
188+
}
189+
}
190+
))
191+
}
192+
59193
/// Generates the function that is called by the python interpreter to initialize the native
60194
/// module
61-
pub fn pymodule_impl(
62-
fnname: &Ident,
63-
options: PyModuleOptions,
64-
doc: PythonDoc,
65-
visibility: &Visibility,
66-
) -> TokenStream {
67-
let name = options.name.unwrap_or_else(|| fnname.unraw());
195+
pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result<TokenStream> {
196+
let options = PyModuleOptions::from_attrs(&mut function.attrs)?;
197+
process_functions_in_module(&options, &mut function)?;
68198
let krate = get_pyo3_crate(&options.krate);
69-
let pyinit_symbol = format!("PyInit_{}", name);
199+
let ident = &function.sig.ident;
200+
let vis = &function.vis;
201+
let doc = get_doc(&function.attrs, None);
70202

71-
quote! {
72-
// Create a module with the same name as the `#[pymodule]` - this way `use <the module>`
73-
// will actually bring both the module and the function into scope.
74-
#[doc(hidden)]
75-
#visibility mod #fnname {
76-
pub(crate) struct MakeDef;
77-
pub static DEF: #krate::impl_::pymodule::ModuleDef = MakeDef::make_def();
78-
pub const NAME: &'static str = concat!(stringify!(#name), "\0");
79-
80-
/// This autogenerated function is called by the python interpreter when importing
81-
/// the module.
82-
#[export_name = #pyinit_symbol]
83-
pub unsafe extern "C" fn init() -> *mut #krate::ffi::PyObject {
84-
#krate::impl_::trampoline::module_init(|py| DEF.make_module(py))
85-
}
203+
let initialization = module_initialization(options, ident);
204+
Ok(quote! {
205+
#function
206+
#vis mod #ident {
207+
#initialization
86208
}
87209

88210
// Generate the definition inside an anonymous function in the same scope as the original function -
@@ -91,28 +213,59 @@ pub fn pymodule_impl(
91213
// inside a function body)
92214
const _: () = {
93215
use #krate::impl_::pymodule as impl_;
94-
impl #fnname::MakeDef {
216+
217+
fn __pyo3_pymodule(module: &#krate::Bound<'_, #krate::types::PyModule>) -> #krate::PyResult<()> {
218+
#ident(module.py(), module.as_gil_ref())
219+
}
220+
221+
impl #ident::MakeDef {
95222
const fn make_def() -> impl_::ModuleDef {
96-
const INITIALIZER: impl_::ModuleInitializer = impl_::ModuleInitializer(#fnname);
97223
unsafe {
98-
impl_::ModuleDef::new(#fnname::NAME, #doc, INITIALIZER)
224+
const INITIALIZER: impl_::ModuleInitializer = impl_::ModuleInitializer(__pyo3_pymodule);
225+
impl_::ModuleDef::new(
226+
#ident::__PYO3_NAME,
227+
#doc,
228+
INITIALIZER
229+
)
99230
}
100231
}
101232
}
102233
};
234+
})
235+
}
236+
237+
fn module_initialization(options: PyModuleOptions, ident: &syn::Ident) -> TokenStream {
238+
let name = options.name.unwrap_or_else(|| ident.unraw());
239+
let krate = get_pyo3_crate(&options.krate);
240+
let pyinit_symbol = format!("PyInit_{}", name);
241+
242+
quote! {
243+
pub const __PYO3_NAME: &'static str = concat!(stringify!(#name), "\0");
244+
245+
pub(super) struct MakeDef;
246+
pub static DEF: #krate::impl_::pymodule::ModuleDef = MakeDef::make_def();
247+
248+
pub fn add_to_module(module: &#krate::Bound<'_, #krate::types::PyModule>) -> #krate::PyResult<()> {
249+
use #krate::prelude::PyModuleMethods;
250+
module.add_submodule(DEF.make_module(module.py())?.bind(module.py()))
251+
}
252+
253+
/// This autogenerated function is called by the python interpreter when importing
254+
/// the module.
255+
#[export_name = #pyinit_symbol]
256+
pub unsafe extern "C" fn __pyo3_init() -> *mut #krate::ffi::PyObject {
257+
#krate::impl_::trampoline::module_init(|py| DEF.make_module(py))
258+
}
103259
}
104260
}
105261

106262
/// Finds and takes care of the #[pyfn(...)] in `#[pymodule]`
107-
pub fn process_functions_in_module(
108-
options: &PyModuleOptions,
109-
func: &mut syn::ItemFn,
110-
) -> syn::Result<()> {
263+
fn process_functions_in_module(options: &PyModuleOptions, func: &mut syn::ItemFn) -> Result<()> {
111264
let mut stmts: Vec<syn::Stmt> = Vec::new();
112265
let krate = get_pyo3_crate(&options.krate);
113266

114267
for mut stmt in func.block.stmts.drain(..) {
115-
if let syn::Stmt::Item(syn::Item::Fn(func)) = &mut stmt {
268+
if let syn::Stmt::Item(Item::Fn(func)) = &mut stmt {
116269
if let Some(pyfn_args) = get_pyfn_attr(&mut func.attrs)? {
117270
let module_name = pyfn_args.modname;
118271
let wrapped_function = impl_wrap_pyfunction(func, pyfn_args.options)?;

pyo3-macros-backend/src/pyfunction.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,12 @@ pub fn impl_wrap_pyfunction(
269269
#vis mod #name {
270270
pub(crate) struct MakeDef;
271271
pub const DEF: #krate::impl_::pyfunction::PyMethodDef = MakeDef::DEF;
272+
273+
pub fn add_to_module(module: &#krate::Bound<'_, #krate::types::PyModule>) -> #krate::PyResult<()> {
274+
use #krate::prelude::PyModuleMethods;
275+
use ::std::convert::Into;
276+
module.add_function(&#krate::types::PyCFunction::internal_new(&DEF, module.as_gil_ref().into())?)
277+
}
272278
}
273279

274280
// Generate the definition inside an anonymous function in the same scope as the original function -

pyo3-macros/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ proc-macro = true
1515

1616
[features]
1717
multiple-pymethods = []
18+
experimental-declarative-modules = []
1819

1920
[dependencies]
2021
proc-macro2 = { version = "1", default-features = false }

pyo3-macros/src/lib.rs

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ use proc_macro::TokenStream;
66
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,
9-
get_doc, process_functions_in_module, pymodule_impl, PyClassArgs, PyClassMethodsType,
10-
PyFunctionOptions, PyModuleOptions,
9+
pymodule_function_impl, pymodule_module_impl, PyClassArgs, PyClassMethodsType,
10+
PyFunctionOptions,
1111
};
1212
use quote::quote;
13-
use syn::{parse::Nothing, parse_macro_input};
13+
use syn::{parse::Nothing, parse_macro_input, Item};
1414

1515
/// A proc macro used to implement Python modules.
1616
///
@@ -36,31 +36,27 @@ use syn::{parse::Nothing, parse_macro_input};
3636
#[proc_macro_attribute]
3737
pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream {
3838
parse_macro_input!(args as Nothing);
39-
40-
let mut ast = parse_macro_input!(input as syn::ItemFn);
41-
let options = match PyModuleOptions::from_attrs(&mut ast.attrs) {
42-
Ok(options) => options,
43-
Err(e) => return e.into_compile_error().into(),
44-
};
45-
46-
if let Err(err) = process_functions_in_module(&options, &mut ast) {
47-
return err.into_compile_error().into();
39+
match parse_macro_input!(input as Item) {
40+
Item::Mod(module) => if cfg!(feature = "experimental-declarative-modules") {
41+
pymodule_module_impl(module)
42+
} else {
43+
Err(syn::Error::new_spanned(
44+
module,
45+
"#[pymodule] requires the 'experimental-declarative-modules' feature to be used on Rust modules.",
46+
))
47+
},
48+
Item::Fn(function) => pymodule_function_impl(function),
49+
unsupported => Err(syn::Error::new_spanned(
50+
unsupported,
51+
"#[pymodule] only supports modules and functions.",
52+
)),
4853
}
49-
50-
let doc = get_doc(&ast.attrs, None);
51-
52-
let expanded = pymodule_impl(&ast.sig.ident, options, doc, &ast.vis);
53-
54-
quote!(
55-
#ast
56-
#expanded
57-
)
54+
.unwrap_or_compile_error()
5855
.into()
5956
}
6057

6158
#[proc_macro_attribute]
6259
pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream {
63-
use syn::Item;
6460
let item = parse_macro_input!(input as Item);
6561
match item {
6662
Item::Struct(struct_) => pyclass_impl(attr, struct_, methods_type()),

0 commit comments

Comments
 (0)