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

View File

@ -31,8 +31,7 @@ fn double(x: usize) -> usize {
x * 2
}
#[pymodule]
#[pyo3(name = "custom_name")]
#[pymodule(name = "custom_name")]
fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> {
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::{
attributes::{
self, take_attributes, take_pyo3_options, CrateAttribute, ModuleAttribute, NameAttribute,
SubmoduleAttribute,
self, kw, take_attributes, take_pyo3_options, CrateAttribute, ModuleAttribute,
NameAttribute, SubmoduleAttribute,
},
get_doc,
pyclass::PyClassPyO3Option,
@ -16,7 +16,7 @@ use std::ffi::CString;
use syn::{
ext::IdentExt,
parse::{Parse, ParseStream},
parse_quote,
parse_quote, parse_quote_spanned,
punctuated::Punctuated,
spanned::Spanned,
token::Comma,
@ -26,105 +26,89 @@ use syn::{
#[derive(Default)]
pub struct PyModuleOptions {
krate: Option<CrateAttribute>,
name: Option<syn::Ident>,
name: Option<NameAttribute>,
module: Option<ModuleAttribute>,
is_submodule: bool,
submodule: Option<kw::submodule>,
}
impl PyModuleOptions {
pub fn from_attrs(attrs: &mut Vec<syn::Attribute>) -> Result<Self> {
impl Parse for PyModuleOptions {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let mut options: PyModuleOptions = Default::default();
for option in take_pyo3_options(attrs)? {
match option {
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)?,
}
}
options.add_attributes(
Punctuated::<PyModulePyO3Option, syn::Token![,]>::parse_terminated(input)?,
)?;
Ok(options)
}
}
fn set_name(&mut self, name: syn::Ident) -> Result<()> {
ensure_spanned!(
self.name.is_none(),
name.span() => "`name` may only be specified once"
);
self.name = Some(name);
Ok(())
impl PyModuleOptions {
fn take_pyo3_options(&mut self, attrs: &mut Vec<syn::Attribute>) -> Result<()> {
self.add_attributes(take_pyo3_options(attrs)?)
}
fn set_crate(&mut self, path: CrateAttribute) -> Result<()> {
fn add_attributes(
&mut self,
attrs: impl IntoIterator<Item = PyModulePyO3Option>,
) -> Result<()> {
macro_rules! set_option {
($key:ident $(, $extra:literal)?) => {
{
ensure_spanned!(
self.krate.is_none(),
path.span() => "`crate` may only be specified once"
self.$key.is_none(),
$key.span() => concat!("`", stringify!($key), "` may only be specified once" $(, $extra)?)
);
self.krate = Some(path);
Ok(())
self.$key = Some($key);
}
};
}
for attr in attrs {
match attr {
PyModulePyO3Option::Crate(krate) => set_option!(krate),
PyModulePyO3Option::Name(name) => set_option!(name),
PyModulePyO3Option::Module(module) => set_option!(module),
PyModulePyO3Option::Submodule(submodule) => set_option!(
submodule,
" (it is implicitly always specified for nested modules)"
),
}
fn set_module(&mut self, name: ModuleAttribute) -> Result<()> {
ensure_spanned!(
self.module.is_none(),
name.span() => "`module` may only be specified once"
);
self.module = Some(name);
Ok(())
}
fn set_submodule(&mut self, submod: SubmoduleAttribute) -> Result<()> {
ensure_spanned!(
!self.is_submodule,
submod.span() => "`submodule` may only be specified once (it is implicitly always specified for nested modules)"
);
self.is_submodule = true;
Ok(())
}
}
pub fn pymodule_module_impl(
mut module: syn::ItemMod,
mut is_submodule: bool,
module: &mut syn::ItemMod,
mut options: PyModuleOptions,
) -> Result<TokenStream> {
let syn::ItemMod {
attrs,
vis,
unsafety: _,
ident,
mod_token: _,
mod_token,
content,
semi: _,
} = &mut module;
} = module;
let items = if let Some((_, items)) = content {
items
} 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 { pyo3_path, .. } = 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 {
format!("{}.{}", module.value.value(), name)
} else {
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_cfg_attrs = Vec::new();
@ -280,7 +264,9 @@ pub fn pymodule_module_impl(
)? {
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) => {
@ -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!(
#(#attrs)*
#vis mod #ident {
#vis #mod_token #ident {
#(#items)*
#initialization
@ -381,13 +368,18 @@ pub fn pymodule_module_impl(
/// Generates the function that is called by the python interpreter to initialize the native
/// module
pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result<TokenStream> {
let options = PyModuleOptions::from_attrs(&mut function.attrs)?;
process_functions_in_module(&options, &mut function)?;
pub fn pymodule_function_impl(
function: &mut syn::ItemFn,
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 { pyo3_path, .. } = ctx;
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 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))));
Ok(quote! {
#function
#[doc(hidden)]
#vis mod #ident {
#initialization

View File

@ -3,14 +3,14 @@
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use proc_macro2::TokenStream as TokenStream2;
use pyo3_macros_backend::{
build_derive_from_pyobject, build_py_class, build_py_enum, build_py_function, build_py_methods,
pymodule_function_impl, pymodule_module_impl, PyClassArgs, PyClassMethodsType,
PyFunctionOptions,
PyFunctionOptions, PyModuleOptions,
};
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.
///
@ -24,6 +24,9 @@ use syn::{parse::Nothing, parse_macro_input, Item};
/// | Annotation | Description |
/// | :- | :- |
/// | `#[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].
///
@ -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")]
#[proc_macro_attribute]
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) => {
let is_submodule = match parse_macro_input!(args as Option<syn::Ident>) {
Some(i) if i == "submodule" => true,
Some(_) => {
return syn::Error::new(
Span::call_site(),
"#[pymodule] only accepts submodule as an argument",
)
.into_compile_error()
.into();
match pymodule_module_impl(module, options) {
// #[pymodule] on a module will rebuild the original ast, so we don't emit it here
Ok(expanded) => return expanded.into(),
Err(e) => Err(e),
}
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,
"#[pymodule] only supports modules and functions.",
)),
}
.unwrap_or_compile_error()
.unwrap_or_compile_error();
quote!(
#ast
#expanded
)
.into()
}

View File

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

View File

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

View File

@ -4,8 +4,14 @@ error: `submodule` may only be specified once (it is implicitly always specified
4 | mod submod {}
| ^^^
error[E0433]: failed to resolve: use of undeclared crate or module `submod`
--> tests/ui/duplicate_pymodule_submodule.rs:4:6
error[E0425]: cannot find value `_PYO3_DEF` in module `submod`
--> 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
|
3 | #[pymodule(some_arg)]

View File

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

View File

@ -1,5 +1,5 @@
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::*;
#[pymodule]
#[path = "empty.rs"] // to silence error related to missing file
mod invalid_pymodule_in_root_module;
fn main() {}

View File

@ -1,13 +1,13 @@
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
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]
| ^
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]
mod module {
use pyo3::prelude::*;
#[pymodule_init]
fn init(m: &PyModule) -> PyResult<()> {
fn init(_m: &Bound<'_, PyModule>) -> PyResult<()> {
Ok(())
}
#[pymodule_init]
fn init2(m: &PyModule) -> PyResult<()> {
fn init2(_m: &Bound<'_, PyModule>) -> PyResult<()> {
Ok(())
}
}

View File

@ -1,5 +1,5 @@
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<()> {
| ^^