From f3a8f7ee70f099bb48b2273ca6036f825b6b8719 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 9 Feb 2024 15:20:09 +0100 Subject: [PATCH] #[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 --- README.md | 8 +- guide/src/class/numeric.md | 5 +- newsfragments/TODO.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 | 13 +- src/macros.rs | 4 +- tests/test_append_to_inittab.rs | 29 +++- tests/test_compile_error.rs | 4 + tests/test_module.rs | 63 ++++++- 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 | 14 ++ tests/ui/invalid_pymodule_in_root_module.rs | 0 tests/ui/invalid_pymodule_trait.rs | 9 + tests/ui/invalid_pymodule_trait.stderr | 6 + .../ui/invalid_pymodule_two_pymodule_init.rs | 16 ++ .../invalid_pymodule_two_pymodule_init.stderr | 7 + 22 files changed, 413 insertions(+), 63 deletions(-) create mode 100644 newsfragments/TODO.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/README.md b/README.md index 98eeeb80242..3546320065e 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,8 @@ ## Usage PyO3 supports the following software versions: - - Python 3.7 and up (CPython and PyPy) - - Rust 1.56 and up +- Python 3.7 and up (CPython and PyPy) +- Rust 1.56 and up You can use PyO3 to write a native Python module in Rust, or to embed Python in a Rust binary. The following sections explain each of these in turn. @@ -183,7 +183,7 @@ about this topic. - Contains an example of building wheels on TravisCI and appveyor using [cibuildwheel](https://github.com/pypa/cibuildwheel) - [ballista-python](https://github.com/apache/arrow-ballista-python) _A Python library that binds to Apache Arrow distributed query engine Ballista._ - [bed-reader](https://github.com/fastlmm/bed-reader) _Read and write the PLINK BED format, simply and efficiently._ - - Shows Rayon/ndarray::parallel (including capturing errors, controlling thread num), Python types to Rust generics, Github Actions + - Shows Rayon/ndarray::parallel (including capturing errors, controlling thread num), Python types to Rust generics, Github Actions - [cryptography](https://github.com/pyca/cryptography/tree/main/src/rust) _Python cryptography library with some functionality in Rust._ - [css-inline](https://github.com/Stranger6667/css-inline/tree/master/bindings/python) _CSS inlining for Python implemented in Rust._ - [datafusion-python](https://github.com/apache/arrow-datafusion-python) _A Python library that binds to Apache Arrow in-memory query engine DataFusion._ @@ -206,7 +206,7 @@ about this topic. - [polars](https://github.com/pola-rs/polars) _Fast multi-threaded DataFrame library in Rust | Python | Node.js._ - [pydantic-core](https://github.com/pydantic/pydantic-core) _Core validation logic for pydantic written in Rust._ - [pyheck](https://github.com/kevinheavey/pyheck) _Fast case conversion library, built by wrapping [heck](https://github.com/withoutboats/heck)._ - - Quite easy to follow as there's not much code. + - Quite easy to follow as there's not much code. - [pyre](https://github.com/Project-Dream-Weaver/pyre-http) _Fast Python HTTP server written in Rust._ - [ril-py](https://github.com/Cryptex-github/ril-py) _A performant and high-level image processing library for Python written in Rust._ - [river](https://github.com/online-ml/river) _Online machine learning in python, the computationally heavy statistics algorithms are implemented in Rust._ diff --git a/guide/src/class/numeric.md b/guide/src/class/numeric.md index ee17ea10bd9..a380017d48d 100644 --- a/guide/src/class/numeric.md +++ b/guide/src/class/numeric.md @@ -327,9 +327,8 @@ impl Number { } #[pymodule] -fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { - m.add_class::()?; - Ok(()) +mod my_module { + use super::Number; } # const SCRIPT: &'static str = r#" # def hash_djb2(s: str): diff --git a/newsfragments/TODO.added.md b/newsfragments/TODO.added.md new file mode 100644 index 00000000000..dd450ca88c6 --- /dev/null +++ b/newsfragments/TODO.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 e65385bf679..1c4d97eedaa 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(py, "sys")?; - let sys_modules: &PyDict = sys.getattr("modules")?.downcast()?; - 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(m.py(), "sys")?; + let sys_modules: &PyDict = sys.getattr("modules")?.downcast()?; + 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 0fe5c3846c2..80fd001b322 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 { @@ -132,6 +132,17 @@ impl ModuleDef { } } +/// Trait to add en element (class, function...) to a module +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 41de9079c40..d81dc10106e 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -167,8 +167,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 e0a57da1b5c..f069ecd48ac 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( + r#" +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_with_functions -assert module_with_functions.foo() == 123 +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 2de23b38324..02c4369b49f 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -2,8 +2,9 @@ use pyo3::prelude::*; -use pyo3::py_run; +use pyo3::exceptions::PyException; use pyo3::types::{IntoPyDict, PyDict, PyTuple}; +use pyo3::{create_exception, py_run}; #[path = "../src/tests/common.rs"] mod common; @@ -465,3 +466,63 @@ 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::module_with_functions; + #[pyo3] + #[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")?) + } +} + +/// 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.module_with_functions.no_parameters() == 42"); + py_assert!( + py, + m, + "m.module_with_functions.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, "m.LocatedClass is None"); + #[cfg(not(Py_LIMITED_API))] + py_assert!(py, m, "m.LocatedClass is not None"); + }) +} 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..5cfd1fdd22c --- /dev/null +++ b/tests/ui/invalid_pymodule_in_root.stderr @@ -0,0 +1,14 @@ +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 + = help: add `#![feature(proc_macro_hygiene)]` to the crate attributes to enable + +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..eea90bbdd72 --- /dev/null +++ b/tests/ui/invalid_pymodule_trait.stderr @@ -0,0 +1,6 @@ +error: only 'use' statements and and pymodule_init functions are allowed in #[pymodule] + --> tests/ui/invalid_pymodule_trait.rs:5:5 + | +5 | / #[pyo3] +6 | | trait Foo {} + | |________________^ 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..1e262e1e36b --- /dev/null +++ b/tests/ui/invalid_pymodule_two_pymodule_init.stderr @@ -0,0 +1,7 @@ +error: only one pymodule_init may be specified + --> tests/ui/invalid_pymodule_two_pymodule_init.rs:11:5 + | +11 | / fn init2(m: &PyModule) -> PyResult<()> { +12 | | Ok(()) +13 | | } + | |_____^