From 929187da7ad5dfc28d24070f3b8115104c3f9c72 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 26 Jan 2026 14:15:03 -0700 Subject: [PATCH 01/19] Use PyModExport and PyABIInfo APIs in pymodule implementation --- pyo3-macros-backend/src/module.rs | 2 +- src/impl_/pymodule.rs | 198 ++++++++++++++++++++++-------- 2 files changed, 146 insertions(+), 54 deletions(-) diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 001c36d1eed..23faf6ab1c5 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -544,7 +544,7 @@ fn module_initialization( #pyo3_path::impl_::trampoline::module_exec(module, #module_exec) } - static SLOTS: impl_::PyModuleSlots<4> = impl_::PyModuleSlotsBuilder::new() + static SLOTS: impl_::PyModuleSlots = impl_::PyModuleSlotsBuilder::new() .with_mod_exec(__pyo3_module_exec) .with_gil_used(#gil_used) .build(); diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 66749e29d1b..daa7ab5e972 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -44,7 +44,14 @@ use crate::{ffi_ptr_ext::FfiPtrExt, PyErr}; /// `Sync` wrapper of `ffi::PyModuleDef`. pub struct ModuleDef { // wrapped in UnsafeCell so that Rust compiler treats this as interior mutability + #[cfg(not(Py_3_15))] ffi_def: UnsafeCell, + #[cfg(Py_3_15)] + name: &'static CStr, + #[cfg(Py_3_15)] + doc: &'static CStr, + #[cfg(Py_3_15)] + slots: &'static PyModuleSlots, /// Interpreter ID where module was initialized (not applicable on PyPy). #[cfg(all( not(any(PyPy, GraalPy)), @@ -60,14 +67,12 @@ unsafe impl Sync for ModuleDef {} impl ModuleDef { /// Make new module definition with given module name. - pub const fn new( + pub const fn new( name: &'static CStr, doc: &'static CStr, - // TODO: it might be nice to make this unsized and not need the - // const N generic parameter, however that might need unsized return values - // or other messy hacks. - slots: &'static PyModuleSlots, + slots: &'static PyModuleSlots, ) -> Self { + #[cfg(not(Py_3_15))] #[allow(clippy::declare_interior_mutable_const)] const INIT: ffi::PyModuleDef = ffi::PyModuleDef { m_base: ffi::PyModuleDef_HEAD_INIT, @@ -81,6 +86,7 @@ impl ModuleDef { m_free: None, }; + #[cfg(not(Py_3_15))] let ffi_def = UnsafeCell::new(ffi::PyModuleDef { m_name: name.as_ptr(), m_doc: doc.as_ptr(), @@ -91,7 +97,14 @@ impl ModuleDef { }); ModuleDef { + #[cfg(not(Py_3_15))] ffi_def, + #[cfg(Py_3_15)] + name, + #[cfg(Py_3_15)] + doc, + #[cfg(Py_3_15)] + slots, // -1 is never expected to be a valid interpreter ID #[cfg(all( not(any(PyPy, GraalPy)), @@ -105,7 +118,14 @@ impl ModuleDef { pub fn init_multi_phase(&'static self) -> *mut ffi::PyObject { // SAFETY: `ffi_def` is correctly initialized in `new()` - unsafe { ffi::PyModuleDef_Init(self.ffi_def.get()) } + #[cfg(not(Py_3_15))] + unsafe { + ffi::PyModuleDef_Init(self.ffi_def.get()) + } + #[cfg(Py_3_15)] + unreachable!( + "Python shouldn't be calling an intialization function in Python 3.15 or newer" + ) } /// Builds a module object directly. Used for [`#[pymodule]`][crate::pymodule] submodules. @@ -157,47 +177,88 @@ impl ModuleDef { static SIMPLE_NAMESPACE: PyOnceLock> = PyOnceLock::new(); let simple_ns = SIMPLE_NAMESPACE.import(py, "types", "SimpleNamespace")?; - let ffi_def = self.ffi_def.get(); - - let name = unsafe { CStr::from_ptr((*ffi_def).m_name).to_str()? }.to_string(); - let kwargs = PyDict::new(py); - kwargs.set_item("name", name)?; - let spec = simple_ns.call((), Some(&kwargs))?; + #[cfg(not(Py_3_15))] + { + let ffi_def = self.ffi_def.get(); + + let name = unsafe { CStr::from_ptr((*ffi_def).m_name).to_str()? }.to_string(); + let kwargs = PyDict::new(py); + kwargs.set_item("name", name)?; + let spec = simple_ns.call((), Some(&kwargs))?; + + self.module + .get_or_try_init(py, || { + let def = self.ffi_def.get(); + let module = unsafe { + ffi::PyModule_FromDefAndSpec(def, spec.as_ptr()).assume_owned_or_err(py)? + } + .cast_into()?; + if unsafe { ffi::PyModule_ExecDef(module.as_ptr(), def) } != 0 { + return Err(PyErr::fetch(py)); + } + Ok(module.unbind()) + }) + .map(|py_module| py_module.clone_ref(py)) + } - self.module - .get_or_try_init(py, || { - let def = self.ffi_def.get(); - let module = unsafe { - ffi::PyModule_FromDefAndSpec(def, spec.as_ptr()).assume_owned_or_err(py)? - } - .cast_into()?; - if unsafe { ffi::PyModule_ExecDef(module.as_ptr(), def) } != 0 { - return Err(PyErr::fetch(py)); - } - Ok(module.unbind()) - }) - .map(|py_module| py_module.clone_ref(py)) + #[cfg(Py_3_15)] + { + let name = self.name; + let doc = self.doc; + let kwargs = PyDict::new(py); + kwargs.set_item("name", name)?; + let spec = simple_ns.call((), Some(&kwargs))?; + + self.module + .get_or_try_init(py, || { + let slots = self.slots.0.get() as *const ffi::PyModuleDef_Slot; + let module = unsafe { ffi::PyModule_FromSlotsAndSpec(slots, spec.as_ptr()) }; + if unsafe { ffi::PyModule_SetDocString(module, doc.as_ptr()) } != 0 { + return Err(PyErr::fetch(py)); + } + let module = unsafe { module.assume_owned_or_err(py)? }.cast_into()?; + if unsafe { ffi::PyModule_Exec(module.as_ptr()) } != 0 { + return Err(PyErr::fetch(py)); + } + Ok(module.unbind()) + }) + .map(|py_module| py_module.clone_ref(py)) + } } } /// Type of the exec slot used to initialise module contents pub type ModuleExecSlot = unsafe extern "C" fn(*mut ffi::PyObject) -> c_int; +const MAX_SLOTS: usize = + // Py_mod_exec and a trailing null entry + 2 + + // Py_mod_gil + cfg!(Py_3_13) as usize + + // Py_mod_name and Py_mod_abi + 2 * (cfg!(Py_3_15) as usize); + /// Builder to create `PyModuleSlots`. The size of the number of slots desired must /// be known up front, and N needs to be at least one greater than the number of /// actual slots pushed due to the need to have a zeroed element on the end. -pub struct PyModuleSlotsBuilder { +pub struct PyModuleSlotsBuilder { // values (initially all zeroed) - values: [ffi::PyModuleDef_Slot; N], + values: [ffi::PyModuleDef_Slot; MAX_SLOTS], // current length len: usize, } -impl PyModuleSlotsBuilder { +// note that macros cannot use conditional compilation, +// so all implementations below must be available in all +// Python versions +// By handling it here we can avoid conditional +// compilation within the macros; they can always emit +// e.g. a `.with_gil_used()` call. +impl PyModuleSlotsBuilder { #[allow(clippy::new_without_default)] pub const fn new() -> Self { Self { - values: [unsafe { std::mem::zeroed() }; N], + values: [unsafe { std::mem::zeroed() }; MAX_SLOTS], len: 0, } } @@ -223,22 +284,44 @@ impl PyModuleSlotsBuilder { { // Silence unused variable warning let _ = gil_used; + self + } + } + + pub const fn with_name(self, name: &'static CStr) -> Self { + #[cfg(Py_3_15)] + { + self.push(ffi::Py_mod_name, name.as_ptr() as *mut c_void) + } + + #[cfg(not(Py_3_15))] + { + // Silence unused variable warning + let _ = name; + self + } + } - // Py_mod_gil didn't exist before 3.13, can just make - // this function a noop. - // - // By handling it here we can avoid conditional - // compilation within the macros; they can always emit - // a `.with_gil_used()` call. + pub const fn with_abi_info(self) -> Self { + #[cfg(Py_3_15)] + { + ffi::PyABIInfo_VAR!(ABI_INFO); + self.push(ffi::Py_mod_abi, std::ptr::addr_of_mut!(ABI_INFO).cast()) + } + + #[cfg(not(Py_3_15))] + { + // Silence unused variable warning + let _ = abi_info; self } } - pub const fn build(self) -> PyModuleSlots { + pub const fn build(self) -> PyModuleSlots { // Required to guarantee there's still a zeroed element // at the end assert!( - self.len < N, + self.len < MAX_SLOTS, "N must be greater than the number of slots pushed" ); PyModuleSlots(UnsafeCell::new(self.values)) @@ -252,13 +335,13 @@ impl PyModuleSlotsBuilder { } /// Wrapper to safely store module slots, to be used in a `ModuleDef`. -pub struct PyModuleSlots(UnsafeCell<[ffi::PyModuleDef_Slot; N]>); +pub struct PyModuleSlots(UnsafeCell<[ffi::PyModuleDef_Slot; MAX_SLOTS]>); // It might be possible to avoid this with SyncUnsafeCell in the future // // SAFETY: the inner values are only accessed within a `ModuleDef`, // which only uses them to build the `ffi::ModuleDef`. -unsafe impl Sync for PyModuleSlots {} +unsafe impl Sync for PyModuleSlots {} /// Trait to add an element (class, function...) to a module. /// @@ -342,10 +425,17 @@ mod tests { } } - static SLOTS: PyModuleSlots<2> = PyModuleSlotsBuilder::new() + static NAME: &CStr = c"test_module"; + static DOC: &CStr = c"some doc"; + + static SLOTS: PyModuleSlots = PyModuleSlotsBuilder::new() .with_mod_exec(module_exec) + .with_gil_used(false) + .with_abi_info() + .with_name(NAME) .build(); - static MODULE_DEF: ModuleDef = ModuleDef::new(c"test_module", c"some doc", &SLOTS); + + static MODULE_DEF: ModuleDef = ModuleDef::new(NAME, DOC, &SLOTS); Python::attach(|py| { let module = MODULE_DEF.make_module(py).unwrap().into_bound(py); @@ -383,24 +473,22 @@ mod tests { static NAME: &CStr = c"test_module"; static DOC: &CStr = c"some doc"; - static SLOTS: PyModuleSlots<2> = PyModuleSlotsBuilder::new().build(); + static SLOTS: PyModuleSlots = PyModuleSlotsBuilder::new().build(); + + let module_def: ModuleDef = ModuleDef::new(NAME, DOC, &SLOTS); + #[cfg(not(Py_3_15))] unsafe { - let module_def: ModuleDef = ModuleDef::new(NAME, DOC, &SLOTS); assert_eq!((*module_def.ffi_def.get()).m_name, NAME.as_ptr() as _); assert_eq!((*module_def.ffi_def.get()).m_doc, DOC.as_ptr() as _); assert_eq!((*module_def.ffi_def.get()).m_slots, SLOTS.0.get().cast()); } - } - - #[test] - #[should_panic] - fn test_module_slots_builder_overflow() { - unsafe extern "C" fn module_exec(_module: *mut ffi::PyObject) -> c_int { - 0 + #[cfg(Py_3_15)] + { + assert_eq!(module_def.name, NAME); + assert_eq!(module_def.doc, DOC); + assert_eq!(module_def.slots.0.get(), SLOTS.0.get()); } - - PyModuleSlotsBuilder::<0>::new().with_mod_exec(module_exec); } #[test] @@ -410,7 +498,11 @@ mod tests { 0 } - PyModuleSlotsBuilder::<2>::new() + PyModuleSlotsBuilder::new() + .with_mod_exec(module_exec) + .with_mod_exec(module_exec) + .with_mod_exec(module_exec) + .with_mod_exec(module_exec) .with_mod_exec(module_exec) .with_mod_exec(module_exec) .build(); From 951901f21208b8f4997a526c31835d145b29b4c6 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 26 Jan 2026 14:26:35 -0700 Subject: [PATCH 02/19] Add PyModExport function --- pyo3-macros-backend/src/module.rs | 12 +++++++++++- src/impl_/pymodule.rs | 21 ++++++++++++++++----- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 23faf6ab1c5..27d55262bbb 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -519,6 +519,7 @@ fn module_initialization( ) -> Result { let Ctx { pyo3_path, .. } = ctx; let pyinit_symbol = format!("PyInit_{name}"); + let pymodexport_symbol = format!("PyModExport_{name}"); let pyo3_name = LitCStr::new(&CString::new(full_name).unwrap(), Span::call_site()); let doc = if let Some(doc) = doc { doc.to_cstr_stream(ctx)? @@ -555,13 +556,22 @@ fn module_initialization( if !is_submodule { result.extend(quote! { /// This autogenerated function is called by the python interpreter when importing - /// the module. + /// the module on Python 3.14 and older. #[doc(hidden)] #[export_name = #pyinit_symbol] pub unsafe extern "C" fn __pyo3_init() -> *mut #pyo3_path::ffi::PyObject { _PYO3_DEF.init_multi_phase() } }); + result.extend(quote! { + /// This autogenerated function is called by the python interpreter when importing + /// the module on Python 3.15 and newer. + #[doc(hidden)] + #[export_name = #pymodexport_symbol] + pub unsafe extern "C" fn __pyo3_export() -> *mut #pyo3_path::ffi::PyModuleDef_Slot { + _PYO3_DEF.get_slots() + } + }); } Ok(result) } diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index daa7ab5e972..0f2b18614bd 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -50,8 +50,7 @@ pub struct ModuleDef { name: &'static CStr, #[cfg(Py_3_15)] doc: &'static CStr, - #[cfg(Py_3_15)] - slots: &'static PyModuleSlots, + slots: Option<&'static PyModuleSlots>, /// Interpreter ID where module was initialized (not applicable on PyPy). #[cfg(all( not(any(PyPy, GraalPy)), @@ -104,7 +103,9 @@ impl ModuleDef { #[cfg(Py_3_15)] doc, #[cfg(Py_3_15)] - slots, + slots: Some(slots), + #[cfg(not(Py_3_15))] + slots: None, // -1 is never expected to be a valid interpreter ID #[cfg(all( not(any(PyPy, GraalPy)), @@ -211,7 +212,7 @@ impl ModuleDef { self.module .get_or_try_init(py, || { - let slots = self.slots.0.get() as *const ffi::PyModuleDef_Slot; + let slots = self.get_slots(); let module = unsafe { ffi::PyModule_FromSlotsAndSpec(slots, spec.as_ptr()) }; if unsafe { ffi::PyModule_SetDocString(module, doc.as_ptr()) } != 0 { return Err(PyErr::fetch(py)); @@ -225,6 +226,16 @@ impl ModuleDef { .map(|py_module| py_module.clone_ref(py)) } } + pub fn get_slots(&'static self) -> *mut ffi::PyModuleDef_Slot { + #[cfg(Py_3_15)] + { + self.slots.unwrap().0.get() as *mut ffi::PyModuleDef_Slot + } + #[cfg(not(Py_3_15))] + { + unsafe { *self.ffi_def.get() }.m_slots + } + } } /// Type of the exec slot used to initialise module contents @@ -487,7 +498,7 @@ mod tests { { assert_eq!(module_def.name, NAME); assert_eq!(module_def.doc, DOC); - assert_eq!(module_def.slots.0.get(), SLOTS.0.get()); + assert_eq!(module_def.slots.unwrap().0.get(), SLOTS.0.get()); } } From ffc8c9f865d836ccba9ef0732797be763d286b6a Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 26 Jan 2026 14:27:52 -0700 Subject: [PATCH 03/19] DNM: temporarily disable append_to_inittab doctest --- guide/src/python-from-rust/calling-existing-code.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guide/src/python-from-rust/calling-existing-code.md b/guide/src/python-from-rust/calling-existing-code.md index 09001929703..eb1cfc46bfb 100644 --- a/guide/src/python-from-rust/calling-existing-code.md +++ b/guide/src/python-from-rust/calling-existing-code.md @@ -141,7 +141,7 @@ The macro **must** be invoked _before_ initializing Python. As an example, the below adds the module `foo` to the embedded interpreter: -```rust +```rust,no_run use pyo3::prelude::*; #[pymodule] From 3aef886f8faaf1ece54dfff1338dddb27aecbc5e Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 26 Jan 2026 15:04:18 -0700 Subject: [PATCH 04/19] fix issues seen on older pythons in CI --- src/impl_/pymodule.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 0f2b18614bd..2cbe882aaa8 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -50,7 +50,8 @@ pub struct ModuleDef { name: &'static CStr, #[cfg(Py_3_15)] doc: &'static CStr, - slots: Option<&'static PyModuleSlots>, + #[cfg(Py_3_15)] + slots: &'static PyModuleSlots, /// Interpreter ID where module was initialized (not applicable on PyPy). #[cfg(all( not(any(PyPy, GraalPy)), @@ -104,8 +105,6 @@ impl ModuleDef { doc, #[cfg(Py_3_15)] slots: Some(slots), - #[cfg(not(Py_3_15))] - slots: None, // -1 is never expected to be a valid interpreter ID #[cfg(all( not(any(PyPy, GraalPy)), @@ -229,11 +228,11 @@ impl ModuleDef { pub fn get_slots(&'static self) -> *mut ffi::PyModuleDef_Slot { #[cfg(Py_3_15)] { - self.slots.unwrap().0.get() as *mut ffi::PyModuleDef_Slot + self.slots.0.get() as *mut ffi::PyModuleDef_Slot } #[cfg(not(Py_3_15))] { - unsafe { *self.ffi_def.get() }.m_slots + unsafe { (*self.ffi_def.get()).m_slots } } } } @@ -322,8 +321,6 @@ impl PyModuleSlotsBuilder { #[cfg(not(Py_3_15))] { - // Silence unused variable warning - let _ = abi_info; self } } @@ -498,7 +495,7 @@ mod tests { { assert_eq!(module_def.name, NAME); assert_eq!(module_def.doc, DOC); - assert_eq!(module_def.slots.unwrap().0.get(), SLOTS.0.get()); + assert_eq!(module_def.slots.0.get(), SLOTS.0.get()); } } From 2dfa5a642a6224c0c66a978a6f37f1c7ed934589 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 26 Jan 2026 15:35:19 -0700 Subject: [PATCH 05/19] fix incorrect ModuleDef setup on 3.15 --- src/impl_/pymodule.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 2cbe882aaa8..71b03c00a35 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -104,7 +104,7 @@ impl ModuleDef { #[cfg(Py_3_15)] doc, #[cfg(Py_3_15)] - slots: Some(slots), + slots, // -1 is never expected to be a valid interpreter ID #[cfg(all( not(any(PyPy, GraalPy)), From 6481f24894e40ad2bfcafb28f77b2fc80d1b351f Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 27 Jan 2026 09:39:11 -0700 Subject: [PATCH 06/19] Expose both the PyInit and PyModExport initialization hooks --- .../python-from-rust/calling-existing-code.md | 2 +- pyo3-macros-backend/src/module.rs | 16 ++++++- src/impl_/pymodule.rs | 47 ++++++++++++------- 3 files changed, 45 insertions(+), 20 deletions(-) diff --git a/guide/src/python-from-rust/calling-existing-code.md b/guide/src/python-from-rust/calling-existing-code.md index eb1cfc46bfb..09001929703 100644 --- a/guide/src/python-from-rust/calling-existing-code.md +++ b/guide/src/python-from-rust/calling-existing-code.md @@ -141,7 +141,7 @@ The macro **must** be invoked _before_ initializing Python. As an example, the below adds the module `foo` to the embedded interpreter: -```rust,no_run +```rust use pyo3::prelude::*; #[pymodule] diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 27d55262bbb..89bc4b035c6 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -545,12 +545,26 @@ fn module_initialization( #pyo3_path::impl_::trampoline::module_exec(module, #module_exec) } + // The full slots, used for the PyModExport initializaiton static SLOTS: impl_::PyModuleSlots = impl_::PyModuleSlotsBuilder::new() .with_mod_exec(__pyo3_module_exec) .with_gil_used(#gil_used) + .with_name(__PYO3_NAME) + .with_doc(#doc) .build(); - impl_::ModuleDef::new(__PYO3_NAME, #doc, &SLOTS) + // Used for old-style PyModuleDef initialization + // CPython doesn't allow specifying slots like the name and docstring that + // can be defined in PyModuleDef, so we skip those slots + static SLOTS_MINIMAL: impl_::PyModuleSlots = impl_::PyModuleSlotsBuilder::new() + .with_mod_exec(__pyo3_module_exec) + .with_gil_used(#gil_used) + .build(); + + // Since the macros need to be written agnostic to the Python version + // we need to explicitly pass the name and docstring for PyModuleDef + // initializaiton. + impl_::ModuleDef::new(__PYO3_NAME, #doc, &SLOTS, &SLOTS_MINIMAL) }; }; if !is_submodule { diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 71b03c00a35..d1ace58814f 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -44,7 +44,6 @@ use crate::{ffi_ptr_ext::FfiPtrExt, PyErr}; /// `Sync` wrapper of `ffi::PyModuleDef`. pub struct ModuleDef { // wrapped in UnsafeCell so that Rust compiler treats this as interior mutability - #[cfg(not(Py_3_15))] ffi_def: UnsafeCell, #[cfg(Py_3_15)] name: &'static CStr, @@ -71,8 +70,11 @@ impl ModuleDef { name: &'static CStr, doc: &'static CStr, slots: &'static PyModuleSlots, + slots_with_no_name_or_doc: &'static PyModuleSlots, ) -> Self { - #[cfg(not(Py_3_15))] + // This is only used in PyO3 for append_to_inittab on Python 3.15 and newer. + // There could also be other tools that need the legacy init hook. + // Opaque PyObject builds won't be able to use this. #[allow(clippy::declare_interior_mutable_const)] const INIT: ffi::PyModuleDef = ffi::PyModuleDef { m_base: ffi::PyModuleDef_HEAD_INIT, @@ -86,18 +88,16 @@ impl ModuleDef { m_free: None, }; - #[cfg(not(Py_3_15))] let ffi_def = UnsafeCell::new(ffi::PyModuleDef { m_name: name.as_ptr(), m_doc: doc.as_ptr(), // TODO: would be slightly nicer to use `[T]::as_mut_ptr()` here, // but that requires mut ptr deref on MSRV. - m_slots: slots.0.get() as _, + m_slots: slots_with_no_name_or_doc.0.get() as _, ..INIT }); ModuleDef { - #[cfg(not(Py_3_15))] ffi_def, #[cfg(Py_3_15)] name, @@ -117,15 +117,7 @@ impl ModuleDef { } pub fn init_multi_phase(&'static self) -> *mut ffi::PyObject { - // SAFETY: `ffi_def` is correctly initialized in `new()` - #[cfg(not(Py_3_15))] - unsafe { - ffi::PyModuleDef_Init(self.ffi_def.get()) - } - #[cfg(Py_3_15)] - unreachable!( - "Python shouldn't be calling an intialization function in Python 3.15 or newer" - ) + unsafe { ffi::PyModuleDef_Init(self.ffi_def.get()) } } /// Builds a module object directly. Used for [`#[pymodule]`][crate::pymodule] submodules. @@ -245,8 +237,8 @@ const MAX_SLOTS: usize = 2 + // Py_mod_gil cfg!(Py_3_13) as usize + - // Py_mod_name and Py_mod_abi - 2 * (cfg!(Py_3_15) as usize); + // Py_mod_name, Py_mod_doc, and Py_mod_abi + 3 * (cfg!(Py_3_15) as usize); /// Builder to create `PyModuleSlots`. The size of the number of slots desired must /// be known up front, and N needs to be at least one greater than the number of @@ -325,6 +317,18 @@ impl PyModuleSlotsBuilder { } } + pub const fn with_doc(self, doc: &'static CStr) -> Self { + #[cfg(Py_3_15)] + { + self.push(ffi::Py_mod_doc, doc.as_ptr() as *mut c_void) + } + + #[cfg(not(Py_3_15))] + { + self + } + } + pub const fn build(self) -> PyModuleSlots { // Required to guarantee there's still a zeroed element // at the end @@ -441,9 +445,16 @@ mod tests { .with_gil_used(false) .with_abi_info() .with_name(NAME) + .with_doc(DOC) + .build(); + + static SLOTS_MINIMAL: PyModuleSlots = PyModuleSlotsBuilder::new() + .with_mod_exec(module_exec) + .with_gil_used(false) + .with_abi_info() .build(); - static MODULE_DEF: ModuleDef = ModuleDef::new(NAME, DOC, &SLOTS); + static MODULE_DEF: ModuleDef = ModuleDef::new(NAME, DOC, &SLOTS, &SLOTS_MINIMAL); Python::attach(|py| { let module = MODULE_DEF.make_module(py).unwrap().into_bound(py); @@ -483,7 +494,7 @@ mod tests { static SLOTS: PyModuleSlots = PyModuleSlotsBuilder::new().build(); - let module_def: ModuleDef = ModuleDef::new(NAME, DOC, &SLOTS); + let module_def: ModuleDef = ModuleDef::new(NAME, DOC, &SLOTS, &SLOTS); #[cfg(not(Py_3_15))] unsafe { From b9ca761ca2c62d61759ea64a9347a2483f751082 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 27 Jan 2026 09:49:29 -0700 Subject: [PATCH 07/19] fix clippy --- src/impl_/pymodule.rs | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index d1ace58814f..825815f92f5 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -45,11 +45,10 @@ use crate::{ffi_ptr_ext::FfiPtrExt, PyErr}; pub struct ModuleDef { // wrapped in UnsafeCell so that Rust compiler treats this as interior mutability ffi_def: UnsafeCell, - #[cfg(Py_3_15)] + #[cfg_attr(not(Py_3_15), allow(dead_code))] name: &'static CStr, - #[cfg(Py_3_15)] + #[cfg_attr(not(Py_3_15), allow(dead_code))] doc: &'static CStr, - #[cfg(Py_3_15)] slots: &'static PyModuleSlots, /// Interpreter ID where module was initialized (not applicable on PyPy). #[cfg(all( @@ -99,11 +98,8 @@ impl ModuleDef { ModuleDef { ffi_def, - #[cfg(Py_3_15)] name, - #[cfg(Py_3_15)] doc, - #[cfg(Py_3_15)] slots, // -1 is never expected to be a valid interpreter ID #[cfg(all( @@ -218,14 +214,7 @@ impl ModuleDef { } } pub fn get_slots(&'static self) -> *mut ffi::PyModuleDef_Slot { - #[cfg(Py_3_15)] - { - self.slots.0.get() as *mut ffi::PyModuleDef_Slot - } - #[cfg(not(Py_3_15))] - { - unsafe { (*self.ffi_def.get()).m_slots } - } + self.slots.0.get() as *mut ffi::PyModuleDef_Slot } } @@ -325,6 +314,8 @@ impl PyModuleSlotsBuilder { #[cfg(not(Py_3_15))] { + // Silence unused variable warning + let _ = doc; self } } @@ -496,18 +487,14 @@ mod tests { let module_def: ModuleDef = ModuleDef::new(NAME, DOC, &SLOTS, &SLOTS); - #[cfg(not(Py_3_15))] unsafe { assert_eq!((*module_def.ffi_def.get()).m_name, NAME.as_ptr() as _); assert_eq!((*module_def.ffi_def.get()).m_doc, DOC.as_ptr() as _); assert_eq!((*module_def.ffi_def.get()).m_slots, SLOTS.0.get().cast()); } - #[cfg(Py_3_15)] - { - assert_eq!(module_def.name, NAME); - assert_eq!(module_def.doc, DOC); - assert_eq!(module_def.slots.0.get(), SLOTS.0.get()); - } + assert_eq!(module_def.name, NAME); + assert_eq!(module_def.doc, DOC); + assert_eq!(module_def.slots.0.get(), SLOTS.0.get()); } #[test] From d807872a5399b4f8c3809512a841272b41a05425 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 27 Jan 2026 10:31:03 -0700 Subject: [PATCH 08/19] add changelog entry --- newsfragments/5753.changed.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/5753.changed.md diff --git a/newsfragments/5753.changed.md b/newsfragments/5753.changed.md new file mode 100644 index 00000000000..5f22cf42516 --- /dev/null +++ b/newsfragments/5753.changed.md @@ -0,0 +1 @@ +Module initialization uses the PyModExport and PyABIInfo APIs on python 3.15 and newer. \ No newline at end of file From 1b07bef16bfb388d32273530e322cef524c12320 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 6 Feb 2026 13:37:31 -0700 Subject: [PATCH 09/19] avoid unnecessary extra quote! use --- pyo3-macros-backend/src/module.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 89bc4b035c6..dbdb32791de 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -576,8 +576,7 @@ fn module_initialization( pub unsafe extern "C" fn __pyo3_init() -> *mut #pyo3_path::ffi::PyObject { _PYO3_DEF.init_multi_phase() } - }); - result.extend(quote! { + /// This autogenerated function is called by the python interpreter when importing /// the module on Python 3.15 and newer. #[doc(hidden)] From 9491557fb0575015a41449de44f246feaf82b433 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 6 Feb 2026 13:39:30 -0700 Subject: [PATCH 10/19] rename MAX_SLOTS as MAX_SLOTS_WITH_TRAILING_NULL --- src/impl_/pymodule.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 825815f92f5..1cae4c31531 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -222,19 +222,20 @@ impl ModuleDef { pub type ModuleExecSlot = unsafe extern "C" fn(*mut ffi::PyObject) -> c_int; const MAX_SLOTS: usize = - // Py_mod_exec and a trailing null entry - 2 + + // Py_mod_exec + 1 + // Py_mod_gil cfg!(Py_3_13) as usize + // Py_mod_name, Py_mod_doc, and Py_mod_abi 3 * (cfg!(Py_3_15) as usize); +const MAX_SLOTS_WITH_TRAILING_NULL: usize = MAX_SLOTS + 1; /// Builder to create `PyModuleSlots`. The size of the number of slots desired must /// be known up front, and N needs to be at least one greater than the number of /// actual slots pushed due to the need to have a zeroed element on the end. pub struct PyModuleSlotsBuilder { // values (initially all zeroed) - values: [ffi::PyModuleDef_Slot; MAX_SLOTS], + values: [ffi::PyModuleDef_Slot; MAX_SLOTS_WITH_TRAILING_NULL], // current length len: usize, } @@ -249,7 +250,7 @@ impl PyModuleSlotsBuilder { #[allow(clippy::new_without_default)] pub const fn new() -> Self { Self { - values: [unsafe { std::mem::zeroed() }; MAX_SLOTS], + values: [unsafe { std::mem::zeroed() }; MAX_SLOTS_WITH_TRAILING_NULL], len: 0, } } @@ -324,7 +325,7 @@ impl PyModuleSlotsBuilder { // Required to guarantee there's still a zeroed element // at the end assert!( - self.len < MAX_SLOTS, + self.len < MAX_SLOTS_WITH_TRAILING_NULL, "N must be greater than the number of slots pushed" ); PyModuleSlots(UnsafeCell::new(self.values)) @@ -338,7 +339,7 @@ impl PyModuleSlotsBuilder { } /// Wrapper to safely store module slots, to be used in a `ModuleDef`. -pub struct PyModuleSlots(UnsafeCell<[ffi::PyModuleDef_Slot; MAX_SLOTS]>); +pub struct PyModuleSlots(UnsafeCell<[ffi::PyModuleDef_Slot; MAX_SLOTS_WITH_TRAILING_NULL]>); // It might be possible to avoid this with SyncUnsafeCell in the future // From bd67e962b674ec989c50d6f55f211add00f2a5bf Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 6 Feb 2026 13:53:16 -0700 Subject: [PATCH 11/19] Move MAX_SLOTS check to push --- src/impl_/pymodule.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 1cae4c31531..71df613e113 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -324,14 +324,14 @@ impl PyModuleSlotsBuilder { pub const fn build(self) -> PyModuleSlots { // Required to guarantee there's still a zeroed element // at the end - assert!( - self.len < MAX_SLOTS_WITH_TRAILING_NULL, - "N must be greater than the number of slots pushed" - ); PyModuleSlots(UnsafeCell::new(self.values)) } const fn push(mut self, slot: c_int, value: *mut c_void) -> Self { + assert!( + self.len <= MAX_SLOTS, + "Cannot add more than MAX_SLOTS slots to a PyModuleSlots", + ); self.values[self.len] = ffi::PyModuleDef_Slot { slot, value }; self.len += 1; self From 07be9c8d13cb68606d7d627a01f8e237d6699068 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 6 Feb 2026 14:32:06 -0700 Subject: [PATCH 12/19] Apply David's suggestions for tests --- src/impl_/pymodule.rs | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 71df613e113..b6ad588b63b 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -329,7 +329,7 @@ impl PyModuleSlotsBuilder { const fn push(mut self, slot: c_int, value: *mut c_void) -> Self { assert!( - self.len <= MAX_SLOTS, + self.len + 1 <= MAX_SLOTS, "Cannot add more than MAX_SLOTS slots to a PyModuleSlots", ); self.values[self.len] = ffi::PyModuleDef_Slot { slot, value }; @@ -416,7 +416,11 @@ mod tests { Python, }; - use super::ModuleDef; + use super::{ModuleDef, MAX_SLOTS}; + + unsafe extern "C" fn module_exec(_module: *mut ffi::PyObject) -> c_int { + 0 + } #[test] fn module_init() { @@ -498,20 +502,30 @@ mod tests { assert_eq!(module_def.slots.0.get(), SLOTS.0.get()); } + #[test] + fn test_build_maximal_slots() { + let builder = PyModuleSlotsBuilder::new() + .with_mod_exec(module_exec) + .with_name(c"test_module") + .with_doc(c"some doc") + .with_gil_used(false) + .with_abi_info(); + + assert!(builder.values[builder.len] == unsafe { std::mem::zeroed() }); + assert!(builder.values[builder.len - 1] != unsafe { std::mem::zeroed() }); + assert!(builder.len == MAX_SLOTS); + + let result = std::panic::catch_unwind(|| builder.with_mod_exec(module_exec).build()); + + assert!(result.is_err()); + } + #[test] #[should_panic] - fn test_module_slots_builder_overflow_2() { - unsafe extern "C" fn module_exec(_module: *mut ffi::PyObject) -> c_int { - 0 + fn test_module_slots_builder_overflow() { + let mut builder = PyModuleSlotsBuilder::new(); + for _ in 0..MAX_SLOTS + 1 { + builder = builder.with_mod_exec(module_exec); } - - PyModuleSlotsBuilder::new() - .with_mod_exec(module_exec) - .with_mod_exec(module_exec) - .with_mod_exec(module_exec) - .with_mod_exec(module_exec) - .with_mod_exec(module_exec) - .with_mod_exec(module_exec) - .build(); } } From ffff4bb05751fb0f9715e027b4028fc519302506 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 6 Feb 2026 14:32:25 -0700 Subject: [PATCH 13/19] Always include abi info slot in PyModule initialization --- pyo3-macros-backend/src/module.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index dbdb32791de..e41e13afd8c 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -548,6 +548,7 @@ fn module_initialization( // The full slots, used for the PyModExport initializaiton static SLOTS: impl_::PyModuleSlots = impl_::PyModuleSlotsBuilder::new() .with_mod_exec(__pyo3_module_exec) + .with_abi_info() .with_gil_used(#gil_used) .with_name(__PYO3_NAME) .with_doc(#doc) @@ -558,6 +559,7 @@ fn module_initialization( // can be defined in PyModuleDef, so we skip those slots static SLOTS_MINIMAL: impl_::PyModuleSlots = impl_::PyModuleSlotsBuilder::new() .with_mod_exec(__pyo3_module_exec) + .with_abi_info() .with_gil_used(#gil_used) .build(); From 2aeff6d0e0f8105d84c828bbf57800bb32e0bccc Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 6 Feb 2026 14:43:53 -0700 Subject: [PATCH 14/19] only unwind panics if panic = "unwind" --- src/impl_/pymodule.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index b6ad588b63b..761eae1d033 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -503,6 +503,7 @@ mod tests { } #[test] + #[cfg(panic = "unwind")] fn test_build_maximal_slots() { let builder = PyModuleSlotsBuilder::new() .with_mod_exec(module_exec) From b637563b1e5f2c179e3173176f77e4353d356824 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 6 Feb 2026 14:59:00 -0700 Subject: [PATCH 15/19] appease clippy --- src/impl_/pymodule.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 761eae1d033..99b9708a194 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -322,14 +322,14 @@ impl PyModuleSlotsBuilder { } pub const fn build(self) -> PyModuleSlots { - // Required to guarantee there's still a zeroed element - // at the end PyModuleSlots(UnsafeCell::new(self.values)) } const fn push(mut self, slot: c_int, value: *mut c_void) -> Self { + // Required to guarantee there's still a zeroed element + // at the end assert!( - self.len + 1 <= MAX_SLOTS, + self.len < MAX_SLOTS, "Cannot add more than MAX_SLOTS slots to a PyModuleSlots", ); self.values[self.len] = ffi::PyModuleDef_Slot { slot, value }; From 92afcb11bb79070bfa5e316e161c2a109378532c Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 29 Jan 2026 08:22:30 -0700 Subject: [PATCH 16/19] try use only slots for both init hooks on 3.15 --- pyo3-macros-backend/src/module.rs | 11 +---------- src/impl_/pymodule.rs | 17 +++++------------ 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index e41e13afd8c..ccb21c7a0dc 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -554,19 +554,10 @@ fn module_initialization( .with_doc(#doc) .build(); - // Used for old-style PyModuleDef initialization - // CPython doesn't allow specifying slots like the name and docstring that - // can be defined in PyModuleDef, so we skip those slots - static SLOTS_MINIMAL: impl_::PyModuleSlots = impl_::PyModuleSlotsBuilder::new() - .with_mod_exec(__pyo3_module_exec) - .with_abi_info() - .with_gil_used(#gil_used) - .build(); - // Since the macros need to be written agnostic to the Python version // we need to explicitly pass the name and docstring for PyModuleDef // initializaiton. - impl_::ModuleDef::new(__PYO3_NAME, #doc, &SLOTS, &SLOTS_MINIMAL) + impl_::ModuleDef::new(__PYO3_NAME, #doc, &SLOTS) }; }; if !is_submodule { diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 99b9708a194..0c7321ab4f3 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -69,7 +69,6 @@ impl ModuleDef { name: &'static CStr, doc: &'static CStr, slots: &'static PyModuleSlots, - slots_with_no_name_or_doc: &'static PyModuleSlots, ) -> Self { // This is only used in PyO3 for append_to_inittab on Python 3.15 and newer. // There could also be other tools that need the legacy init hook. @@ -88,11 +87,13 @@ impl ModuleDef { }; let ffi_def = UnsafeCell::new(ffi::PyModuleDef { + #[cfg(not(Py_3_15))] m_name: name.as_ptr(), + #[cfg(not(Py_3_15))] m_doc: doc.as_ptr(), // TODO: would be slightly nicer to use `[T]::as_mut_ptr()` here, // but that requires mut ptr deref on MSRV. - m_slots: slots_with_no_name_or_doc.0.get() as _, + m_slots: slots.0.get() as _, ..INIT }); @@ -444,13 +445,7 @@ mod tests { .with_doc(DOC) .build(); - static SLOTS_MINIMAL: PyModuleSlots = PyModuleSlotsBuilder::new() - .with_mod_exec(module_exec) - .with_gil_used(false) - .with_abi_info() - .build(); - - static MODULE_DEF: ModuleDef = ModuleDef::new(NAME, DOC, &SLOTS, &SLOTS_MINIMAL); + static MODULE_DEF: ModuleDef = ModuleDef::new(NAME, DOC, &SLOTS); Python::attach(|py| { let module = MODULE_DEF.make_module(py).unwrap().into_bound(py); @@ -490,11 +485,9 @@ mod tests { static SLOTS: PyModuleSlots = PyModuleSlotsBuilder::new().build(); - let module_def: ModuleDef = ModuleDef::new(NAME, DOC, &SLOTS, &SLOTS); + let module_def: ModuleDef = ModuleDef::new(NAME, DOC, &SLOTS); unsafe { - assert_eq!((*module_def.ffi_def.get()).m_name, NAME.as_ptr() as _); - assert_eq!((*module_def.ffi_def.get()).m_doc, DOC.as_ptr() as _); assert_eq!((*module_def.ffi_def.get()).m_slots, SLOTS.0.get().cast()); } assert_eq!(module_def.name, NAME); From e5f3c0b9e49f56c25157fa18150da1f058216c2a Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 30 Jan 2026 08:07:49 -0700 Subject: [PATCH 17/19] Always pass m_name and m_doc, following cpython-gh-144340 --- src/impl_/pymodule.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 0c7321ab4f3..bdda42773ca 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -45,9 +45,7 @@ use crate::{ffi_ptr_ext::FfiPtrExt, PyErr}; pub struct ModuleDef { // wrapped in UnsafeCell so that Rust compiler treats this as interior mutability ffi_def: UnsafeCell, - #[cfg_attr(not(Py_3_15), allow(dead_code))] name: &'static CStr, - #[cfg_attr(not(Py_3_15), allow(dead_code))] doc: &'static CStr, slots: &'static PyModuleSlots, /// Interpreter ID where module was initialized (not applicable on PyPy). @@ -87,9 +85,7 @@ impl ModuleDef { }; let ffi_def = UnsafeCell::new(ffi::PyModuleDef { - #[cfg(not(Py_3_15))] m_name: name.as_ptr(), - #[cfg(not(Py_3_15))] m_doc: doc.as_ptr(), // TODO: would be slightly nicer to use `[T]::as_mut_ptr()` here, // but that requires mut ptr deref on MSRV. From 93ecb4a58d76835c9cc25b3f8f90ce09691f3959 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 10 Mar 2026 11:27:26 -0600 Subject: [PATCH 18/19] fix conditional compilation --- src/impl_/pymodule.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index bdda42773ca..1b2201b852e 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -45,7 +45,9 @@ use crate::{ffi_ptr_ext::FfiPtrExt, PyErr}; pub struct ModuleDef { // wrapped in UnsafeCell so that Rust compiler treats this as interior mutability ffi_def: UnsafeCell, + #[cfg(Py_3_15)] name: &'static CStr, + #[cfg(Py_3_15)] doc: &'static CStr, slots: &'static PyModuleSlots, /// Interpreter ID where module was initialized (not applicable on PyPy). @@ -95,7 +97,9 @@ impl ModuleDef { ModuleDef { ffi_def, + #[cfg(Py_3_15)] name, + #[cfg(Py_3_15)] doc, slots, // -1 is never expected to be a valid interpreter ID @@ -486,8 +490,11 @@ mod tests { unsafe { assert_eq!((*module_def.ffi_def.get()).m_slots, SLOTS.0.get().cast()); } - assert_eq!(module_def.name, NAME); - assert_eq!(module_def.doc, DOC); + #[cfg(Py_3_15)] + { + assert_eq!(module_def.name, NAME); + assert_eq!(module_def.doc, DOC); + } assert_eq!(module_def.slots.0.get(), SLOTS.0.get()); } From 78b169c5998d1a0fa67907622bc74fc5dd26876e Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 11 Mar 2026 08:54:35 -0600 Subject: [PATCH 19/19] fix ffi-check in 3.15.0a7 --- pyo3-ffi/src/cpython/initconfig.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyo3-ffi/src/cpython/initconfig.rs b/pyo3-ffi/src/cpython/initconfig.rs index 6b0ae2e5dec..dc1e9402f7d 100644 --- a/pyo3-ffi/src/cpython/initconfig.rs +++ b/pyo3-ffi/src/cpython/initconfig.rs @@ -155,6 +155,8 @@ pub struct PyConfig { pub enable_gil: c_int, #[cfg(all(Py_3_14, Py_GIL_DISABLED))] pub tlbc_enabled: c_int, + #[cfg(Py_3_15)] + pub lazy_imports: c_int, pub pathconfig_warnings: c_int, #[cfg(Py_3_10)] pub program_name: *mut wchar_t,