allow `#[pymodule(...)]` to accept all relevant `#[pyo3(...)]` options (#4330)

This commit is contained in:
David Hewitt 2024-07-10 23:38:38 +01:00 committed by GitHub
parent 6be80647cb
commit a5a3f3f7f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 125 additions and 120 deletions

View File

@ -75,8 +75,7 @@ impl ExampleContainer {
} }
} }
#[pymodule] #[pymodule(name = "getitem")]
#[pyo3(name = "getitem")]
fn example(m: &Bound<'_, PyModule>) -> PyResult<()> { fn example(m: &Bound<'_, PyModule>) -> PyResult<()> {
// ? -https://github.com/PyO3/maturin/issues/475 // ? -https://github.com/PyO3/maturin/issues/475
m.add_class::<ExampleContainer>()?; m.add_class::<ExampleContainer>()?;

View File

@ -31,8 +31,7 @@ fn double(x: usize) -> usize {
x * 2 x * 2
} }
#[pymodule] #[pymodule(name = "custom_name")]
#[pyo3(name = "custom_name")]
fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> { fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(double, m)?) m.add_function(wrap_pyfunction!(double, m)?)
} }

View File

@ -0,0 +1 @@
`#[pymodule(...)]` now directly accepts all relevant `#[pyo3(...)]` options.

View File

