#[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:
parent
c06bb8f1f1
commit
e0e3981e17
|
@ -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",
|
||||
|
|
2
newsfragments/3815.added.md
Normal file
2
newsfragments/3815.added.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
The ability to create Python modules with a Rust `mod` block
|
||||
behind the `experimental-declarative-modules` feature.
|
|
@ -22,7 +22,7 @@ mod pymethod;
|
|||
mod quotes;
|
||||
|
||||
pub use frompyobject::build_derive_from_pyobject;
|
||||
pub use module::{process_functions_in_module, pymodule_impl, PyModuleOptions};
|
||||
pub use module::{pymodule_function_impl, pymodule_module_impl, PyModuleOptions};
|
||||
pub use pyclass::{build_py_class, build_py_enum, PyClassArgs};
|
||||
pub use pyfunction::{build_py_function, PyFunctionOptions};
|
||||
pub use pyimpl::{build_py_methods, PyClassMethodsType};
|
||||
|
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
use crate::{
|
||||
attributes::{self, take_attributes, take_pyo3_options, CrateAttribute, NameAttribute},
|
||||
get_doc,
|
||||
pyfunction::{impl_wrap_pyfunction, PyFunctionOptions},
|
||||
utils::{get_pyo3_crate, PythonDoc},
|
||||
utils::get_pyo3_crate,
|
||||
};
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
|
@ -12,7 +13,7 @@ use syn::{
|
|||
parse::{Parse, ParseStream},
|
||||
spanned::Spanned,
|
||||
token::Comma,
|
||||
Ident, Path, Result, Visibility,
|
||||
Item, Path, Result,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -56,33 +57,154 @@ impl PyModuleOptions {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result<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
|
||||
/// module
|
||||
pub fn pymodule_impl(
|
||||
fnname: &Ident,
|
||||
options: PyModuleOptions,
|
||||
doc: PythonDoc,
|
||||
visibility: &Visibility,
|
||||
) -> TokenStream {
|
||||
let name = options.name.unwrap_or_else(|| fnname.unraw());
|
||||
pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result<TokenStream> {
|
||||
let options = PyModuleOptions::from_attrs(&mut function.attrs)?;
|
||||
process_functions_in_module(&options, &mut function)?;
|
||||
let krate = get_pyo3_crate(&options.krate);
|
||||
let pyinit_symbol = format!("PyInit_{}", name);
|
||||
let ident = &function.sig.ident;
|
||||
let vis = &function.vis;
|
||||
let doc = get_doc(&function.attrs, None);
|
||||
|
||||
quote! {
|
||||
// Create a module with the same name as the `#[pymodule]` - this way `use <the module>`
|
||||
// will actually bring both the module and the function into scope.
|
||||
#[doc(hidden)]
|
||||
#visibility mod #fnname {
|
||||
pub(crate) struct MakeDef;
|
||||
pub static DEF: #krate::impl_::pymodule::ModuleDef = MakeDef::make_def();
|
||||
pub const NAME: &'static str = concat!(stringify!(#name), "\0");
|
||||
|
||||
/// This autogenerated function is called by the python interpreter when importing
|
||||
/// the module.
|
||||
#[export_name = #pyinit_symbol]
|
||||
pub unsafe extern "C" fn init() -> *mut #krate::ffi::PyObject {
|
||||
#krate::impl_::trampoline::module_init(|py| DEF.make_module(py))
|
||||
}
|
||||
let initialization = module_initialization(options, ident);
|
||||
Ok(quote! {
|
||||
#function
|
||||
#vis mod #ident {
|
||||
#initialization
|
||||
}
|
||||
|
||||
// Generate the definition inside an anonymous function in the same scope as the original function -
|
||||
|
@ -91,28 +213,59 @@ pub fn pymodule_impl(
|
|||
// inside a function body)
|
||||
const _: () = {
|
||||
use #krate::impl_::pymodule as impl_;
|
||||
impl #fnname::MakeDef {
|
||||
|
||||
fn __pyo3_pymodule(module: &#krate::Bound<'_, #krate::types::PyModule>) -> #krate::PyResult<()> {
|
||||
#ident(module.py(), module.as_gil_ref())
|
||||
}
|
||||
|
||||
impl #ident::MakeDef {
|
||||
const fn make_def() -> impl_::ModuleDef {
|
||||
const INITIALIZER: impl_::ModuleInitializer = impl_::ModuleInitializer(#fnname);
|
||||
unsafe {
|
||||
impl_::ModuleDef::new(#fnname::NAME, #doc, INITIALIZER)
|
||||
const INITIALIZER: impl_::ModuleInitializer = impl_::ModuleInitializer(__pyo3_pymodule);
|
||||
impl_::ModuleDef::new(
|
||||
#ident::__PYO3_NAME,
|
||||
#doc,
|
||||
INITIALIZER
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
fn module_initialization(options: PyModuleOptions, ident: &syn::Ident) -> TokenStream {
|
||||
let name = options.name.unwrap_or_else(|| ident.unraw());
|
||||
let krate = get_pyo3_crate(&options.krate);
|
||||
let pyinit_symbol = format!("PyInit_{}", name);
|
||||
|
||||
quote! {
|
||||
pub const __PYO3_NAME: &'static str = concat!(stringify!(#name), "\0");
|
||||
|
||||
pub(super) struct MakeDef;
|
||||
pub static DEF: #krate::impl_::pymodule::ModuleDef = MakeDef::make_def();
|
||||
|
||||
pub fn add_to_module(module: &#krate::Bound<'_, #krate::types::PyModule>) -> #krate::PyResult<()> {
|
||||
use #krate::prelude::PyModuleMethods;
|
||||
module.add_submodule(DEF.make_module(module.py())?.bind(module.py()))
|
||||
}
|
||||
|
||||
/// This autogenerated function is called by the python interpreter when importing
|
||||
/// the module.
|
||||
#[export_name = #pyinit_symbol]
|
||||
pub unsafe extern "C" fn __pyo3_init() -> *mut #krate::ffi::PyObject {
|
||||
#krate::impl_::trampoline::module_init(|py| DEF.make_module(py))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds and takes care of the #[pyfn(...)] in `#[pymodule]`
|
||||
pub fn process_functions_in_module(
|
||||
options: &PyModuleOptions,
|
||||
func: &mut syn::ItemFn,
|
||||
) -> syn::Result<()> {
|
||||
fn process_functions_in_module(options: &PyModuleOptions, func: &mut syn::ItemFn) -> Result<()> {
|
||||
let mut stmts: Vec<syn::Stmt> = Vec::new();
|
||||
let krate = get_pyo3_crate(&options.krate);
|
||||
|
||||
for mut stmt in func.block.stmts.drain(..) {
|
||||
if let syn::Stmt::Item(syn::Item::Fn(func)) = &mut stmt {
|
||||
if let syn::Stmt::Item(Item::Fn(func)) = &mut stmt {
|
||||
if let Some(pyfn_args) = get_pyfn_attr(&mut func.attrs)? {
|
||||
let module_name = pyfn_args.modname;
|
||||
let wrapped_function = impl_wrap_pyfunction(func, pyfn_args.options)?;
|
||||
|
|
|
@ -269,6 +269,12 @@ pub fn impl_wrap_pyfunction(
|
|||
#vis mod #name {
|
||||
pub(crate) struct MakeDef;
|
||||
pub const DEF: #krate::impl_::pyfunction::PyMethodDef = MakeDef::DEF;
|
||||
|
||||
pub fn add_to_module(module: &#krate::Bound<'_, #krate::types::PyModule>) -> #krate::PyResult<()> {
|
||||
use #krate::prelude::PyModuleMethods;
|
||||
use ::std::convert::Into;
|
||||
module.add_function(&#krate::types::PyCFunction::internal_new(&DEF, module.as_gil_ref().into())?)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the definition inside an anonymous function in the same scope as the original function -
|
||||
|
|
|
@ -15,6 +15,7 @@ proc-macro = true
|
|||
|
||||
[features]
|
||||
multiple-pymethods = []
|
||||
experimental-declarative-modules = []
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = { version = "1", default-features = false }
|
||||
|
|
|
@ -6,11 +6,11 @@ use proc_macro::TokenStream;
|
|||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use pyo3_macros_backend::{
|
||||
build_derive_from_pyobject, build_py_class, build_py_enum, build_py_function, build_py_methods,
|
||||
get_doc, process_functions_in_module, pymodule_impl, PyClassArgs, PyClassMethodsType,
|
||||
PyFunctionOptions, PyModuleOptions,
|
||||
pymodule_function_impl, pymodule_module_impl, PyClassArgs, PyClassMethodsType,
|
||||
PyFunctionOptions,
|
||||
};
|
||||
use quote::quote;
|
||||
use syn::{parse::Nothing, parse_macro_input};
|
||||
use syn::{parse::Nothing, parse_macro_input, Item};
|
||||
|
||||
/// A proc macro used to implement Python modules.
|
||||
///
|
||||
|
@ -36,31 +36,27 @@ use syn::{parse::Nothing, parse_macro_input};
|
|||
#[proc_macro_attribute]
|
||||
pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
parse_macro_input!(args as Nothing);
|
||||
|
||||
let mut ast = parse_macro_input!(input as syn::ItemFn);
|
||||
let options = match PyModuleOptions::from_attrs(&mut ast.attrs) {
|
||||
Ok(options) => options,
|
||||
Err(e) => return e.into_compile_error().into(),
|
||||
};
|
||||
|
||||
if let Err(err) = process_functions_in_module(&options, &mut ast) {
|
||||
return err.into_compile_error().into();
|
||||
match parse_macro_input!(input as Item) {
|
||||
Item::Mod(module) => if cfg!(feature = "experimental-declarative-modules") {
|
||||
pymodule_module_impl(module)
|
||||
} else {
|
||||
Err(syn::Error::new_spanned(
|
||||
module,
|
||||
"#[pymodule] requires the 'experimental-declarative-modules' feature to be used on Rust modules.",
|
||||
))
|
||||
},
|
||||
Item::Fn(function) => pymodule_function_impl(function),
|
||||
unsupported => Err(syn::Error::new_spanned(
|
||||
unsupported,
|
||||
"#[pymodule] only supports modules and functions.",
|
||||
)),
|
||||
}
|
||||
|
||||
let doc = get_doc(&ast.attrs, None);
|
||||
|
||||
let expanded = pymodule_impl(&ast.sig.ident, options, doc, &ast.vis);
|
||||
|
||||
quote!(
|
||||
#ast
|
||||
#expanded
|
||||
)
|
||||
.unwrap_or_compile_error()
|
||||
.into()
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
use syn::Item;
|
||||
let item = parse_macro_input!(input as Item);
|
||||
match item {
|
||||
Item::Struct(struct_) => pyclass_impl(attr, struct_, methods_type()),
|
||||
|
|
|
@ -7,7 +7,8 @@ use portable_atomic::{AtomicI64, Ordering};
|
|||
|
||||
#[cfg(not(PyPy))]
|
||||
use crate::exceptions::PyImportError;
|
||||
use crate::{ffi, sync::GILOnceCell, types::PyModule, Py, PyResult, Python};
|
||||
use crate::types::module::PyModuleMethods;
|
||||
use crate::{ffi, sync::GILOnceCell, types::PyModule, Bound, Py, PyResult, PyTypeInfo, Python};
|
||||
|
||||
/// `Sync` wrapper of `ffi::PyModuleDef`.
|
||||
pub struct ModuleDef {
|
||||
|
@ -22,7 +23,7 @@ pub struct ModuleDef {
|
|||
}
|
||||
|
||||
/// Wrapper to enable initializer to be used in const fns.
|
||||
pub struct ModuleInitializer(pub for<'py> fn(Python<'py>, &PyModule) -> PyResult<()>);
|
||||
pub struct ModuleInitializer(pub for<'py> fn(&Bound<'py, PyModule>) -> PyResult<()>);
|
||||
|
||||
unsafe impl Sync for ModuleDef {}
|
||||
|
||||
|
@ -126,18 +127,34 @@ impl ModuleDef {
|
|||
ffi::PyModule_Create(self.ffi_def.get()),
|
||||
)?
|
||||
};
|
||||
(self.initializer.0)(py, module.as_ref(py))?;
|
||||
self.initializer.0(module.bind(py))?;
|
||||
Ok(module)
|
||||
})
|
||||
.map(|py_module| py_module.clone_ref(py))
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait to add an element (class, function...) to a module.
|
||||
///
|
||||
/// Currently only implemented for classes.
|
||||
pub trait PyAddToModule {
|
||||
fn add_to_module(module: &Bound<'_, PyModule>) -> PyResult<()>;
|
||||
}
|
||||
|
||||
impl<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)]
|
||||
mod tests {
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use crate::{types::any::PyAnyMethods, types::PyModule, PyResult, Python};
|
||||
use crate::{
|
||||
types::{any::PyAnyMethods, module::PyModuleMethods, PyModule},
|
||||
Bound, PyResult, Python,
|
||||
};
|
||||
|
||||
use super::{ModuleDef, ModuleInitializer};
|
||||
|
||||
|
@ -147,7 +164,7 @@ mod tests {
|
|||
ModuleDef::new(
|
||||
"test_module\0",
|
||||
"some doc\0",
|
||||
ModuleInitializer(|_, m| {
|
||||
ModuleInitializer(|m| {
|
||||
m.add("SOME_CONSTANT", 42)?;
|
||||
Ok(())
|
||||
}),
|
||||
|
@ -192,7 +209,7 @@ mod tests {
|
|||
static INIT_CALLED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn init(_: Python<'_>, _: &PyModule) -> PyResult<()> {
|
||||
fn init(_: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
INIT_CALLED.store(true, Ordering::SeqCst);
|
||||
Ok(())
|
||||
}
|
||||
|
@ -203,8 +220,7 @@ mod tests {
|
|||
assert_eq!((*module_def.ffi_def.get()).m_doc, DOC.as_ptr() as _);
|
||||
|
||||
Python::with_gil(|py| {
|
||||
module_def.initializer.0(py, py.import_bound("builtins").unwrap().into_gil_ref())
|
||||
.unwrap();
|
||||
module_def.initializer.0(&py.import_bound("builtins").unwrap()).unwrap();
|
||||
assert!(INIT_CALLED.load(Ordering::SeqCst));
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#![cfg(all(feature = "macros", not(PyPy)))]
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[pyfunction]
|
||||
|
@ -7,26 +8,52 @@ fn foo() -> usize {
|
|||
}
|
||||
|
||||
#[pymodule]
|
||||
fn module_with_functions(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
|
||||
fn module_fn_with_functions(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
|
||||
m.add_function(wrap_pyfunction!(foo, m)?).unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-declarative-modules")]
|
||||
#[pymodule]
|
||||
mod module_mod_with_functions {
|
||||
#[pymodule_export]
|
||||
use super::foo;
|
||||
}
|
||||
|
||||
#[cfg(not(PyPy))]
|
||||
#[test]
|
||||
fn test_module_append_to_inittab() {
|
||||
use pyo3::append_to_inittab;
|
||||
append_to_inittab!(module_with_functions);
|
||||
|
||||
append_to_inittab!(module_fn_with_functions);
|
||||
|
||||
#[cfg(feature = "experimental-declarative-modules")]
|
||||
append_to_inittab!(module_mod_with_functions);
|
||||
|
||||
Python::with_gil(|py| {
|
||||
py.run_bound(
|
||||
r#"
|
||||
import module_with_functions
|
||||
assert module_with_functions.foo() == 123
|
||||
import module_fn_with_functions
|
||||
assert module_fn_with_functions.foo() == 123
|
||||
"#,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.map_err(|e| e.display(py))
|
||||
.unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
#[cfg(feature = "experimental-declarative-modules")]
|
||||
Python::with_gil(|py| {
|
||||
py.run_bound(
|
||||
r#"
|
||||
import module_mod_with_functions
|
||||
assert module_mod_with_functions.foo() == 123
|
||||
"#,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.map_err(|e| e.display(py))
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -40,4 +40,12 @@ fn test_compile_errors() {
|
|||
t.compile_fail("tests/ui/not_send2.rs");
|
||||
t.compile_fail("tests/ui/get_set_all.rs");
|
||||
t.compile_fail("tests/ui/traverse.rs");
|
||||
#[cfg(feature = "experimental-declarative-modules")]
|
||||
t.compile_fail("tests/ui/invalid_pymodule_in_root.rs");
|
||||
#[cfg(feature = "experimental-declarative-modules")]
|
||||
t.compile_fail("tests/ui/invalid_pymodule_glob.rs");
|
||||
#[cfg(feature = "experimental-declarative-modules")]
|
||||
t.compile_fail("tests/ui/invalid_pymodule_trait.rs");
|
||||
#[cfg(feature = "experimental-declarative-modules")]
|
||||
t.compile_fail("tests/ui/invalid_pymodule_two_pymodule_init.rs");
|
||||
}
|
||||
|
|
101
tests/test_declarative_module.rs
Normal file
101
tests/test_declarative_module.rs
Normal 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')");
|
||||
})
|
||||
}
|
14
tests/ui/invalid_pymodule_glob.rs
Normal file
14
tests/ui/invalid_pymodule_glob.rs
Normal file
|
@ -0,0 +1,14 @@
|
|||
use pyo3::prelude::*;
|
||||
|
||||
#[pyfunction]
|
||||
fn foo() -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
#[pymodule]
|
||||
mod module {
|
||||
#[pymodule_export]
|
||||
use super::*;
|
||||
}
|
||||
|
||||
fn main() {}
|
5
tests/ui/invalid_pymodule_glob.stderr
Normal file
5
tests/ui/invalid_pymodule_glob.stderr
Normal file
|
@ -0,0 +1,5 @@
|
|||
error: #[pymodule] cannot import glob statements
|
||||
--> tests/ui/invalid_pymodule_glob.rs:11:16
|
||||
|
|
||||
11 | use super::*;
|
||||
| ^
|
6
tests/ui/invalid_pymodule_in_root.rs
Normal file
6
tests/ui/invalid_pymodule_in_root.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
use pyo3::prelude::*;
|
||||
|
||||
#[pymodule]
|
||||
mod invalid_pymodule_in_root_module;
|
||||
|
||||
fn main() {}
|
13
tests/ui/invalid_pymodule_in_root.stderr
Normal file
13
tests/ui/invalid_pymodule_in_root.stderr
Normal 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;
|
||||
| ^^^
|
9
tests/ui/invalid_pymodule_trait.rs
Normal file
9
tests/ui/invalid_pymodule_trait.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
use pyo3::prelude::*;
|
||||
|
||||
#[pymodule]
|
||||
mod module {
|
||||
#[pymodule_export]
|
||||
trait Foo {}
|
||||
}
|
||||
|
||||
fn main() {}
|
5
tests/ui/invalid_pymodule_trait.stderr
Normal file
5
tests/ui/invalid_pymodule_trait.stderr
Normal 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]
|
||||
| ^
|
16
tests/ui/invalid_pymodule_two_pymodule_init.rs
Normal file
16
tests/ui/invalid_pymodule_two_pymodule_init.rs
Normal 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() {}
|
5
tests/ui/invalid_pymodule_two_pymodule_init.stderr
Normal file
5
tests/ui/invalid_pymodule_two_pymodule_init.stderr
Normal 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<()> {
|
||||
| ^^
|
Loading…
Reference in a new issue