From e0e3981e17c7b0f9b221ac1566e5a2eadf1a4f6b Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Sat, 24 Feb 2024 14:50:18 +0100 Subject: [PATCH] #[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 Co-authored-by: Georg Brandl * 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 Co-authored-by: Georg Brandl --- Cargo.toml | 4 + newsfragments/3815.added.md | 2 + pyo3-macros-backend/src/lib.rs | 2 +- pyo3-macros-backend/src/module.rs | 219 +++++++++++++++--- pyo3-macros-backend/src/pyfunction.rs | 6 + pyo3-macros/Cargo.toml | 1 + pyo3-macros/src/lib.rs | 40 ++-- src/impl_/pymodule.rs | 32 ++- src/macros.rs | 4 +- tests/test_append_to_inittab.rs | 37 ++- tests/test_compile_error.rs | 8 + tests/test_declarative_module.rs | 101 ++++++++ tests/ui/invalid_pymodule_glob.rs | 14 ++ tests/ui/invalid_pymodule_glob.stderr | 5 + tests/ui/invalid_pymodule_in_root.rs | 6 + tests/ui/invalid_pymodule_in_root.stderr | 13 ++ tests/ui/invalid_pymodule_trait.rs | 9 + tests/ui/invalid_pymodule_trait.stderr | 5 + .../ui/invalid_pymodule_two_pymodule_init.rs | 16 ++ .../invalid_pymodule_two_pymodule_init.stderr | 5 + 20 files changed, 458 insertions(+), 71 deletions(-) create mode 100644 newsfragments/3815.added.md create mode 100644 tests/test_declarative_module.rs create mode 100644 tests/ui/invalid_pymodule_glob.rs create mode 100644 tests/ui/invalid_pymodule_glob.stderr create mode 100644 tests/ui/invalid_pymodule_in_root.rs create mode 100644 tests/ui/invalid_pymodule_in_root.stderr create mode 100644 tests/ui/invalid_pymodule_trait.rs create mode 100644 tests/ui/invalid_pymodule_trait.stderr create mode 100644 tests/ui/invalid_pymodule_two_pymodule_init.rs create mode 100644 tests/ui/invalid_pymodule_two_pymodule_init.stderr diff --git a/Cargo.toml b/Cargo.toml index 4d1899cb..e7364a7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,9 @@ default = ["macros"] # and IntoPy traits experimental-inspect = [] +# Enables annotating Rust inline modules with #[pymodule] to build Python modules declaratively +experimental-declarative-modules = ["pyo3-macros/experimental-declarative-modules", "macros"] + # Enables macros: #[pyclass], #[pymodule], #[pyfunction] etc. macros = ["pyo3-macros", "indoc", "unindent"] @@ -114,6 +117,7 @@ full = [ "chrono-tz", "either", "experimental-inspect", + "experimental-declarative-modules", "eyre", "hashbrown", "indexmap", diff --git a/newsfragments/3815.added.md b/newsfragments/3815.added.md new file mode 100644 index 00000000..e4fd3e93 --- /dev/null +++ b/newsfragments/3815.added.md @@ -0,0 +1,2 @@ +The ability to create Python modules with a Rust `mod` block +behind the `experimental-declarative-modules` feature. \ No newline at end of file diff --git a/pyo3-macros-backend/src/lib.rs b/pyo3-macros-backend/src/lib.rs index 745a8471..a9d75a2a 100644 --- a/pyo3-macros-backend/src/lib.rs +++ b/pyo3-macros-backend/src/lib.rs @@ -22,7 +22,7 @@ mod pymethod; mod quotes; pub use frompyobject::build_derive_from_pyobject; -pub use module::{process_functions_in_module, pymodule_impl, PyModuleOptions}; +pub use module::{pymodule_function_impl, pymodule_module_impl, PyModuleOptions}; pub use pyclass::{build_py_class, build_py_enum, PyClassArgs}; pub use pyfunction::{build_py_function, PyFunctionOptions}; pub use pyimpl::{build_py_methods, PyClassMethodsType}; diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index ccd84bb3..6907e484 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -2,8 +2,9 @@ use crate::{ attributes::{self, take_attributes, take_pyo3_options, CrateAttribute, NameAttribute}, + get_doc, pyfunction::{impl_wrap_pyfunction, PyFunctionOptions}, - utils::{get_pyo3_crate, PythonDoc}, + utils::get_pyo3_crate, }; use proc_macro2::TokenStream; use quote::quote; @@ -12,7 +13,7 @@ use syn::{ parse::{Parse, ParseStream}, spanned::Spanned, token::Comma, - Ident, Path, Result, Visibility, + Item, Path, Result, }; #[derive(Default)] @@ -56,33 +57,154 @@ impl PyModuleOptions { } } +pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { + let syn::ItemMod { + attrs, + vis, + unsafety: _, + ident, + mod_token: _, + content, + semi: _, + } = &mut module; + let items = if let Some((_, items)) = content { + items + } else { + bail_spanned!(module.span() => "`#[pymodule]` can only be used on inline modules") + }; + let options = PyModuleOptions::from_attrs(attrs)?; + let krate = get_pyo3_crate(&options.krate); + let doc = get_doc(attrs, None); + + let mut module_items = Vec::new(); + let mut module_items_cfg_attrs = Vec::new(); + + fn extract_use_items( + source: &syn::UseTree, + cfg_attrs: &[syn::Attribute], + target_items: &mut Vec, + target_cfg_attrs: &mut Vec>, + ) -> Result<()> { + match source { + syn::UseTree::Name(name) => { + target_items.push(name.ident.clone()); + target_cfg_attrs.push(cfg_attrs.to_vec()); + } + syn::UseTree::Path(path) => { + extract_use_items(&path.tree, cfg_attrs, target_items, target_cfg_attrs)? + } + syn::UseTree::Group(group) => { + for tree in &group.items { + extract_use_items(tree, cfg_attrs, target_items, target_cfg_attrs)? + } + } + syn::UseTree::Glob(glob) => { + bail_spanned!(glob.span() => "#[pymodule] cannot import glob statements") + } + syn::UseTree::Rename(rename) => { + target_items.push(rename.rename.clone()); + target_cfg_attrs.push(cfg_attrs.to_vec()); + } + } + Ok(()) + } + + let mut pymodule_init = None; + + for item in &mut *items { + match item { + Item::Use(item_use) => { + let mut is_pyo3 = false; + item_use.attrs.retain(|attr| { + let found = attr.path().is_ident("pymodule_export"); + is_pyo3 |= found; + !found + }); + if is_pyo3 { + let cfg_attrs = item_use + .attrs + .iter() + .filter(|attr| attr.path().is_ident("cfg")) + .cloned() + .collect::>(); + extract_use_items( + &item_use.tree, + &cfg_attrs, + &mut module_items, + &mut module_items_cfg_attrs, + )?; + } + } + Item::Fn(item_fn) => { + let mut is_module_init = false; + item_fn.attrs.retain(|attr| { + let found = attr.path().is_ident("pymodule_init"); + is_module_init |= found; + !found + }); + if is_module_init { + ensure_spanned!(pymodule_init.is_none(), item_fn.span() => "only one pymodule_init may be specified"); + let ident = &item_fn.sig.ident; + pymodule_init = Some(quote! { #ident(module)?; }); + } else { + bail_spanned!(item.span() => "only 'use' statements and and pymodule_init functions are allowed in #[pymodule]") + } + } + item => { + bail_spanned!(item.span() => "only 'use' statements and and pymodule_init functions are allowed in #[pymodule]") + } + } + } + + let initialization = module_initialization(options, ident); + Ok(quote!( + #vis mod #ident { + #(#items)* + + #initialization + + impl MakeDef { + const fn make_def() -> #krate::impl_::pymodule::ModuleDef { + use #krate::impl_::pymodule as impl_; + const INITIALIZER: impl_::ModuleInitializer = impl_::ModuleInitializer(__pyo3_pymodule); + unsafe { + impl_::ModuleDef::new( + __PYO3_NAME, + #doc, + INITIALIZER + ) + } + } + } + + fn __pyo3_pymodule(module: &#krate::Bound<'_, #krate::types::PyModule>) -> #krate::PyResult<()> { + use #krate::impl_::pymodule::PyAddToModule; + #( + #(#module_items_cfg_attrs)* + #module_items::add_to_module(module)?; + )* + #pymodule_init + Ok(()) + } + } + )) +} + /// Generates the function that is called by the python interpreter to initialize the native /// module -pub fn pymodule_impl( - fnname: &Ident, - options: PyModuleOptions, - doc: PythonDoc, - visibility: &Visibility, -) -> TokenStream { - let name = options.name.unwrap_or_else(|| fnname.unraw()); +pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result { + let options = PyModuleOptions::from_attrs(&mut function.attrs)?; + process_functions_in_module(&options, &mut function)?; let krate = get_pyo3_crate(&options.krate); - let pyinit_symbol = format!("PyInit_{}", name); + let ident = &function.sig.ident; + let vis = &function.vis; + let doc = get_doc(&function.attrs, None); - quote! { - // Create a module with the same name as the `#[pymodule]` - this way `use ` - // will actually bring both the module and the function into scope. - #[doc(hidden)] - #visibility mod #fnname { - pub(crate) struct MakeDef; - pub static DEF: #krate::impl_::pymodule::ModuleDef = MakeDef::make_def(); - pub const NAME: &'static str = concat!(stringify!(#name), "\0"); - - /// This autogenerated function is called by the python interpreter when importing - /// the module. - #[export_name = #pyinit_symbol] - pub unsafe extern "C" fn init() -> *mut #krate::ffi::PyObject { - #krate::impl_::trampoline::module_init(|py| DEF.make_module(py)) - } + let initialization = module_initialization(options, ident); + Ok(quote! { + #function + #vis mod #ident { + #initialization } // Generate the definition inside an anonymous function in the same scope as the original function - @@ -91,28 +213,59 @@ pub fn pymodule_impl( // inside a function body) const _: () = { use #krate::impl_::pymodule as impl_; - impl #fnname::MakeDef { + + fn __pyo3_pymodule(module: &#krate::Bound<'_, #krate::types::PyModule>) -> #krate::PyResult<()> { + #ident(module.py(), module.as_gil_ref()) + } + + impl #ident::MakeDef { const fn make_def() -> impl_::ModuleDef { - const INITIALIZER: impl_::ModuleInitializer = impl_::ModuleInitializer(#fnname); unsafe { - impl_::ModuleDef::new(#fnname::NAME, #doc, INITIALIZER) + const INITIALIZER: impl_::ModuleInitializer = impl_::ModuleInitializer(__pyo3_pymodule); + impl_::ModuleDef::new( + #ident::__PYO3_NAME, + #doc, + INITIALIZER + ) } } } }; + }) +} + +fn module_initialization(options: PyModuleOptions, ident: &syn::Ident) -> TokenStream { + let name = options.name.unwrap_or_else(|| ident.unraw()); + let krate = get_pyo3_crate(&options.krate); + let pyinit_symbol = format!("PyInit_{}", name); + + quote! { + pub const __PYO3_NAME: &'static str = concat!(stringify!(#name), "\0"); + + pub(super) struct MakeDef; + pub static DEF: #krate::impl_::pymodule::ModuleDef = MakeDef::make_def(); + + pub fn add_to_module(module: &#krate::Bound<'_, #krate::types::PyModule>) -> #krate::PyResult<()> { + use #krate::prelude::PyModuleMethods; + module.add_submodule(DEF.make_module(module.py())?.bind(module.py())) + } + + /// This autogenerated function is called by the python interpreter when importing + /// the module. + #[export_name = #pyinit_symbol] + pub unsafe extern "C" fn __pyo3_init() -> *mut #krate::ffi::PyObject { + #krate::impl_::trampoline::module_init(|py| DEF.make_module(py)) + } } } /// Finds and takes care of the #[pyfn(...)] in `#[pymodule]` -pub fn process_functions_in_module( - options: &PyModuleOptions, - func: &mut syn::ItemFn, -) -> syn::Result<()> { +fn process_functions_in_module(options: &PyModuleOptions, func: &mut syn::ItemFn) -> Result<()> { let mut stmts: Vec = Vec::new(); let krate = get_pyo3_crate(&options.krate); for mut stmt in func.block.stmts.drain(..) { - if let syn::Stmt::Item(syn::Item::Fn(func)) = &mut stmt { + if let syn::Stmt::Item(Item::Fn(func)) = &mut stmt { if let Some(pyfn_args) = get_pyfn_attr(&mut func.attrs)? { let module_name = pyfn_args.modname; let wrapped_function = impl_wrap_pyfunction(func, pyfn_args.options)?; diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index b265a34d..7b48585c 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -269,6 +269,12 @@ pub fn impl_wrap_pyfunction( #vis mod #name { pub(crate) struct MakeDef; pub const DEF: #krate::impl_::pyfunction::PyMethodDef = MakeDef::DEF; + + pub fn add_to_module(module: &#krate::Bound<'_, #krate::types::PyModule>) -> #krate::PyResult<()> { + use #krate::prelude::PyModuleMethods; + use ::std::convert::Into; + module.add_function(&#krate::types::PyCFunction::internal_new(&DEF, module.as_gil_ref().into())?) + } } // Generate the definition inside an anonymous function in the same scope as the original function - diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 576c94a2..a0368a5f 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -15,6 +15,7 @@ proc-macro = true [features] multiple-pymethods = [] +experimental-declarative-modules = [] [dependencies] proc-macro2 = { version = "1", default-features = false } diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index d00ede89..64756a1c 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -6,11 +6,11 @@ use proc_macro::TokenStream; 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, - get_doc, process_functions_in_module, pymodule_impl, PyClassArgs, PyClassMethodsType, - PyFunctionOptions, PyModuleOptions, + pymodule_function_impl, pymodule_module_impl, PyClassArgs, PyClassMethodsType, + PyFunctionOptions, }; use quote::quote; -use syn::{parse::Nothing, parse_macro_input}; +use syn::{parse::Nothing, parse_macro_input, Item}; /// A proc macro used to implement Python modules. /// @@ -36,31 +36,27 @@ use syn::{parse::Nothing, parse_macro_input}; #[proc_macro_attribute] pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream { parse_macro_input!(args as Nothing); - - let mut ast = parse_macro_input!(input as syn::ItemFn); - let options = match PyModuleOptions::from_attrs(&mut ast.attrs) { - Ok(options) => options, - Err(e) => return e.into_compile_error().into(), - }; - - if let Err(err) = process_functions_in_module(&options, &mut ast) { - return err.into_compile_error().into(); + match parse_macro_input!(input as Item) { + Item::Mod(module) => if cfg!(feature = "experimental-declarative-modules") { + pymodule_module_impl(module) + } else { + Err(syn::Error::new_spanned( + module, + "#[pymodule] requires the 'experimental-declarative-modules' feature to be used on Rust modules.", + )) + }, + Item::Fn(function) => pymodule_function_impl(function), + unsupported => Err(syn::Error::new_spanned( + unsupported, + "#[pymodule] only supports modules and functions.", + )), } - - let doc = get_doc(&ast.attrs, None); - - let expanded = pymodule_impl(&ast.sig.ident, options, doc, &ast.vis); - - quote!( - #ast - #expanded - ) + .unwrap_or_compile_error() .into() } #[proc_macro_attribute] pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream { - use syn::Item; let item = parse_macro_input!(input as Item); match item { Item::Struct(struct_) => pyclass_impl(attr, struct_, methods_type()), diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 7103f8b3..9fff799c 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -7,7 +7,8 @@ use portable_atomic::{AtomicI64, Ordering}; #[cfg(not(PyPy))] use crate::exceptions::PyImportError; -use crate::{ffi, sync::GILOnceCell, types::PyModule, Py, PyResult, Python}; +use crate::types::module::PyModuleMethods; +use crate::{ffi, sync::GILOnceCell, types::PyModule, Bound, Py, PyResult, PyTypeInfo, Python}; /// `Sync` wrapper of `ffi::PyModuleDef`. pub struct ModuleDef { @@ -22,7 +23,7 @@ pub struct ModuleDef { } /// Wrapper to enable initializer to be used in const fns. -pub struct ModuleInitializer(pub for<'py> fn(Python<'py>, &PyModule) -> PyResult<()>); +pub struct ModuleInitializer(pub for<'py> fn(&Bound<'py, PyModule>) -> PyResult<()>); unsafe impl Sync for ModuleDef {} @@ -126,18 +127,34 @@ impl ModuleDef { ffi::PyModule_Create(self.ffi_def.get()), )? }; - (self.initializer.0)(py, module.as_ref(py))?; + self.initializer.0(module.bind(py))?; Ok(module) }) .map(|py_module| py_module.clone_ref(py)) } } +/// Trait to add an element (class, function...) to a module. +/// +/// Currently only implemented for classes. +pub trait PyAddToModule { + fn add_to_module(module: &Bound<'_, PyModule>) -> PyResult<()>; +} + +impl PyAddToModule for T { + fn add_to_module(module: &Bound<'_, PyModule>) -> PyResult<()> { + module.add(Self::NAME, Self::type_object_bound(module.py())) + } +} + #[cfg(test)] mod tests { use std::sync::atomic::{AtomicBool, Ordering}; - use crate::{types::any::PyAnyMethods, types::PyModule, PyResult, Python}; + use crate::{ + types::{any::PyAnyMethods, module::PyModuleMethods, PyModule}, + Bound, PyResult, Python, + }; use super::{ModuleDef, ModuleInitializer}; @@ -147,7 +164,7 @@ mod tests { ModuleDef::new( "test_module\0", "some doc\0", - ModuleInitializer(|_, m| { + ModuleInitializer(|m| { m.add("SOME_CONSTANT", 42)?; Ok(()) }), @@ -192,7 +209,7 @@ mod tests { static INIT_CALLED: AtomicBool = AtomicBool::new(false); #[allow(clippy::unnecessary_wraps)] - fn init(_: Python<'_>, _: &PyModule) -> PyResult<()> { + fn init(_: &Bound<'_, PyModule>) -> PyResult<()> { INIT_CALLED.store(true, Ordering::SeqCst); Ok(()) } @@ -203,8 +220,7 @@ mod tests { assert_eq!((*module_def.ffi_def.get()).m_doc, DOC.as_ptr() as _); Python::with_gil(|py| { - module_def.initializer.0(py, py.import_bound("builtins").unwrap().into_gil_ref()) - .unwrap(); + module_def.initializer.0(&py.import_bound("builtins").unwrap()).unwrap(); assert!(INIT_CALLED.load(Ordering::SeqCst)); }) } diff --git a/src/macros.rs b/src/macros.rs index 29c2033d..9b0d2816 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -169,8 +169,8 @@ macro_rules! append_to_inittab { ); } $crate::ffi::PyImport_AppendInittab( - $module::NAME.as_ptr() as *const ::std::os::raw::c_char, - ::std::option::Option::Some($module::init), + $module::__PYO3_NAME.as_ptr() as *const ::std::os::raw::c_char, + ::std::option::Option::Some($module::__pyo3_init), ); } }; diff --git a/tests/test_append_to_inittab.rs b/tests/test_append_to_inittab.rs index 00cccdbb..59ecaf42 100644 --- a/tests/test_append_to_inittab.rs +++ b/tests/test_append_to_inittab.rs @@ -1,4 +1,5 @@ #![cfg(all(feature = "macros", not(PyPy)))] + use pyo3::prelude::*; #[pyfunction] @@ -7,26 +8,52 @@ fn foo() -> usize { } #[pymodule] -fn module_with_functions(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn module_fn_with_functions(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(foo, m)?).unwrap(); Ok(()) } +#[cfg(feature = "experimental-declarative-modules")] +#[pymodule] +mod module_mod_with_functions { + #[pymodule_export] + use super::foo; +} + #[cfg(not(PyPy))] #[test] fn test_module_append_to_inittab() { use pyo3::append_to_inittab; - append_to_inittab!(module_with_functions); + + append_to_inittab!(module_fn_with_functions); + + #[cfg(feature = "experimental-declarative-modules")] + append_to_inittab!(module_mod_with_functions); + Python::with_gil(|py| { py.run_bound( r#" -import module_with_functions -assert module_with_functions.foo() == 123 +import module_fn_with_functions +assert module_fn_with_functions.foo() == 123 "#, None, None, ) .map_err(|e| e.display(py)) .unwrap(); - }) + }); + + #[cfg(feature = "experimental-declarative-modules")] + Python::with_gil(|py| { + py.run_bound( + r#" +import module_mod_with_functions +assert module_mod_with_functions.foo() == 123 +"#, + None, + None, + ) + .map_err(|e| e.display(py)) + .unwrap(); + }); } diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index adcef887..5f2d25db 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -40,4 +40,12 @@ fn test_compile_errors() { t.compile_fail("tests/ui/not_send2.rs"); t.compile_fail("tests/ui/get_set_all.rs"); t.compile_fail("tests/ui/traverse.rs"); + #[cfg(feature = "experimental-declarative-modules")] + t.compile_fail("tests/ui/invalid_pymodule_in_root.rs"); + #[cfg(feature = "experimental-declarative-modules")] + t.compile_fail("tests/ui/invalid_pymodule_glob.rs"); + #[cfg(feature = "experimental-declarative-modules")] + t.compile_fail("tests/ui/invalid_pymodule_trait.rs"); + #[cfg(feature = "experimental-declarative-modules")] + t.compile_fail("tests/ui/invalid_pymodule_two_pymodule_init.rs"); } diff --git a/tests/test_declarative_module.rs b/tests/test_declarative_module.rs new file mode 100644 index 00000000..86913d9b --- /dev/null +++ b/tests/test_declarative_module.rs @@ -0,0 +1,101 @@ +#![cfg(feature = "experimental-declarative-modules")] + +use pyo3::create_exception; +use pyo3::exceptions::PyException; +use pyo3::prelude::*; + +#[path = "../src/tests/common.rs"] +mod common; + +#[pyclass] +struct ValueClass { + value: usize, +} + +#[pymethods] +impl ValueClass { + #[new] + fn new(value: usize) -> ValueClass { + ValueClass { value } + } +} + +#[pyclass(module = "module")] +struct LocatedClass {} + +#[pyfunction] +fn double(x: usize) -> usize { + x * 2 +} + +create_exception!( + declarative_module, + MyError, + PyException, + "Some description." +); + +/// A module written using declarative syntax. +#[pymodule] +mod declarative_module { + #[pymodule_export] + use super::declarative_submodule; + #[pymodule_export] + // This is not a real constraint but to test cfg attribute support + #[cfg(not(Py_LIMITED_API))] + use super::LocatedClass; + use super::*; + #[pymodule_export] + use super::{declarative_module2, double, MyError, ValueClass as Value}; + + #[pymodule_init] + fn init(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add("double2", m.getattr("double")?) + } +} + +#[pyfunction] +fn double_value(v: &ValueClass) -> usize { + v.value * 2 +} + +#[pymodule] +mod declarative_submodule { + #[pymodule_export] + use super::{double, double_value}; +} + +/// A module written using declarative syntax. +#[pymodule] +#[pyo3(name = "declarative_module_renamed")] +mod declarative_module2 { + #[pymodule_export] + use super::double; +} + +#[test] +fn test_declarative_module() { + Python::with_gil(|py| { + let m = pyo3::wrap_pymodule!(declarative_module)(py).into_bound(py); + py_assert!( + py, + m, + "m.__doc__ == 'A module written using declarative syntax.'" + ); + + py_assert!(py, m, "m.double(2) == 4"); + py_assert!(py, m, "m.double2(3) == 6"); + py_assert!(py, m, "m.declarative_submodule.double(4) == 8"); + py_assert!( + py, + m, + "m.declarative_submodule.double_value(m.ValueClass(1)) == 2" + ); + py_assert!(py, m, "str(m.MyError('foo')) == 'foo'"); + py_assert!(py, m, "m.declarative_module_renamed.double(2) == 4"); + #[cfg(Py_LIMITED_API)] + py_assert!(py, m, "not hasattr(m, 'LocatedClass')"); + #[cfg(not(Py_LIMITED_API))] + py_assert!(py, m, "hasattr(m, 'LocatedClass')"); + }) +} diff --git a/tests/ui/invalid_pymodule_glob.rs b/tests/ui/invalid_pymodule_glob.rs new file mode 100644 index 00000000..107cdf93 --- /dev/null +++ b/tests/ui/invalid_pymodule_glob.rs @@ -0,0 +1,14 @@ +use pyo3::prelude::*; + +#[pyfunction] +fn foo() -> usize { + 0 +} + +#[pymodule] +mod module { + #[pymodule_export] + use super::*; +} + +fn main() {} diff --git a/tests/ui/invalid_pymodule_glob.stderr b/tests/ui/invalid_pymodule_glob.stderr new file mode 100644 index 00000000..237e0203 --- /dev/null +++ b/tests/ui/invalid_pymodule_glob.stderr @@ -0,0 +1,5 @@ +error: #[pymodule] cannot import glob statements + --> tests/ui/invalid_pymodule_glob.rs:11:16 + | +11 | use super::*; + | ^ diff --git a/tests/ui/invalid_pymodule_in_root.rs b/tests/ui/invalid_pymodule_in_root.rs new file mode 100644 index 00000000..47af4205 --- /dev/null +++ b/tests/ui/invalid_pymodule_in_root.rs @@ -0,0 +1,6 @@ +use pyo3::prelude::*; + +#[pymodule] +mod invalid_pymodule_in_root_module; + +fn main() {} diff --git a/tests/ui/invalid_pymodule_in_root.stderr b/tests/ui/invalid_pymodule_in_root.stderr new file mode 100644 index 00000000..91783be0 --- /dev/null +++ b/tests/ui/invalid_pymodule_in_root.stderr @@ -0,0 +1,13 @@ +error[E0658]: non-inline modules in proc macro input are unstable + --> tests/ui/invalid_pymodule_in_root.rs:4:1 + | +4 | mod invalid_pymodule_in_root_module; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: see issue #54727 for more information + +error: `#[pymodule]` can only be used on inline modules + --> tests/ui/invalid_pymodule_in_root.rs:4:1 + | +4 | mod invalid_pymodule_in_root_module; + | ^^^ diff --git a/tests/ui/invalid_pymodule_trait.rs b/tests/ui/invalid_pymodule_trait.rs new file mode 100644 index 00000000..6649a354 --- /dev/null +++ b/tests/ui/invalid_pymodule_trait.rs @@ -0,0 +1,9 @@ +use pyo3::prelude::*; + +#[pymodule] +mod module { + #[pymodule_export] + trait Foo {} +} + +fn main() {} diff --git a/tests/ui/invalid_pymodule_trait.stderr b/tests/ui/invalid_pymodule_trait.stderr new file mode 100644 index 00000000..3ed12861 --- /dev/null +++ b/tests/ui/invalid_pymodule_trait.stderr @@ -0,0 +1,5 @@ +error: only 'use' statements and and pymodule_init functions are allowed in #[pymodule] + --> tests/ui/invalid_pymodule_trait.rs:5:5 + | +5 | #[pymodule_export] + | ^ diff --git a/tests/ui/invalid_pymodule_two_pymodule_init.rs b/tests/ui/invalid_pymodule_two_pymodule_init.rs new file mode 100644 index 00000000..d676b0fa --- /dev/null +++ b/tests/ui/invalid_pymodule_two_pymodule_init.rs @@ -0,0 +1,16 @@ +use pyo3::prelude::*; + +#[pymodule] +mod module { + #[pymodule_init] + fn init(m: &PyModule) -> PyResult<()> { + Ok(()) + } + + #[pymodule_init] + fn init2(m: &PyModule) -> PyResult<()> { + Ok(()) + } +} + +fn main() {} diff --git a/tests/ui/invalid_pymodule_two_pymodule_init.stderr b/tests/ui/invalid_pymodule_two_pymodule_init.stderr new file mode 100644 index 00000000..9f0900f9 --- /dev/null +++ b/tests/ui/invalid_pymodule_two_pymodule_init.stderr @@ -0,0 +1,5 @@ +error: only one pymodule_init may be specified + --> tests/ui/invalid_pymodule_two_pymodule_init.rs:11:5 + | +11 | fn init2(m: &PyModule) -> PyResult<()> { + | ^^