@ -2,8 +2,8 @@
use crate::{ use crate::{
attributes::{ attributes::{
self, take_attributes, take_pyo3_options, CrateAttribute, ModuleAttribute, NameAttribute, self, kw, take_attributes, take_pyo3_options, CrateAttribute, ModuleAttribute,
SubmoduleAttribute, NameAttribute, SubmoduleAttribute,
}, },
get_doc, get_doc,
pyclass::PyClassPyO3Option, pyclass::PyClassPyO3Option,
@ -16,7 +16,7 @@ use std::ffi::CString;
use syn::{ use syn::{
ext::IdentExt, ext::IdentExt,
parse::{Parse, ParseStream}, parse::{Parse, ParseStream},
parse_quote, parse_quote, parse_quote_spanned,
punctuated::Punctuated, punctuated::Punctuated,
spanned::Spanned, spanned::Spanned,
token::Comma, token::Comma,
@ -26,105 +26,89 @@ use syn::{
#[derive(Default)] #[derive(Default)]
pub struct PyModuleOptions { pub struct PyModuleOptions {
krate: Option<CrateAttribute>, krate: Option<CrateAttribute>,
name: Option<syn::Ident>, name: Option<NameAttribute>,
module: Option<ModuleAttribute>, module: Option<ModuleAttribute>,
is_submodule: bool, submodule: Option<kw::submodule>,
} }
impl PyModuleOptions { impl Parse for PyModuleOptions {
pub fn from_attrs(attrs: &mut Vec<syn::Attribute>) -> Result<Self> { fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let mut options: PyModuleOptions = Default::default(); let mut options: PyModuleOptions = Default::default();
for option in take_pyo3_options(attrs)? { options.add_attributes(
match option { Punctuated::<PyModulePyO3Option, syn::Token![,]>::parse_terminated(input)?,
PyModulePyO3Option::Name(name) => options.set_name(name.value.0)?, )?;
PyModulePyO3Option::Crate(path) => options.set_crate(path)?,
PyModulePyO3Option::Module(module) => options.set_module(module)?,
PyModulePyO3Option::Submodule(submod) => options.set_submodule(submod)?,
}
}
Ok(options) Ok(options)
} }
}
fn set_name(&mut self, name: syn::Ident) -> Result<()> { impl PyModuleOptions {
ensure_spanned!( fn take_pyo3_options(&mut self, attrs: &mut Vec<syn::Attribute>) -> Result<()> {
self.name.is_none(), self.add_attributes(take_pyo3_options(attrs)?)
name.span() => "`name` may only be specified once"
);
self.name = Some(name);
Ok(())
} }
fn set_crate(&mut self, path: CrateAttribute) -> Result<()> { fn add_attributes(
ensure_spanned!( &mut self,
self.krate.is_none(), attrs: impl IntoIterator<Item = PyModulePyO3Option>,
path.span() => "`crate` may only be specified once" ) -> Result<()> {
); macro_rules! set_option {
($key:ident $(, $extra:literal)?) => {
self.krate = Some(path); {
Ok(()) ensure_spanned!(
} self.$key.is_none(),
$key.span() => concat!("`", stringify!($key), "` may only be specified once" $(, $extra)?)
fn set_module(&mut self, name: ModuleAttribute) -> Result<()> { );
ensure_spanned!( self.$key = Some($key);
self.module.is_none(), }
name.span() => "`module` may only be specified once" };
); }
for attr in attrs {
self.module = Some(name); match attr {
Ok(()) PyModulePyO3Option::Crate(krate) => set_option!(krate),
} PyModulePyO3Option::Name(name) => set_option!(name),
PyModulePyO3Option::Module(module) => set_option!(module),
fn set_submodule(&mut self, submod: SubmoduleAttribute) -> Result<()> { PyModulePyO3Option::Submodule(submodule) => set_option!(
ensure_spanned!( submodule,
!self.is_submodule, " (it is implicitly always specified for nested modules)"
submod.span() => "`submodule` may only be specified once (it is implicitly always specified for nested modules)" ),
); }
}
self.is_submodule = true;
Ok(()) Ok(())
} }
} }
pub fn pymodule_module_impl( pub fn pymodule_module_impl(
mut module: syn::ItemMod, module: &mut syn::ItemMod,
mut is_submodule: bool, mut options: PyModuleOptions,
) -> Result<TokenStream> { ) -> Result<TokenStream> {
let syn::ItemMod { let syn::ItemMod {
attrs, attrs,
vis, vis,
unsafety: _, unsafety: _,
ident, ident,
mod_token: _, mod_token,
content, content,
semi: _, semi: _,
} = &mut module; } = module;
let items = if let Some((_, items)) = content { let items = if let Some((_, items)) = content {
items items
} else { } else {
bail_spanned!(module.span() => "`#[pymodule]` can only be used on inline modules") bail_spanned!(mod_token.span() => "`#[pymodule]` can only be used on inline modules")
}; };
let options = PyModuleOptions::from_attrs(attrs)?; options.take_pyo3_options(attrs)?;
let ctx = &Ctx::new(&options.krate, None); let ctx = &Ctx::new(&options.krate, None);
let Ctx { pyo3_path, .. } = ctx; let Ctx { pyo3_path, .. } = ctx;
let doc = get_doc(attrs, None, ctx); let doc = get_doc(attrs, None, ctx);
let name = options.name.unwrap_or_else(|| ident.unraw()); let name = options
.name
.map_or_else(|| ident.unraw(), |name| name.value.0);
let full_name = if let Some(module) = &options.module { let full_name = if let Some(module) = &options.module {
format!("{}.{}", module.value.value(), name) format!("{}.{}", module.value.value(), name)
} else { } else {
name.to_string() name.to_string()
}; };
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 = Vec::new();
let mut module_items_cfg_attrs = Vec::new(); let mut module_items_cfg_attrs = Vec::new();
@ -280,7 +264,9 @@ pub fn pymodule_module_impl(
)? { )? {
set_module_attribute(&mut item_mod.attrs, &full_name); set_module_attribute(&mut item_mod.attrs, &full_name);
} }
item_mod.attrs.push(parse_quote!(#[pyo3(submodule)])); item_mod
.attrs
.push(parse_quote_spanned!(item_mod.mod_token.span()=> #[pyo3(submodule)]));
} }
} }
Item::ForeignMod(item) => { Item::ForeignMod(item) => {
@ -358,10 +344,11 @@ pub fn pymodule_module_impl(
) )
} }
}}; }};
let initialization = module_initialization(&name, ctx, module_def, is_submodule); let initialization = module_initialization(&name, ctx, module_def, options.submodule.is_some());
Ok(quote!( Ok(quote!(
#(#attrs)* #(#attrs)*
#vis mod #ident { #vis #mod_token #ident {
#(#items)* #(#items)*
#initialization #initialization
@ -381,13 +368,18 @@ pub fn pymodule_module_impl(
/// Generates the function that is called by the python interpreter to initialize the native /// Generates the function that is called by the python interpreter to initialize the native
/// module /// module
pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result<TokenStream> { pub fn pymodule_function_impl(
let options = PyModuleOptions::from_attrs(&mut function.attrs)?; function: &mut syn::ItemFn,
process_functions_in_module(&options, &mut function)?; mut options: PyModuleOptions,
) -> Result<TokenStream> {
options.take_pyo3_options(&mut function.attrs)?;
process_functions_in_module(&options, function)?;
let ctx = &Ctx::new(&options.krate, None); let ctx = &Ctx::new(&options.krate, None);
let Ctx { pyo3_path, .. } = ctx; let Ctx { pyo3_path, .. } = ctx;
let ident = &function.sig.ident; let ident = &function.sig.ident;
let name = options.name.unwrap_or_else(|| ident.unraw()); let name = options
.name
.map_or_else(|| ident.unraw(), |name| name.value.0);
let vis = &function.vis; let vis = &function.vis;
let doc = get_doc(&function.attrs, None, ctx); let doc = get_doc(&function.attrs, None, ctx);
@ -402,7 +394,6 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result<TokenStream>
.push(quote!(::std::convert::Into::into(#pyo3_path::impl_::pymethods::BoundRef(module)))); .push(quote!(::std::convert::Into::into(#pyo3_path::impl_::pymethods::BoundRef(module))));
Ok(quote! { Ok(quote! {
#function
#[doc(hidden)] #[doc(hidden)]
#vis mod #ident { #vis mod #ident {
#initialization #initialization

View File

@ -3,14 +3,14 @@
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
use proc_macro::TokenStream; use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2}; use proc_macro2::TokenStream as TokenStream2;
use pyo3_macros_backend::{ use pyo3_macros_backend::{
build_derive_from_pyobject, build_py_class, build_py_enum, build_py_function, build_py_methods, build_derive_from_pyobject, build_py_class, build_py_enum, build_py_function, build_py_methods,
pymodule_function_impl, pymodule_module_impl, PyClassArgs, PyClassMethodsType, pymodule_function_impl, pymodule_module_impl, PyClassArgs, PyClassMethodsType,
PyFunctionOptions, PyFunctionOptions, PyModuleOptions,
}; };
use quote::quote; use quote::quote;
use syn::{parse::Nothing, parse_macro_input, Item}; use syn::{parse_macro_input, Item};
/// A proc macro used to implement Python modules. /// A proc macro used to implement Python modules.
/// ///
@ -24,6 +24,9 @@ use syn::{parse::Nothing, parse_macro_input, Item};
/// | Annotation | Description | /// | Annotation | Description |
/// | :- | :- | /// | :- | :- |
/// | `#[pyo3(name = "...")]` | Defines the name of the module in Python. | /// | `#[pyo3(name = "...")]` | Defines the name of the module in Python. |
/// | `#[pyo3(submodule)]` | Skips adding a `PyInit_` FFI symbol to the compiled binary. |
/// | `#[pyo3(module = "...")]` | Defines the Python `dotted.path` to the parent module for use in introspection. |
/// | `#[pyo3(crate = "pyo3")]` | Defines the path to PyO3 to use code generated by the macro. |
/// ///
/// For more on creating Python modules see the [module section of the guide][1]. /// For more on creating Python modules see the [module section of the guide][1].
/// ///
@ -35,32 +38,29 @@ use syn::{parse::Nothing, parse_macro_input, Item};
#[doc = concat!("[1]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/module.html")] #[doc = concat!("[1]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/module.html")]
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream { pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream {
match parse_macro_input!(input as Item) { let options = parse_macro_input!(args as PyModuleOptions);
let mut ast = parse_macro_input!(input as Item);
let expanded = match &mut ast {
Item::Mod(module) => { Item::Mod(module) => {
let is_submodule = match parse_macro_input!(args as Option<syn::Ident>) { match pymodule_module_impl(module, options) {
Some(i) if i == "submodule" => true, // #[pymodule] on a module will rebuild the original ast, so we don't emit it here
Some(_) => { Ok(expanded) => return expanded.into(),
return syn::Error::new( Err(e) => Err(e),
Span::call_site(), }
"#[pymodule] only accepts submodule as an argument",
)
.into_compile_error()
.into();
}
None => false,
};
pymodule_module_impl(module, is_submodule)
}
Item::Fn(function) => {
parse_macro_input!(args as Nothing);
pymodule_function_impl(function)
} }
Item::Fn(function) => pymodule_function_impl(function, options),
unsupported => Err(syn::Error::new_spanned( unsupported => Err(syn::Error::new_spanned(
unsupported, unsupported,
"#[pymodule] only supports modules and functions.", "#[pymodule] only supports modules and functions.",
)), )),
} }
.unwrap_or_compile_error() .unwrap_or_compile_error();
quote!(
#ast
#expanded
)
.into() .into()
} }

View File

@ -49,8 +49,7 @@ create_exception!(
"Some description." "Some description."
); );
#[pymodule] #[pymodule(submodule)]
#[pyo3(submodule)]
mod external_submodule {} mod external_submodule {}
/// A module written using declarative syntax. /// A module written using declarative syntax.
@ -144,8 +143,7 @@ mod declarative_submodule {
use super::{double, double_value}; use super::{double, double_value};
} }
#[pymodule] #[pymodule(name = "declarative_module_renamed")]
#[pyo3(name = "declarative_module_renamed")]
mod declarative_module2 { mod declarative_module2 {
#[pymodule_export] #[pymodule_export]
use super::double; use super::double;

View File

@ -138,8 +138,7 @@ fn test_module_with_explicit_py_arg() {
}); });
} }
#[pymodule] #[pymodule(name = "other_name")]
#[pyo3(name = "other_name")]
fn some_name(m: &Bound<'_, PyModule>) -> PyResult<()> { fn some_name(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add("other_name", "other_name")?; m.add("other_name", "other_name")?;
Ok(()) Ok(())

View File

@ -4,8 +4,14 @@ error: `submodule` may only be specified once (it is implicitly always specified
4 | mod submod {} 4 | mod submod {}
| ^^^ | ^^^
error[E0433]: failed to resolve: use of undeclared crate or module `submod` error[E0425]: cannot find value `_PYO3_DEF` in module `submod`
--> tests/ui/duplicate_pymodule_submodule.rs:4:6 --> tests/ui/duplicate_pymodule_submodule.rs:1:1
|
1 | #[pyo3::pymodule]
| ^^^^^^^^^^^^^^^^^ not found in `submod`
|
= note: this error originates in the attribute macro `pyo3::pymodule` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider importing this static
|
3 + use crate::mymodule::_PYO3_DEF;
| |
4 | mod submod {}
| ^^^^^^ use of undeclared crate or module `submod`

1
tests/ui/empty.rs Normal file
View File

@ -0,0 +1 @@
// see invalid_pymodule_in_root.rs

View File

@ -1,4 +1,4 @@
error: unexpected token error: expected one of: `name`, `crate`, `module`, `submodule`
--> tests/ui/invalid_pymodule_args.rs:3:12 --> tests/ui/invalid_pymodule_args.rs:3:12
| |
3 | #[pymodule(some_arg)] 3 | #[pymodule(some_arg)]

View File

@ -1,3 +1,5 @@
#![allow(unused_imports)]
use pyo3::prelude::*; use pyo3::prelude::*;
#[pyfunction] #[pyfunction]

View File

@ -1,5 +1,5 @@
error: #[pymodule] cannot import glob statements error: #[pymodule] cannot import glob statements
--> tests/ui/invalid_pymodule_glob.rs:11:16 --> tests/ui/invalid_pymodule_glob.rs:13:16
| |
11 | use super::*; 13 | use super::*;
| ^ | ^

View File

@ -1,6 +1,7 @@
use pyo3::prelude::*; use pyo3::prelude::*;
#[pymodule] #[pymodule]
#[path = "empty.rs"] // to silence error related to missing file
mod invalid_pymodule_in_root_module; mod invalid_pymodule_in_root_module;
fn main() {} fn main() {}

View File

@ -1,13 +1,13 @@
error[E0658]: non-inline modules in proc macro input are unstable error[E0658]: non-inline modules in proc macro input are unstable
--> tests/ui/invalid_pymodule_in_root.rs:4:1 --> tests/ui/invalid_pymodule_in_root.rs:5:1
| |
4 | mod invalid_pymodule_in_root_module; 5 | mod invalid_pymodule_in_root_module;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| |
= note: see issue #54727 <https://github.com/rust-lang/rust/issues/54727> for more information = note: see issue #54727 <https://github.com/rust-lang/rust/issues/54727> for more information
error: `#[pymodule]` can only be used on inline modules error: `#[pymodule]` can only be used on inline modules
--> tests/ui/invalid_pymodule_in_root.rs:4:1 --> tests/ui/invalid_pymodule_in_root.rs:5:1
| |
4 | mod invalid_pymodule_in_root_module; 5 | mod invalid_pymodule_in_root_module;
| ^^^ | ^^^

View File

@ -3,3 +3,9 @@ error: `#[pymodule_export]` may only be used on `use` statements
| |
5 | #[pymodule_export] 5 | #[pymodule_export]
| ^ | ^
error: cannot find attribute `pymodule_export` in this scope
--> tests/ui/invalid_pymodule_trait.rs:5:7
|
5 | #[pymodule_export]
| ^^^^^^^^^^^^^^^

View File

@ -2,13 +2,15 @@ use pyo3::prelude::*;
#[pymodule] #[pymodule]
mod module { mod module {
use pyo3::prelude::*;
#[pymodule_init] #[pymodule_init]
fn init(m: &PyModule) -> PyResult<()> { fn init(_m: &Bound<'_, PyModule>) -> PyResult<()> {
Ok(()) Ok(())
} }
#[pymodule_init] #[pymodule_init]
fn init2(m: &PyModule) -> PyResult<()> { fn init2(_m: &Bound<'_, PyModule>) -> PyResult<()> {
Ok(()) Ok(())
} }
} }

View File

@ -1,5 +1,5 @@
error: only one `#[pymodule_init]` may be specified error: only one `#[pymodule_init]` may be specified
--> tests/ui/invalid_pymodule_two_pymodule_init.rs:11:5 --> tests/ui/invalid_pymodule_two_pymodule_init.rs:13:5
| |
11 | fn init2(m: &PyModule) -> PyResult<()> { 13 | fn init2(_m: &Bound<'_, PyModule>) -> PyResult<()> {
| ^^ | ^^