#[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
|
# 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",
|
||||||
|
|
|
@ -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;
|
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};
|
||||||
|
|
|
@ -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)?;
|
||||||
|
|
|
@ -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 -
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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()),
|
||||||
|
|
|
@ -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));
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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')");
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
|
#[pyfunction]
|
||||||
|
fn foo() -> usize {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pymodule]
|
||||||
|
mod module {
|
||||||
|
#[pymodule_export]
|
||||||
|
use super::*;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
|
@ -0,0 +1,5 @@
|
||||||
|
error: #[pymodule] cannot import glob statements
|
||||||
|
--> tests/ui/invalid_pymodule_glob.rs:11:16
|
||||||
|
|
|
||||||
|
11 | use super::*;
|
||||||
|
| ^
|
|
@ -0,0 +1,6 @@
|
||||||
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
|
#[pymodule]
|
||||||
|
mod invalid_pymodule_in_root_module;
|
||||||
|
|
||||||
|
fn main() {}
|
|
@ -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;
|
||||||
|
| ^^^
|
|
@ -0,0 +1,9 @@
|
||||||
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
|
#[pymodule]
|
||||||
|
mod module {
|
||||||
|
#[pymodule_export]
|
||||||
|
trait Foo {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
|
@ -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]
|
||||||
|
| ^
|
|
@ -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() {}
|
|
@ -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 New Issue