#[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 <mail@davidhewitt.dev>
Co-authored-by: Georg Brandl <georg@python.org>

* 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 <mail@davidhewitt.dev>
Co-authored-by: Georg Brandl <georg@python.org>
This commit is contained in:
Thomas Tanon 2024-02-24 14:50:18 +01:00 committed by GitHub
parent c06bb8f1f1
commit e0e3981e17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 458 additions and 71 deletions

View File

@ -69,6 +69,9 @@ default = ["macros"]
# and IntoPy traits # and IntoPy traits
experimental-inspect = [] 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. # Enables macros: #[pyclass], #[pymodule], #[pyfunction] etc.
macros = ["pyo3-macros", "indoc", "unindent"] macros = ["pyo3-macros", "indoc", "unindent"]
@ -114,6 +117,7 @@ full = [
"chrono-tz", "chrono-tz",
"either", "either",
"experimental-inspect", "experimental-inspect",
"experimental-declarative-modules",
"eyre", "eyre",
"hashbrown", "hashbrown",
"indexmap", "indexmap",

View File

@ -0,0 +1,2 @@
The ability to create Python modules with a Rust `mod` block
behind the `experimental-declarative-modules` feature.

View File

@ -22,7 +22,7 @@ mod pymethod;
mod quotes; mod quotes;
pub use frompyobject::build_derive_from_pyobject; 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 pyclass::{build_py_class, build_py_enum, PyClassArgs};
pub use pyfunction::{build_py_function, PyFunctionOptions}; pub use pyfunction::{build_py_function, PyFunctionOptions};
pub use pyimpl::{build_py_methods, PyClassMethodsType}; pub use pyimpl::{build_py_methods, PyClassMethodsType};

View File

@ -2,8 +2,9 @@
use crate::{ use crate::{
attributes::{self, take_attributes, take_pyo3_options, CrateAttribute, NameAttribute}, attributes::{self, take_attributes, take_pyo3_options, CrateAttribute, NameAttribute},
get_doc,
pyfunction::{impl_wrap_pyfunction, PyFunctionOptions}, pyfunction::{impl_wrap_pyfunction, PyFunctionOptions},
utils::{get_pyo3_crate, PythonDoc}, utils::get_pyo3_crate,
}; };
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use quote::quote; use quote::quote;
@ -12,7 +13,7 @@ use syn::{
parse::{Parse, ParseStream}, parse::{Parse, ParseStream},
spanned::Spanned, spanned::Spanned,
token::Comma, token::Comma,
Ident, Path, Result, Visibility, Item, Path, Result,
}; };
#[derive(Default)] #[derive(Default)]
@ -56,33 +57,154 @@ impl PyModuleOptions {
} }
} }
pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result<TokenStream> {
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<syn::Ident>,
target_cfg_attrs: &mut Vec<Vec<syn::Attribute>>,
) -> 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::<Vec<_>>();
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 /// Generates the function that is called by the python interpreter to initialize the native
/// module /// module
pub fn pymodule_impl( pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result<TokenStream> {
fnname: &Ident, let options = PyModuleOptions::from_attrs(&mut function.attrs)?;
options: PyModuleOptions, process_functions_in_module(&options, &mut function)?;
doc: PythonDoc,
visibility: &Visibility,
) -> TokenStream {
let name = options.name.unwrap_or_else(|| fnname.unraw());
let krate = get_pyo3_crate(&options.krate); 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! { let initialization = module_initialization(options, ident);
// Create a module with the same name as the `#[pymodule]` - this way `use <the module>` Ok(quote! {
// will actually bring both the module and the function into scope. #function
#[doc(hidden)] #vis mod #ident {
#visibility mod #fnname { #initialization
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))
}
} }
// Generate the definition inside an anonymous function in the same scope as the original function - // 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) // inside a function body)
const _: () = { const _: () = {
use #krate::impl_::pymodule as impl_; 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 fn make_def() -> impl_::ModuleDef {
const INITIALIZER: impl_::ModuleInitializer = impl_::ModuleInitializer(#fnname);
unsafe { 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]` /// Finds and takes care of the #[pyfn(...)] in `#[pymodule]`
pub fn process_functions_in_module( fn process_functions_in_module(options: &PyModuleOptions, func: &mut syn::ItemFn) -> Result<()> {
options: &PyModuleOptions,
func: &mut syn::ItemFn,
) -> syn::Result<()> {
let mut stmts: Vec<syn::Stmt> = Vec::new(); let mut stmts: Vec<syn::Stmt> = Vec::new();
let krate = get_pyo3_crate(&options.krate); let krate = get_pyo3_crate(&options.krate);
for mut stmt in func.block.stmts.drain(..) { 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)? { if let Some(pyfn_args) = get_pyfn_attr(&mut func.attrs)? {
let module_name = pyfn_args.modname; let module_name = pyfn_args.modname;
let wrapped_function = impl_wrap_pyfunction(func, pyfn_args.options)?; let wrapped_function = impl_wrap_pyfunction(func, pyfn_args.options)?;

View File

@ -269,6 +269,12 @@ pub fn impl_wrap_pyfunction(
#vis mod #name { #vis mod #name {
pub(crate) struct MakeDef; pub(crate) struct MakeDef;
pub const DEF: #krate::impl_::pyfunction::PyMethodDef = MakeDef::DEF; 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 - // Generate the definition inside an anonymous function in the same scope as the original function -

View File

@ -15,6 +15,7 @@ proc-macro = true
[features] [features]
multiple-pymethods = [] multiple-pymethods = []
experimental-declarative-modules = []
[dependencies] [dependencies]
proc-macro2 = { version = "1", default-features = false } proc-macro2 = { version = "1", default-features = false }

View File

@ -6,11 +6,11 @@ use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2; use proc_macro2::TokenStream as TokenStream2;
use pyo3_macros_backend::{ use pyo3_macros_backend::{
build_derive_from_pyobject, build_py_class, build_py_enum, build_py_function, build_py_methods, build_derive_from_pyobject, build_py_class, build_py_enum, build_py_function, build_py_methods,
get_doc, process_functions_in_module, pymodule_impl, PyClassArgs, PyClassMethodsType, pymodule_function_impl, pymodule_module_impl, PyClassArgs, PyClassMethodsType,
PyFunctionOptions, PyModuleOptions, PyFunctionOptions,
}; };
use quote::quote; 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. /// A proc macro used to implement Python modules.
/// ///
@ -36,31 +36,27 @@ use syn::{parse::Nothing, parse_macro_input};
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream { pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream {
parse_macro_input!(args as Nothing); parse_macro_input!(args as Nothing);
match parse_macro_input!(input as Item) {
let mut ast = parse_macro_input!(input as syn::ItemFn); Item::Mod(module) => if cfg!(feature = "experimental-declarative-modules") {
let options = match PyModuleOptions::from_attrs(&mut ast.attrs) { pymodule_module_impl(module)
Ok(options) => options, } else {
Err(e) => return e.into_compile_error().into(), Err(syn::Error::new_spanned(
}; module,
"#[pymodule] requires the 'experimental-declarative-modules' feature to be used on Rust modules.",
if let Err(err) = process_functions_in_module(&options, &mut ast) { ))
return err.into_compile_error().into(); },
Item::Fn(function) => pymodule_function_impl(function),
unsupported => Err(syn::Error::new_spanned(
unsupported,
"#[pymodule] only supports modules and functions.",
)),
} }
.unwrap_or_compile_error()
let doc = get_doc(&ast.attrs, None);
let expanded = pymodule_impl(&ast.sig.ident, options, doc, &ast.vis);
quote!(
#ast
#expanded
)
.into() .into()
} }
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream { pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream {
use syn::Item;
let item = parse_macro_input!(input as Item); let item = parse_macro_input!(input as Item);
match item { match item {
Item::Struct(struct_) => pyclass_impl(attr, struct_, methods_type()), Item::Struct(struct_) => pyclass_impl(attr, struct_, methods_type()),

View File

@ -7,7 +7,8 @@ use portable_atomic::{AtomicI64, Ordering};
#[cfg(not(PyPy))] #[cfg(not(PyPy))]
use crate::exceptions::PyImportError; 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`. /// `Sync` wrapper of `ffi::PyModuleDef`.
pub struct ModuleDef { pub struct ModuleDef {
@ -22,7 +23,7 @@ pub struct ModuleDef {
} }
/// Wrapper to enable initializer to be used in const fns. /// 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 {} unsafe impl Sync for ModuleDef {}
@ -126,18 +127,34 @@ impl ModuleDef {
ffi::PyModule_Create(self.ffi_def.get()), ffi::PyModule_Create(self.ffi_def.get()),
)? )?
}; };
(self.initializer.0)(py, module.as_ref(py))?; self.initializer.0(module.bind(py))?;
Ok(module) Ok(module)
}) })
.map(|py_module| py_module.clone_ref(py)) .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<T: PyTypeInfo> PyAddToModule for T {
fn add_to_module(module: &Bound<'_, PyModule>) -> PyResult<()> {
module.add(Self::NAME, Self::type_object_bound(module.py()))
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::sync::atomic::{AtomicBool, Ordering}; 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}; use super::{ModuleDef, ModuleInitializer};
@ -147,7 +164,7 @@ mod tests {
ModuleDef::new( ModuleDef::new(
"test_module\0", "test_module\0",
"some doc\0", "some doc\0",
ModuleInitializer(|_, m| { ModuleInitializer(|m| {
m.add("SOME_CONSTANT", 42)?; m.add("SOME_CONSTANT", 42)?;
Ok(()) Ok(())
}), }),
@ -192,7 +209,7 @@ mod tests {
static INIT_CALLED: AtomicBool = AtomicBool::new(false); static INIT_CALLED: AtomicBool = AtomicBool::new(false);
#[allow(clippy::unnecessary_wraps)] #[allow(clippy::unnecessary_wraps)]
fn init(_: Python<'_>, _: &PyModule) -> PyResult<()> { fn init(_: &Bound<'_, PyModule>) -> PyResult<()> {
INIT_CALLED.store(true, Ordering::SeqCst); INIT_CALLED.store(true, Ordering::SeqCst);
Ok(()) Ok(())
} }
@ -203,8 +220,7 @@ mod tests {
assert_eq!((*module_def.ffi_def.get()).m_doc, DOC.as_ptr() as _); assert_eq!((*module_def.ffi_def.get()).m_doc, DOC.as_ptr() as _);
Python::with_gil(|py| { Python::with_gil(|py| {
module_def.initializer.0(py, py.import_bound("builtins").unwrap().into_gil_ref()) module_def.initializer.0(&py.import_bound("builtins").unwrap()).unwrap();
.unwrap();
assert!(INIT_CALLED.load(Ordering::SeqCst)); assert!(INIT_CALLED.load(Ordering::SeqCst));
}) })
} }

View File

@ -169,8 +169,8 @@ macro_rules! append_to_inittab {
); );
} }
$crate::ffi::PyImport_AppendInittab( $crate::ffi::PyImport_AppendInittab(
$module::NAME.as_ptr() as *const ::std::os::raw::c_char, $module::__PYO3_NAME.as_ptr() as *const ::std::os::raw::c_char,
::std::option::Option::Some($module::init), ::std::option::Option::Some($module::__pyo3_init),
); );
} }
}; };

View File

@ -1,4 +1,5 @@
#![cfg(all(feature = "macros", not(PyPy)))] #![cfg(all(feature = "macros", not(PyPy)))]
use pyo3::prelude::*; use pyo3::prelude::*;
#[pyfunction] #[pyfunction]
@ -7,26 +8,52 @@ fn foo() -> usize {
} }
#[pymodule] #[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(); m.add_function(wrap_pyfunction!(foo, m)?).unwrap();
Ok(()) Ok(())
} }
#[cfg(feature = "experimental-declarative-modules")]
#[pymodule]
mod module_mod_with_functions {
#[pymodule_export]
use super::foo;
}
#[cfg(not(PyPy))] #[cfg(not(PyPy))]
#[test] #[test]
fn test_module_append_to_inittab() { fn test_module_append_to_inittab() {
use pyo3::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| { Python::with_gil(|py| {
py.run_bound( py.run_bound(
r#" r#"
import module_with_functions import module_fn_with_functions
assert module_with_functions.foo() == 123 assert module_fn_with_functions.foo() == 123
"#, "#,
None, None,
None, None,
) )
.map_err(|e| e.display(py)) .map_err(|e| e.display(py))
.unwrap(); .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();
});
} }

View File

@ -40,4 +40,12 @@ fn test_compile_errors() {
t.compile_fail("tests/ui/not_send2.rs"); t.compile_fail("tests/ui/not_send2.rs");
t.compile_fail("tests/ui/get_set_all.rs"); t.compile_fail("tests/ui/get_set_all.rs");
t.compile_fail("tests/ui/traverse.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");
} }

View File

@ -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')");
})
}

View File

@ -0,0 +1,14 @@
use pyo3::prelude::*;
#[pyfunction]
fn foo() -> usize {
0
}
#[pymodule]
mod module {
#[pymodule_export]
use super::*;
}
fn main() {}

View File

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

View File

@ -0,0 +1,6 @@
use pyo3::prelude::*;
#[pymodule]
mod invalid_pymodule_in_root_module;
fn main() {}

View File

@ -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 <https://github.com/rust-lang/rust/issues/54727> for more information
error: `#[pymodule]` can only be used on inline modules
--> tests/ui/invalid_pymodule_in_root.rs:4:1
|
4 | mod invalid_pymodule_in_root_module;
| ^^^

View File

@ -0,0 +1,9 @@
use pyo3::prelude::*;
#[pymodule]
mod module {
#[pymodule_export]
trait Foo {}
}
fn main() {}

View File

@ -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]
| ^

View File

@ -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() {}

View File

@ -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<()> {
| ^^