Skip to content
1 change: 1 addition & 0 deletions newsfragments/5525.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
added PEP-489 multi-phase initialization for pymodules
74 changes: 40 additions & 34 deletions pyo3-macros-backend/src/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use crate::{
get_doc,
pyclass::PyClassPyO3Option,
pyfunction::{impl_wrap_pyfunction, PyFunctionOptions},
utils::{has_attribute, has_attribute_with_namespace, Ctx, IdentOrStr},
utils::{has_attribute, has_attribute_with_namespace, Ctx, IdentOrStr, PythonDoc},
};
use proc_macro2::{Span, TokenStream};
use quote::quote;
Expand Down Expand Up @@ -389,23 +389,15 @@ pub fn pymodule_module_impl(
#[cfg(not(feature = "experimental-inspect"))]
let introspection_id = quote! {};

let module_def = quote! {{
use #pyo3_path::impl_::pymodule as impl_;
const INITIALIZER: impl_::ModuleInitializer = impl_::ModuleInitializer(__pyo3_pymodule);
unsafe {
impl_::ModuleDef::new(
__PYO3_NAME,
#doc,
INITIALIZER
)
}
}};
let gil_used = options.gil_used.is_some_and(|op| op.value.value);

let initialization = module_initialization(
&name,
ctx,
module_def,
quote! { __pyo3_pymodule },
options.submodule.is_some(),
options.gil_used.is_none_or(|op| op.value.value),
gil_used,
doc,
);

let module_consts_names = module_consts.iter().map(|i| i.unraw().to_string());
Expand Down Expand Up @@ -456,12 +448,15 @@ pub fn pymodule_function_impl(
let vis = &function.vis;
let doc = get_doc(&function.attrs, None, ctx)?;

let gil_used = options.gil_used.is_some_and(|op| op.value.value);

let initialization = module_initialization(
&name,
ctx,
quote! { MakeDef::make_def() },
quote! { ModuleExec::__pyo3_module_exec },
false,
options.gil_used.is_none_or(|op| op.value.value),
gil_used,
doc,
);

#[cfg(feature = "experimental-inspect")]
Expand Down Expand Up @@ -495,20 +490,9 @@ pub fn pymodule_function_impl(
// (and `super` doesn't always refer to the outer scope, e.g. if the `#[pymodule] is
// inside a function body)
#[allow(unknown_lints, non_local_definitions)]
impl #ident::MakeDef {
const fn make_def() -> #pyo3_path::impl_::pymodule::ModuleDef {
fn __pyo3_pymodule(module: &#pyo3_path::Bound<'_, #pyo3_path::types::PyModule>) -> #pyo3_path::PyResult<()> {
#ident(#(#module_args),*)
}

const INITIALIZER: #pyo3_path::impl_::pymodule::ModuleInitializer = #pyo3_path::impl_::pymodule::ModuleInitializer(__pyo3_pymodule);
unsafe {
#pyo3_path::impl_::pymodule::ModuleDef::new(
#ident::__PYO3_NAME,
#doc,
INITIALIZER
)
}
impl #ident::ModuleExec {
fn __pyo3_module_exec(module: &#pyo3_path::Bound<'_, #pyo3_path::types::PyModule>) -> #pyo3_path::PyResult<()> {
#ident(#(#module_args),*)
}
}
})
Expand All @@ -517,9 +501,10 @@ pub fn pymodule_function_impl(
fn module_initialization(
name: &syn::Ident,
ctx: &Ctx,
module_def: TokenStream,
module_exec: TokenStream,
is_submodule: bool,
gil_used: bool,
doc: PythonDoc,
) -> TokenStream {
let Ctx { pyo3_path, .. } = ctx;
let pyinit_symbol = format!("PyInit_{name}");
Expand All @@ -530,9 +515,27 @@ fn module_initialization(
#[doc(hidden)]
pub const __PYO3_NAME: &'static ::std::ffi::CStr = #pyo3_name;

pub(super) struct MakeDef;
// This structure exists for `fn` modules declared within `fn` bodies, where due to the hidden
// module (used for importing) the `fn` to initialize the module cannot be seen from the #module_def
// declaration just below.
#[doc(hidden)]
pub static _PYO3_DEF: #pyo3_path::impl_::pymodule::ModuleDef = #module_def;
pub(super) struct ModuleExec;

#[doc(hidden)]
pub static _PYO3_DEF: #pyo3_path::impl_::pymodule::ModuleDef = {
use #pyo3_path::impl_::pymodule as impl_;

unsafe extern "C" fn __pyo3_module_exec(module: *mut #pyo3_path::ffi::PyObject) -> ::std::os::raw::c_int {
#pyo3_path::impl_::trampoline::module_exec(module, #module_exec)
}

static SLOTS: impl_::PyModuleSlots<4> = impl_::PyModuleSlotsBuilder::new()
.with_mod_exec(__pyo3_module_exec)
.with_gil_used(#gil_used)
.build();

impl_::ModuleDef::new(__PYO3_NAME, #doc, &SLOTS)
};
#[doc(hidden)]
// so wrapped submodules can see what gil_used is
pub static __PYO3_GIL_USED: bool = #gil_used;
Expand All @@ -544,7 +547,10 @@ fn module_initialization(
#[doc(hidden)]
#[export_name = #pyinit_symbol]
pub unsafe extern "C" fn __pyo3_init() -> *mut #pyo3_path::ffi::PyObject {
unsafe { #pyo3_path::impl_::trampoline::module_init(|py| _PYO3_DEF.make_module(py, #gil_used)) }
_PYO3_DEF.init_multi_phase(
unsafe { #pyo3_path::Python::assume_attached() },
#gil_used
)
}
});
}
Expand Down
1 change: 1 addition & 0 deletions pytests/tests/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def test_multiple_imports_same_interpreter_ok():
spec = importlib.util.find_spec("pyo3_pytests.pyo3_pytests")

module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
assert dir(module) == dir(pyo3_pytests.pyo3_pytests)


Expand Down
Loading
Loading