From ceeae211667455b6bddd37b3a26dea894cd19a4e Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 9 Feb 2024 15:20:09 +0100 Subject: [PATCH 1/7] #[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 --- newsfragments/3815.added.md | 1 + pyo3-macros-backend/src/lib.rs | 4 +- pyo3-macros-backend/src/module.rs | 160 +++++++++++++++++- pyo3-macros-backend/src/pyfunction.rs | 4 + pyo3-macros/src/lib.rs | 12 +- pytests/src/lib.rs | 92 +++++----- src/impl_/pymodule.rs | 15 +- src/macros.rs | 4 +- tests/test_append_to_inittab.rs | 29 +++- tests/test_compile_error.rs | 4 + tests/test_module.rs | 75 +++++++- 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_in_root_module.rs | 0 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, 417 insertions(+), 56 deletions(-) create mode 100644 newsfragments/3815.added.md 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_in_root_module.rs 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/newsfragments/3815.added.md b/newsfragments/3815.added.md new file mode 100644 index 00000000000..dd450ca88c6 --- /dev/null +++ b/newsfragments/3815.added.md @@ -0,0 +1 @@ +The ability to create Python modules with a Rust `mod` block. \ No newline at end of file diff --git a/pyo3-macros-backend/src/lib.rs b/pyo3-macros-backend/src/lib.rs index 745a8471c2b..2b18c0fc973 100644 --- a/pyo3-macros-backend/src/lib.rs +++ b/pyo3-macros-backend/src/lib.rs @@ -22,7 +22,9 @@ mod pymethod; mod quotes; pub use frompyobject::build_derive_from_pyobject; -pub use module::{process_functions_in_module, pymodule_impl, PyModuleOptions}; +pub use module::{ + process_functions_in_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 ccd84bb363a..775967767d9 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -2,6 +2,7 @@ use crate::{ attributes::{self, take_attributes, take_pyo3_options, CrateAttribute, NameAttribute}, + get_doc, pyfunction::{impl_wrap_pyfunction, PyFunctionOptions}, utils::{get_pyo3_crate, PythonDoc}, }; @@ -56,9 +57,156 @@ 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 doc = get_doc(attrs, None); + + let name = options.name.unwrap_or_else(|| ident.unraw()); + let krate = get_pyo3_crate(&options.krate); + let pyinit_symbol = format!("PyInit_{}", name); + + 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 { + syn::Item::Use(item_use) => { + let mut is_pyo3 = false; + item_use.attrs.retain(|attr| { + let found = attr.path().is_ident("pyo3"); + 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, + )?; + } + } + syn::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)?; }); + } + } + item => { + bail_spanned!(item.span() => "only 'use' statements and and pymodule_init functions are allowed in #[pymodule]") + } + } + } + + Ok(quote! { + #vis #mod_token #ident { + #(#items)* + + pub const __PYO3_NAME: &'static str = concat!(stringify!(#name), "\0"); + + pub(crate) struct MakeDef; + 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) + } + } + } + + pub static DEF: #krate::impl_::pymodule::ModuleDef = unsafe { + use #krate::impl_::pymodule as impl_; + impl_::ModuleDef::new(concat!(stringify!(#name), "\0"), #doc, impl_::ModuleInitializer(__pyo3_pymodule)) + }; + + pub fn add_to_module(module: &#krate::types::PyModule) -> #krate::PyResult<()> { + module.add_submodule(DEF.make_module(module.py())?.into_ref(module.py())) + } + + pub fn __pyo3_pymodule(_py: #krate::Python, module: &#krate::types::PyModule) -> #krate::PyResult<()> { + use #krate::impl_::pymodule::PyAddToModule; + #( + #(#module_items_cfg_attrs)* + #module_items::add_to_module(module)?; + )* + #pymodule_init + Ok(()) + } + + /// 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)) + } + } + }) +} + /// Generates the function that is called by the python interpreter to initialize the native /// module -pub fn pymodule_impl( +pub fn pymodule_function_impl( fnname: &Ident, options: PyModuleOptions, doc: PythonDoc, @@ -75,14 +223,18 @@ pub fn pymodule_impl( #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"); + pub const __PYO3_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 { + pub unsafe extern "C" fn __pyo3_init() -> *mut #krate::ffi::PyObject { #krate::impl_::trampoline::module_init(|py| DEF.make_module(py)) } + + pub fn add_to_module(module: &#krate::types::PyModule) -> #krate::PyResult<()> { + module.add_submodule(DEF.make_module(module.py())?.into_ref(module.py())) + } } // Generate the definition inside an anonymous function in the same scope as the original function - @@ -95,7 +247,7 @@ pub fn pymodule_impl( const fn make_def() -> impl_::ModuleDef { const INITIALIZER: impl_::ModuleInitializer = impl_::ModuleInitializer(#fnname); unsafe { - impl_::ModuleDef::new(#fnname::NAME, #doc, INITIALIZER) + impl_::ModuleDef::new(#fnname::__PYO3_NAME, #doc, INITIALIZER) } } } diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index b265a34d39f..bc20ee9d28c 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -269,6 +269,10 @@ 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::types::PyModule) -> #krate::PyResult<()> { + module.add_function(#krate::impl_::pyfunction::_wrap_pyfunction(&DEF, module)?) + } } // Generate the definition inside an anonymous function in the same scope as the original function - diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index d00ede89143..dcf719c553d 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -6,8 +6,8 @@ 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, + get_doc, process_functions_in_module, pymodule_function_impl, pymodule_module_impl, + PyClassArgs, PyClassMethodsType, PyFunctionOptions, PyModuleOptions, }; use quote::quote; use syn::{parse::Nothing, parse_macro_input}; @@ -37,6 +37,12 @@ use syn::{parse::Nothing, parse_macro_input}; pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream { parse_macro_input!(args as Nothing); + if let Ok(module) = syn::parse(input.clone()) { + return pymodule_module_impl(module) + .unwrap_or_compile_error() + .into(); + } + let mut ast = parse_macro_input!(input as syn::ItemFn); let options = match PyModuleOptions::from_attrs(&mut ast.attrs) { Ok(options) => options, @@ -49,7 +55,7 @@ pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream { let doc = get_doc(&ast.attrs, None); - let expanded = pymodule_impl(&ast.sig.ident, options, doc, &ast.vis); + let expanded = pymodule_function_impl(&ast.sig.ident, options, doc, &ast.vis); quote!( #ast diff --git a/pytests/src/lib.rs b/pytests/src/lib.rs index bfd80edb719..baad8f17195 100644 --- a/pytests/src/lib.rs +++ b/pytests/src/lib.rs @@ -1,7 +1,4 @@ use pyo3::prelude::*; -use pyo3::types::PyDict; -use pyo3::wrap_pymodule; - pub mod awaitable; pub mod buf_and_str; pub mod comparisons; @@ -18,43 +15,60 @@ pub mod sequence; pub mod subclassing; #[pymodule] -fn pyo3_pytests(py: Python<'_>, m: &PyModule) -> PyResult<()> { - m.add_wrapped(wrap_pymodule!(awaitable::awaitable))?; +mod pyo3_pytests { + use super::*; + #[pyo3] + use awaitable::awaitable; + #[pyo3] #[cfg(not(Py_LIMITED_API))] - m.add_wrapped(wrap_pymodule!(buf_and_str::buf_and_str))?; - m.add_wrapped(wrap_pymodule!(comparisons::comparisons))?; + use buf_and_str::buf_and_str; + #[pyo3] + use comparisons::comparisons; #[cfg(not(Py_LIMITED_API))] - m.add_wrapped(wrap_pymodule!(datetime::datetime))?; - m.add_wrapped(wrap_pymodule!(dict_iter::dict_iter))?; - m.add_wrapped(wrap_pymodule!(enums::enums))?; - m.add_wrapped(wrap_pymodule!(misc::misc))?; - m.add_wrapped(wrap_pymodule!(objstore::objstore))?; - m.add_wrapped(wrap_pymodule!(othermod::othermod))?; - m.add_wrapped(wrap_pymodule!(path::path))?; - m.add_wrapped(wrap_pymodule!(pyclasses::pyclasses))?; - m.add_wrapped(wrap_pymodule!(pyfunctions::pyfunctions))?; - m.add_wrapped(wrap_pymodule!(sequence::sequence))?; - m.add_wrapped(wrap_pymodule!(subclassing::subclassing))?; - - // Inserting to sys.modules allows importing submodules nicely from Python - // e.g. import pyo3_pytests.buf_and_str as bas - - let sys = PyModule::import_bound(py, "sys")?; - let sys_modules = sys.getattr("modules")?.downcast_into::()?; - sys_modules.set_item("pyo3_pytests.awaitable", m.getattr("awaitable")?)?; - sys_modules.set_item("pyo3_pytests.buf_and_str", m.getattr("buf_and_str")?)?; - sys_modules.set_item("pyo3_pytests.comparisons", m.getattr("comparisons")?)?; - sys_modules.set_item("pyo3_pytests.datetime", m.getattr("datetime")?)?; - sys_modules.set_item("pyo3_pytests.dict_iter", m.getattr("dict_iter")?)?; - sys_modules.set_item("pyo3_pytests.enums", m.getattr("enums")?)?; - sys_modules.set_item("pyo3_pytests.misc", m.getattr("misc")?)?; - sys_modules.set_item("pyo3_pytests.objstore", m.getattr("objstore")?)?; - sys_modules.set_item("pyo3_pytests.othermod", m.getattr("othermod")?)?; - sys_modules.set_item("pyo3_pytests.path", m.getattr("path")?)?; - sys_modules.set_item("pyo3_pytests.pyclasses", m.getattr("pyclasses")?)?; - sys_modules.set_item("pyo3_pytests.pyfunctions", m.getattr("pyfunctions")?)?; - sys_modules.set_item("pyo3_pytests.sequence", m.getattr("sequence")?)?; - sys_modules.set_item("pyo3_pytests.subclassing", m.getattr("subclassing")?)?; + #[pyo3] + use datetime::datetime; + #[pyo3] + use dict_iter::dict_iter; + #[pyo3] + use enums::enums; + #[pyo3] + use misc::misc; + #[pyo3] + use objstore::objstore; + #[pyo3] + use othermod::othermod; + #[pyo3] + use path::path; + #[pyo3] + use pyclasses::pyclasses; + #[pyo3] + use pyfunctions::pyfunctions; + use pyo3::types::PyDict; + #[pyo3] + use sequence::sequence; + #[pyo3] + use subclassing::subclassing; - Ok(()) + #[pymodule_init] + fn init(m: &PyModule) -> PyResult<()> { + // Inserting to sys.modules allows importing submodules nicely from Python + // e.g. import pyo3_pytests.buf_and_str as bas + let sys = PyModule::import_bound(m.py(), "sys")?; + let sys_modules = sys.getattr("modules")?.downcast_into::()?; + sys_modules.set_item("pyo3_pytests.awaitable", m.getattr("awaitable")?)?; + sys_modules.set_item("pyo3_pytests.buf_and_str", m.getattr("buf_and_str")?)?; + sys_modules.set_item("pyo3_pytests.comparisons", m.getattr("comparisons")?)?; + sys_modules.set_item("pyo3_pytests.datetime", m.getattr("datetime")?)?; + sys_modules.set_item("pyo3_pytests.dict_iter", m.getattr("dict_iter")?)?; + sys_modules.set_item("pyo3_pytests.enums", m.getattr("enums")?)?; + sys_modules.set_item("pyo3_pytests.misc", m.getattr("misc")?)?; + sys_modules.set_item("pyo3_pytests.objstore", m.getattr("objstore")?)?; + sys_modules.set_item("pyo3_pytests.othermod", m.getattr("othermod")?)?; + sys_modules.set_item("pyo3_pytests.path", m.getattr("path")?)?; + sys_modules.set_item("pyo3_pytests.pyclasses", m.getattr("pyclasses")?)?; + sys_modules.set_item("pyo3_pytests.pyfunctions", m.getattr("pyfunctions")?)?; + sys_modules.set_item("pyo3_pytests.sequence", m.getattr("sequence")?)?; + sys_modules.set_item("pyo3_pytests.subclassing", m.getattr("subclassing")?)?; + Ok(()) + } } diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 7103f8b3938..0c75578af52 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -7,7 +7,7 @@ use portable_atomic::{AtomicI64, Ordering}; #[cfg(not(PyPy))] use crate::exceptions::PyImportError; -use crate::{ffi, sync::GILOnceCell, types::PyModule, Py, PyResult, Python}; +use crate::{ffi, sync::GILOnceCell, types::PyModule, Py, PyResult, PyTypeInfo, Python}; /// `Sync` wrapper of `ffi::PyModuleDef`. pub struct ModuleDef { @@ -133,6 +133,19 @@ impl ModuleDef { } } +/// Trait to add an element (class, function...) to a module. +/// +/// Currently only implemented for classes. +pub trait PyAddToModule { + fn add_to_module(module: &PyModule) -> PyResult<()>; +} + +impl PyAddToModule for T { + fn add_to_module(module: &PyModule) -> PyResult<()> { + module.add(Self::NAME, Self::type_object(module.py())) + } +} + #[cfg(test)] mod tests { use std::sync::atomic::{AtomicBool, Ordering}; diff --git a/src/macros.rs b/src/macros.rs index 29c2033dd4b..9b0d2816882 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 00cccdbb49e..6e1c8e0616b 100644 --- a/tests/test_append_to_inittab.rs +++ b/tests/test_append_to_inittab.rs @@ -7,26 +7,45 @@ 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(()) } +#[pymodule] +mod module_mod_with_functions { + #[pyo3] + 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); + 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(); + }); + Python::with_gil(|py| { + py.run( + 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 adcef887f5c..d6df9c64e39 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -40,4 +40,8 @@ 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"); + t.compile_fail("tests/ui/invalid_pymodule_in_root.rs"); + t.compile_fail("tests/ui/invalid_pymodule_glob.rs"); + t.compile_fail("tests/ui/invalid_pymodule_trait.rs"); + t.compile_fail("tests/ui/invalid_pymodule_two_pymodule_init.rs"); } diff --git a/tests/test_module.rs b/tests/test_module.rs index 9d14f243d50..73fe968407f 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -2,9 +2,10 @@ use pyo3::prelude::*; -use pyo3::py_run; +use pyo3::exceptions::PyException; use pyo3::types::PyString; use pyo3::types::{IntoPyDict, PyDict, PyTuple}; +use pyo3::{create_exception, py_run}; #[path = "../src/tests/common.rs"] mod common; @@ -484,3 +485,75 @@ fn test_module_doc_hidden() { py_assert!(py, m, "m.__doc__ == ''"); }) } + +create_exception!( + declarative_module, + MyError, + PyException, + "Some description." +); + +/// A module written using declarative syntax. +#[pymodule] +mod declarative_module { + #[pyo3] + use super::declarative_submodule; + #[pyo3] + // This is not a real constraint but to test cfg attribute support + #[cfg(not(Py_LIMITED_API))] + use super::LocatedClass; + use super::*; + #[pyo3] + use super::{declarative_module2, double, MyError, ValueClass as Value}; + + #[pymodule_init] + fn init(m: &PyModule) -> PyResult<()> { + m.add("double2", m.getattr("double")?) + } +} + +#[pyfunction] +fn double_value(v: &ValueClass) -> usize { + v.value * 2 +} + +#[pymodule] +mod declarative_submodule { + #[pyo3] + use super::{double, double_value}; +} + +/// A module written using declarative syntax. +#[pymodule] +#[pyo3(name = "declarative_module_renamed")] +mod declarative_module2 { + #[pyo3] + use super::double; +} + +#[test] +fn test_declarative_module() { + Python::with_gil(|py| { + let m = pyo3::wrap_pymodule!(declarative_module)(py).into_ref(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 00000000000..f47a6c72c7f --- /dev/null +++ b/tests/ui/invalid_pymodule_glob.rs @@ -0,0 +1,14 @@ +use pyo3::prelude::*; + +#[pyfunction] +fn foo() -> usize { + 0 +} + +#[pymodule] +mod module { + #[pyo3] + 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 00000000000..237e02037aa --- /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 00000000000..47af4205f71 --- /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 00000000000..91783be0e97 --- /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_in_root_module.rs b/tests/ui/invalid_pymodule_in_root_module.rs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/ui/invalid_pymodule_trait.rs b/tests/ui/invalid_pymodule_trait.rs new file mode 100644 index 00000000000..0156fc36962 --- /dev/null +++ b/tests/ui/invalid_pymodule_trait.rs @@ -0,0 +1,9 @@ +use pyo3::prelude::*; + +#[pymodule] +mod module { + #[pyo3] + 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 00000000000..b8e51f7c7dc --- /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 | #[pyo3] + | ^ 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 00000000000..d676b0fa277 --- /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 00000000000..9f0900f9348 --- /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<()> { + | ^^ From ed39b3120271534d30b647b1ca5e3e51473014ec Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 9 Feb 2024 15:20:09 +0100 Subject: [PATCH 2/7] tests: group exported imports --- pytests/src/lib.rs | 35 +++++++---------------------------- 1 file changed, 7 insertions(+), 28 deletions(-) diff --git a/pytests/src/lib.rs b/pytests/src/lib.rs index baad8f17195..b81dfdad256 100644 --- a/pytests/src/lib.rs +++ b/pytests/src/lib.rs @@ -17,37 +17,16 @@ pub mod subclassing; #[pymodule] mod pyo3_pytests { use super::*; - #[pyo3] - use awaitable::awaitable; - #[pyo3] - #[cfg(not(Py_LIMITED_API))] - use buf_and_str::buf_and_str; - #[pyo3] - use comparisons::comparisons; - #[cfg(not(Py_LIMITED_API))] - #[pyo3] - use datetime::datetime; - #[pyo3] - use dict_iter::dict_iter; - #[pyo3] - use enums::enums; - #[pyo3] - use misc::misc; - #[pyo3] - use objstore::objstore; - #[pyo3] - use othermod::othermod; - #[pyo3] - use path::path; - #[pyo3] - use pyclasses::pyclasses; - #[pyo3] - use pyfunctions::pyfunctions; use pyo3::types::PyDict; #[pyo3] - use sequence::sequence; + use { + awaitable::awaitable, comparisons::comparisons, dict_iter::dict_iter, enums::enums, + misc::misc, objstore::objstore, othermod::othermod, path::path, pyclasses::pyclasses, + pyfunctions::pyfunctions, sequence::sequence, subclassing::subclassing, + }; + #[cfg(not(Py_LIMITED_API))] #[pyo3] - use subclassing::subclassing; + use {buf_and_str::buf_and_str, datetime::datetime}; #[pymodule_init] fn init(m: &PyModule) -> PyResult<()> { From f8d39d2863a4e8b0b17aee122f3b0076f6eb1281 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 16 Feb 2024 14:32:13 +0100 Subject: [PATCH 3/7] Consolidate pymodule macro code to avoid duplicates --- pyo3-macros-backend/src/lib.rs | 4 +- pyo3-macros-backend/src/module.rs | 134 +++++++++++++++--------------- pyo3-macros/src/lib.rs | 39 +++------ 3 files changed, 77 insertions(+), 100 deletions(-) diff --git a/pyo3-macros-backend/src/lib.rs b/pyo3-macros-backend/src/lib.rs index 2b18c0fc973..a9d75a2a6fe 100644 --- a/pyo3-macros-backend/src/lib.rs +++ b/pyo3-macros-backend/src/lib.rs @@ -22,9 +22,7 @@ mod pymethod; mod quotes; pub use frompyobject::build_derive_from_pyobject; -pub use module::{ - process_functions_in_module, pymodule_function_impl, pymodule_module_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 775967767d9..c014e5b6177 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -4,7 +4,7 @@ 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; @@ -13,7 +13,7 @@ use syn::{ parse::{Parse, ParseStream}, spanned::Spanned, token::Comma, - Ident, Path, Result, Visibility, + Item, Path, Result, }; #[derive(Default)] @@ -63,7 +63,7 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { vis, unsafety: _, ident, - mod_token, + mod_token: _, content, semi: _, } = &mut module; @@ -73,11 +73,8 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { bail_spanned!(module.span() => "`#[pymodule]` can only be used on inline modules") }; let options = PyModuleOptions::from_attrs(attrs)?; - let doc = get_doc(attrs, None); - - let name = options.name.unwrap_or_else(|| ident.unraw()); let krate = get_pyo3_crate(&options.krate); - let pyinit_symbol = format!("PyInit_{}", name); + let doc = get_doc(&attrs, None); let mut module_items = Vec::new(); let mut module_items_cfg_attrs = Vec::new(); @@ -116,7 +113,7 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { for item in &mut *items { match item { - syn::Item::Use(item_use) => { + Item::Use(item_use) => { let mut is_pyo3 = false; item_use.attrs.retain(|attr| { let found = attr.path().is_ident("pyo3"); @@ -138,7 +135,7 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { )?; } } - syn::Item::Fn(item_fn) => { + Item::Fn(item_fn) => { let mut is_module_init = false; item_fn.attrs.retain(|attr| { let found = attr.path().is_ident("pymodule_init"); @@ -157,35 +154,30 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { } } - Ok(quote! { - #vis #mod_token #ident { + let initialization = module_initialization(options, ident); + Ok(quote!( + #vis mod #ident { + use #krate::impl_::pymodule as impl_; + #(#items)* - pub const __PYO3_NAME: &'static str = concat!(stringify!(#name), "\0"); + #initialization - pub(crate) struct MakeDef; impl MakeDef { - const fn make_def() -> #krate::impl_::pymodule::ModuleDef { - use #krate::impl_::pymodule as impl_; - + const fn make_def() ->impl_::ModuleDef { const INITIALIZER: impl_::ModuleInitializer = impl_::ModuleInitializer(__pyo3_pymodule); unsafe { - impl_::ModuleDef::new(__PYO3_NAME, #doc, INITIALIZER) + impl_::ModuleDef::new( + __PYO3_NAME, + #doc, + INITIALIZER + ) } } } - pub static DEF: #krate::impl_::pymodule::ModuleDef = unsafe { - use #krate::impl_::pymodule as impl_; - impl_::ModuleDef::new(concat!(stringify!(#name), "\0"), #doc, impl_::ModuleInitializer(__pyo3_pymodule)) - }; - - pub fn add_to_module(module: &#krate::types::PyModule) -> #krate::PyResult<()> { - module.add_submodule(DEF.make_module(module.py())?.into_ref(module.py())) - } - - pub fn __pyo3_pymodule(_py: #krate::Python, module: &#krate::types::PyModule) -> #krate::PyResult<()> { - use #krate::impl_::pymodule::PyAddToModule; + fn __pyo3_pymodule(_py: #krate::Python, module: &#krate::types::PyModule) -> #krate::PyResult<()> { + use impl_::PyAddToModule; #( #(#module_items_cfg_attrs)* #module_items::add_to_module(module)?; @@ -194,47 +186,25 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { Ok(()) } - /// 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)) - } } - }) + )) } /// Generates the function that is called by the python interpreter to initialize the native /// module -pub fn pymodule_function_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); - - 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 __PYO3_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 __pyo3_init() -> *mut #krate::ffi::PyObject { - #krate::impl_::trampoline::module_init(|py| DEF.make_module(py)) - } + let ident = &function.sig.ident; + let vis = &function.vis; + let doc = get_doc(&function.attrs, None); - pub fn add_to_module(module: &#krate::types::PyModule) -> #krate::PyResult<()> { - module.add_submodule(DEF.make_module(module.py())?.into_ref(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 - @@ -243,23 +213,49 @@ pub fn pymodule_function_impl( // inside a function body) const _: () = { use #krate::impl_::pymodule as impl_; - impl #fnname::MakeDef { + impl #ident::MakeDef { const fn make_def() -> impl_::ModuleDef { - const INITIALIZER: impl_::ModuleInitializer = impl_::ModuleInitializer(#fnname); unsafe { - impl_::ModuleDef::new(#fnname::__PYO3_NAME, #doc, INITIALIZER) + const INITIALIZER: impl_::ModuleInitializer = impl_::ModuleInitializer(#ident); + 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::types::PyModule) -> #krate::PyResult<()> { + module.add_submodule(DEF.make_module(module.py())?.into_ref(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); diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index dcf719c553d..8dbf2782d5b 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_function_impl, pymodule_module_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,37 +36,20 @@ use syn::{parse::Nothing, parse_macro_input}; #[proc_macro_attribute] pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream { parse_macro_input!(args as Nothing); - - if let Ok(module) = syn::parse(input.clone()) { - return pymodule_module_impl(module) - .unwrap_or_compile_error() - .into(); + match parse_macro_input!(input as Item) { + Item::Mod(module) => pymodule_module_impl(module), + Item::Fn(function) => pymodule_function_impl(function), + unsupported => Err(syn::Error::new_spanned( + unsupported, + "#[pymodule] only supports modules and functions.", + )), } - - 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(); - } - - let doc = get_doc(&ast.attrs, None); - - let expanded = pymodule_function_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()), From e3d6f820afa061999def5671f010349d1e3b5a2e Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 16 Feb 2024 15:35:11 +0100 Subject: [PATCH 4/7] Makes pymodule_init take Bound<'_, PyModule> --- pyo3-macros-backend/src/module.rs | 29 +++++++++++++++------------ pyo3-macros-backend/src/pyfunction.rs | 6 ++++-- pytests/src/lib.rs | 2 +- src/impl_/pymodule.rs | 25 +++++++++++++---------- tests/test_append_to_inittab.rs | 2 +- tests/test_module.rs | 2 +- 6 files changed, 37 insertions(+), 29 deletions(-) diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index c014e5b6177..317a348a313 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -74,7 +74,7 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { }; let options = PyModuleOptions::from_attrs(attrs)?; let krate = get_pyo3_crate(&options.krate); - let doc = get_doc(&attrs, None); + let doc = get_doc(attrs, None); let mut module_items = Vec::new(); let mut module_items_cfg_attrs = Vec::new(); @@ -157,14 +157,13 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { let initialization = module_initialization(options, ident); Ok(quote!( #vis mod #ident { - use #krate::impl_::pymodule as impl_; - #(#items)* #initialization impl MakeDef { - const fn make_def() ->impl_::ModuleDef { + 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( @@ -176,8 +175,8 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { } } - fn __pyo3_pymodule(_py: #krate::Python, module: &#krate::types::PyModule) -> #krate::PyResult<()> { - use impl_::PyAddToModule; + 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)?; @@ -185,7 +184,6 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { #pymodule_init Ok(()) } - } )) } @@ -200,7 +198,7 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result let vis = &function.vis; let doc = get_doc(&function.attrs, None); - let initialization = module_initialization(options, &ident); + let initialization = module_initialization(options, ident); Ok(quote! { #function #vis mod #ident { @@ -213,15 +211,19 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result // inside a function body) const _: () = { use #krate::impl_::pymodule as impl_; + + 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 { unsafe { - const INITIALIZER: impl_::ModuleInitializer = impl_::ModuleInitializer(#ident); + const INITIALIZER: impl_::ModuleInitializer = impl_::ModuleInitializer(__pyo3_pymodule); impl_::ModuleDef::new( #ident::__PYO3_NAME, #doc, INITIALIZER - ) } } @@ -241,8 +243,9 @@ fn module_initialization(options: PyModuleOptions, ident: &syn::Ident) -> TokenS pub(super) struct MakeDef; pub static DEF: #krate::impl_::pymodule::ModuleDef = MakeDef::make_def(); - pub fn add_to_module(module: &#krate::types::PyModule) -> #krate::PyResult<()> { - module.add_submodule(DEF.make_module(module.py())?.into_ref(module.py())) + 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 @@ -260,7 +263,7 @@ fn process_functions_in_module(options: &PyModuleOptions, func: &mut syn::ItemFn 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 bc20ee9d28c..7b48585cddc 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -270,8 +270,10 @@ pub fn impl_wrap_pyfunction( pub(crate) struct MakeDef; pub const DEF: #krate::impl_::pyfunction::PyMethodDef = MakeDef::DEF; - pub fn add_to_module(module: &#krate::types::PyModule) -> #krate::PyResult<()> { - module.add_function(#krate::impl_::pyfunction::_wrap_pyfunction(&DEF, module)?) + 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())?) } } diff --git a/pytests/src/lib.rs b/pytests/src/lib.rs index b81dfdad256..dba8e0edf3d 100644 --- a/pytests/src/lib.rs +++ b/pytests/src/lib.rs @@ -29,7 +29,7 @@ mod pyo3_pytests { use {buf_and_str::buf_and_str, datetime::datetime}; #[pymodule_init] - fn init(m: &PyModule) -> PyResult<()> { + fn init(m: &Bound<'_, PyModule>) -> PyResult<()> { // Inserting to sys.modules allows importing submodules nicely from Python // e.g. import pyo3_pytests.buf_and_str as bas let sys = PyModule::import_bound(m.py(), "sys")?; diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 0c75578af52..9fff799c37b 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, PyTypeInfo, 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,7 +127,7 @@ 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)) @@ -137,12 +138,12 @@ impl ModuleDef { /// /// Currently only implemented for classes. pub trait PyAddToModule { - fn add_to_module(module: &PyModule) -> PyResult<()>; + fn add_to_module(module: &Bound<'_, PyModule>) -> PyResult<()>; } impl PyAddToModule for T { - fn add_to_module(module: &PyModule) -> PyResult<()> { - module.add(Self::NAME, Self::type_object(module.py())) + fn add_to_module(module: &Bound<'_, PyModule>) -> PyResult<()> { + module.add(Self::NAME, Self::type_object_bound(module.py())) } } @@ -150,7 +151,10 @@ impl PyAddToModule for T { 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}; @@ -160,7 +164,7 @@ mod tests { ModuleDef::new( "test_module\0", "some doc\0", - ModuleInitializer(|_, m| { + ModuleInitializer(|m| { m.add("SOME_CONSTANT", 42)?; Ok(()) }), @@ -205,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(()) } @@ -216,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/tests/test_append_to_inittab.rs b/tests/test_append_to_inittab.rs index 6e1c8e0616b..4b964e67c0e 100644 --- a/tests/test_append_to_inittab.rs +++ b/tests/test_append_to_inittab.rs @@ -37,7 +37,7 @@ assert module_fn_with_functions.foo() == 123 .unwrap(); }); Python::with_gil(|py| { - py.run( + py.run_bound( r#" import module_mod_with_functions assert module_mod_with_functions.foo() == 123 diff --git a/tests/test_module.rs b/tests/test_module.rs index 73fe968407f..6702b828737 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -507,7 +507,7 @@ mod declarative_module { use super::{declarative_module2, double, MyError, ValueClass as Value}; #[pymodule_init] - fn init(m: &PyModule) -> PyResult<()> { + fn init(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add("double2", m.getattr("double")?) } } From cff28c583bf2ae68816a566c68dbf1cfba2d2d4f Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 21 Feb 2024 10:13:24 +0100 Subject: [PATCH 5/7] Renames #[pyo3] to #[pymodule_export] --- pyo3-macros-backend/src/module.rs | 2 +- pytests/src/lib.rs | 5 +++-- tests/test_append_to_inittab.rs | 3 ++- tests/test_module.rs | 10 +++++----- tests/ui/invalid_pymodule_glob.rs | 2 +- tests/ui/invalid_pymodule_in_root_module.rs | 0 tests/ui/invalid_pymodule_trait.rs | 2 +- tests/ui/invalid_pymodule_trait.stderr | 2 +- 8 files changed, 14 insertions(+), 12 deletions(-) delete mode 100644 tests/ui/invalid_pymodule_in_root_module.rs diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 317a348a313..00ee8a3e847 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -116,7 +116,7 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { Item::Use(item_use) => { let mut is_pyo3 = false; item_use.attrs.retain(|attr| { - let found = attr.path().is_ident("pyo3"); + let found = attr.path().is_ident("pymodule_export"); is_pyo3 |= found; !found }); diff --git a/pytests/src/lib.rs b/pytests/src/lib.rs index dba8e0edf3d..9ffbed9cf27 100644 --- a/pytests/src/lib.rs +++ b/pytests/src/lib.rs @@ -1,4 +1,5 @@ use pyo3::prelude::*; + pub mod awaitable; pub mod buf_and_str; pub mod comparisons; @@ -18,14 +19,14 @@ pub mod subclassing; mod pyo3_pytests { use super::*; use pyo3::types::PyDict; - #[pyo3] + #[pymodule_export] use { awaitable::awaitable, comparisons::comparisons, dict_iter::dict_iter, enums::enums, misc::misc, objstore::objstore, othermod::othermod, path::path, pyclasses::pyclasses, pyfunctions::pyfunctions, sequence::sequence, subclassing::subclassing, }; #[cfg(not(Py_LIMITED_API))] - #[pyo3] + #[pymodule_export] use {buf_and_str::buf_and_str, datetime::datetime}; #[pymodule_init] diff --git a/tests/test_append_to_inittab.rs b/tests/test_append_to_inittab.rs index 4b964e67c0e..06ecc0ef893 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] @@ -14,7 +15,7 @@ fn module_fn_with_functions(_py: Python<'_>, m: &PyModule) -> PyResult<()> { #[pymodule] mod module_mod_with_functions { - #[pyo3] + #[pymodule_export] use super::foo; } diff --git a/tests/test_module.rs b/tests/test_module.rs index 6702b828737..06174c05269 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -496,14 +496,14 @@ create_exception!( /// A module written using declarative syntax. #[pymodule] mod declarative_module { - #[pyo3] + #[pymodule_export] use super::declarative_submodule; - #[pyo3] + #[pymodule_export] // This is not a real constraint but to test cfg attribute support #[cfg(not(Py_LIMITED_API))] use super::LocatedClass; use super::*; - #[pyo3] + #[pymodule_export] use super::{declarative_module2, double, MyError, ValueClass as Value}; #[pymodule_init] @@ -519,7 +519,7 @@ fn double_value(v: &ValueClass) -> usize { #[pymodule] mod declarative_submodule { - #[pyo3] + #[pymodule_export] use super::{double, double_value}; } @@ -527,7 +527,7 @@ mod declarative_submodule { #[pymodule] #[pyo3(name = "declarative_module_renamed")] mod declarative_module2 { - #[pyo3] + #[pymodule_export] use super::double; } diff --git a/tests/ui/invalid_pymodule_glob.rs b/tests/ui/invalid_pymodule_glob.rs index f47a6c72c7f..107cdf9382a 100644 --- a/tests/ui/invalid_pymodule_glob.rs +++ b/tests/ui/invalid_pymodule_glob.rs @@ -7,7 +7,7 @@ fn foo() -> usize { #[pymodule] mod module { - #[pyo3] + #[pymodule_export] use super::*; } diff --git a/tests/ui/invalid_pymodule_in_root_module.rs b/tests/ui/invalid_pymodule_in_root_module.rs deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/ui/invalid_pymodule_trait.rs b/tests/ui/invalid_pymodule_trait.rs index 0156fc36962..6649a3547a0 100644 --- a/tests/ui/invalid_pymodule_trait.rs +++ b/tests/ui/invalid_pymodule_trait.rs @@ -2,7 +2,7 @@ use pyo3::prelude::*; #[pymodule] mod module { - #[pyo3] + #[pymodule_export] trait Foo {} } diff --git a/tests/ui/invalid_pymodule_trait.stderr b/tests/ui/invalid_pymodule_trait.stderr index b8e51f7c7dc..3ed128617f5 100644 --- a/tests/ui/invalid_pymodule_trait.stderr +++ b/tests/ui/invalid_pymodule_trait.stderr @@ -1,5 +1,5 @@ error: only 'use' statements and and pymodule_init functions are allowed in #[pymodule] --> tests/ui/invalid_pymodule_trait.rs:5:5 | -5 | #[pyo3] +5 | #[pymodule_export] | ^ From beb75da311982c011b7ae02240d0a7d0d530a524 Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 21 Feb 2024 10:35:02 +0100 Subject: [PATCH 6/7] Gates #[pymodule] mod behind the experimental-declarative-modules feature --- Cargo.toml | 4 ++ newsfragments/3815.added.md | 3 +- pyo3-macros/Cargo.toml | 1 + pyo3-macros/src/lib.rs | 9 ++- pytests/src/lib.rs | 72 ++++++++++++---------- tests/test_append_to_inittab.rs | 7 +++ tests/test_compile_error.rs | 4 ++ tests/test_declarative_module.rs | 101 +++++++++++++++++++++++++++++++ tests/test_module.rs | 75 +---------------------- 9 files changed, 167 insertions(+), 109 deletions(-) create mode 100644 tests/test_declarative_module.rs diff --git a/Cargo.toml b/Cargo.toml index 4d1899cbdb9..e7364a7c9f5 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 index dd450ca88c6..e4fd3e9315a 100644 --- a/newsfragments/3815.added.md +++ b/newsfragments/3815.added.md @@ -1 +1,2 @@ -The ability to create Python modules with a Rust `mod` block. \ No newline at end of file +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/Cargo.toml b/pyo3-macros/Cargo.toml index 576c94a2bc1..a0368a5f364 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 8dbf2782d5b..64756a1c73b 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -37,7 +37,14 @@ use syn::{parse::Nothing, parse_macro_input, Item}; pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream { parse_macro_input!(args as Nothing); match parse_macro_input!(input as Item) { - Item::Mod(module) => pymodule_module_impl(module), + 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, diff --git a/pytests/src/lib.rs b/pytests/src/lib.rs index 9ffbed9cf27..bfd80edb719 100644 --- a/pytests/src/lib.rs +++ b/pytests/src/lib.rs @@ -1,4 +1,6 @@ use pyo3::prelude::*; +use pyo3::types::PyDict; +use pyo3::wrap_pymodule; pub mod awaitable; pub mod buf_and_str; @@ -16,39 +18,43 @@ pub mod sequence; pub mod subclassing; #[pymodule] -mod pyo3_pytests { - use super::*; - use pyo3::types::PyDict; - #[pymodule_export] - use { - awaitable::awaitable, comparisons::comparisons, dict_iter::dict_iter, enums::enums, - misc::misc, objstore::objstore, othermod::othermod, path::path, pyclasses::pyclasses, - pyfunctions::pyfunctions, sequence::sequence, subclassing::subclassing, - }; +fn pyo3_pytests(py: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_wrapped(wrap_pymodule!(awaitable::awaitable))?; #[cfg(not(Py_LIMITED_API))] - #[pymodule_export] - use {buf_and_str::buf_and_str, datetime::datetime}; + m.add_wrapped(wrap_pymodule!(buf_and_str::buf_and_str))?; + m.add_wrapped(wrap_pymodule!(comparisons::comparisons))?; + #[cfg(not(Py_LIMITED_API))] + m.add_wrapped(wrap_pymodule!(datetime::datetime))?; + m.add_wrapped(wrap_pymodule!(dict_iter::dict_iter))?; + m.add_wrapped(wrap_pymodule!(enums::enums))?; + m.add_wrapped(wrap_pymodule!(misc::misc))?; + m.add_wrapped(wrap_pymodule!(objstore::objstore))?; + m.add_wrapped(wrap_pymodule!(othermod::othermod))?; + m.add_wrapped(wrap_pymodule!(path::path))?; + m.add_wrapped(wrap_pymodule!(pyclasses::pyclasses))?; + m.add_wrapped(wrap_pymodule!(pyfunctions::pyfunctions))?; + m.add_wrapped(wrap_pymodule!(sequence::sequence))?; + m.add_wrapped(wrap_pymodule!(subclassing::subclassing))?; + + // Inserting to sys.modules allows importing submodules nicely from Python + // e.g. import pyo3_pytests.buf_and_str as bas + + let sys = PyModule::import_bound(py, "sys")?; + let sys_modules = sys.getattr("modules")?.downcast_into::()?; + sys_modules.set_item("pyo3_pytests.awaitable", m.getattr("awaitable")?)?; + sys_modules.set_item("pyo3_pytests.buf_and_str", m.getattr("buf_and_str")?)?; + sys_modules.set_item("pyo3_pytests.comparisons", m.getattr("comparisons")?)?; + sys_modules.set_item("pyo3_pytests.datetime", m.getattr("datetime")?)?; + sys_modules.set_item("pyo3_pytests.dict_iter", m.getattr("dict_iter")?)?; + sys_modules.set_item("pyo3_pytests.enums", m.getattr("enums")?)?; + sys_modules.set_item("pyo3_pytests.misc", m.getattr("misc")?)?; + sys_modules.set_item("pyo3_pytests.objstore", m.getattr("objstore")?)?; + sys_modules.set_item("pyo3_pytests.othermod", m.getattr("othermod")?)?; + sys_modules.set_item("pyo3_pytests.path", m.getattr("path")?)?; + sys_modules.set_item("pyo3_pytests.pyclasses", m.getattr("pyclasses")?)?; + sys_modules.set_item("pyo3_pytests.pyfunctions", m.getattr("pyfunctions")?)?; + sys_modules.set_item("pyo3_pytests.sequence", m.getattr("sequence")?)?; + sys_modules.set_item("pyo3_pytests.subclassing", m.getattr("subclassing")?)?; - #[pymodule_init] - fn init(m: &Bound<'_, PyModule>) -> PyResult<()> { - // Inserting to sys.modules allows importing submodules nicely from Python - // e.g. import pyo3_pytests.buf_and_str as bas - let sys = PyModule::import_bound(m.py(), "sys")?; - let sys_modules = sys.getattr("modules")?.downcast_into::()?; - sys_modules.set_item("pyo3_pytests.awaitable", m.getattr("awaitable")?)?; - sys_modules.set_item("pyo3_pytests.buf_and_str", m.getattr("buf_and_str")?)?; - sys_modules.set_item("pyo3_pytests.comparisons", m.getattr("comparisons")?)?; - sys_modules.set_item("pyo3_pytests.datetime", m.getattr("datetime")?)?; - sys_modules.set_item("pyo3_pytests.dict_iter", m.getattr("dict_iter")?)?; - sys_modules.set_item("pyo3_pytests.enums", m.getattr("enums")?)?; - sys_modules.set_item("pyo3_pytests.misc", m.getattr("misc")?)?; - sys_modules.set_item("pyo3_pytests.objstore", m.getattr("objstore")?)?; - sys_modules.set_item("pyo3_pytests.othermod", m.getattr("othermod")?)?; - sys_modules.set_item("pyo3_pytests.path", m.getattr("path")?)?; - sys_modules.set_item("pyo3_pytests.pyclasses", m.getattr("pyclasses")?)?; - sys_modules.set_item("pyo3_pytests.pyfunctions", m.getattr("pyfunctions")?)?; - sys_modules.set_item("pyo3_pytests.sequence", m.getattr("sequence")?)?; - sys_modules.set_item("pyo3_pytests.subclassing", m.getattr("subclassing")?)?; - Ok(()) - } + Ok(()) } diff --git a/tests/test_append_to_inittab.rs b/tests/test_append_to_inittab.rs index 06ecc0ef893..59ecaf42909 100644 --- a/tests/test_append_to_inittab.rs +++ b/tests/test_append_to_inittab.rs @@ -13,6 +13,7 @@ fn module_fn_with_functions(_py: Python<'_>, m: &PyModule) -> PyResult<()> { Ok(()) } +#[cfg(feature = "experimental-declarative-modules")] #[pymodule] mod module_mod_with_functions { #[pymodule_export] @@ -23,8 +24,12 @@ mod module_mod_with_functions { #[test] fn test_module_append_to_inittab() { use pyo3::append_to_inittab; + 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#" @@ -37,6 +42,8 @@ assert module_fn_with_functions.foo() == 123 .map_err(|e| e.display(py)) .unwrap(); }); + + #[cfg(feature = "experimental-declarative-modules")] Python::with_gil(|py| { py.run_bound( r#" diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index d6df9c64e39..5f2d25db92f 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -40,8 +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 00000000000..86913d9b800 --- /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/test_module.rs b/tests/test_module.rs index 06174c05269..9d14f243d50 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -2,10 +2,9 @@ use pyo3::prelude::*; -use pyo3::exceptions::PyException; +use pyo3::py_run; use pyo3::types::PyString; use pyo3::types::{IntoPyDict, PyDict, PyTuple}; -use pyo3::{create_exception, py_run}; #[path = "../src/tests/common.rs"] mod common; @@ -485,75 +484,3 @@ fn test_module_doc_hidden() { py_assert!(py, m, "m.__doc__ == ''"); }) } - -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_ref(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')"); - }) -} From 391b8f91caff364908f9ca472cee0db6af146adc Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 23 Feb 2024 16:49:24 +0100 Subject: [PATCH 7/7] Properly fails on functions inside of declarative modules --- pyo3-macros-backend/src/module.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 00ee8a3e847..6907e484f71 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -146,6 +146,8 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { 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 => {