From eb53c2d8d04fcb2cee44dcc2deb243d3de243e85 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 26 Jan 2026 14:15:03 -0700 Subject: [PATCH 01/74] 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 e41b509428ad4ec892196eb30c7de8114d9278b7 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 26 Jan 2026 14:26:35 -0700 Subject: [PATCH 02/74] 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 91becaf7b7a67d3173db6f071a45df29a38d4d64 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 26 Jan 2026 14:27:52 -0700 Subject: [PATCH 03/74] 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 1020688e8efb805c3d57b1852b14a7c03de0e977 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 26 Jan 2026 15:04:18 -0700 Subject: [PATCH 04/74] 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 3afa9ae44078aa84faf3b3fdca2d2d37d516bc4d Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 26 Jan 2026 15:35:19 -0700 Subject: [PATCH 05/74] 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 f8d6caec00eff714b9d84cfa53cccdf964dcc0b5 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 27 Jan 2026 09:39:11 -0700 Subject: [PATCH 06/74] 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 a59087431c97a37f06499141c72efd746457e00c Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 27 Jan 2026 09:49:29 -0700 Subject: [PATCH 07/74] 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 a96219f707f6efbb400b40cf877c9ff22fb814ab Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 27 Jan 2026 10:31:03 -0700 Subject: [PATCH 08/74] 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 e7ac9c08bafa587ee98cecd217db246172425f29 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 29 Jan 2026 08:22:30 -0700 Subject: [PATCH 09/74] try use only slots for both init hooks on 3.15 --- pyo3-macros-backend/src/module.rs | 10 +--------- src/impl_/pymodule.rs | 17 +++++------------ 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 89bc4b035c6..6dec3f05a56 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -553,18 +553,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_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 825815f92f5..585c889bd8f 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 }); @@ -439,13 +440,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); @@ -485,11 +480,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 d981be7e3846b2b561f5a82690b37fc4056e21ba Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 30 Jan 2026 08:07:49 -0700 Subject: [PATCH 10/74] 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 585c889bd8f..5e89f56f6f3 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 55b6acdf26e2bb5f13d98190d641355a87d4b49b Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 13 Feb 2026 07:52:35 -0700 Subject: [PATCH 11/74] WIP: opaque pyobject support (without Py_GIL_DISABLED) --- noxfile.py | 5 +---- pyo3-build-config/src/impl_.rs | 32 ++++++++++++++++++++++++++++++-- pyo3-build-config/src/lib.rs | 1 + pyo3-ffi/src/moduleobject.rs | 1 + pyo3-ffi/src/object.rs | 27 +++++++++++++++++++++++++++ pyo3-ffi/src/refcount.rs | 3 ++- src/impl_/pymodule.rs | 22 ++++++++++++++++++---- 7 files changed, 80 insertions(+), 11 deletions(-) diff --git a/noxfile.py b/noxfile.py index 5004b75c2c4..2e1b57b7dcf 100644 --- a/noxfile.py +++ b/noxfile.py @@ -82,10 +82,7 @@ def _supported_interpreter_versions( PY_VERSIONS = _supported_interpreter_versions("cpython") -# We don't yet support abi3-py315 but do support cp315 and cp315t -# version-specific builds ABI3_PY_VERSIONS = [p for p in PY_VERSIONS if not p.endswith("t")] -ABI3_PY_VERSIONS.remove("3.15") PYPY_VERSIONS = _supported_interpreter_versions("pypy") @@ -124,7 +121,7 @@ def test_rust(session: nox.Session): # We need to pass the feature set to the test command # so that it can be used in the test code # (e.g. for `#[cfg(feature = "abi3-py37")]`) - if feature_set and "abi3" in feature_set and FREE_THREADED_BUILD: + if feature_set and "abi3" in feature_set and FREE_THREADED_BUILD and sys.version_info < (3, 15): # free-threaded builds don't support abi3 yet continue diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 167013f5c42..cf724d41f14 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -40,7 +40,7 @@ const MINIMUM_SUPPORTED_VERSION_GRAALPY: PythonVersion = PythonVersion { }; /// Maximum Python version that can be used as minimum required Python version with abi3. -pub(crate) const ABI3_MAX_MINOR: u8 = 14; +pub(crate) const ABI3_MAX_MINOR: u8 = 15; #[cfg(test)] thread_local! { @@ -190,8 +190,11 @@ impl InterpreterConfig { } // If Py_GIL_DISABLED is set, do not build with limited API support - if self.abi3 && !self.is_free_threaded() { + if self.abi3 && !(self.is_free_threaded()) { out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); + if self.version.minor >= 15 { + out.push("cargo:rustc-cfg=_Py_OPAQUE_PYOBJECT".to_owned()); + } } for flag in &self.build_flags.0 { @@ -3203,6 +3206,31 @@ mod tests { "cargo:rustc-cfg=Py_LIMITED_API".to_owned(), ] ); + + let interpreter_config = InterpreterConfig { + implementation: PythonImplementation::CPython, + version: PythonVersion { + major: 3, + minor: 15, + }, + ..interpreter_config + }; + assert_eq!( + interpreter_config.build_script_outputs(), + [ + "cargo:rustc-cfg=Py_3_7".to_owned(), + "cargo:rustc-cfg=Py_3_8".to_owned(), + "cargo:rustc-cfg=Py_3_9".to_owned(), + "cargo:rustc-cfg=Py_3_10".to_owned(), + "cargo:rustc-cfg=Py_3_11".to_owned(), + "cargo:rustc-cfg=Py_3_12".to_owned(), + "cargo:rustc-cfg=Py_3_13".to_owned(), + "cargo:rustc-cfg=Py_3_14".to_owned(), + "cargo:rustc-cfg=Py_3_15".to_owned(), + "cargo:rustc-cfg=Py_LIMITED_API".to_owned(), + "cargo:rustc-cfg=_Py_OPAQUE_PYOBJECT".to_owned(), + ] + ); } #[test] diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 156ecd309f6..ff2e77d374b 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -254,6 +254,7 @@ pub fn print_expected_cfgs() { println!("cargo:rustc-check-cfg=cfg(Py_LIMITED_API)"); println!("cargo:rustc-check-cfg=cfg(Py_GIL_DISABLED)"); + println!("cargo:rustc-check-cfg=cfg(_Py_OPAQUE_PYOBJECT)"); println!("cargo:rustc-check-cfg=cfg(PyPy)"); println!("cargo:rustc-check-cfg=cfg(GraalPy)"); println!("cargo:rustc-check-cfg=cfg(py_sys_config, values(\"Py_DEBUG\", \"Py_REF_DEBUG\", \"Py_TRACE_REFS\", \"COUNT_ALLOCS\"))"); diff --git a/pyo3-ffi/src/moduleobject.rs b/pyo3-ffi/src/moduleobject.rs index ace202d969e..6c8d8272e6d 100644 --- a/pyo3-ffi/src/moduleobject.rs +++ b/pyo3-ffi/src/moduleobject.rs @@ -61,6 +61,7 @@ pub struct PyModuleDef_Base { pub m_copy: *mut PyObject, } +#[cfg(not(_Py_OPAQUE_PYOBJECT))] #[allow( clippy::declare_interior_mutable_const, reason = "contains atomic refcount on free-threaded builds" diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index ddeabb9be3f..3b060fb0be8 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -1,4 +1,5 @@ use crate::pyport::{Py_hash_t, Py_ssize_t}; +#[cfg(not(_Py_OPAQUE_PYOBJECT))] #[cfg(Py_GIL_DISABLED)] use crate::refcount; #[cfg(Py_GIL_DISABLED)] @@ -6,6 +7,7 @@ use crate::PyMutex; use std::ffi::{c_char, c_int, c_uint, c_ulong, c_void}; use std::mem; use std::ptr; +#[cfg(not(_Py_OPAQUE_PYOBJECT))] #[cfg(Py_GIL_DISABLED)] use std::sync::atomic::{AtomicIsize, AtomicU32}; @@ -92,6 +94,7 @@ const _PyObject_MIN_ALIGNMENT: usize = 4; // not currently possible to use constant variables with repr(align()), see // https://github.com/rust-lang/rust/issues/52840 +#[cfg(not(_Py_OPAQUE_PYOBJECT))] #[cfg_attr(not(all(Py_3_15, Py_GIL_DISABLED)), repr(C))] #[cfg_attr(all(Py_3_15, Py_GIL_DISABLED), repr(C, align(4)))] #[derive(Debug)] @@ -121,8 +124,10 @@ pub struct PyObject { pub ob_type: *mut PyTypeObject, } +#[cfg(not(_Py_OPAQUE_PYOBJECT))] const _: () = assert!(std::mem::align_of::() >= _PyObject_MIN_ALIGNMENT); +#[cfg(not(_Py_OPAQUE_PYOBJECT))] #[allow( clippy::declare_interior_mutable_const, reason = "contains atomic refcount on free-threaded builds" @@ -157,10 +162,14 @@ pub const PyObject_HEAD_INIT: PyObject = PyObject { ob_type: std::ptr::null_mut(), }; +#[cfg(_Py_OPAQUE_PYOBJECT)] +opaque_struct!(pub PyObject); + // skipped _Py_UNOWNED_TID // skipped _PyObject_CAST +#[cfg(not(_Py_OPAQUE_PYOBJECT))] #[repr(C)] #[derive(Debug)] pub struct PyVarObject { @@ -172,6 +181,9 @@ pub struct PyVarObject { pub _ob_size_graalpy: Py_ssize_t, } +#[cfg(_Py_OPAQUE_PYOBJECT)] +opaque_struct!(pub PyVarObject); + // skipped private _PyVarObject_CAST #[inline] @@ -219,6 +231,16 @@ extern "C" { pub fn Py_TYPE(ob: *mut PyObject) -> *mut PyTypeObject; } +#[cfg_attr(windows, link(name = "pythonXY"))] +#[cfg(all(Py_LIMITED_API, Py_3_15))] +extern "C" { + #[cfg_attr(PyPy, link_name = "PyPy_SIZE")] + pub fn Py_SIZE(ob: *mut PyObject) -> Py_ssize_t; + #[cfg_attr(PyPy, link_name = "PyPy_IS_TYPE")] + pub fn Py_IS_TYPE(ob: *mut PyObject, tp: *mut PyTypeObject) -> c_int; + // skipped Py_SET_SIZE +} + // skip _Py_TYPE compat shim #[cfg_attr(windows, link(name = "pythonXY"))] @@ -229,6 +251,7 @@ extern "C" { pub static mut PyBool_Type: PyTypeObject; } +#[cfg(not(all(Py_LIMITED_API, Py_3_15)))] #[inline] pub unsafe fn Py_SIZE(ob: *mut PyObject) -> Py_ssize_t { #[cfg(not(GraalPy))] @@ -241,6 +264,7 @@ pub unsafe fn Py_SIZE(ob: *mut PyObject) -> Py_ssize_t { _Py_SIZE(ob) } +#[cfg(not(all(Py_LIMITED_API, Py_3_15)))] #[inline] pub unsafe fn Py_IS_TYPE(ob: *mut PyObject, tp: *mut PyTypeObject) -> c_int { (Py_TYPE(ob) == tp) as c_int @@ -390,6 +414,9 @@ extern "C" { #[inline] pub unsafe fn PyObject_TypeCheck(ob: *mut PyObject, tp: *mut PyTypeObject) -> c_int { + dbg!(ob); + dbg!(Py_TYPE(ob)); + dbg!(tp); (Py_IS_TYPE(ob, tp) != 0 || PyType_IsSubtype(Py_TYPE(ob), tp) != 0) as c_int } diff --git a/pyo3-ffi/src/refcount.rs b/pyo3-ffi/src/refcount.rs index 745eaa69a97..f155e6c4d23 100644 --- a/pyo3-ffi/src/refcount.rs +++ b/pyo3-ffi/src/refcount.rs @@ -11,7 +11,7 @@ use std::ffi::c_uint; #[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))] use std::ffi::c_ulong; use std::ptr; -#[cfg(Py_GIL_DISABLED)] +#[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API)))] use std::sync::atomic::Ordering::Relaxed; #[cfg(all(Py_3_14, not(Py_3_15)))] @@ -116,6 +116,7 @@ pub unsafe fn Py_REFCNT(ob: *mut PyObject) -> Py_ssize_t { } } +#[cfg(not(_Py_OPAQUE_PYOBJECT))] #[cfg(Py_3_12)] #[inline(always)] unsafe fn _Py_IsImmortal(op: *mut PyObject) -> c_int { diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 5e89f56f6f3..32cc6d76606 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -35,15 +35,20 @@ use crate::prelude::PyTypeMethods; use crate::{ ffi, impl_::pyfunction::PyFunctionDef, - sync::PyOnceLock, - types::{any::PyAnyMethods, dict::PyDictMethods, PyDict, PyModule, PyModuleMethods}, - Bound, Py, PyAny, PyClass, PyResult, PyTypeInfo, Python, + types::{PyModule, PyModuleMethods}, + Bound, PyClass, PyResult, PyTypeInfo, }; use crate::{ffi_ptr_ext::FfiPtrExt, PyErr}; +use crate::{ + sync::PyOnceLock, + types::{any::PyAnyMethods, dict::PyDictMethods, PyDict}, + Py, PyAny, Python, +}; /// `Sync` wrapper of `ffi::PyModuleDef`. pub struct ModuleDef { // wrapped in UnsafeCell so that Rust compiler treats this as interior mutability + #[cfg(not(_Py_OPAQUE_PYOBJECT))] ffi_def: UnsafeCell, name: &'static CStr, doc: &'static CStr, @@ -71,6 +76,7 @@ impl ModuleDef { // 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. + #[cfg(not(_Py_OPAQUE_PYOBJECT))] #[allow(clippy::declare_interior_mutable_const)] const INIT: ffi::PyModuleDef = ffi::PyModuleDef { m_base: ffi::PyModuleDef_HEAD_INIT, @@ -84,6 +90,7 @@ impl ModuleDef { m_free: None, }; + #[cfg(not(_Py_OPAQUE_PYOBJECT))] let ffi_def = UnsafeCell::new(ffi::PyModuleDef { m_name: name.as_ptr(), m_doc: doc.as_ptr(), @@ -94,6 +101,7 @@ impl ModuleDef { }); ModuleDef { + #[cfg(not(_Py_OPAQUE_PYOBJECT))] ffi_def, name, doc, @@ -110,7 +118,12 @@ impl ModuleDef { } pub fn init_multi_phase(&'static self) -> *mut ffi::PyObject { - unsafe { ffi::PyModuleDef_Init(self.ffi_def.get()) } + #[cfg(not(_Py_OPAQUE_PYOBJECT))] + unsafe { + ffi::PyModuleDef_Init(self.ffi_def.get()) + } + #[cfg(_Py_OPAQUE_PYOBJECT)] + panic!("TODO: fix this panic"); } /// Builds a module object directly. Used for [`#[pymodule]`][crate::pymodule] submodules. @@ -478,6 +491,7 @@ mod tests { let module_def: ModuleDef = ModuleDef::new(NAME, DOC, &SLOTS); + #[cfg(not(_Py_OPAQUE_PYOBJECT))] unsafe { assert_eq!((*module_def.ffi_def.get()).m_slots, SLOTS.0.get().cast()); } From 733aa824fe1b6ce5fdc58981b5decab0d6f22004 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 13 Feb 2026 10:23:04 -0700 Subject: [PATCH 12/74] delete debug prints --- pyo3-ffi/src/object.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index 3b060fb0be8..2c8c23d9f9e 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -414,9 +414,6 @@ extern "C" { #[inline] pub unsafe fn PyObject_TypeCheck(ob: *mut PyObject, tp: *mut PyTypeObject) -> c_int { - dbg!(ob); - dbg!(Py_TYPE(ob)); - dbg!(tp); (Py_IS_TYPE(ob, tp) != 0 || PyType_IsSubtype(Py_TYPE(ob), tp) != 0) as c_int } From c43061e290b9bd140dd32f9b589adbf88fd5a689 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 13 Feb 2026 11:02:36 -0700 Subject: [PATCH 13/74] WIP: fix segfault --- src/impl_/pyclass.rs | 27 +++++++++++++++------------ src/pycell.rs | 14 ++++++++++---- src/pycell/impl_.rs | 20 ++++++++++---------- src/types/any.rs | 13 +++++++++++-- 4 files changed, 46 insertions(+), 28 deletions(-) diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 396c079ac0f..d949d4b0a6d 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1496,7 +1496,7 @@ mod tests { (offset_of!(ExpectedLayout, contents) + offset_of!(FrozenClass, value)) as ffi::Py_ssize_t ); - assert_eq!(member.flags, ffi::Py_READONLY); + assert_eq!(member.flags & ffi::Py_READONLY, ffi::Py_READONLY); } _ => panic!("Expected a StructMember"), } @@ -1608,17 +1608,20 @@ mod tests { // SAFETY: def.doc originated from a CStr assert_eq!(unsafe { CStr::from_ptr(def.doc) }, c"My field doc"); assert_eq!(def.type_code, ffi::Py_T_OBJECT_EX); - #[allow(irrefutable_let_patterns)] - let PyObjectOffset::Absolute(contents_offset) = - ::Layout::CONTENTS_OFFSET - else { - panic!() - }; - assert_eq!( - def.offset, - contents_offset + FIELD_OFFSET as ffi::Py_ssize_t - ); - assert_eq!(def.flags, ffi::Py_READONLY); + #[cfg(not(_Py_OPAQUE_PYOBJECT))] + { + #[allow(irrefutable_let_patterns)] + let PyObjectOffset::Absolute(contents_offset) = + ::Layout::CONTENTS_OFFSET + else { + panic!() + }; + assert_eq!( + def.offset, + contents_offset + FIELD_OFFSET as ffi::Py_ssize_t + ); + } + assert_eq!(def.flags & ffi::Py_READONLY, ffi::Py_READONLY); } #[test] diff --git a/src/pycell.rs b/src/pycell.rs index 80c922114b4..f674dd9a425 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -830,10 +830,16 @@ mod tests { Python::attach(|py| { let obj = SubSubClass::new(py).into_bound(py); let pyref = obj.borrow(); - assert_eq!(pyref.as_super().as_super().val1, 10); - assert_eq!(pyref.as_super().val2, 15); - assert_eq!(pyref.as_ref().val2, 15); // `as_ref` also works - assert_eq!(pyref.val3, 20); + dbg!(&pyref.inner); + dbg!(&pyref.as_super().inner); + dbg!(std::any::type_name_of_val(&pyref.inner.get_class_object())); + dbg!(std::any::type_name_of_val( + &pyref.as_super().inner.get_class_object() + )); + // assert_eq!(pyref.as_super().as_super().val1, 10); + // assert_eq!(pyref.as_super().val2, 15); + // assert_eq!(pyref.as_ref().val2, 15); // `as_ref` also works + // assert_eq!(pyref.val3, 20); assert_eq!(SubSubClass::get_values(pyref), (10, 15, 20)); }); } diff --git a/src/pycell/impl_.rs b/src/pycell/impl_.rs index 276eaafc600..727499e6004 100644 --- a/src/pycell/impl_.rs +++ b/src/pycell/impl_.rs @@ -634,16 +634,16 @@ mod tests { #[pyclass(crate = "crate", extends = BaseWithData)] struct ChildWithoutData; - #[test] - fn test_inherited_size() { - let base_size = PyStaticClassObject::::BASIC_SIZE; - assert!(base_size > 0); // negative indicates variable sized - assert_eq!( - base_size, - PyStaticClassObject::::BASIC_SIZE - ); - assert!(base_size < PyStaticClassObject::::BASIC_SIZE); - } + // #[test] + // fn test_inherited_size() { + // let base_size = PyStaticClassObject::::BASIC_SIZE; + // assert!(base_size > 0); // negative indicates variable sized + // assert_eq!( + // base_size, + // PyStaticClassObject::::BASIC_SIZE + // ); + // assert!(base_size < PyStaticClassObject::::BASIC_SIZE); + // } fn assert_mutable>() {} fn assert_immutable>() {} diff --git a/src/types/any.rs b/src/types/any.rs index b1691960a78..2302ce19c75 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -4,7 +4,10 @@ use crate::conversion::{FromPyObject, IntoPyObject}; use crate::err::{PyErr, PyResult}; use crate::exceptions::{PyAttributeError, PyTypeError}; use crate::ffi_ptr_ext::FfiPtrExt; -use crate::impl_::pycell::PyStaticClassObject; +#[cfg(not(_Py_OPAQUE_PYOBJECT))] +use crate::impl_::pycell::{PyClassObjectBase, PyStaticClassObject}; +#[cfg(_Py_OPAQUE_PYOBJECT)] +use crate::impl_::pycell::{PyVariableClassObject, PyVariableClassObjectBase}; use crate::instance::Bound; use crate::internal::get_slot::TP_DESCR_GET; use crate::py_result_ext::PyResultExt; @@ -53,10 +56,16 @@ pyobject_native_type_info!( pyobject_native_type_sized!(PyAny, ffi::PyObject); // We cannot use `pyobject_subclassable_native_type!()` because it cfgs out on `Py_LIMITED_API`. impl crate::impl_::pyclass::PyClassBaseType for PyAny { - type LayoutAsBase = crate::impl_::pycell::PyClassObjectBase; + #[cfg(_Py_OPAQUE_PYOBJECT)] + type LayoutAsBase = PyVariableClassObjectBase; + #[cfg(not(_Py_OPAQUE_PYOBJECT))] + type LayoutAsBase = PyClassObjectBase; type BaseNativeType = PyAny; type Initializer = crate::impl_::pyclass_init::PyNativeTypeInitializer; type PyClassMutability = crate::pycell::impl_::ImmutableClass; + #[cfg(_Py_OPAQUE_PYOBJECT)] + type Layout = PyVariableClassObject; + #[cfg(not(_Py_OPAQUE_PYOBJECT))] type Layout = PyStaticClassObject; } From 3812a644048c3a2f49e0e1853c68bb316832b77a Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 13 Feb 2026 12:00:23 -0700 Subject: [PATCH 14/74] disable append_to_inittab tests --- guide/src/python-from-rust/calling-existing-code.md | 1 + tests/test_append_to_inittab.rs | 2 +- 2 files changed, 2 insertions(+), 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..efbcabc0198 100644 --- a/guide/src/python-from-rust/calling-existing-code.md +++ b/guide/src/python-from-rust/calling-existing-code.md @@ -154,6 +154,7 @@ mod foo { } } +# #[cfg(not(_Py_OPAQUE_PYOBJECT))] fn main() -> PyResult<()> { pyo3::append_to_inittab!(foo); Python::attach(|py| Python::run(py, c"import foo; foo.add_one(6)", None, None)) diff --git a/tests/test_append_to_inittab.rs b/tests/test_append_to_inittab.rs index e147967a0c7..ba28a6fde68 100644 --- a/tests/test_append_to_inittab.rs +++ b/tests/test_append_to_inittab.rs @@ -19,7 +19,7 @@ mod module_mod_with_functions { use super::foo; } -#[cfg(not(any(PyPy, GraalPy)))] +#[cfg(not(any(PyPy, GraalPy, _Py_OPAQUE_PYOBJECT)))] #[test] fn test_module_append_to_inittab() { use pyo3::append_to_inittab; From 25a65a6e5ddc735c78f7c000d58fd51c841a8f61 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 13 Feb 2026 13:10:42 -0700 Subject: [PATCH 15/74] fix clippy --- src/pycell.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/pycell.rs b/src/pycell.rs index f674dd9a425..80c922114b4 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -830,16 +830,10 @@ mod tests { Python::attach(|py| { let obj = SubSubClass::new(py).into_bound(py); let pyref = obj.borrow(); - dbg!(&pyref.inner); - dbg!(&pyref.as_super().inner); - dbg!(std::any::type_name_of_val(&pyref.inner.get_class_object())); - dbg!(std::any::type_name_of_val( - &pyref.as_super().inner.get_class_object() - )); - // assert_eq!(pyref.as_super().as_super().val1, 10); - // assert_eq!(pyref.as_super().val2, 15); - // assert_eq!(pyref.as_ref().val2, 15); // `as_ref` also works - // assert_eq!(pyref.val3, 20); + assert_eq!(pyref.as_super().as_super().val1, 10); + assert_eq!(pyref.as_super().val2, 15); + assert_eq!(pyref.as_ref().val2, 15); // `as_ref` also works + assert_eq!(pyref.val3, 20); assert_eq!(SubSubClass::get_values(pyref), (10, 15, 20)); }); } From 4a83024bae4dc5531545476afd19bcbb4c085ef4 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 13 Feb 2026 13:11:54 -0700 Subject: [PATCH 16/74] fix ruff --- noxfile.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 2e1b57b7dcf..a75ee7d70bb 100644 --- a/noxfile.py +++ b/noxfile.py @@ -121,7 +121,12 @@ def test_rust(session: nox.Session): # We need to pass the feature set to the test command # so that it can be used in the test code # (e.g. for `#[cfg(feature = "abi3-py37")]`) - if feature_set and "abi3" in feature_set and FREE_THREADED_BUILD and sys.version_info < (3, 15): + if ( + feature_set + and "abi3" in feature_set + and FREE_THREADED_BUILD + and sys.version_info < (3, 15) + ): # free-threaded builds don't support abi3 yet continue From 9d0e2edf7bdc28ce2739e5d8bd867e21f925df2b Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 13 Feb 2026 13:19:47 -0700 Subject: [PATCH 17/74] implement David's suggestion for pyobject_subclassable_native_type --- src/types/any.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/types/any.rs b/src/types/any.rs index 2302ce19c75..84ac173595a 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -4,10 +4,8 @@ use crate::conversion::{FromPyObject, IntoPyObject}; use crate::err::{PyErr, PyResult}; use crate::exceptions::{PyAttributeError, PyTypeError}; use crate::ffi_ptr_ext::FfiPtrExt; -#[cfg(not(_Py_OPAQUE_PYOBJECT))] +#[cfg(not(all(Py_3_12, Py_LIMITED_API)))] use crate::impl_::pycell::{PyClassObjectBase, PyStaticClassObject}; -#[cfg(_Py_OPAQUE_PYOBJECT)] -use crate::impl_::pycell::{PyVariableClassObject, PyVariableClassObjectBase}; use crate::instance::Bound; use crate::internal::get_slot::TP_DESCR_GET; use crate::py_result_ext::PyResultExt; @@ -54,21 +52,19 @@ pyobject_native_type_info!( ); pyobject_native_type_sized!(PyAny, ffi::PyObject); -// We cannot use `pyobject_subclassable_native_type!()` because it cfgs out on `Py_LIMITED_API`. +// We cannot use `pyobject_subclassable_native_type!()` because it cfgs out on `Py_LIMITED_API` for Python < 3.12. +#[cfg(not(all(Py_3_12, Py_LIMITED_API)))] impl crate::impl_::pyclass::PyClassBaseType for PyAny { - #[cfg(_Py_OPAQUE_PYOBJECT)] - type LayoutAsBase = PyVariableClassObjectBase; - #[cfg(not(_Py_OPAQUE_PYOBJECT))] type LayoutAsBase = PyClassObjectBase; type BaseNativeType = PyAny; type Initializer = crate::impl_::pyclass_init::PyNativeTypeInitializer; type PyClassMutability = crate::pycell::impl_::ImmutableClass; - #[cfg(_Py_OPAQUE_PYOBJECT)] - type Layout = PyVariableClassObject; - #[cfg(not(_Py_OPAQUE_PYOBJECT))] type Layout = PyStaticClassObject; } +#[cfg(all(Py_3_12, Py_LIMITED_API))] +pyobject_subclassable_native_type!(PyAny, ffi::PyObject); + /// This trait represents the Python APIs which are usable on all Python objects. /// /// It is recommended you import this trait via `use pyo3::prelude::*` rather than From a78b5dfc2164452fcbdbef1532b16f663c88a0a1 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 13 Feb 2026 13:45:58 -0700 Subject: [PATCH 18/74] replace skipped test with real test --- src/impl_/pyclass.rs | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index d949d4b0a6d..1587617f16c 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1608,19 +1608,15 @@ mod tests { // SAFETY: def.doc originated from a CStr assert_eq!(unsafe { CStr::from_ptr(def.doc) }, c"My field doc"); assert_eq!(def.type_code, ffi::Py_T_OBJECT_EX); - #[cfg(not(_Py_OPAQUE_PYOBJECT))] - { - #[allow(irrefutable_let_patterns)] - let PyObjectOffset::Absolute(contents_offset) = - ::Layout::CONTENTS_OFFSET - else { - panic!() - }; - assert_eq!( - def.offset, - contents_offset + FIELD_OFFSET as ffi::Py_ssize_t - ); - } + #[allow(irrefutable_let_patterns)] + let contents_offset = match ::Layout::CONTENTS_OFFSET { + PyObjectOffset::Absolute(contents_offset) => contents_offset, + PyObjectOffset::Relative(contents_offset) => contents_offset, + }; + assert_eq!( + def.offset, + contents_offset + FIELD_OFFSET as ffi::Py_ssize_t + ); assert_eq!(def.flags & ffi::Py_READONLY, ffi::Py_READONLY); } From 42a73e1aed4ff4065e81351f15c661a2e878bd91 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 13 Feb 2026 13:55:53 -0700 Subject: [PATCH 19/74] fix check-feature-powerset --- Cargo.toml | 3 ++- pyo3-build-config/Cargo.toml | 3 ++- pyo3-ffi/Cargo.toml | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7cffab03b9b..7c23b80387b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -116,7 +116,8 @@ abi3-py310 = ["abi3-py311", "pyo3-build-config/abi3-py310", "pyo3-ffi/abi3-py310 abi3-py311 = ["abi3-py312", "pyo3-build-config/abi3-py311", "pyo3-ffi/abi3-py311"] abi3-py312 = ["abi3-py313", "pyo3-build-config/abi3-py312", "pyo3-ffi/abi3-py312"] abi3-py313 = ["abi3-py314", "pyo3-build-config/abi3-py313", "pyo3-ffi/abi3-py313"] -abi3-py314 = ["abi3", "pyo3-build-config/abi3-py314", "pyo3-ffi/abi3-py314"] +abi3-py314 = ["abi3-py315", "pyo3-build-config/abi3-py314", "pyo3-ffi/abi3-py314"] +abi3-py315 = ["abi3", "pyo3-build-config/abi3-py315", "pyo3-ffi/abi3-py315"] # Automatically generates `python3.dll` import libraries for Windows targets. generate-import-lib = ["pyo3-ffi/generate-import-lib"] diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index 82a70b008b9..5da31a0b93f 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -41,7 +41,8 @@ abi3-py310 = ["abi3-py311"] abi3-py311 = ["abi3-py312"] abi3-py312 = ["abi3-py313"] abi3-py313 = ["abi3-py314"] -abi3-py314 = ["abi3"] +abi3-py314 = ["abi3-py315"] +abi3-py315 = ["abi3"] [package.metadata.docs.rs] features = ["resolve-config"] diff --git a/pyo3-ffi/Cargo.toml b/pyo3-ffi/Cargo.toml index d64a7dadda0..a4989095daa 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -33,7 +33,8 @@ abi3-py310 = ["abi3-py311", "pyo3-build-config/abi3-py310"] abi3-py311 = ["abi3-py312", "pyo3-build-config/abi3-py311"] abi3-py312 = ["abi3-py313", "pyo3-build-config/abi3-py312"] abi3-py313 = ["abi3-py314", "pyo3-build-config/abi3-py313"] -abi3-py314 = ["abi3", "pyo3-build-config/abi3-py314"] +abi3-py314 = ["abi3-py315", "pyo3-build-config/abi3-py314"] +abi3-py315 = ["abi3", "pyo3-build-config/abi3-py315"] # Automatically generates `python3.dll` import libraries for Windows targets. generate-import-lib = ["pyo3-build-config/generate-import-lib"] From 060c3ca275a5ea36c81673b526efd3970b0a46b6 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 13 Feb 2026 14:22:38 -0700 Subject: [PATCH 20/74] fix clippy-all --- src/impl_/pyclass.rs | 3 ++- src/impl_/pymodule.rs | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 1587617f16c..ef5f62ec19d 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1608,9 +1608,10 @@ mod tests { // SAFETY: def.doc originated from a CStr assert_eq!(unsafe { CStr::from_ptr(def.doc) }, c"My field doc"); assert_eq!(def.type_code, ffi::Py_T_OBJECT_EX); - #[allow(irrefutable_let_patterns)] + #[allow(clippy::infallible_destructuring_match)] let contents_offset = match ::Layout::CONTENTS_OFFSET { PyObjectOffset::Absolute(contents_offset) => contents_offset, + #[cfg(Py_3_12)] PyObjectOffset::Relative(contents_offset) => contents_offset, }; assert_eq!( diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 32cc6d76606..87c196d5a80 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -50,7 +50,9 @@ pub struct ModuleDef { // wrapped in UnsafeCell so that Rust compiler treats this as interior mutability #[cfg(not(_Py_OPAQUE_PYOBJECT))] 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). @@ -103,7 +105,9 @@ impl ModuleDef { ModuleDef { #[cfg(not(_Py_OPAQUE_PYOBJECT))] ffi_def, + #[cfg(Py_3_15)] name, + #[cfg(Py_3_15)] doc, slots, // -1 is never expected to be a valid interpreter ID @@ -495,7 +499,9 @@ mod tests { unsafe { assert_eq!((*module_def.ffi_def.get()).m_slots, SLOTS.0.get().cast()); } + #[cfg(Py_3_15)] assert_eq!(module_def.name, NAME); + #[cfg(Py_3_15)] assert_eq!(module_def.doc, DOC); assert_eq!(module_def.slots.0.get(), SLOTS.0.get()); } From c1bd2c781ab3327ca67a485bf67fdab6005eef30 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 13 Feb 2026 14:58:20 -0700 Subject: [PATCH 21/74] skip test that depend on struct layout on opaque pyobject builds --- src/impl_/pyclass.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index ef5f62ec19d..fd19c1c453b 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1458,6 +1458,7 @@ pub trait ExtractPyClassWithClone {} #[cfg(test)] #[cfg(feature = "macros")] mod tests { + #[cfg(not(_Py_OPAQUE_PYOBJECT))] use crate::pycell::impl_::PyClassObjectContents; use super::*; @@ -1486,11 +1487,13 @@ mod tests { Some(PyMethodDefType::StructMember(member)) => { assert_eq!(unsafe { CStr::from_ptr(member.name) }, c"value"); assert_eq!(member.type_code, ffi::Py_T_OBJECT_EX); + #[cfg(not(_Py_OPAQUE_PYOBJECT))] #[repr(C)] struct ExpectedLayout { ob_base: ffi::PyObject, contents: PyClassObjectContents, } + #[cfg(not(_Py_OPAQUE_PYOBJECT))] assert_eq!( member.offset, (offset_of!(ExpectedLayout, contents) + offset_of!(FrozenClass, value)) From ba8b09a2eab4de8aa166e8a24f9558b0165ead47 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 16 Feb 2026 11:49:05 -0700 Subject: [PATCH 22/74] Expose PyModuleDef as an opaque pointer on opaque PyObject builds --- pyo3-ffi/src/moduleobject.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyo3-ffi/src/moduleobject.rs b/pyo3-ffi/src/moduleobject.rs index 6c8d8272e6d..88b2d3b5f29 100644 --- a/pyo3-ffi/src/moduleobject.rs +++ b/pyo3-ffi/src/moduleobject.rs @@ -1,3 +1,4 @@ +#[cfg(not(_Py_OPAQUE_PYOBJECT))] use crate::methodobject::PyMethodDef; use crate::object::*; use crate::pyport::Py_ssize_t; @@ -52,6 +53,7 @@ extern "C" { pub static mut PyModuleDef_Type: PyTypeObject; } +#[cfg(not(_Py_OPAQUE_PYOBJECT))] #[repr(C)] pub struct PyModuleDef_Base { pub ob_base: PyObject, @@ -152,6 +154,7 @@ extern "C" { pub fn PyModule_GetToken(module: *mut PyObject, result: *mut *mut c_void) -> c_int; } +#[cfg(not(_Py_OPAQUE_PYOBJECT))] #[repr(C)] pub struct PyModuleDef { pub m_base: PyModuleDef_Base, @@ -165,3 +168,7 @@ pub struct PyModuleDef { pub m_clear: Option, pub m_free: Option, } + +// from pytypedefs.h +#[cfg(_Py_OPAQUE_PYOBJECT)] +opaque_struct!(pub PyModuleDef); From f15a7fc41f2d85eff9adddd58968656e1caf3657 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 16 Feb 2026 11:49:24 -0700 Subject: [PATCH 23/74] add comments about location of opaque pointers in CPython headers --- pyo3-ffi/src/object.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index 2c8c23d9f9e..613cc9efad1 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -11,6 +11,7 @@ use std::ptr; #[cfg(Py_GIL_DISABLED)] use std::sync::atomic::{AtomicIsize, AtomicU32}; +// from pytypedefs.h #[cfg(Py_LIMITED_API)] opaque_struct!(pub PyTypeObject); @@ -162,6 +163,7 @@ pub const PyObject_HEAD_INIT: PyObject = PyObject { ob_type: std::ptr::null_mut(), }; +// from pytypedefs.h #[cfg(_Py_OPAQUE_PYOBJECT)] opaque_struct!(pub PyObject); @@ -181,6 +183,7 @@ pub struct PyVarObject { pub _ob_size_graalpy: Py_ssize_t, } +// from pytypedefs.h #[cfg(_Py_OPAQUE_PYOBJECT)] opaque_struct!(pub PyVarObject); From 3fa17d00c27369d35a55ae4704f263c018c3ddf4 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 16 Feb 2026 12:10:48 -0700 Subject: [PATCH 24/74] fix test_inherited_size --- src/pycell/impl_.rs | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/src/pycell/impl_.rs b/src/pycell/impl_.rs index 727499e6004..36080aafcb8 100644 --- a/src/pycell/impl_.rs +++ b/src/pycell/impl_.rs @@ -625,6 +625,9 @@ mod tests { #[pyclass(crate = "crate", extends = ImmutableChildOfImmutableBase, frozen)] struct ImmutableChildOfImmutableChildOfImmutableBase; + #[pyclass(crate = "crate")] + struct BaseWithoutData; + #[pyclass(crate = "crate", subclass)] struct BaseWithData(#[allow(unused)] u64); @@ -634,16 +637,35 @@ mod tests { #[pyclass(crate = "crate", extends = BaseWithData)] struct ChildWithoutData; - // #[test] - // fn test_inherited_size() { - // let base_size = PyStaticClassObject::::BASIC_SIZE; - // assert!(base_size > 0); // negative indicates variable sized - // assert_eq!( - // base_size, - // PyStaticClassObject::::BASIC_SIZE - // ); - // assert!(base_size < PyStaticClassObject::::BASIC_SIZE); - // } + #[test] + fn test_inherited_size() { + #[cfg(_Py_OPAQUE_PYOBJECT)] + type ClassObject = PyVariableClassObject; + #[cfg(not(_Py_OPAQUE_PYOBJECT))] + type ClassObject = PyStaticClassObject; + + let base_without_data_size = ClassObject::::BASIC_SIZE; + let base_with_data_size = ClassObject::::BASIC_SIZE; + let child_without_data_size = ClassObject::::BASIC_SIZE; + let child_with_data_size = ClassObject::::BASIC_SIZE; + #[cfg(_Py_OPAQUE_PYOBJECT)] + { + assert!(base_without_data_size < 0); // negative indicates variable sized + assert!(base_with_data_size < base_without_data_size); + assert_eq!(child_without_data_size, 0); + assert_eq!( + base_with_data_size - base_without_data_size, + child_with_data_size + ); + } + #[cfg(not(_Py_OPAQUE_PYOBJECT))] + { + assert!(base_without_data_size > 0); + assert!(base_with_data_size > base_without_data_size); + assert_eq!(base_with_data_size, child_without_data_size); + assert!(base_with_data_size < child_with_data_size); + } + } fn assert_mutable>() {} fn assert_immutable>() {} From 1970421e7a3449a841bc1f45358871cb322bdc60 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 17 Feb 2026 11:22:40 -0700 Subject: [PATCH 25/74] Fix doctest on _Py_OPAQUE_PYOBJECT builds --- guide/src/python-from-rust/calling-existing-code.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/guide/src/python-from-rust/calling-existing-code.md b/guide/src/python-from-rust/calling-existing-code.md index efbcabc0198..0ee9672306e 100644 --- a/guide/src/python-from-rust/calling-existing-code.md +++ b/guide/src/python-from-rust/calling-existing-code.md @@ -159,6 +159,8 @@ fn main() -> PyResult<()> { pyo3::append_to_inittab!(foo); Python::attach(|py| Python::run(py, c"import foo; foo.add_one(6)", None, None)) } +# #[cfg(_Py_OPAQUE_PYOBJECT)] +# fn main() -> () {} ``` If `append_to_inittab` cannot be used due to constraints in the program, an alternative is to create a module using [`PyModule::new`] and insert it manually into `sys.modules`: From f80849e2d81a801759e0bf96dbf44ea3a8249b40 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 3 Mar 2026 13:44:48 -0700 Subject: [PATCH 26/74] fix build error on non-opaque builds --- src/impl_/pyclass.rs | 6 +++--- src/pycell/impl_.rs | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index fd19c1c453b..0d5c5cbb9b2 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1458,7 +1458,7 @@ pub trait ExtractPyClassWithClone {} #[cfg(test)] #[cfg(feature = "macros")] mod tests { - #[cfg(not(_Py_OPAQUE_PYOBJECT))] + #[cfg(not(all(Py_LIMITED_API, Py_3_12)))] use crate::pycell::impl_::PyClassObjectContents; use super::*; @@ -1487,13 +1487,13 @@ mod tests { Some(PyMethodDefType::StructMember(member)) => { assert_eq!(unsafe { CStr::from_ptr(member.name) }, c"value"); assert_eq!(member.type_code, ffi::Py_T_OBJECT_EX); - #[cfg(not(_Py_OPAQUE_PYOBJECT))] + #[cfg(not(all(Py_LIMITED_API, Py_3_12)))] #[repr(C)] struct ExpectedLayout { ob_base: ffi::PyObject, contents: PyClassObjectContents, } - #[cfg(not(_Py_OPAQUE_PYOBJECT))] + #[cfg(not(all(Py_LIMITED_API, Py_3_12)))] assert_eq!( member.offset, (offset_of!(ExpectedLayout, contents) + offset_of!(FrozenClass, value)) diff --git a/src/pycell/impl_.rs b/src/pycell/impl_.rs index 36080aafcb8..d823b7370ef 100644 --- a/src/pycell/impl_.rs +++ b/src/pycell/impl_.rs @@ -639,16 +639,16 @@ mod tests { #[test] fn test_inherited_size() { - #[cfg(_Py_OPAQUE_PYOBJECT)] + #[cfg(all(Py_LIMITED_API, Py_3_12))] type ClassObject = PyVariableClassObject; - #[cfg(not(_Py_OPAQUE_PYOBJECT))] + #[cfg(not(all(Py_LIMITED_API, Py_3_12)))] type ClassObject = PyStaticClassObject; let base_without_data_size = ClassObject::::BASIC_SIZE; let base_with_data_size = ClassObject::::BASIC_SIZE; let child_without_data_size = ClassObject::::BASIC_SIZE; let child_with_data_size = ClassObject::::BASIC_SIZE; - #[cfg(_Py_OPAQUE_PYOBJECT)] + #[cfg(all(Py_LIMITED_API, Py_3_12))] { assert!(base_without_data_size < 0); // negative indicates variable sized assert!(base_with_data_size < base_without_data_size); @@ -658,7 +658,7 @@ mod tests { child_with_data_size ); } - #[cfg(not(_Py_OPAQUE_PYOBJECT))] + #[cfg(not(all(Py_LIMITED_API, Py_3_12)))] { assert!(base_without_data_size > 0); assert!(base_with_data_size > base_without_data_size); From c0805a94f2ead12b471c9437f8f04cde415e2b89 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 3 Mar 2026 13:55:29 -0700 Subject: [PATCH 27/74] mark BaseWithoutData as subclassable --- src/pycell/impl_.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pycell/impl_.rs b/src/pycell/impl_.rs index d823b7370ef..32fcabe8d84 100644 --- a/src/pycell/impl_.rs +++ b/src/pycell/impl_.rs @@ -625,7 +625,7 @@ mod tests { #[pyclass(crate = "crate", extends = ImmutableChildOfImmutableBase, frozen)] struct ImmutableChildOfImmutableChildOfImmutableBase; - #[pyclass(crate = "crate")] + #[pyclass(crate = "crate", subclass)] struct BaseWithoutData; #[pyclass(crate = "crate", subclass)] From 719cef52175107bfdbbe7a8f2ea50210dd76801d Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 3 Mar 2026 14:45:54 -0700 Subject: [PATCH 28/74] relax assert for Windows --- src/pycell/impl_.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pycell/impl_.rs b/src/pycell/impl_.rs index 32fcabe8d84..5d91b8bd857 100644 --- a/src/pycell/impl_.rs +++ b/src/pycell/impl_.rs @@ -653,10 +653,7 @@ mod tests { assert!(base_without_data_size < 0); // negative indicates variable sized assert!(base_with_data_size < base_without_data_size); assert_eq!(child_without_data_size, 0); - assert_eq!( - base_with_data_size - base_without_data_size, - child_with_data_size - ); + assert!(base_with_data_size - base_without_data_size < child_with_data_size); } #[cfg(not(all(Py_LIMITED_API, Py_3_12)))] { From 072ef0a3e18de6c07fcbc7ba490e0841b2610106 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 4 Mar 2026 10:22:46 -0700 Subject: [PATCH 29/74] Make PyAny a PyVarObject only on the opaque PyObject build --- src/impl_/pyclass.rs | 6 +++--- src/pycell/impl_.rs | 8 ++++---- src/types/any.rs | 9 +++++---- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 0d5c5cbb9b2..fd19c1c453b 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1458,7 +1458,7 @@ pub trait ExtractPyClassWithClone {} #[cfg(test)] #[cfg(feature = "macros")] mod tests { - #[cfg(not(all(Py_LIMITED_API, Py_3_12)))] + #[cfg(not(_Py_OPAQUE_PYOBJECT))] use crate::pycell::impl_::PyClassObjectContents; use super::*; @@ -1487,13 +1487,13 @@ mod tests { Some(PyMethodDefType::StructMember(member)) => { assert_eq!(unsafe { CStr::from_ptr(member.name) }, c"value"); assert_eq!(member.type_code, ffi::Py_T_OBJECT_EX); - #[cfg(not(all(Py_LIMITED_API, Py_3_12)))] + #[cfg(not(_Py_OPAQUE_PYOBJECT))] #[repr(C)] struct ExpectedLayout { ob_base: ffi::PyObject, contents: PyClassObjectContents, } - #[cfg(not(all(Py_LIMITED_API, Py_3_12)))] + #[cfg(not(_Py_OPAQUE_PYOBJECT))] assert_eq!( member.offset, (offset_of!(ExpectedLayout, contents) + offset_of!(FrozenClass, value)) diff --git a/src/pycell/impl_.rs b/src/pycell/impl_.rs index 5d91b8bd857..bc21aac3a42 100644 --- a/src/pycell/impl_.rs +++ b/src/pycell/impl_.rs @@ -639,23 +639,23 @@ mod tests { #[test] fn test_inherited_size() { - #[cfg(all(Py_LIMITED_API, Py_3_12))] + #[cfg(_Py_OPAQUE_PYOBJECT)] type ClassObject = PyVariableClassObject; - #[cfg(not(all(Py_LIMITED_API, Py_3_12)))] + #[cfg(not(_Py_OPAQUE_PYOBJECT))] type ClassObject = PyStaticClassObject; let base_without_data_size = ClassObject::::BASIC_SIZE; let base_with_data_size = ClassObject::::BASIC_SIZE; let child_without_data_size = ClassObject::::BASIC_SIZE; let child_with_data_size = ClassObject::::BASIC_SIZE; - #[cfg(all(Py_LIMITED_API, Py_3_12))] + #[cfg(_Py_OPAQUE_PYOBJECT)] { assert!(base_without_data_size < 0); // negative indicates variable sized assert!(base_with_data_size < base_without_data_size); assert_eq!(child_without_data_size, 0); assert!(base_with_data_size - base_without_data_size < child_with_data_size); } - #[cfg(not(all(Py_LIMITED_API, Py_3_12)))] + #[cfg(not(_Py_OPAQUE_PYOBJECT))] { assert!(base_without_data_size > 0); assert!(base_with_data_size > base_without_data_size); diff --git a/src/types/any.rs b/src/types/any.rs index 06814281d6c..a6a5025d4a5 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -4,7 +4,7 @@ use crate::conversion::{FromPyObject, IntoPyObject}; use crate::err::{PyErr, PyResult}; use crate::exceptions::{PyAttributeError, PyTypeError}; use crate::ffi_ptr_ext::FfiPtrExt; -#[cfg(not(all(Py_3_12, Py_LIMITED_API)))] +#[cfg(not(_Py_OPAQUE_PYOBJECT))] use crate::impl_::pycell::{PyClassObjectBase, PyStaticClassObject}; use crate::instance::Bound; use crate::internal::get_slot::TP_DESCR_GET; @@ -52,8 +52,9 @@ pyobject_native_type_info!( ); pyobject_native_type_sized!(PyAny, ffi::PyObject); -// We cannot use `pyobject_subclassable_native_type!()` because it cfgs out on `Py_LIMITED_API` for Python < 3.12. -#[cfg(not(all(Py_3_12, Py_LIMITED_API)))] +// We could use pyobject_subclassable_native_type here, but for now only on +// opaque PyObject builds to not introduce behavior changes on older Python releases +#[cfg(not(_Py_OPAQUE_PYOBJECT))] impl crate::impl_::pyclass::PyClassBaseType for PyAny { type LayoutAsBase = PyClassObjectBase; type BaseNativeType = PyAny; @@ -62,7 +63,7 @@ impl crate::impl_::pyclass::PyClassBaseType for PyAny { type Layout = PyStaticClassObject; } -#[cfg(all(Py_3_12, Py_LIMITED_API))] +#[cfg(_Py_OPAQUE_PYOBJECT)] pyobject_subclassable_native_type!(PyAny, ffi::PyObject); /// This trait represents the Python APIs which are usable on all Python objects. From 264307fe76f5d70658bd33019d67eaa2bcfe5913 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 24 Mar 2026 12:36:28 -0600 Subject: [PATCH 30/74] fix merge conflict resolution error --- src/impl_/pymodule.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index bc1aeb42ab6..64d30101108 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -182,7 +182,6 @@ impl ModuleDef { { let ffi_def = self.ffi_def.get(); - let name = unsafe { CStr::from_ptr((*ffi_def).m_name).to_str()? }.to_string(); let m_name = unsafe { CStr::from_ptr((*ffi_def).m_name) }; let name = m_name .to_str() From ed48e752860738756928c2f8b9c5503523f37816 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 24 Mar 2026 12:36:36 -0600 Subject: [PATCH 31/74] fix buggy assertion --- src/pycell/impl_.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pycell/impl_.rs b/src/pycell/impl_.rs index 650c609b0db..0496c78f94f 100644 --- a/src/pycell/impl_.rs +++ b/src/pycell/impl_.rs @@ -652,7 +652,10 @@ mod tests { assert!(base_without_data_size < 0); // negative indicates variable sized assert!(base_with_data_size < base_without_data_size); assert_eq!(child_without_data_size, 0); - assert!(base_with_data_size - base_without_data_size < child_with_data_size); + assert_eq!( + base_with_data_size - base_without_data_size, + child_with_data_size + ); } #[cfg(not(_Py_OPAQUE_PYOBJECT))] { From b4138c46478009e61c870639b09df04e420ed876 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 24 Mar 2026 13:13:58 -0600 Subject: [PATCH 32/74] Expose critical section in the limited API starting in Python 3.15 --- pyo3-build-config/src/impl_.rs | 2 +- pyo3-ffi/src/cpython/mod.rs | 9 ++++-- pyo3-ffi/src/critical_section.rs | 54 ++++++++++++++++++++++++++++++++ pyo3-ffi/src/impl_/mod.rs | 4 +-- pyo3-ffi/src/lib.rs | 6 ++++ pyo3-ffi/src/object.rs | 2 +- src/sync/critical_section.rs | 8 ++--- src/types/list.rs | 18 +++++------ 8 files changed, 84 insertions(+), 19 deletions(-) create mode 100644 pyo3-ffi/src/critical_section.rs diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index d9e3744acfe..146b1ad7bab 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -194,7 +194,7 @@ impl InterpreterConfig { } // If Py_GIL_DISABLED is set, do not build with limited API support - if self.abi3 && !(self.is_free_threaded()) { + if self.abi3 && !(self.is_free_threaded() && self.version.minor < 15) { out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); if self.version.minor >= 15 { out.push("cargo:rustc-cfg=_Py_OPAQUE_PYOBJECT".to_owned()); diff --git a/pyo3-ffi/src/cpython/mod.rs b/pyo3-ffi/src/cpython/mod.rs index 300efbe359b..4397670b938 100644 --- a/pyo3-ffi/src/cpython/mod.rs +++ b/pyo3-ffi/src/cpython/mod.rs @@ -50,8 +50,13 @@ pub use self::ceval::*; pub use self::code::*; pub use self::compile::*; pub use self::complexobject::*; -#[cfg(Py_3_13)] -pub use self::critical_section::*; +#[cfg(all(Py_3_13, not(all(Py_3_15, Py_LIMITED_API))))] +pub use self::critical_section::{ + PyCriticalSection, PyCriticalSection2, PyCriticalSection2_Begin, PyCriticalSection2_End, + PyCriticalSection_Begin, PyCriticalSection_End, +}; +#[cfg(Py_3_14)] +pub use self::critical_section::{PyCriticalSection2_BeginMutex, PyCriticalSection_BeginMutex}; pub use self::descrobject::*; pub use self::dictobject::*; pub use self::floatobject::*; diff --git a/pyo3-ffi/src/critical_section.rs b/pyo3-ffi/src/critical_section.rs new file mode 100644 index 00000000000..1938bbb8d13 --- /dev/null +++ b/pyo3-ffi/src/critical_section.rs @@ -0,0 +1,54 @@ +#[cfg(not(Py_LIMITED_API))] +use crate::PyMutex; +use crate::PyObject; +// from typedefs.h +#[cfg(all(Py_3_15, Py_LIMITED_API))] +opaque_struct!(pub PyMutex); + +#[cfg(Py_3_15)] +#[repr(C)] +pub struct PyCriticalSection_v1 { + _cs_prev: usize, + _cs_mutex: *mut PyMutex, +} + +#[cfg(Py_3_15)] +#[repr(C)] +pub struct PyCriticalSection2_v1 { + _cs_base: PyCriticalSection_v1, + _cs_mutex2: *mut PyMutex, +} + +extern "C" { + pub fn PyCriticalSection_Begin_v1(c: *mut PyCriticalSection_v1, op: *mut PyObject); + pub fn PyCriticalSection_Env_v1(c: *mut PyCriticalSection_v1); + pub fn PyCriticalSection2_Begin_v1( + c: *mut PyCriticalSection_v1, + a: *mut PyObject, + b: *mut PyObject, + ); + pub fn PyCriticalSection2_Env_v1(c: *mut PyCriticalSection_v1); +} + +#[cfg(Py_3_15)] +#[repr(C)] +pub struct PyCriticalSection_v0 { + _cs: *mut PyCriticalSection_v1, +} + +#[cfg(Py_3_15)] +#[repr(C)] +pub struct PyCriticalSection2_v0 { + _cs: *mut PyCriticalSection2_v1, +} + +extern "C" { + pub fn PyCriticalSection_Begin_v0(c: *mut PyCriticalSection_v0, op: *mut PyObject); + pub fn PyCriticalSection_End_v0(c: *mut PyCriticalSection_v0); + pub fn PyCriticalSection2_Begin_v0( + c: *mut PyCriticalSection2_v0, + a: *mut PyObject, + b: *mut PyObject, + ); + pub fn PyCriticalSection2_End_v0(c: *mut PyCriticalSection2_v0); +} diff --git a/pyo3-ffi/src/impl_/mod.rs b/pyo3-ffi/src/impl_/mod.rs index 064df213ba6..9ebd31437f2 100644 --- a/pyo3-ffi/src/impl_/mod.rs +++ b/pyo3-ffi/src/impl_/mod.rs @@ -1,4 +1,4 @@ -#[cfg(Py_GIL_DISABLED)] +#[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API)))] mod atomic_c_ulong { pub struct GetAtomicCULong(); @@ -17,6 +17,6 @@ mod atomic_c_ulong { } /// Typedef for an atomic integer to match the platform-dependent c_ulong type. -#[cfg(Py_GIL_DISABLED)] +#[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API)))] #[doc(hidden)] pub type AtomicCULong = atomic_c_ulong::TYPE; diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index 52d3b93ccf3..5592d19b4bd 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -439,6 +439,8 @@ pub use self::compile::*; pub use self::complexobject::*; #[cfg(all(Py_3_8, not(Py_LIMITED_API)))] pub use self::context::*; +#[cfg(Py_3_15)] +pub use self::critical_section::*; #[cfg(not(Py_LIMITED_API))] pub use self::datetime::*; pub use self::descrobject::*; @@ -506,6 +508,8 @@ mod compile; mod complexobject; #[cfg(all(Py_3_8, not(Py_LIMITED_API)))] mod context; // It's actually 3.7.1, but no cfg for patches. +#[cfg(Py_3_15)] +mod critical_section; #[cfg(not(Py_LIMITED_API))] pub(crate) mod datetime; mod descrobject; @@ -594,3 +598,5 @@ mod cpython; #[cfg(not(Py_LIMITED_API))] pub use self::cpython::*; +#[cfg(any(all(Py_3_13, not(Py_LIMITED_API)), all(Py_3_15, Py_LIMITED_API)))] +pub use self::cpython::{PyCriticalSection, PyCriticalSection2}; diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index c44f4039584..fdb3cb63d7d 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -2,7 +2,7 @@ use crate::pyport::{Py_hash_t, Py_ssize_t}; #[cfg(not(_Py_OPAQUE_PYOBJECT))] #[cfg(Py_GIL_DISABLED)] use crate::refcount; -#[cfg(Py_GIL_DISABLED)] +#[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API)))] use crate::PyMutex; use std::ffi::{c_char, c_int, c_uint, c_ulong, c_void}; use std::mem; diff --git a/src/sync/critical_section.rs b/src/sync/critical_section.rs index 278be0da12a..4c825db3f37 100644 --- a/src/sync/critical_section.rs +++ b/src/sync/critical_section.rs @@ -46,10 +46,10 @@ use crate::{types::PyAny, Bound}; #[cfg(all(Py_3_14, not(Py_LIMITED_API)))] use std::cell::UnsafeCell; -#[cfg(Py_GIL_DISABLED)] +#[cfg(any(Py_GIL_DISABLED, all(Py_3_15, Py_LIMITED_API)))] struct CSGuard(crate::ffi::PyCriticalSection); -#[cfg(Py_GIL_DISABLED)] +#[cfg(any(Py_GIL_DISABLED, all(Py_3_15, Py_LIMITED_API)))] impl Drop for CSGuard { fn drop(&mut self) { unsafe { @@ -58,10 +58,10 @@ impl Drop for CSGuard { } } -#[cfg(Py_GIL_DISABLED)] +#[cfg(any(Py_GIL_DISABLED, all(Py_3_15, Py_LIMITED_API)))] struct CS2Guard(crate::ffi::PyCriticalSection2); -#[cfg(Py_GIL_DISABLED)] +#[cfg(any(Py_GIL_DISABLED, all(Py_3_15, Py_LIMITED_API)))] impl Drop for CS2Guard { fn drop(&mut self) { unsafe { diff --git a/src/types/list.rs b/src/types/list.rs index 5d8b29bb310..00bec2e88a3 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -680,7 +680,7 @@ impl<'py> Iterator for BoundListIterator<'py> { } #[inline] - #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] + #[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API), not(feature = "nightly")))] fn fold(mut self, init: B, mut f: F) -> B where Self: Sized, @@ -696,7 +696,7 @@ impl<'py> Iterator for BoundListIterator<'py> { } #[inline] - #[cfg(all(Py_GIL_DISABLED, feature = "nightly"))] + #[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API), feature = "nightly"))] fn try_fold(&mut self, init: B, mut f: F) -> R where Self: Sized, @@ -713,7 +713,7 @@ impl<'py> Iterator for BoundListIterator<'py> { } #[inline] - #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] + #[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API), not(feature = "nightly")))] fn all(&mut self, mut f: F) -> bool where Self: Sized, @@ -730,7 +730,7 @@ impl<'py> Iterator for BoundListIterator<'py> { } #[inline] - #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] + #[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API), not(feature = "nightly")))] fn any(&mut self, mut f: F) -> bool where Self: Sized, @@ -747,7 +747,7 @@ impl<'py> Iterator for BoundListIterator<'py> { } #[inline] - #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] + #[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API), not(feature = "nightly")))] fn find

(&mut self, mut predicate: P) -> Option where Self: Sized, @@ -764,7 +764,7 @@ impl<'py> Iterator for BoundListIterator<'py> { } #[inline] - #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] + #[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API), not(feature = "nightly")))] fn find_map(&mut self, mut f: F) -> Option where Self: Sized, @@ -781,7 +781,7 @@ impl<'py> Iterator for BoundListIterator<'py> { } #[inline] - #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] + #[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API), not(feature = "nightly")))] fn position

(&mut self, mut predicate: P) -> Option where Self: Sized, @@ -853,7 +853,7 @@ impl DoubleEndedIterator for BoundListIterator<'_> { } #[inline] - #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] + #[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API), not(feature = "nightly")))] fn rfold(mut self, init: B, mut f: F) -> B where Self: Sized, @@ -869,7 +869,7 @@ impl DoubleEndedIterator for BoundListIterator<'_> { } #[inline] - #[cfg(all(Py_GIL_DISABLED, feature = "nightly"))] + #[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API), feature = "nightly"))] fn try_rfold(&mut self, init: B, mut f: F) -> R where Self: Sized, From f3ee6af66753385dcc86931274735e1773f8817d Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 24 Mar 2026 14:22:59 -0600 Subject: [PATCH 33/74] expose critical section API in limited API --- examples/maturin-starter/Cargo.toml | 2 +- pyo3-ffi/src/cpython/critical_section.rs | 26 +---------- pyo3-ffi/src/cpython/mod.rs | 5 -- pyo3-ffi/src/critical_section.rs | 59 +++++++++--------------- pyo3-ffi/src/lib.rs | 3 -- 5 files changed, 25 insertions(+), 70 deletions(-) diff --git a/examples/maturin-starter/Cargo.toml b/examples/maturin-starter/Cargo.toml index b8bf0d2017a..a1dfa2b2da4 100644 --- a/examples/maturin-starter/Cargo.toml +++ b/examples/maturin-starter/Cargo.toml @@ -12,6 +12,6 @@ crate-type = ["cdylib"] pyo3 = { path = "../../" } [features] -abi3 = ["pyo3/abi3-py37"] +abi3 = ["pyo3/abi3-py315"] [workspace] diff --git a/pyo3-ffi/src/cpython/critical_section.rs b/pyo3-ffi/src/cpython/critical_section.rs index 5db205b9840..670d45ae914 100644 --- a/pyo3-ffi/src/cpython/critical_section.rs +++ b/pyo3-ffi/src/cpython/critical_section.rs @@ -1,38 +1,14 @@ #[cfg(any(Py_3_14, Py_GIL_DISABLED))] use crate::PyMutex; -use crate::PyObject; - -#[repr(C)] -#[cfg(Py_GIL_DISABLED)] -pub struct PyCriticalSection { - _cs_prev: usize, - _cs_mutex: *mut PyMutex, -} - -#[repr(C)] -#[cfg(Py_GIL_DISABLED)] -pub struct PyCriticalSection2 { - _cs_base: PyCriticalSection, - _cs_mutex2: *mut PyMutex, -} - -#[cfg(not(Py_GIL_DISABLED))] -opaque_struct!(pub PyCriticalSection); - -#[cfg(not(Py_GIL_DISABLED))] -opaque_struct!(pub PyCriticalSection2); +use crate::{PyCriticalSection, PyCriticalSection2}; extern_libpython! { - pub fn PyCriticalSection_Begin(c: *mut PyCriticalSection, op: *mut PyObject); #[cfg(Py_3_14)] pub fn PyCriticalSection_BeginMutex(c: *mut PyCriticalSection, m: *mut PyMutex); - pub fn PyCriticalSection_End(c: *mut PyCriticalSection); - pub fn PyCriticalSection2_Begin(c: *mut PyCriticalSection2, a: *mut PyObject, b: *mut PyObject); #[cfg(Py_3_14)] pub fn PyCriticalSection2_BeginMutex( c: *mut PyCriticalSection2, m1: *mut PyMutex, m2: *mut PyMutex, ); - pub fn PyCriticalSection2_End(c: *mut PyCriticalSection2); } diff --git a/pyo3-ffi/src/cpython/mod.rs b/pyo3-ffi/src/cpython/mod.rs index 4397670b938..f873fb59435 100644 --- a/pyo3-ffi/src/cpython/mod.rs +++ b/pyo3-ffi/src/cpython/mod.rs @@ -50,11 +50,6 @@ pub use self::ceval::*; pub use self::code::*; pub use self::compile::*; pub use self::complexobject::*; -#[cfg(all(Py_3_13, not(all(Py_3_15, Py_LIMITED_API))))] -pub use self::critical_section::{ - PyCriticalSection, PyCriticalSection2, PyCriticalSection2_Begin, PyCriticalSection2_End, - PyCriticalSection_Begin, PyCriticalSection_End, -}; #[cfg(Py_3_14)] pub use self::critical_section::{PyCriticalSection2_BeginMutex, PyCriticalSection_BeginMutex}; pub use self::descrobject::*; diff --git a/pyo3-ffi/src/critical_section.rs b/pyo3-ffi/src/critical_section.rs index 1938bbb8d13..513d5b06ffb 100644 --- a/pyo3-ffi/src/critical_section.rs +++ b/pyo3-ffi/src/critical_section.rs @@ -1,54 +1,41 @@ -#[cfg(not(Py_LIMITED_API))] +#[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API)))] use crate::PyMutex; +#[cfg(any(all(Py_GIL_DISABLED, Py_3_13), all(Py_LIMITED_API, Py_3_15)))] use crate::PyObject; -// from typedefs.h + #[cfg(all(Py_3_15, Py_LIMITED_API))] opaque_struct!(pub PyMutex); -#[cfg(Py_3_15)] +#[cfg(any( + all(Py_GIL_DISABLED, Py_3_13, not(Py_LIMITED_API)), + all(Py_GIL_DISABLED, Py_3_15, Py_LIMITED_API) +))] #[repr(C)] -pub struct PyCriticalSection_v1 { +pub struct PyCriticalSection { _cs_prev: usize, _cs_mutex: *mut PyMutex, } -#[cfg(Py_3_15)] +#[cfg(any( + all(Py_GIL_DISABLED, Py_3_13, not(Py_LIMITED_API)), + all(Py_GIL_DISABLED, Py_3_15, Py_LIMITED_API) +))] #[repr(C)] -pub struct PyCriticalSection2_v1 { - _cs_base: PyCriticalSection_v1, +pub struct PyCriticalSection2 { + _cs_base: PyCriticalSection, _cs_mutex2: *mut PyMutex, } -extern "C" { - pub fn PyCriticalSection_Begin_v1(c: *mut PyCriticalSection_v1, op: *mut PyObject); - pub fn PyCriticalSection_Env_v1(c: *mut PyCriticalSection_v1); - pub fn PyCriticalSection2_Begin_v1( - c: *mut PyCriticalSection_v1, - a: *mut PyObject, - b: *mut PyObject, - ); - pub fn PyCriticalSection2_Env_v1(c: *mut PyCriticalSection_v1); -} - -#[cfg(Py_3_15)] -#[repr(C)] -pub struct PyCriticalSection_v0 { - _cs: *mut PyCriticalSection_v1, -} +#[cfg(all(not(Py_GIL_DISABLED), Py_3_15, Py_LIMITED_API))] +opaque_struct!(pub PyCriticalSection); -#[cfg(Py_3_15)] -#[repr(C)] -pub struct PyCriticalSection2_v0 { - _cs: *mut PyCriticalSection2_v1, -} +#[cfg(all(not(Py_GIL_DISABLED), Py_3_15, Py_LIMITED_API))] +opaque_struct!(pub PyCriticalSection2); +#[cfg(any(all(Py_GIL_DISABLED, Py_3_13), all(Py_LIMITED_API, Py_3_15)))] extern "C" { - pub fn PyCriticalSection_Begin_v0(c: *mut PyCriticalSection_v0, op: *mut PyObject); - pub fn PyCriticalSection_End_v0(c: *mut PyCriticalSection_v0); - pub fn PyCriticalSection2_Begin_v0( - c: *mut PyCriticalSection2_v0, - a: *mut PyObject, - b: *mut PyObject, - ); - pub fn PyCriticalSection2_End_v0(c: *mut PyCriticalSection2_v0); + pub fn PyCriticalSection_Begin(c: *mut PyCriticalSection, op: *mut PyObject); + pub fn PyCriticalSection_End(c: *mut PyCriticalSection); + pub fn PyCriticalSection2_Begin(c: *mut PyCriticalSection2, a: *mut PyObject, b: *mut PyObject); + pub fn PyCriticalSection2_End(c: *mut PyCriticalSection2); } diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index 5592d19b4bd..3db7c864ba9 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -439,7 +439,6 @@ pub use self::compile::*; pub use self::complexobject::*; #[cfg(all(Py_3_8, not(Py_LIMITED_API)))] pub use self::context::*; -#[cfg(Py_3_15)] pub use self::critical_section::*; #[cfg(not(Py_LIMITED_API))] pub use self::datetime::*; @@ -598,5 +597,3 @@ mod cpython; #[cfg(not(Py_LIMITED_API))] pub use self::cpython::*; -#[cfg(any(all(Py_3_13, not(Py_LIMITED_API)), all(Py_3_15, Py_LIMITED_API)))] -pub use self::cpython::{PyCriticalSection, PyCriticalSection2}; From ba47f50524741e0cf19ff73ea0c2bb4bd2b7e75c Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 25 Mar 2026 14:39:00 -0600 Subject: [PATCH 34/74] disable warning in pyo3-ffi build script on sufficiently new Pythons --- pyo3-ffi/build.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 525ce6127b6..5e0254a448d 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -127,7 +127,7 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { if interpreter_config.abi3 { match interpreter_config.implementation { PythonImplementation::CPython => { - if interpreter_config.is_free_threaded() { + if interpreter_config.is_free_threaded() && interpreter_config.version.minor < 15 { warn!( "The free-threaded build of CPython does not yet support abi3 so the build artifacts will be version-specific." ) From e52cb598fe24345c0e75b34d098a03b81f7011b2 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 26 Mar 2026 08:28:58 -0600 Subject: [PATCH 35/74] fixup features --- Cargo.toml | 6 + noxfile.py | 2 +- pyo3-build-config/Cargo.toml | 4 + pyo3-build-config/src/impl_.rs | 319 +++++++++++++++++++++++---------- pyo3-build-config/src/lib.rs | 12 +- pyo3-ffi/Cargo.toml | 6 + pyo3-ffi/build.rs | 16 +- 7 files changed, 252 insertions(+), 113 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cb502c34f70..39648417646 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -119,6 +119,12 @@ abi3-py313 = ["abi3-py314", "pyo3-build-config/abi3-py313", "pyo3-ffi/abi3-py313 abi3-py314 = ["abi3-py315", "pyo3-build-config/abi3-py314", "pyo3-ffi/abi3-py314"] abi3-py315 = ["abi3", "pyo3-build-config/abi3-py315", "pyo3-ffi/abi3-py315"] +# Use the free-threaded limited API. See https://www.python.org/dev/peps/pep-803/ for more. +abi3t = ["pyo3-build-config/abi3t", "pyo3-ffi/abi3t"] + +# With abi3t, we can manually set the minimum Python version. +abi3t-py315 = ["abi3t", "pyo3-build-config/abi3t-py315"] + # deprecated: no longer needed, raw-dylib is used instead generate-import-lib = ["pyo3-ffi/generate-import-lib"] diff --git a/noxfile.py b/noxfile.py index 18c3f2b250d..09bca9dbfeb 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1159,7 +1159,7 @@ def test_version_limits(session: nox.Session): # "An abi3-py3* feature must be specified when compiling without a Python # interpreter." # - # then `ABI3_MAX_MINOR` in `pyo3-build-config/src/impl_.rs` is probably outdated. + # then `STABLE_ABI_MAX_MINOR` in `pyo3-build-config/src/impl_.rs` is probably outdated. assert f"version=3.{max_minor_version}" in stderr, ( f"Expected to see version=3.{max_minor_version}, got: \n\n{stderr}" ) diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index 20335180a29..b3d88183b37 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -42,5 +42,9 @@ abi3-py313 = ["abi3-py314"] abi3-py314 = ["abi3-py315"] abi3-py315 = ["abi3"] +# These features are enabled by pyo3 when building free-threaded Stable ABI extension modules. +abi3t = [] +abi3t-py315 = ["abi3t"] + [package.metadata.docs.rs] features = ["resolve-config"] diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 146b1ad7bab..3ae2ced3bb4 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -44,7 +44,7 @@ const MINIMUM_SUPPORTED_VERSION_GRAALPY: PythonVersion = PythonVersion { }; /// Maximum Python version that can be used as minimum required Python version with abi3. -pub(crate) const ABI3_MAX_MINOR: u8 = 15; +pub(crate) const STABLE_ABI_MAX_MINOR: u8 = 15; #[cfg(test)] thread_local! { @@ -83,6 +83,54 @@ pub fn target_triple_from_env() -> Triple { .expect("Unrecognized TARGET environment variable value") } +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum CPythonABI { + ABI3, + ABI3t, + VersionSpecific, +} + +impl CPythonABI { + fn from_build_env() -> Result { + let abi3 = is_abi3(); + let abi3t = is_abi3t(); + ensure!( + !(abi3 && abi3t), + "Cannot simultaneously build for abi3 and abi3t ABIs" + ); + if abi3 { + return Ok(CPythonABI::ABI3); + } else if abi3t { + return Ok(CPythonABI::ABI3t); + } else { + return Ok(CPythonABI::VersionSpecific); + } + } +} + +impl Display for CPythonABI { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CPythonABI::ABI3 => write!(f, "abi3"), + CPythonABI::ABI3t => write!(f, "abi3t"), + CPythonABI::VersionSpecific => write!(f, "version_specific"), + } + } +} + +impl FromStr for CPythonABI { + type Err = crate::errors::Error; + + fn from_str(value: &str) -> Result { + match value { + "abi3" => Ok(CPythonABI::ABI3), + "abi3t" => Ok(CPythonABI::ABI3t), + "version_specific" => Ok(CPythonABI::VersionSpecific), + _ => Err(format!("Unrecognized ABI name: {value}").into()), + } + } +} + /// Configuration needed by PyO3 to build for the correct Python implementation. /// /// Usually this is queried directly from the Python interpreter, or overridden using the @@ -109,8 +157,8 @@ pub struct InterpreterConfig { /// Whether linking against the stable/limited Python 3 API. /// - /// Serialized to `abi3`. - pub abi3: bool, + /// FIXME: serialization? + pub stable_abi: CPythonABI, /// The name of the link library defining Python. /// @@ -193,12 +241,20 @@ impl InterpreterConfig { PythonImplementation::GraalPy => out.push("cargo:rustc-cfg=GraalPy".to_owned()), } - // If Py_GIL_DISABLED is set, do not build with limited API support - if self.abi3 && !(self.is_free_threaded() && self.version.minor < 15) { - out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); - if self.version.minor >= 15 { + match self.stable_abi { + CPythonABI::ABI3 => { + if !self.is_free_threaded() { + out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); + } + } + CPythonABI::ABI3t => { + out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); + if !self.is_free_threaded() { + out.push("cargo:rustc-cfg=Py_GIL_DISABLED".to_owned()); + } out.push("cargo:rustc-cfg=_Py_OPAQUE_PYOBJECT".to_owned()); } + CPythonABI::VersionSpecific => {} } for flag in &self.build_flags.0 { @@ -312,7 +368,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .context("failed to parse minor version")?, }; - let abi3 = is_abi3(); + let stable_abi = CPythonABI::from_build_env()?; let implementation = map["implementation"].parse()?; @@ -329,7 +385,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) default_lib_name_windows( version, implementation, - abi3, + stable_abi, map["mingw"].as_str() == "True", // This is the best heuristic currently available to detect debug build // on Windows from sysconfig - e.g. ext_suffix may be @@ -341,7 +397,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) default_lib_name_unix( version, implementation, - abi3, + stable_abi, cygwin, map.get("ld_version").map(String::as_str), gil_disabled, @@ -368,7 +424,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) version, implementation, shared, - abi3, + stable_abi, lib_name: Some(lib_name), lib_dir, executable: map.get("executable").cloned(), @@ -423,11 +479,11 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) None => false, }; let cygwin = soabi.ends_with("cygwin"); - let abi3 = is_abi3(); + let stable_abi = CPythonABI::from_build_env()?; let lib_name = Some(default_lib_name_unix( version, implementation, - abi3, + stable_abi, cygwin, sysconfigdata.get_value("LDVERSION"), gil_disabled, @@ -441,7 +497,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) implementation, version, shared: shared || framework, - abi3, + stable_abi, lib_dir, lib_name, executable: None, @@ -475,8 +531,9 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) // // TODO: abi3 is a property of the build mode, not the interpreter. Should this be // removed from `InterpreterConfig`? - config.abi3 |= is_abi3(); + config.stable_abi = CPythonABI::from_build_env()?; config.fixup_for_abi3_version(get_abi3_version())?; + config.fixup_for_abi3t_version(get_abi3t_version())?; Ok(config) }) @@ -518,7 +575,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let mut implementation = None; let mut version = None; let mut shared = None; - let mut abi3 = None; + let mut stable_abi = None; let mut lib_name = None; let mut lib_dir = None; let mut executable = None; @@ -543,7 +600,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) "implementation" => parse_value!(implementation, value), "version" => parse_value!(version, value), "shared" => parse_value!(shared, value), - "abi3" => parse_value!(abi3, value), + "stable_abi" => parse_value!(stable_abi, value), "lib_name" => parse_value!(lib_name, value), "lib_dir" => parse_value!(lib_dir, value), "executable" => parse_value!(executable, value), @@ -562,14 +619,14 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let version = version.ok_or("missing value for version")?; let implementation = implementation.unwrap_or(PythonImplementation::CPython); - let abi3 = abi3.unwrap_or(false); + let stable_abi = stable_abi.unwrap_or(CPythonABI::VersionSpecific); let build_flags = build_flags.unwrap_or_default(); Ok(InterpreterConfig { implementation, version, shared: shared.unwrap_or(true), - abi3, + stable_abi, lib_name, lib_dir, executable, @@ -592,7 +649,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) self.lib_name = Some(default_lib_name_for_target( self.version, self.implementation, - self.abi3, + self.stable_abi, self.is_free_threaded(), target, )); @@ -647,7 +704,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) write_line!(implementation)?; write_line!(version)?; write_line!(shared)?; - write_line!(abi3)?; + write_line!(stable_abi)?; write_option_line!(lib_name)?; write_option_line!(lib_dir)?; write_option_line!(executable)?; @@ -709,7 +766,31 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) return Ok(()); } - if let Some(version) = abi3_version { + self.fixup_for_stable_abi_version(abi3_version, is_abi3)?; + + Ok(()) + } + + /// Updates configured ABI to build for to the requested abi3t version + /// This is a no-op for platforms where abi3t is not supported + fn fixup_for_abi3t_version(&mut self, abi3t_version: Option) -> Result<()> { + // PyPy, GraalPy, and the free-threaded build don't support abi3; don't adjust the version + if self.implementation.is_pypy() || self.implementation.is_graalpy() { + return Ok(()); + } + + self.fixup_for_stable_abi_version(abi3t_version, is_abi3t)?; + + Ok(()) + } + + /// Core logic for pinning a Python stable ABI version to minimum and maximum supported versions + fn fixup_for_stable_abi_version( + &mut self, + abi_version: Option, + abi_check: impl Fn() -> bool, + ) -> Result<()> { + if let Some(version) = abi_version { ensure!( version <= self.version, "cannot set a minimum Python version {} higher than the interpreter version {} \ @@ -718,11 +799,10 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) self.version, version.minor, ); - self.version = version; - } else if is_abi3() && self.version.minor > ABI3_MAX_MINOR { - warn!("Automatically falling back to abi3-py3{ABI3_MAX_MINOR} because current Python is higher than the maximum supported"); - self.version.minor = ABI3_MAX_MINOR; + } else if abi_check() && self.version.minor > STABLE_ABI_MAX_MINOR { + warn!("Automatically falling back to abi3-py3{STABLE_ABI_MAX_MINOR} because current Python is higher than the maximum supported"); + self.version.minor = STABLE_ABI_MAX_MINOR; } Ok(()) @@ -849,15 +929,32 @@ fn is_abi3() -> bool { || env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY").is_some_and(|os_str| os_str == "1") } +/// Checks if `abi3t` or any of the `abi3t-py3*` features is enabled for the PyO3 crate. +/// +/// Must be called from a PyO3 crate build script. +fn is_abi3t() -> bool { + cargo_env_var("CARGO_FEATURE_ABI3T").is_some() + || env_var("PYO3_USE_ABI3T_FORWARD_COMPATIBILITY").is_some_and(|os_str| os_str == "1") +} + /// Gets the minimum supported Python version from PyO3 `abi3-py*` features. /// /// Must be called from a PyO3 crate build script. pub fn get_abi3_version() -> Option { - let minor_version = (MINIMUM_SUPPORTED_VERSION.minor..=ABI3_MAX_MINOR) + let minor_version = (MINIMUM_SUPPORTED_VERSION.minor..=STABLE_ABI_MAX_MINOR) .find(|i| cargo_env_var(&format!("CARGO_FEATURE_ABI3_PY3{i}")).is_some()); minor_version.map(|minor| PythonVersion { major: 3, minor }) } +/// Gets the minimum supported Python version from PyO3 `abi3t-py*` features. +/// +/// Must be called from a PyO3 crate build script. +pub fn get_abi3t_version() -> Option { + let minor_version = (MINIMUM_SUPPORTED_VERSION.minor..=STABLE_ABI_MAX_MINOR) + .find(|i| cargo_env_var(&format!("CARGO_FEATURE_ABI3T_PY3{i}")).is_some()); + minor_version.map(|minor| PythonVersion { major: 3, minor }) +} + /// Checks if the `extension-module` feature is enabled for the PyO3 crate. /// /// This can be triggered either by: @@ -1568,7 +1665,7 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result Result Result Result { // FIXME: PyPy & GraalPy do not support the Stable ABI. let implementation = PythonImplementation::CPython; - let abi3 = true; + let stable_abi = CPythonABI::ABI3; let lib_name = if host.operating_system == OperatingSystem::Windows { Some(default_lib_name_windows( version, implementation, - abi3, + stable_abi, false, false, false, @@ -1631,7 +1728,7 @@ fn default_abi3_config(host: &Triple, version: PythonVersion) -> Result String { if target.operating_system == OperatingSystem::Windows { - default_lib_name_windows(version, implementation, abi3, false, false, gil_disabled).unwrap() + default_lib_name_windows( + version, + implementation, + stable_abi, + false, + false, + gil_disabled, + ) + .unwrap() } else { default_lib_name_unix( version, implementation, - abi3, + stable_abi, target.operating_system == OperatingSystem::Cygwin, None, gil_disabled, @@ -1703,7 +1808,7 @@ fn default_lib_name_for_target( fn default_lib_name_windows( version: PythonVersion, implementation: PythonImplementation, - abi3: bool, + stable_abi: CPythonABI, mingw: bool, debug: bool, gil_disabled: bool, @@ -1717,11 +1822,15 @@ fn default_lib_name_windows( // CPython bug: linking against python3_d.dll raises error // https://github.com/python/cpython/issues/101614 Ok(format!("python{}{}_d", version.major, version.minor)) - } else if abi3 && !(gil_disabled || implementation.is_pypy() || implementation.is_graalpy()) { + } else if (stable_abi == CPythonABI::ABI3 + && !(gil_disabled || implementation.is_pypy() || implementation.is_graalpy())) + || (stable_abi == CPythonABI::ABI3t + && !(implementation.is_pypy() || implementation.is_graalpy())) + { if debug { - Ok(WINDOWS_ABI3_DEBUG_LIB_NAME.to_owned()) + Ok(WINDOWS_STABLE_ABI_DEBUG_LIB_NAME.to_owned()) } else { - Ok(WINDOWS_ABI3_LIB_NAME.to_owned()) + Ok(WINDOWS_STABLE_ABI_LIB_NAME.to_owned()) } } else if mingw { ensure!( @@ -1747,7 +1856,7 @@ fn default_lib_name_windows( fn default_lib_name_unix( version: PythonVersion, implementation: PythonImplementation, - abi3: bool, + stable_abi: CPythonABI, cygwin: bool, ld_version: Option<&str>, gil_disabled: bool, @@ -1756,7 +1865,7 @@ fn default_lib_name_unix( PythonImplementation::CPython => match ld_version { Some(ld_version) => Ok(format!("python{ld_version}")), None => { - if cygwin && abi3 { + if cygwin && !matches!(stable_abi, CPythonABI::VersionSpecific) { Ok("python3".to_string()) } else if version > PythonVersion::PY37 { // PEP 3149 ABI version tags are finally gone @@ -1893,11 +2002,15 @@ pub fn find_interpreter() -> Result { /// Locates and extracts the build host Python interpreter configuration. /// /// Lowers the configured Python version to `abi3_version` if required. -fn get_host_interpreter(abi3_version: Option) -> Result { +fn get_host_interpreter( + abi3_version: Option, + abi3t_version: Option, +) -> Result { let interpreter_path = find_interpreter()?; let mut interpreter_config = InterpreterConfig::from_interpreter(interpreter_path)?; interpreter_config.fixup_for_abi3_version(abi3_version)?; + interpreter_config.fixup_for_abi3t_version(abi3t_version)?; Ok(interpreter_config) } @@ -1925,26 +2038,33 @@ pub fn make_cross_compile_config() -> Result> { pub fn make_interpreter_config() -> Result { let host = Triple::host(); let abi3_version = get_abi3_version(); + let abi3t_version = get_abi3t_version(); + + ensure!( + !(abi3_version.is_some() && abi3t_version.is_some()), + "Cannot enable abi3 and abit3t features" + ); // See if we can safely skip the Python interpreter configuration detection. - // Unix "abi3" extension modules can usually be built without any interpreter. - let need_interpreter = abi3_version.is_none() || require_libdir_for_target(&host); + // Unix stable ABI extension modules can usually be built without any interpreter. + let need_interpreter = + (abi3_version.is_none() && abi3t_version.is_none()) || require_libdir_for_target(&host); if have_python_interpreter() { - match get_host_interpreter(abi3_version) { + match get_host_interpreter(abi3_version, abi3t_version) { Ok(interpreter_config) => return Ok(interpreter_config), // Bail if the interpreter configuration is required to build. Err(e) if need_interpreter => return Err(e), _ => { - // Fall back to the "abi3" defaults just as if `PYO3_NO_PYTHON` + // Fall back to the stable ABI just as if `PYO3_NO_PYTHON` // environment variable was set. warn!("Compiling without a working Python interpreter."); } } } else { ensure!( - abi3_version.is_some(), - "An abi3-py3* feature must be specified when compiling without a Python interpreter." + abi3_version.is_some() || abi3t_version.is_some(), + "An abi3-py3* or abi3t-py3* feature must be specified when compiling without a Python interpreter." ); }; @@ -1995,7 +2115,7 @@ mod tests { #[test] fn test_config_file_roundtrip() { let config = InterpreterConfig { - abi3: true, + stable_abi: CPythonABI::ABI3, build_flags: BuildFlags::default(), pointer_width: Some(32), executable: Some("executable".into()), @@ -2016,7 +2136,7 @@ mod tests { // And some different options, for variety let config = InterpreterConfig { - abi3: false, + stable_abi: CPythonABI::VersionSpecific, build_flags: { let mut flags = HashSet::new(); flags.insert(BuildFlag::Py_DEBUG); @@ -2046,7 +2166,7 @@ mod tests { #[test] fn test_config_file_roundtrip_with_escaping() { let config = InterpreterConfig { - abi3: true, + stable_abi: CPythonABI::VersionSpecific, build_flags: BuildFlags::default(), pointer_width: Some(32), executable: Some("executable".into()), @@ -2076,7 +2196,7 @@ mod tests { version: PythonVersion { major: 3, minor: 7 }, implementation: PythonImplementation::CPython, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: None, lib_dir: None, executable: None, @@ -2099,7 +2219,7 @@ mod tests { version: PythonVersion { major: 3, minor: 7 }, implementation: PythonImplementation::CPython, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: None, lib_dir: None, executable: None, @@ -2199,7 +2319,7 @@ mod tests { assert_eq!( InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), InterpreterConfig { - abi3: false, + stable_abi: CPythonABI::VersionSpecific, build_flags: BuildFlags::from_sysconfigdata(&sysconfigdata), pointer_width: Some(64), executable: None, @@ -2229,7 +2349,7 @@ mod tests { assert_eq!( InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), InterpreterConfig { - abi3: false, + stable_abi: CPythonABI::VersionSpecific, build_flags: BuildFlags::from_sysconfigdata(&sysconfigdata), pointer_width: Some(64), executable: None, @@ -2256,7 +2376,7 @@ mod tests { assert_eq!( InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), InterpreterConfig { - abi3: false, + stable_abi: CPythonABI::VersionSpecific, build_flags: BuildFlags::from_sysconfigdata(&sysconfigdata), pointer_width: Some(64), executable: None, @@ -2283,7 +2403,7 @@ mod tests { implementation: PythonImplementation::CPython, version: PythonVersion { major: 3, minor: 7 }, shared: true, - abi3: true, + stable_abi: CPythonABI::ABI3, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -2307,7 +2427,7 @@ mod tests { implementation: PythonImplementation::CPython, version: PythonVersion { major: 3, minor: 9 }, shared: true, - abi3: true, + stable_abi: CPythonABI::ABI3, lib_name: None, lib_dir: None, executable: None, @@ -2342,7 +2462,7 @@ mod tests { implementation: PythonImplementation::CPython, version: PythonVersion { major: 3, minor: 7 }, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python37".into()), lib_dir: Some("C:\\some\\path".into()), executable: None, @@ -2377,7 +2497,7 @@ mod tests { implementation: PythonImplementation::CPython, version: PythonVersion { major: 3, minor: 8 }, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python38".into()), lib_dir: Some("/usr/lib/mingw".into()), executable: None, @@ -2412,7 +2532,7 @@ mod tests { implementation: PythonImplementation::CPython, version: PythonVersion { major: 3, minor: 9 }, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python3.9".into()), lib_dir: Some("/usr/arm64/lib".into()), executable: None, @@ -2449,7 +2569,7 @@ mod tests { minor: 11 }, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: Some("pypy3.11-c".into()), lib_dir: None, executable: None, @@ -2464,12 +2584,13 @@ mod tests { #[test] fn default_lib_name_windows() { + use CPythonABI::*; use PythonImplementation::*; assert_eq!( super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, CPython, - false, + VersionSpecific, false, false, false, @@ -2480,7 +2601,7 @@ mod tests { assert!(super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, CPython, - false, + VersionSpecific, false, false, true, @@ -2490,7 +2611,7 @@ mod tests { super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, CPython, - true, + ABI3, false, false, false, @@ -2502,7 +2623,7 @@ mod tests { super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, CPython, - false, + VersionSpecific, true, false, false, @@ -2514,7 +2635,7 @@ mod tests { super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, CPython, - true, + ABI3, true, false, false, @@ -2526,7 +2647,7 @@ mod tests { super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, PyPy, - true, + ABI3, false, false, false, @@ -2541,7 +2662,7 @@ mod tests { minor: 11 }, PyPy, - false, + ABI3, false, false, false, @@ -2553,7 +2674,7 @@ mod tests { super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, CPython, - false, + ABI3, false, true, false, @@ -2567,7 +2688,7 @@ mod tests { super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, CPython, - true, + ABI3, false, true, false, @@ -2582,7 +2703,7 @@ mod tests { minor: 10 }, CPython, - true, + ABI3, false, true, false, @@ -2597,7 +2718,7 @@ mod tests { minor: 12, }, CPython, - false, + VersionSpecific, false, false, true, @@ -2610,7 +2731,7 @@ mod tests { minor: 12, }, CPython, - false, + VersionSpecific, true, false, true, @@ -2623,7 +2744,7 @@ mod tests { minor: 13 }, CPython, - false, + VersionSpecific, false, false, true, @@ -2638,7 +2759,7 @@ mod tests { minor: 13 }, CPython, - true, // abi3 true should not affect the free-threaded lib name + ABI3, // abi3 true should not affect the free-threaded lib name false, false, true, @@ -2653,7 +2774,7 @@ mod tests { minor: 13 }, CPython, - false, + VersionSpecific, false, true, true, @@ -2665,13 +2786,14 @@ mod tests { #[test] fn default_lib_name_unix() { + use CPythonABI::*; use PythonImplementation::*; // Defaults to python3.7m for CPython 3.7 assert_eq!( super::default_lib_name_unix( PythonVersion { major: 3, minor: 7 }, CPython, - false, + VersionSpecific, false, None, false @@ -2684,7 +2806,7 @@ mod tests { super::default_lib_name_unix( PythonVersion { major: 3, minor: 8 }, CPython, - false, + VersionSpecific, false, None, false @@ -2696,7 +2818,7 @@ mod tests { super::default_lib_name_unix( PythonVersion { major: 3, minor: 9 }, CPython, - false, + VersionSpecific, false, None, false @@ -2709,7 +2831,7 @@ mod tests { super::default_lib_name_unix( PythonVersion { major: 3, minor: 9 }, CPython, - false, + VersionSpecific, false, Some("3.7md"), false @@ -2726,7 +2848,7 @@ mod tests { minor: 11 }, PyPy, - false, + VersionSpecific, false, None, false @@ -2739,7 +2861,7 @@ mod tests { super::default_lib_name_unix( PythonVersion { major: 3, minor: 9 }, PyPy, - false, + VersionSpecific, false, Some("3.11d"), false @@ -2756,7 +2878,7 @@ mod tests { minor: 13 }, CPython, - false, + VersionSpecific, false, None, true @@ -2771,7 +2893,7 @@ mod tests { minor: 12, }, CPython, - false, + VersionSpecific, false, None, true, @@ -2785,7 +2907,7 @@ mod tests { minor: 13 }, CPython, - true, + ABI3, true, None, false @@ -2849,7 +2971,7 @@ mod tests { #[test] fn interpreter_version_reduced_to_abi3() { let mut config = InterpreterConfig { - abi3: true, + stable_abi: CPythonABI::ABI3, build_flags: BuildFlags::default(), pointer_width: None, executable: None, @@ -2872,7 +2994,7 @@ mod tests { #[test] fn abi3_version_cannot_be_higher_than_interpreter() { let mut config = InterpreterConfig { - abi3: true, + stable_abi: CPythonABI::ABI3, build_flags: BuildFlags::new(), pointer_width: None, executable: None, @@ -2937,7 +3059,7 @@ mod tests { assert_eq!( parsed_config, InterpreterConfig { - abi3: false, + stable_abi: CPythonABI::VersionSpecific, build_flags: BuildFlags(interpreter_config.build_flags.0.clone()), pointer_width: Some(64), executable: None, @@ -3075,7 +3197,7 @@ mod tests { minor: 11, }, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -3119,7 +3241,7 @@ mod tests { implementation: PythonImplementation::CPython, version: PythonVersion { major: 3, minor: 9 }, shared: true, - abi3: true, + stable_abi: CPythonABI::ABI3, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -3176,7 +3298,6 @@ mod tests { "cargo:rustc-cfg=Py_3_14".to_owned(), "cargo:rustc-cfg=Py_3_15".to_owned(), "cargo:rustc-cfg=Py_LIMITED_API".to_owned(), - "cargo:rustc-cfg=_Py_OPAQUE_PYOBJECT".to_owned(), ] ); } @@ -3192,7 +3313,7 @@ mod tests { minor: 13, }, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -3226,7 +3347,7 @@ mod tests { implementation: PythonImplementation::CPython, version: PythonVersion { major: 3, minor: 7 }, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -3281,7 +3402,7 @@ mod tests { implementation: PythonImplementation::CPython, version: PythonVersion { major: 3, minor: 9 }, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: None, lib_dir: None, executable: None, @@ -3344,7 +3465,7 @@ mod tests { config.build_flags.0.remove(&BuildFlag::Py_GIL_DISABLED); // abi3 - config.abi3 = true; + config.stable_abi = CPythonABI::ABI3; config.lib_name = None; config.apply_default_lib_name_to_config_file(&unix); assert_eq!(config.lib_name, Some("python3.13".into())); diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index e1960740510..27510c2d4ae 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -34,7 +34,7 @@ use target_lexicon::OperatingSystem; /// | Flag | Description | /// | ---- | ----------- | /// | `#[cfg(Py_3_7)]`, `#[cfg(Py_3_8)]`, `#[cfg(Py_3_9)]`, `#[cfg(Py_3_10)]` | These attributes mark code only for a given Python version and up. For example, `#[cfg(Py_3_7)]` marks code which can run on Python 3.7 **and newer**. | -/// | `#[cfg(Py_LIMITED_API)]` | This marks code which is run when compiling with PyO3's `abi3` feature enabled. | +/// | `#[cfg(Py_LIMITED_API)]` | This marks code which is run when compiling with PyO3's `abi3` or `abi3t` features enabled. | /// | `#[cfg(Py_GIL_DISABLED)]` | This marks code which is run on the free-threaded interpreter. | /// | `#[cfg(PyPy)]` | This marks code which is run when compiling for PyPy. | /// | `#[cfg(GraalPy)]` | This marks code which is run when compiling for GraalPy. | @@ -273,13 +273,13 @@ pub fn print_expected_cfgs() { // allow `Py_3_*` cfgs from the minimum supported version up to the // maximum minor version (+1 for development for the next) - for i in impl_::MINIMUM_SUPPORTED_VERSION.minor..=impl_::ABI3_MAX_MINOR + 1 { + for i in impl_::MINIMUM_SUPPORTED_VERSION.minor..=impl_::STABLE_ABI_MAX_MINOR + 1 { println!("cargo:rustc-check-cfg=cfg(Py_3_{i})"); } // pyo3_dll cfg for raw-dylib linking on Windows let mut dll_names = vec!["python3".to_string(), "python3_d".to_string()]; - for i in impl_::MINIMUM_SUPPORTED_VERSION.minor..=impl_::ABI3_MAX_MINOR + 1 { + for i in impl_::MINIMUM_SUPPORTED_VERSION.minor..=impl_::STABLE_ABI_MAX_MINOR + 1 { dll_names.push(format!("python3{i}")); dll_names.push(format!("python3{i}_d")); if i >= 13 { @@ -316,7 +316,7 @@ pub mod pyo3_build_script_impl { } pub use crate::impl_::{ cargo_env_var, env_var, is_linking_libpython_for_target, make_cross_compile_config, - target_triple_from_env, InterpreterConfig, PythonVersion, + target_triple_from_env, CPythonABI, InterpreterConfig, PythonVersion, }; pub enum BuildConfigSource { /// Config was provided by `PYO3_CONFIG_FILE`. @@ -495,7 +495,7 @@ mod tests { minor: 13, }, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: None, lib_dir: None, executable: None, @@ -538,7 +538,7 @@ mod tests { minor: 13, }, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: None, lib_dir: None, executable: None, diff --git a/pyo3-ffi/Cargo.toml b/pyo3-ffi/Cargo.toml index 86153331a75..3d4b183ee35 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -36,6 +36,12 @@ abi3-py313 = ["abi3-py314", "pyo3-build-config/abi3-py313"] abi3-py314 = ["abi3-py315", "pyo3-build-config/abi3-py314"] abi3-py315 = ["abi3", "pyo3-build-config/abi3-py315"] +# Use the free-threaded limited API. See https://www.python.org/dev/peps/pep-803/ for more. +abi3t = ["pyo3-build-config/abi3t"] + +# With abi3t, we can manually set the minimum Python version. +abi3t-py315 = ["abi3t", "pyo3-build-config/abi3t-py315"] + # deprecated: no longer needed, raw-dylib is used instead generate-import-lib = ["pyo3-build-config/generate-import-lib"] diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 5e0254a448d..da85c3fa748 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -2,7 +2,7 @@ use pyo3_build_config::{ bail, ensure, print_feature_cfgs, pyo3_build_script_impl::{ cargo_env_var, env_var, errors::Result, is_linking_libpython_for_target, - resolve_build_config, target_triple_from_env, BuildConfig, BuildConfigSource, + resolve_build_config, target_triple_from_env, BuildConfig, BuildConfigSource, CPythonABI, InterpreterConfig, MaximumVersionExceeded, PythonVersion, }, warn, PythonImplementation, @@ -67,10 +67,12 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { ); } else if interpreter_config.version > v_plus_1 { let mut error = MaximumVersionExceeded::new(interpreter_config, versions.max); - if interpreter_config.is_free_threaded() { - error.add_help( - "the free-threaded build of CPython does not support the limited API so this check cannot be suppressed.", - ); + let major = interpreter_config.version.major; + let minor = interpreter_config.version.minor; + if interpreter_config.is_free_threaded() && interpreter_config.version.minor >= 15 { + error.add_help(&format!( + "the free-threaded build of CPython {major}{minor} does not support the limited API so this check cannot be suppressed.", + )); return Err(error.finish().into()); } @@ -124,12 +126,12 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { } } - if interpreter_config.abi3 { + if let CPythonABI::ABI3 = interpreter_config.stable_abi { match interpreter_config.implementation { PythonImplementation::CPython => { if interpreter_config.is_free_threaded() && interpreter_config.version.minor < 15 { warn!( - "The free-threaded build of CPython does not yet support abi3 so the build artifacts will be version-specific." + "The free-threaded build of CPython does not support abi3 so the build artifacts will be version-specific. Did you mean to enable the abi3t feature?" ) } } From 3cf60b1d819a0cd604f3845e24c13020a8f6d10d Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 26 Mar 2026 08:32:15 -0600 Subject: [PATCH 36/74] Add missing error handling for `PyModule_FromSlotsAndSpec` --- src/impl_/pymodule.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 64d30101108..8665c86ee3c 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -223,11 +223,14 @@ impl ModuleDef { self.module .get_or_try_init(py, || { 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 { + let module = unsafe { + ffi::PyModule_FromSlotsAndSpec(slots, spec.as_ptr()) + .assume_owned_or_err(py)? + } + .cast_into()?; + if unsafe { ffi::PyModule_SetDocString(module.as_ptr(), 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)); } From 449eb8aa80bfc32017568a56f40fdffc66df5b43 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 26 Mar 2026 09:08:48 -0600 Subject: [PATCH 37/74] passes cargo tests with the abi3t feature enabled --- tests/test_compile_error.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 423f0dd5cea..b93338dba7c 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -78,8 +78,13 @@ fn test_compile_errors() { t.pass("tests/ui/pymodule_missing_docs.rs"); #[cfg(not(any(Py_LIMITED_API, feature = "experimental-inspect")))] t.pass("tests/ui/forbid_unsafe.rs"); - #[cfg(all(Py_LIMITED_API, not(feature = "experimental-async")))] + #[cfg(all( + Py_LIMITED_API, + not(feature = "experimental-async"), + not(_Py_OPAQUE_PYOBJECT) + ))] // output changes with async feature + // opaque PyObject builds can inherit from builtins t.compile_fail("tests/ui/abi3_inheritance.rs"); #[cfg(all(Py_LIMITED_API, not(Py_3_9)))] t.compile_fail("tests/ui/abi3_weakref.rs"); From 5371fd8f153574f0454067d525816718463dc9d1 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 26 Mar 2026 09:35:36 -0600 Subject: [PATCH 38/74] passes unit tests on GIL-enabled build with abi3t feature --- pyo3-ffi/src/modsupport.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyo3-ffi/src/modsupport.rs b/pyo3-ffi/src/modsupport.rs index 4697be5bb5b..b7e9ebddd47 100644 --- a/pyo3-ffi/src/modsupport.rs +++ b/pyo3-ffi/src/modsupport.rs @@ -157,9 +157,11 @@ const _PyABIInfo_DEFAULT_FLAG_STABLE: u16 = 0; // skipped PyABIInfo_DEFAULT_ABI_VERSION: depends on Py_VERSION_HEX -#[cfg(all(Py_3_15, Py_GIL_DISABLED))] +#[cfg(all(Py_3_15, Py_LIMITED_API, _Py_OPAQUE_PYOBJECT))] +const _PyABIInfo_DEFAULT_FLAG_FT: u16 = PyABIInfo_FREETHREADING_AGNOSTIC; +#[cfg(all(Py_3_15, Py_GIL_DISABLED, not(Py_LIMITED_API)))] const _PyABIInfo_DEFAULT_FLAG_FT: u16 = PyABIInfo_FREETHREADED; -#[cfg(all(Py_3_15, not(Py_GIL_DISABLED)))] +#[cfg(all(Py_3_15, not(Py_GIL_DISABLED), not(Py_LIMITED_API)))] const _PyABIInfo_DEFAULT_FLAG_FT: u16 = PyABIInfo_GIL; #[cfg(Py_3_15)] From 19d78d257797023e98f5f6e8733f39936d24a792 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 26 Mar 2026 09:36:48 -0600 Subject: [PATCH 39/74] fix merge mistake --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 8b6254deb99..a4ec99577bd 100644 --- a/noxfile.py +++ b/noxfile.py @@ -134,7 +134,7 @@ def test_rust(session: nox.Session): feature_set and "abi3" in feature_set and "full" in feature_set - and sys.version_info >= (3, 8) + and sys.version_info >= (3, 9) ): # run abi3-py38 tests to check abi3 forward compatibility _run_cargo_test( From 14bf4d121f8842e68cee2cec893bb782d3d4f3f9 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 27 Mar 2026 10:57:53 -0600 Subject: [PATCH 40/74] rustfmt --- pyo3-ffi/src/structmember.rs | 6 +++--- src/pyclass/create_type_object.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyo3-ffi/src/structmember.rs b/pyo3-ffi/src/structmember.rs index 2d9dd5a4a1f..4aa6c24db4f 100644 --- a/pyo3-ffi/src/structmember.rs +++ b/pyo3-ffi/src/structmember.rs @@ -2,6 +2,8 @@ use std::ffi::c_int; pub use crate::PyMemberDef; +#[allow(deprecated)] +pub use crate::_Py_T_OBJECT as T_OBJECT; pub use crate::Py_T_BOOL as T_BOOL; pub use crate::Py_T_BYTE as T_BYTE; pub use crate::Py_T_CHAR as T_CHAR; @@ -19,12 +21,10 @@ pub use crate::Py_T_UINT as T_UINT; pub use crate::Py_T_ULONG as T_ULONG; pub use crate::Py_T_ULONGLONG as T_ULONGLONG; pub use crate::Py_T_USHORT as T_USHORT; -#[allow(deprecated)] -pub use crate::_Py_T_OBJECT as T_OBJECT; -pub use crate::Py_T_PYSSIZET as T_PYSSIZET; #[allow(deprecated)] pub use crate::_Py_T_NONE as T_NONE; +pub use crate::Py_T_PYSSIZET as T_PYSSIZET; /* Flags */ pub use crate::Py_READONLY as READONLY; diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index 9c72a9b4d1a..063b615c8a4 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -11,7 +11,7 @@ use crate::{ assign_sequence_item_from_mapping, get_sequence_item_from_mapping, tp_dealloc, tp_dealloc_with_gc, PyClassImpl, PyClassItemsIter, PyObjectOffset, }, - pymethods::{Getter, PyGetterDef, PyMethodDefType, PySetterDef, Setter, _call_clear}, + pymethods::{_call_clear, Getter, PyGetterDef, PyMethodDefType, PySetterDef, Setter}, trampoline::trampoline, }, pycell::impl_::PyClassObjectLayout, From 91a041953f3f0f219ffb717e321dac56cc56b2b4 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 27 Mar 2026 11:57:09 -0600 Subject: [PATCH 41/74] fix FIXME --- pyo3-build-config/src/impl_.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 864e861d347..37e626a7b94 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -157,7 +157,7 @@ pub struct InterpreterConfig { /// Whether linking against the stable/limited Python 3 API. /// - /// FIXME: serialization? + /// Serialized to `stable_abi`. pub stable_abi: CPythonABI, /// The name of the link library defining Python. From 9f237205932a823022091cfa90d541b3c0dedd2b Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 3 Apr 2026 11:05:21 -0600 Subject: [PATCH 42/74] replace extern "C" with extern_libpython! --- pyo3-ffi/src/object.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index 3b81667afaa..6713dbc5499 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -226,7 +226,7 @@ extern_libpython! { #[cfg_attr(windows, link(name = "pythonXY"))] #[cfg(all(Py_LIMITED_API, Py_3_15))] -extern "C" { +extern_libpython! { #[cfg_attr(PyPy, link_name = "PyPy_SIZE")] pub fn Py_SIZE(ob: *mut PyObject) -> Py_ssize_t; #[cfg_attr(PyPy, link_name = "PyPy_IS_TYPE")] From d38c274553398da908953eb5d01cd5c72b057b1a Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 3 Apr 2026 11:38:37 -0600 Subject: [PATCH 43/74] fix test --- pyo3-build-config/src/impl_.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index de01cd32882..08308c15111 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -3143,7 +3143,6 @@ mod tests { assert_eq!( interpreter_config.build_script_outputs(), [ - "cargo:rustc-cfg=Py_3_7".to_owned(), "cargo:rustc-cfg=Py_3_8".to_owned(), "cargo:rustc-cfg=Py_3_9".to_owned(), "cargo:rustc-cfg=Py_3_10".to_owned(), From 15ff21cf636d6e5dab5c3616ad3fa93ca17e59f1 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 6 Apr 2026 09:49:39 -0600 Subject: [PATCH 44/74] fix merge error --- noxfile.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/noxfile.py b/noxfile.py index a1012582027..2995370965a 100644 --- a/noxfile.py +++ b/noxfile.py @@ -152,9 +152,7 @@ def test_rust(session: nox.Session): features=feature_set.replace("abi3t", "abi3t-py315"), extra_flags=flags, ) - - - + @nox.session(name="test-py", venv_backend="none") def test_py(session: nox.Session) -> None: From 930e1a97a878722bb937e36ca3c7c72c2e1745c1 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 6 Apr 2026 11:46:28 -0600 Subject: [PATCH 45/74] Allow abi3t builds without critical section bindings --- Cargo.toml | 2 +- pyo3-ffi/src/cpython/critical_section.rs | 26 ++++++- pyo3-ffi/src/cpython/dictobject.rs | 1 - pyo3-ffi/src/cpython/mod.rs | 4 +- pyo3-ffi/src/critical_section.rs | 41 ----------- pyo3-ffi/src/dictobject.rs | 7 ++ pyo3-ffi/src/lib.rs | 3 - src/conversions/std/num.rs | 30 ++++++-- src/pybacked.rs | 90 ++++++++++++++++-------- src/sync.rs | 11 ++- src/sync/critical_section.rs | 32 ++++++--- src/types/bytearray.rs | 12 ++++ src/types/code.rs | 51 ++++++++------ src/types/dict.rs | 12 +++- src/types/list.rs | 7 ++ 15 files changed, 207 insertions(+), 122 deletions(-) delete mode 100644 pyo3-ffi/src/critical_section.rs diff --git a/Cargo.toml b/Cargo.toml index 362d3b552f6..d5ce7e1afc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,7 +85,7 @@ parking_lot = { version = "0.12.3", features = ["arc_lock"] } pyo3-build-config = { path = "pyo3-build-config", version = "=0.28.3", features = ["resolve-config"] } [features] -default = ["macros"] +default = ["macros", "abi3t"] # Enables support for `async fn` for `#[pyfunction]` and `#[pymethods]`. experimental-async = ["macros", "pyo3-macros/experimental-async"] diff --git a/pyo3-ffi/src/cpython/critical_section.rs b/pyo3-ffi/src/cpython/critical_section.rs index 670d45ae914..5db205b9840 100644 --- a/pyo3-ffi/src/cpython/critical_section.rs +++ b/pyo3-ffi/src/cpython/critical_section.rs @@ -1,14 +1,38 @@ #[cfg(any(Py_3_14, Py_GIL_DISABLED))] use crate::PyMutex; -use crate::{PyCriticalSection, PyCriticalSection2}; +use crate::PyObject; + +#[repr(C)] +#[cfg(Py_GIL_DISABLED)] +pub struct PyCriticalSection { + _cs_prev: usize, + _cs_mutex: *mut PyMutex, +} + +#[repr(C)] +#[cfg(Py_GIL_DISABLED)] +pub struct PyCriticalSection2 { + _cs_base: PyCriticalSection, + _cs_mutex2: *mut PyMutex, +} + +#[cfg(not(Py_GIL_DISABLED))] +opaque_struct!(pub PyCriticalSection); + +#[cfg(not(Py_GIL_DISABLED))] +opaque_struct!(pub PyCriticalSection2); extern_libpython! { + pub fn PyCriticalSection_Begin(c: *mut PyCriticalSection, op: *mut PyObject); #[cfg(Py_3_14)] pub fn PyCriticalSection_BeginMutex(c: *mut PyCriticalSection, m: *mut PyMutex); + pub fn PyCriticalSection_End(c: *mut PyCriticalSection); + pub fn PyCriticalSection2_Begin(c: *mut PyCriticalSection2, a: *mut PyObject, b: *mut PyObject); #[cfg(Py_3_14)] pub fn PyCriticalSection2_BeginMutex( c: *mut PyCriticalSection2, m1: *mut PyMutex, m2: *mut PyMutex, ); + pub fn PyCriticalSection2_End(c: *mut PyCriticalSection2); } diff --git a/pyo3-ffi/src/cpython/dictobject.rs b/pyo3-ffi/src/cpython/dictobject.rs index 37991ee4ebe..4c93f068e2b 100644 --- a/pyo3-ffi/src/cpython/dictobject.rs +++ b/pyo3-ffi/src/cpython/dictobject.rs @@ -43,7 +43,6 @@ pub struct PyDictObject { // skipped private _PyDict_GetItemStringWithError // skipped PyDict_SetDefault -// skipped PyDict_SetDefaultRef // skipped PyDict_GET_SIZE // skipped PyDict_ContainsString diff --git a/pyo3-ffi/src/cpython/mod.rs b/pyo3-ffi/src/cpython/mod.rs index 0f15010cf6e..6e73d3bab64 100644 --- a/pyo3-ffi/src/cpython/mod.rs +++ b/pyo3-ffi/src/cpython/mod.rs @@ -51,8 +51,8 @@ pub use self::ceval::*; pub use self::code::*; pub use self::compile::*; pub use self::complexobject::*; -#[cfg(Py_3_14)] -pub use self::critical_section::{PyCriticalSection2_BeginMutex, PyCriticalSection_BeginMutex}; +#[cfg(Py_3_13)] +pub use self::critical_section::*; pub use self::descrobject::*; pub use self::dictobject::*; pub use self::floatobject::*; diff --git a/pyo3-ffi/src/critical_section.rs b/pyo3-ffi/src/critical_section.rs deleted file mode 100644 index 513d5b06ffb..00000000000 --- a/pyo3-ffi/src/critical_section.rs +++ /dev/null @@ -1,41 +0,0 @@ -#[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API)))] -use crate::PyMutex; -#[cfg(any(all(Py_GIL_DISABLED, Py_3_13), all(Py_LIMITED_API, Py_3_15)))] -use crate::PyObject; - -#[cfg(all(Py_3_15, Py_LIMITED_API))] -opaque_struct!(pub PyMutex); - -#[cfg(any( - all(Py_GIL_DISABLED, Py_3_13, not(Py_LIMITED_API)), - all(Py_GIL_DISABLED, Py_3_15, Py_LIMITED_API) -))] -#[repr(C)] -pub struct PyCriticalSection { - _cs_prev: usize, - _cs_mutex: *mut PyMutex, -} - -#[cfg(any( - all(Py_GIL_DISABLED, Py_3_13, not(Py_LIMITED_API)), - all(Py_GIL_DISABLED, Py_3_15, Py_LIMITED_API) -))] -#[repr(C)] -pub struct PyCriticalSection2 { - _cs_base: PyCriticalSection, - _cs_mutex2: *mut PyMutex, -} - -#[cfg(all(not(Py_GIL_DISABLED), Py_3_15, Py_LIMITED_API))] -opaque_struct!(pub PyCriticalSection); - -#[cfg(all(not(Py_GIL_DISABLED), Py_3_15, Py_LIMITED_API))] -opaque_struct!(pub PyCriticalSection2); - -#[cfg(any(all(Py_GIL_DISABLED, Py_3_13), all(Py_LIMITED_API, Py_3_15)))] -extern "C" { - pub fn PyCriticalSection_Begin(c: *mut PyCriticalSection, op: *mut PyObject); - pub fn PyCriticalSection_End(c: *mut PyCriticalSection); - pub fn PyCriticalSection2_Begin(c: *mut PyCriticalSection2, a: *mut PyObject, b: *mut PyObject); - pub fn PyCriticalSection2_End(c: *mut PyCriticalSection2); -} diff --git a/pyo3-ffi/src/dictobject.rs b/pyo3-ffi/src/dictobject.rs index 4afa2ffb5e8..c449132fa98 100644 --- a/pyo3-ffi/src/dictobject.rs +++ b/pyo3-ffi/src/dictobject.rs @@ -78,6 +78,13 @@ extern_libpython! { key: *const c_char, result: *mut *mut PyObject, ) -> c_int; + #[cfg(any(Py_3_13, all(Py_LIMITED_API, Py_3_15)))] + pub fn PyDict_SetDefaultRef( + mp: *mut PyObject, + key: *mut PyObject, + default_value: *mut PyObject, + result: *mut *mut PyObject, + ) -> c_int; // skipped 3.10 / ex-non-limited PyObject_GenericGetDict } diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index 1b739efcecc..df72ac7b23e 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -439,7 +439,6 @@ pub use self::compile::*; pub use self::complexobject::*; #[cfg(not(Py_LIMITED_API))] pub use self::context::*; -pub use self::critical_section::*; #[cfg(not(Py_LIMITED_API))] pub use self::datetime::*; pub use self::descrobject::*; @@ -505,8 +504,6 @@ mod compile; mod complexobject; #[cfg(not(Py_LIMITED_API))] mod context; -#[cfg(Py_3_15)] -mod critical_section; #[cfg(not(Py_LIMITED_API))] pub(crate) mod datetime; mod descrobject; diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index fb122c279ef..ec7701b8f63 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -6,7 +6,9 @@ use crate::inspect::PyStaticExpr; use crate::py_result_ext::PyResultExt; #[cfg(feature = "experimental-inspect")] use crate::type_object::PyTypeInfo; -use crate::types::{PyByteArray, PyByteArrayMethods, PyBytes, PyInt}; +#[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] +use crate::types::{PyByteArray, PyByteArrayMethods}; +use crate::types::{PyBytes, PyInt}; use crate::{exceptions, ffi, Borrowed, Bound, FromPyObject, PyAny, PyErr, PyResult, Python}; use std::convert::Infallible; use std::ffi::c_long; @@ -255,18 +257,30 @@ impl<'py> FromPyObject<'_, 'py> for u8 { obj: Borrowed<'_, 'py, PyAny>, _: crate::conversion::private::Token, ) -> Option> { - if let Ok(bytes) = obj.cast::() { - Some(BytesSequenceExtractor::Bytes(bytes)) - } else if let Ok(byte_array) = obj.cast::() { - Some(BytesSequenceExtractor::ByteArray(byte_array)) - } else { - None + #[cfg(all(Py_GIL_DISABLED, Py_LIMITED_API))] + { + if let Ok(bytes) = obj.cast::() { + Some(BytesSequenceExtractor::Bytes(bytes)) + } else { + None + } + } + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + { + if let Ok(bytes) = obj.cast::() { + Some(BytesSequenceExtractor::Bytes(bytes)) + } else if let Ok(byte_array) = obj.cast::() { + Some(BytesSequenceExtractor::ByteArray(byte_array)); + } else { + None + } } } } pub(crate) enum BytesSequenceExtractor<'a, 'py> { Bytes(Borrowed<'a, 'py, PyBytes>), + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] ByteArray(Borrowed<'a, 'py, PyByteArray>), } @@ -285,6 +299,7 @@ impl BytesSequenceExtractor<'_, '_> { match self { BytesSequenceExtractor::Bytes(b) => copy_slice(b.as_bytes()), + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] BytesSequenceExtractor::ByteArray(b) => { crate::sync::critical_section::with_critical_section(b, || { // Safety: b is protected by a critical section @@ -301,6 +316,7 @@ impl FromPyObjectSequence for BytesSequenceExtractor<'_, '_> { fn to_vec(&self) -> Vec { match self { BytesSequenceExtractor::Bytes(b) => b.as_bytes().to_vec(), + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] BytesSequenceExtractor::ByteArray(b) => b.to_vec(), } } diff --git a/src/pybacked.rs b/src/pybacked.rs index d7feaa2bfa2..91000065cf9 100644 --- a/src/pybacked.rs +++ b/src/pybacked.rs @@ -4,14 +4,15 @@ use crate::inspect::PyStaticExpr; #[cfg(feature = "experimental-inspect")] use crate::type_hint_union; +#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] +use crate::types::bytearray::{PyByteArray, PyByteArrayMethods}; use crate::{ - types::{ - bytearray::PyByteArrayMethods, bytes::PyBytesMethods, string::PyStringMethods, PyByteArray, - PyBytes, PyString, PyTuple, - }, + types::{bytes::PyBytesMethods, string::PyStringMethods, PyBytes, PyString, PyTuple}, Borrowed, Bound, CastError, FromPyObject, IntoPyObject, Py, PyAny, PyErr, PyTypeInfo, Python, }; -use std::{borrow::Borrow, convert::Infallible, ops::Deref, ptr::NonNull, sync::Arc}; +#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] +use std::sync::Arc; +use std::{borrow::Borrow, convert::Infallible, ops::Deref, ptr::NonNull}; /// An equivalent to `String` where the storage is owned by a Python `bytes` or `str` object. /// @@ -189,6 +190,7 @@ pub struct PyBackedBytes { #[cfg_attr(feature = "py-clone", derive(Clone))] enum PyBackedBytesStorage { Python(Py), + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] Rust(Arc<[u8]>), } @@ -202,6 +204,7 @@ impl PyBackedBytes { PyBackedBytesStorage::Python(bytes) => { PyBackedBytesStorage::Python(bytes.clone_ref(py)) } + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] PyBackedBytesStorage::Rust(bytes) => PyBackedBytesStorage::Rust(bytes.clone()), }, data: self.data, @@ -265,6 +268,7 @@ impl From> for PyBackedBytes { } } +#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] impl From> for PyBackedBytes { fn from(py_bytearray: Bound<'_, PyByteArray>) -> Self { let s = Arc::<[u8]>::from(py_bytearray.to_vec()); @@ -283,23 +287,39 @@ impl<'a, 'py> FromPyObject<'a, 'py> for PyBackedBytes { const INPUT_TYPE: PyStaticExpr = type_hint_union!(PyBytes::TYPE_HINT, PyByteArray::TYPE_HINT); fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { - if let Ok(bytes) = obj.cast::() { - Ok(Self::from(bytes.to_owned())) - } else if let Ok(bytearray) = obj.cast::() { - Ok(Self::from(bytearray.to_owned())) - } else { - Err(CastError::new( - obj, - PyTuple::new( - obj.py(), - [ - PyBytes::type_object(obj.py()), - PyByteArray::type_object(obj.py()), - ], - ) - .unwrap() - .into_any(), - )) + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] + { + if let Ok(bytes) = obj.cast::() { + Ok(Self::from(bytes.to_owned())) + } else if let Ok(bytearray) = obj.cast::() { + Ok(Self::from(bytearray.to_owned())) + } else { + Err(CastError::new( + obj, + PyTuple::new( + obj.py(), + [ + PyBytes::type_object(obj.py()), + PyByteArray::type_object(obj.py()), + ], + ) + .unwrap() + .into_any(), + )) + } + } + #[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))] + { + if let Ok(bytes) = obj.cast::() { + Ok(Self::from(bytes.to_owned())) + } else { + Err(CastError::new( + obj, + PyTuple::new(obj.py(), [PyBytes::type_object(obj.py())]) + .unwrap() + .into_any(), + )) + } } } } @@ -315,6 +335,7 @@ impl<'py> IntoPyObject<'py> for PyBackedBytes { fn into_pyobject(self, py: Python<'py>) -> Result { match self.storage { PyBackedBytesStorage::Python(bytes) => Ok(bytes.into_bound(py)), + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] PyBackedBytesStorage::Rust(bytes) => Ok(PyBytes::new(py, &bytes)), } } @@ -331,6 +352,7 @@ impl<'py> IntoPyObject<'py> for &PyBackedBytes { fn into_pyobject(self, py: Python<'py>) -> Result { match &self.storage { PyBackedBytesStorage::Python(bytes) => Ok(bytes.bind(py).clone()), + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] PyBackedBytesStorage::Rust(bytes) => Ok(PyBytes::new(py, bytes)), } } @@ -496,6 +518,7 @@ mod test { } #[test] + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] fn py_backed_bytes_from_bytearray() { Python::attach(|py| { let b = PyByteArray::new(py, b"abcde"); @@ -517,6 +540,7 @@ mod test { } #[test] + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] fn rust_backed_bytes_into_pyobject() { Python::attach(|py| { let orig_bytes = PyByteArray::new(py, b"abcde"); @@ -668,6 +692,7 @@ mod test { let b1: PyBackedBytes = PyBytes::new(py, b"abcde").into(); let b2 = b1.clone_ref(py); assert_eq!(b1, b2); + #[cfg_attr(all(Py_GIL_DISABLED, Py_LIMITED_API), allow(irrefutable_let_patterns))] let (PyBackedBytesStorage::Python(s1), PyBackedBytesStorage::Python(s2)) = (&b1.storage, &b2.storage) else { @@ -694,6 +719,7 @@ mod test { } #[test] + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] fn test_backed_bytes_from_bytearray_clone_ref() { Python::attach(|py| { let b1: PyBackedBytes = PyByteArray::new(py, b"abcde").into(); @@ -712,6 +738,7 @@ mod test { } #[test] + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] fn test_backed_bytes_eq() { Python::attach(|py| { let b1: PyBackedBytes = PyBytes::new(py, b"abcde").into(); @@ -741,16 +768,19 @@ mod test { b1.hash(&mut hasher); hasher.finish() }; + assert_eq!(h, h1); - let b2: PyBackedBytes = PyByteArray::new(py, b"abcde").into(); - let h2 = { - let mut hasher = DefaultHasher::new(); - b2.hash(&mut hasher); - hasher.finish() - }; + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] + { + let b2: PyBackedBytes = PyByteArray::new(py, b"abcde").into(); + let h2 = { + let mut hasher = DefaultHasher::new(); + b2.hash(&mut hasher); + hasher.finish() + }; - assert_eq!(h, h1); - assert_eq!(h, h2); + assert_eq!(h, h2); + } }); } diff --git a/src/sync.rs b/src/sync.rs index cb71ccd10f5..fbe6c0437e3 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -9,12 +9,9 @@ //! interpreter. //! //! This module provides synchronization primitives which are able to synchronize under these conditions. -use crate::{ - internal::state::SuspendAttach, - sealed::Sealed, - types::{PyAny, PyString}, - Bound, Py, Python, -}; +#[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] +use crate::types::PyAny; +use crate::{internal::state::SuspendAttach, sealed::Sealed, types::PyString, Bound, Py, Python}; use std::{ cell::UnsafeCell, marker::PhantomData, @@ -30,6 +27,7 @@ pub(crate) mod once_lock; since = "0.28.0", note = "use pyo3::sync::critical_section::with_critical_section instead" )] +#[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] pub fn with_critical_section(object: &Bound<'_, PyAny>, f: F) -> R where F: FnOnce() -> R, @@ -42,6 +40,7 @@ where since = "0.28.0", note = "use pyo3::sync::critical_section::with_critical_section2 instead" )] +#[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] pub fn with_critical_section2(a: &Bound<'_, PyAny>, b: &Bound<'_, PyAny>, f: F) -> R where F: FnOnce() -> R, diff --git a/src/sync/critical_section.rs b/src/sync/critical_section.rs index 4c825db3f37..7fe91058fa3 100644 --- a/src/sync/critical_section.rs +++ b/src/sync/critical_section.rs @@ -42,14 +42,15 @@ use crate::types::PyMutex; #[cfg(all(Py_3_14, not(Py_LIMITED_API)))] use crate::Python; +#[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] use crate::{types::PyAny, Bound}; #[cfg(all(Py_3_14, not(Py_LIMITED_API)))] use std::cell::UnsafeCell; -#[cfg(any(Py_GIL_DISABLED, all(Py_3_15, Py_LIMITED_API)))] +#[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API)))] struct CSGuard(crate::ffi::PyCriticalSection); -#[cfg(any(Py_GIL_DISABLED, all(Py_3_15, Py_LIMITED_API)))] +#[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API)))] impl Drop for CSGuard { fn drop(&mut self) { unsafe { @@ -58,10 +59,10 @@ impl Drop for CSGuard { } } -#[cfg(any(Py_GIL_DISABLED, all(Py_3_15, Py_LIMITED_API)))] +#[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API)))] struct CS2Guard(crate::ffi::PyCriticalSection2); -#[cfg(any(Py_GIL_DISABLED, all(Py_3_15, Py_LIMITED_API)))] +#[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API)))] impl Drop for CS2Guard { fn drop(&mut self) { unsafe { @@ -125,6 +126,7 @@ impl EnteredCriticalSection<'_, T> { /// /// This is structurally equivalent to the use of the paired Py_BEGIN_CRITICAL_SECTION and /// Py_END_CRITICAL_SECTION C-API macros. +#[cfg(not(Py_LIMITED_API))] #[cfg_attr(not(Py_GIL_DISABLED), allow(unused_variables))] pub fn with_critical_section(object: &Bound<'_, PyAny>, f: F) -> R where @@ -152,6 +154,7 @@ where /// /// This is structurally equivalent to the use of the paired /// Py_BEGIN_CRITICAL_SECTION2 and Py_END_CRITICAL_SECTION2 C-API macros. +#[cfg(not(Py_LIMITED_API))] #[cfg_attr(not(Py_GIL_DISABLED), allow(unused_variables))] pub fn with_critical_section2(a: &Bound<'_, PyAny>, b: &Bound<'_, PyAny>, f: F) -> R where @@ -268,30 +271,40 @@ where #[cfg(test)] mod tests { #[cfg(feature = "macros")] + #[cfg(not(Py_LIMITED_API))] use super::{with_critical_section, with_critical_section2}; #[cfg(all(not(Py_LIMITED_API), Py_3_14))] use super::{with_critical_section_mutex, with_critical_section_mutex2}; #[cfg(all(not(Py_LIMITED_API), Py_3_14))] use crate::types::PyMutex; - #[cfg(feature = "macros")] + #[cfg(all(not(all(Py_LIMITED_API, Py_GIL_DISABLED)), feature = "macros"))] use std::sync::atomic::{AtomicBool, Ordering}; - #[cfg(any(feature = "macros", all(not(Py_LIMITED_API), Py_3_14)))] + #[cfg(all( + not(all(Py_LIMITED_API, Py_GIL_DISABLED)), + any(feature = "macros", all(not(Py_LIMITED_API), Py_3_14)) + ))] use std::sync::Barrier; - #[cfg(feature = "macros")] + #[cfg(all(not(all(Py_LIMITED_API, Py_GIL_DISABLED)), feature = "macros"))] use crate::Py; - #[cfg(any(feature = "macros", all(not(Py_LIMITED_API), Py_3_14)))] + #[cfg(all( + not(all(Py_LIMITED_API, Py_GIL_DISABLED)), + any(feature = "macros", all(not(Py_LIMITED_API), Py_3_14)) + ))] use crate::Python; + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] #[cfg(feature = "macros")] #[crate::pyclass(crate = "crate")] struct VecWrapper(Vec); + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] #[cfg(feature = "macros")] #[crate::pyclass(crate = "crate")] struct BoolWrapper(AtomicBool); #[cfg(feature = "macros")] + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] #[test] fn test_critical_section() { let barrier = Barrier::new(2); @@ -357,6 +370,7 @@ mod tests { #[cfg(feature = "macros")] #[test] + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] fn test_critical_section2() { let barrier = Barrier::new(3); @@ -439,6 +453,7 @@ mod tests { #[cfg(feature = "macros")] #[test] + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] fn test_critical_section2_same_object_no_deadlock() { let barrier = Barrier::new(2); @@ -504,6 +519,7 @@ mod tests { #[cfg(feature = "macros")] #[test] + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] fn test_critical_section2_two_containers() { let (vec1, vec2) = Python::attach(|py| { ( diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index bbcaee28da5..ebb6d712bd7 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -2,6 +2,7 @@ use crate::err::{PyErr, PyResult}; use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::{Borrowed, Bound}; use crate::py_result_ext::PyResultExt; +#[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] use crate::sync::critical_section::with_critical_section; use crate::{ffi, PyAny, Python}; use std::slice; @@ -131,10 +132,14 @@ pub trait PyByteArrayMethods<'py>: crate::sealed::Sealed { /// /// ```rust /// use pyo3::prelude::*; + /// # #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] /// use pyo3::exceptions::PyRuntimeError; + /// # #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] /// use pyo3::sync::critical_section::with_critical_section; + /// # #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] /// use pyo3::types::PyByteArray; /// + /// # #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] /// #[pyfunction] /// fn a_valid_function(bytes: &Bound<'_, PyByteArray>) -> PyResult<()> { /// let section = with_critical_section(bytes, || { @@ -155,6 +160,9 @@ pub trait PyByteArrayMethods<'py>: crate::sealed::Sealed { /// /// Ok(()) /// } + /// # #[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))] + /// # fn main() -> () {} + /// # #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] /// # fn main() -> PyResult<()> { /// # Python::attach(|py| -> PyResult<()> { /// # let fun = wrap_pyfunction!(a_valid_function, py)?; @@ -236,6 +244,7 @@ pub trait PyByteArrayMethods<'py>: crate::sealed::Sealed { /// pyo3::py_run!(py, bytearray, "assert bytearray == b'Hello World.'"); /// # }); /// ``` + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] fn to_vec(&self) -> Vec; /// Resizes the bytearray object to the new length `len`. @@ -268,6 +277,7 @@ impl<'py> PyByteArrayMethods<'py> for Bound<'py, PyByteArray> { unsafe { self.as_borrowed().as_bytes_mut() } } + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] fn to_vec(&self) -> Vec { with_critical_section(self, || { // SAFETY: @@ -358,6 +368,7 @@ mod tests { } #[test] + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] fn test_to_vec() { Python::attach(|py| { let src = b"Hello Python"; @@ -459,6 +470,7 @@ mod tests { any(Py_3_14, not(all(Py_3_13, Py_GIL_DISABLED))) ))] #[test] + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] fn test_data_integrity_in_critical_section() { use crate::instance::Py; use crate::sync::{critical_section::with_critical_section, MutexExt}; diff --git a/src/types/code.rs b/src/types/code.rs index 8d38a8fc826..4d4405a6fdb 100644 --- a/src/types/code.rs +++ b/src/types/code.rs @@ -6,9 +6,9 @@ use crate::py_result_ext::PyResultExt; use crate::sync::PyOnceLock; #[cfg(any(Py_LIMITED_API, PyPy))] use crate::types::{PyType, PyTypeMethods}; +use crate::{ffi, Bound, PyAny, PyResult, Python}; #[cfg(any(Py_LIMITED_API, PyPy))] -use crate::Py; -use crate::{ffi, Bound, PyAny, PyErr, PyResult, Python}; +use crate::{Py, PyErr}; use std::ffi::CStr; /// Represents a Python code object. @@ -118,6 +118,8 @@ impl<'py> PyCodeMethods<'py> for Bound<'py, PyCode> { None => attr.cast::()?, }; let locals = locals.unwrap_or(globals); + // Inherit current builtins. + let builtins = unsafe { ffi::PyEval_GetBuiltins() }; // If `globals` don't provide `__builtins__`, most of the code will fail if Python // version is <3.10. That's probably not what user intended, so insert `__builtins__` @@ -127,26 +129,33 @@ impl<'py> PyCodeMethods<'py> for Bound<'py, PyCode> { // - https://github.com/python/cpython/pull/24564 (the same fix in CPython 3.10) // - https://github.com/PyO3/pyo3/issues/3370 let builtins_s = crate::intern!(self.py(), "__builtins__"); - let has_builtins = globals.contains(builtins_s)?; - if !has_builtins { - crate::sync::critical_section::with_critical_section(globals, || { - // check if another thread set __builtins__ while this thread was blocked on the critical section - let has_builtins = globals.contains(builtins_s)?; - if !has_builtins { - // Inherit current builtins. - let builtins = unsafe { ffi::PyEval_GetBuiltins() }; - - // `PyDict_SetItem` doesn't take ownership of `builtins`, but `PyEval_GetBuiltins` - // seems to return a borrowed reference, so no leak here. - if unsafe { - ffi::PyDict_SetItem(globals.as_ptr(), builtins_s.as_ptr(), builtins) - } == -1 - { - return Err(PyErr::fetch(self.py())); - } + #[cfg(any(all(Py_LIMITED_API, not(Py_3_15)), not(Py_3_13)))] + { + let has_builtins = globals.contains(builtins_s)?; + if !has_builtins { + // `PyDict_SetItem` doesn't take ownership of `builtins`, but `PyEval_GetBuiltins` + // seems to return a borrowed reference, so no leak here. + if unsafe { ffi::PyDict_SetItem(globals.as_ptr(), builtins_s.as_ptr(), builtins) } + == -1 + { + return Err(PyErr::fetch(self.py())); } - Ok(()) - })?; + } + } + #[cfg(any(all(Py_LIMITED_API, Py_3_15), Py_3_13))] + { + let mut result: *mut ffi::PyObject = std::ptr::null_mut(); + if unsafe { + ffi::PyDict_SetDefaultRef( + globals.as_ptr(), + builtins_s.as_ptr(), + builtins, + &mut result, + ) + } == -1 + { + return Err(PyErr::fetch(self.py())); + } } unsafe { diff --git a/src/types/dict.rs b/src/types/dict.rs index 0f03a217f79..5ac9cde8420 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -182,6 +182,7 @@ pub trait PyDictMethods<'py>: crate::sealed::Sealed { /// nightly feature is not enabled because we cannot implement an optimised version of /// `iter().try_fold()` on stable yet. If your iteration is infallible then this method has the /// same performance as `.iter().for_each()`. + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] fn locked_for_each(&self, closure: F) -> PyResult<()> where F: Fn(Bound<'py, PyAny>, Bound<'py, PyAny>) -> PyResult<()>; @@ -347,6 +348,7 @@ impl<'py> PyDictMethods<'py> for Bound<'py, PyDict> { BoundDictIterator::new(self.clone()) } + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] fn locked_for_each(&self, f: F) -> PyResult<()> where F: Fn(Bound<'py, PyAny>, Bound<'py, PyAny>) -> PyResult<()>, @@ -486,6 +488,7 @@ impl DictIterImpl { } } + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] #[cfg(Py_GIL_DISABLED)] #[inline] fn with_critical_section(&mut self, dict: &Bound<'_, PyDict>, f: F) -> R @@ -505,6 +508,7 @@ impl<'py> Iterator for BoundDictIterator<'py> { #[inline] fn next(&mut self) -> Option { + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] #[cfg(Py_GIL_DISABLED)] { self.inner @@ -512,7 +516,7 @@ impl<'py> Iterator for BoundDictIterator<'py> { inner.next_unchecked(&self.dict) }) } - #[cfg(not(Py_GIL_DISABLED))] + #[cfg(any(all(Py_GIL_DISABLED, Py_LIMITED_API), not(Py_GIL_DISABLED)))] { unsafe { self.inner.next_unchecked(&self.dict) } } @@ -534,6 +538,7 @@ impl<'py> Iterator for BoundDictIterator<'py> { #[inline] #[cfg(Py_GIL_DISABLED)] + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] fn fold(mut self, init: B, mut f: F) -> B where Self: Sized, @@ -566,6 +571,7 @@ impl<'py> Iterator for BoundDictIterator<'py> { } #[inline] + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] fn all(&mut self, mut f: F) -> bool where @@ -583,6 +589,7 @@ impl<'py> Iterator for BoundDictIterator<'py> { } #[inline] + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] fn any(&mut self, mut f: F) -> bool where @@ -600,6 +607,7 @@ impl<'py> Iterator for BoundDictIterator<'py> { } #[inline] + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] fn find

(&mut self, mut predicate: P) -> Option where @@ -617,6 +625,7 @@ impl<'py> Iterator for BoundDictIterator<'py> { } #[inline] + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] fn find_map(&mut self, mut f: F) -> Option where @@ -634,6 +643,7 @@ impl<'py> Iterator for BoundDictIterator<'py> { } #[inline] + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] fn position

(&mut self, mut predicate: P) -> Option where diff --git a/src/types/list.rs b/src/types/list.rs index 00bec2e88a3..ffadd0521cf 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -211,6 +211,7 @@ pub trait PyListMethods<'py>: crate::sealed::Sealed { /// iterator. Otherwise, the list will not be modified during iteration. /// /// This is equivalent to for_each if the GIL is enabled. + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] fn locked_for_each(&self, closure: F) -> PyResult<()> where F: Fn(Bound<'py, PyAny>) -> PyResult<()>; @@ -420,6 +421,7 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> { } /// Iterates over a list while holding a critical section, calling a closure on each item + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] fn locked_for_each(&self, closure: F) -> PyResult<()> where F: Fn(Bound<'py, PyAny>) -> PyResult<()>, @@ -517,6 +519,7 @@ impl<'py> BoundListIterator<'py> { #[inline] #[cfg(not(feature = "nightly"))] + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] fn nth( index: &mut Index, length: &mut Length, @@ -588,6 +591,7 @@ impl<'py> BoundListIterator<'py> { #[inline] #[cfg(not(feature = "nightly"))] + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] fn nth_back( index: &mut Index, length: &mut Length, @@ -616,6 +620,7 @@ impl<'py> BoundListIterator<'py> { } #[allow(dead_code)] + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] fn with_critical_section( &mut self, f: impl FnOnce(&mut Index, &mut Length, &Bound<'py, PyList>) -> R, @@ -653,6 +658,7 @@ impl<'py> Iterator for BoundListIterator<'py> { #[inline] #[cfg(not(feature = "nightly"))] + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] fn nth(&mut self, n: usize) -> Option { self.with_critical_section(|index, length, list| Self::nth(index, length, list, n)) } @@ -848,6 +854,7 @@ impl DoubleEndedIterator for BoundListIterator<'_> { #[inline] #[cfg(not(feature = "nightly"))] + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] fn nth_back(&mut self, n: usize) -> Option { self.with_critical_section(|index, length, list| Self::nth_back(index, length, list, n)) } From a609f8c703358d73585cc64c8c16276043e9d742 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 6 Apr 2026 11:49:17 -0600 Subject: [PATCH 46/74] add FIXME --- src/types/dict.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types/dict.rs b/src/types/dict.rs index 5ac9cde8420..1d940d359b4 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -518,6 +518,7 @@ impl<'py> Iterator for BoundDictIterator<'py> { } #[cfg(any(all(Py_GIL_DISABLED, Py_LIMITED_API), not(Py_GIL_DISABLED)))] { + // FIXME: Unsafe with Py_GIL_DISABLED, but no critical sections in the stable ABI unsafe { self.inner.next_unchecked(&self.dict) } } } From dd5b3a9ab90a55cacde21e4ecfd9ec335006f462 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 6 Apr 2026 11:51:43 -0600 Subject: [PATCH 47/74] remove default feature --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index d5ce7e1afc0..362d3b552f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,7 +85,7 @@ parking_lot = { version = "0.12.3", features = ["arc_lock"] } pyo3-build-config = { path = "pyo3-build-config", version = "=0.28.3", features = ["resolve-config"] } [features] -default = ["macros", "abi3t"] +default = ["macros"] # Enables support for `async fn` for `#[pyfunction]` and `#[pymethods]`. experimental-async = ["macros", "pyo3-macros/experimental-async"] From 7419b86414a2d78b5ba5e7c9d45b9a7a9bc6d644 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 6 Apr 2026 11:52:37 -0600 Subject: [PATCH 48/74] fix syntax error in noxfile --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 2995370965a..201278d3353 100644 --- a/noxfile.py +++ b/noxfile.py @@ -140,7 +140,7 @@ def test_rust(session: nox.Session): ) if ( - feature_set, + feature_set and "abi3" in feature_set and "full" in feature_set and sys.version_info >= (3, 16) From 603aaf968c4a44ce1508d58e596bb01c9978c68c Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 6 Apr 2026 12:00:14 -0600 Subject: [PATCH 49/74] fix issues spotted by clippy --- noxfile.py | 2 +- pyo3-build-config/src/impl_.rs | 6 +++--- src/conversions/std/num.rs | 2 +- src/pybacked.rs | 10 ++++++++-- src/types/code.rs | 4 ++-- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/noxfile.py b/noxfile.py index 201278d3353..ff14dc67f31 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1515,7 +1515,7 @@ def _get_feature_sets() -> Tuple[Optional[str], ...]: if is_rust_nightly(): features += ",nightly" - if is_free_threaded(): + if FREE_THREADED_BUILD: if sys.version_info >= (3, 15): return (None, "abi3t", features, f"abi3t,{features}") else: diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index db4e11f8ff4..bae43d55fee 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -99,11 +99,11 @@ impl CPythonABI { "Cannot simultaneously build for abi3 and abi3t ABIs" ); if abi3 { - return Ok(CPythonABI::ABI3); + Ok(CPythonABI::ABI3) } else if abi3t { - return Ok(CPythonABI::ABI3t); + Ok(CPythonABI::ABI3t) } else { - return Ok(CPythonABI::VersionSpecific); + Ok(CPythonABI::VersionSpecific) } } } diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index ec7701b8f63..e0f65e10e99 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -270,7 +270,7 @@ impl<'py> FromPyObject<'_, 'py> for u8 { if let Ok(bytes) = obj.cast::() { Some(BytesSequenceExtractor::Bytes(bytes)) } else if let Ok(byte_array) = obj.cast::() { - Some(BytesSequenceExtractor::ByteArray(byte_array)); + Some(BytesSequenceExtractor::ByteArray(byte_array)) } else { None } diff --git a/src/pybacked.rs b/src/pybacked.rs index 91000065cf9..0fc0bf4b875 100644 --- a/src/pybacked.rs +++ b/src/pybacked.rs @@ -4,8 +4,13 @@ use crate::inspect::PyStaticExpr; #[cfg(feature = "experimental-inspect")] use crate::type_hint_union; +#[cfg(any( + feature = "experimental-inspect", + not(all(Py_LIMITED_API, Py_GIL_DISABLED)) +))] +use crate::types::bytearray::PyByteArray; #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] -use crate::types::bytearray::{PyByteArray, PyByteArrayMethods}; +use crate::types::bytearray::PyByteArrayMethods; use crate::{ types::{bytes::PyBytesMethods, string::PyStringMethods, PyBytes, PyString, PyTuple}, Borrowed, Bound, CastError, FromPyObject, IntoPyObject, Py, PyAny, PyErr, PyTypeInfo, Python, @@ -706,6 +711,7 @@ mod test { } #[cfg(feature = "py-clone")] + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] #[test] fn test_backed_bytes_from_bytearray_clone() { Python::attach(|py| { @@ -718,8 +724,8 @@ mod test { }); } - #[test] #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] + #[test] fn test_backed_bytes_from_bytearray_clone_ref() { Python::attach(|py| { let b1: PyBackedBytes = PyByteArray::new(py, b"abcde").into(); diff --git a/src/types/code.rs b/src/types/code.rs index 4d4405a6fdb..7bf663a42b8 100644 --- a/src/types/code.rs +++ b/src/types/code.rs @@ -6,9 +6,9 @@ use crate::py_result_ext::PyResultExt; use crate::sync::PyOnceLock; #[cfg(any(Py_LIMITED_API, PyPy))] use crate::types::{PyType, PyTypeMethods}; -use crate::{ffi, Bound, PyAny, PyResult, Python}; #[cfg(any(Py_LIMITED_API, PyPy))] -use crate::{Py, PyErr}; +use crate::Py; +use crate::{ffi, Bound, PyAny, PyErr, PyResult, Python}; use std::ffi::CStr; /// Represents a Python code object. From c9b91570875c94319cbd7e4829f117ccc04208f1 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 7 Apr 2026 09:31:46 -0600 Subject: [PATCH 50/74] bring over refactoring from PyO3 PR --- pyo3-ffi/src/compat/py_3_13.rs | 47 ++++++++++++++++++++++++++++++++++ pyo3-ffi/src/dictobject.rs | 2 +- src/types/code.rs | 43 +++++++++++-------------------- 3 files changed, 63 insertions(+), 29 deletions(-) diff --git a/pyo3-ffi/src/compat/py_3_13.rs b/pyo3-ffi/src/compat/py_3_13.rs index 08bdf5cba18..26b9f2698fb 100644 --- a/pyo3-ffi/src/compat/py_3_13.rs +++ b/pyo3-ffi/src/compat/py_3_13.rs @@ -130,3 +130,50 @@ compat_function!( crate::_PyThreadState_UncheckedGet() } ); + +compat_function!( + originally_defined_for(all(Py_3_13, any(not(Py_LIMITED_API), Py_3_15))); + + #[inline] + pub unsafe fn PyDict_SetDefaultRef( + mp: *mut crate::PyObject, + key: *mut crate::PyObject, + default_value: *mut crate::PyObject, + result: *mut *mut crate::PyObject, + ) -> std::ffi::c_int { + use crate::{ + compat::{PyDict_GetItemRef, Py_NewRef}, + PyDict_SetItem, PyObject, Py_DECREF, + }; + let mut value: *mut PyObject = std::ptr::null_mut(); + if PyDict_GetItemRef(mp, key, &mut value) < 0 { + // get error + if !result.is_null() { + *result = std::ptr::null_mut(); + } + return -1; + } + if !value.is_null() { + // present + if !result.is_null() { + *result = value; + } else { + Py_DECREF(value); + } + return 1; + } + + // missing, set the item + if PyDict_SetItem(mp, key, default_value) < 0 { + // set error + if !result.is_null() { + *result = std::ptr::null_mut(); + } + return -1; + } + if !result.is_null() { + *result = Py_NewRef(default_value); + } + 0 + } +); diff --git a/pyo3-ffi/src/dictobject.rs b/pyo3-ffi/src/dictobject.rs index c449132fa98..4df1265d4b8 100644 --- a/pyo3-ffi/src/dictobject.rs +++ b/pyo3-ffi/src/dictobject.rs @@ -78,7 +78,7 @@ extern_libpython! { key: *const c_char, result: *mut *mut PyObject, ) -> c_int; - #[cfg(any(Py_3_13, all(Py_LIMITED_API, Py_3_15)))] + #[cfg(all(Py_3_13, any(not(Py_LIMITED_API), Py_3_15)))] pub fn PyDict_SetDefaultRef( mp: *mut PyObject, key: *mut PyObject, diff --git a/src/types/code.rs b/src/types/code.rs index 7bf663a42b8..fdb2fe4383d 100644 --- a/src/types/code.rs +++ b/src/types/code.rs @@ -118,8 +118,6 @@ impl<'py> PyCodeMethods<'py> for Bound<'py, PyCode> { None => attr.cast::()?, }; let locals = locals.unwrap_or(globals); - // Inherit current builtins. - let builtins = unsafe { ffi::PyEval_GetBuiltins() }; // If `globals` don't provide `__builtins__`, most of the code will fail if Python // version is <3.10. That's probably not what user intended, so insert `__builtins__` @@ -129,35 +127,24 @@ impl<'py> PyCodeMethods<'py> for Bound<'py, PyCode> { // - https://github.com/python/cpython/pull/24564 (the same fix in CPython 3.10) // - https://github.com/PyO3/pyo3/issues/3370 let builtins_s = crate::intern!(self.py(), "__builtins__"); - #[cfg(any(all(Py_LIMITED_API, not(Py_3_15)), not(Py_3_13)))] + let mut result: *mut ffi::PyObject = std::ptr::null_mut(); + if unsafe { + ffi::compat::PyDict_SetDefaultRef( + globals.as_ptr(), + builtins_s.as_ptr(), + // safety: the interpreter will keep the borrowed reference to + // builtins alive at least until SetDefaultRef finishes + ffi::PyEval_GetBuiltins(), + &mut result, + ) + } == -1 { - let has_builtins = globals.contains(builtins_s)?; - if !has_builtins { - // `PyDict_SetItem` doesn't take ownership of `builtins`, but `PyEval_GetBuiltins` - // seems to return a borrowed reference, so no leak here. - if unsafe { ffi::PyDict_SetItem(globals.as_ptr(), builtins_s.as_ptr(), builtins) } - == -1 - { - return Err(PyErr::fetch(self.py())); - } - } - } - #[cfg(any(all(Py_LIMITED_API, Py_3_15), Py_3_13))] - { - let mut result: *mut ffi::PyObject = std::ptr::null_mut(); - if unsafe { - ffi::PyDict_SetDefaultRef( - globals.as_ptr(), - builtins_s.as_ptr(), - builtins, - &mut result, - ) - } == -1 - { - return Err(PyErr::fetch(self.py())); - } + return Err(PyErr::fetch(self.py())); } + // release ownership of result + unsafe { ffi::Py_DECREF(result) }; + unsafe { ffi::PyEval_EvalCode(self.as_ptr(), globals.as_ptr(), locals.as_ptr()) .assume_owned_or_err(self.py()) From 7560348e05c975446e817905e9a8e49cf8fe23b1 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 7 Apr 2026 15:09:45 -0600 Subject: [PATCH 51/74] working! --- src/types/dict.rs | 140 +++++++++++++++++++++++++++++++--------------- 1 file changed, 96 insertions(+), 44 deletions(-) diff --git a/src/types/dict.rs b/src/types/dict.rs index 1d940d359b4..32d0e30199b 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -420,9 +420,14 @@ pub struct BoundDictIterator<'py> { enum DictIterImpl { DictIter { + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] ppos: ffi::Py_ssize_t, + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] di_used: ffi::Py_ssize_t, + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] remaining: ffi::Py_ssize_t, + #[cfg(all(Py_GIL_DISABLED, Py_LIMITED_API))] + iter: *mut ffi::PyObject, }, } @@ -437,52 +442,81 @@ impl DictIterImpl { ) -> Option<(Bound<'py, PyAny>, Bound<'py, PyAny>)> { match self { Self::DictIter { + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] di_used, + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] remaining, + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] ppos, + #[cfg(all(Py_GIL_DISABLED, Py_LIMITED_API))] + iter, .. } => { - let ma_used = dict_len(dict); - - // These checks are similar to what CPython does. - // - // If the dimension of the dict changes e.g. key-value pairs are removed - // or added during iteration, this will panic next time when `next` is called - if *di_used != ma_used { - *di_used = -1; - panic!("dictionary changed size during iteration"); - }; - - // If the dict is changed in such a way that the length remains constant - // then this will panic at the end of iteration - similar to this: - // - // d = {"a":1, "b":2, "c": 3} - // - // for k, v in d.items(): - // d[f"{k}_"] = 4 - // del d[k] - // print(k) - // - if *remaining == -1 { - *di_used = -1; - panic!("dictionary keys changed during iteration"); - }; - - let mut key: *mut ffi::PyObject = std::ptr::null_mut(); - let mut value: *mut ffi::PyObject = std::ptr::null_mut(); - - if unsafe { ffi::PyDict_Next(dict.as_ptr(), ppos, &mut key, &mut value) != 0 } { - *remaining -= 1; + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] + { + let ma_used = dict_len(dict); + + // These checks are similar to what CPython does. + // + // If the dimension of the dict changes e.g. key-value pairs are removed + // or added during iteration, this will panic next time when `next` is called + if *di_used != ma_used { + *di_used = -1; + panic!("dictionary changed size during iteration"); + }; + + // If the dict is changed in such a way that the length remains constant + // then this will panic at the end of iteration - similar to this: + // + // d = {"a":1, "b":2, "c": 3} + // + // for k, v in d.items(): + // d[f"{k}_"] = 4 + // del d[k] + // print(k) + // + if *remaining == -1 { + *di_used = -1; + panic!("dictionary keys changed during iteration"); + }; + + let mut key: *mut ffi::PyObject = std::ptr::null_mut(); + let mut value: *mut ffi::PyObject = std::ptr::null_mut(); + + if unsafe { ffi::PyDict_Next(dict.as_ptr(), ppos, &mut key, &mut value) != 0 } { + *remaining -= 1; + let py = dict.py(); + // Safety: + // - PyDict_Next returns borrowed values + // - we have already checked that `PyDict_Next` succeeded, so we can assume these to be non-null + Some(( + unsafe { key.assume_borrowed_unchecked(py).to_owned() }, + unsafe { value.assume_borrowed_unchecked(py).to_owned() }, + )) + } else { + None + } + } + #[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))] + { let py = dict.py(); - // Safety: - // - PyDict_Next returns borrowed values - // - we have already checked that `PyDict_Next` succeeded, so we can assume these to be non-null - Some(( - unsafe { key.assume_borrowed_unchecked(py).to_owned() }, - unsafe { value.assume_borrowed_unchecked(py).to_owned() }, - )) - } else { - None + let mut key: *mut ffi::PyObject = std::ptr::null_mut(); + let key = match unsafe { ffi::compat::PyIter_NextItem(*iter, &mut key) } { + -1 => panic!( + "Iterating over dictionary failed with error '{}'", + PyErr::fetch(py) + ), + 0 => return None, + 1 => unsafe { key.assume_owned_unchecked(py) }, + x => panic!("Unknown return value from PyIter_NextItem: {}", x), + }; + let value = match dict.get_item(&key) { + Ok(value) => value?, + Err(e) => { + panic!("Iterating over dictionary failed with error '{}'", e) + } + }; + Some((key, value)) } } } @@ -525,10 +559,14 @@ impl<'py> Iterator for BoundDictIterator<'py> { #[inline] fn size_hint(&self) -> (usize, Option) { + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] let len = self.len(); + #[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))] + let len = 0; (len, Some(len)) } + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] #[inline] fn count(self) -> usize where @@ -664,6 +702,7 @@ impl<'py> Iterator for BoundDictIterator<'py> { } } +#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] impl ExactSizeIterator for BoundDictIterator<'_> { fn len(&self) -> usize { match self.inner { @@ -674,15 +713,26 @@ impl ExactSizeIterator for BoundDictIterator<'_> { impl<'py> BoundDictIterator<'py> { fn new(dict: Bound<'py, PyDict>) -> Self { - let remaining = dict_len(&dict); - + #[cfg(all(Py_GIL_DISABLED, Py_LIMITED_API))] + let iter = { + let new_iter = unsafe { ffi::PyObject_GetIter(dict.as_ptr()) }; + assert!( + !new_iter.is_null(), + "Converting dict to iterator failed with error '{}'", + PyErr::fetch(dict.py()) + ); + new_iter + }; Self { dict, + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] inner: DictIterImpl::DictIter { ppos: 0, - di_used: remaining, + di_used: dict_len(&dict), remaining, }, + #[cfg(all(Py_GIL_DISABLED, Py_LIMITED_API))] + inner: DictIterImpl::DictIter { iter }, } } } @@ -835,7 +885,8 @@ where #[cfg(test)] mod tests { use super::*; - use crate::types::{PyAnyMethods as _, PyTuple}; + use crate::types::PyAnyMethods as _; + use crate::types::PyTuple; use std::collections::{BTreeMap, HashMap}; #[test] @@ -1231,6 +1282,7 @@ mod tests { } #[test] + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] fn test_iter_size_hint() { Python::attach(|py| { let mut v = HashMap::new(); From becc8af8a280c41934d7c761b326115c67b8c48d Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 8 Apr 2026 08:43:40 -0600 Subject: [PATCH 52/74] Fix memory leak of iterator --- src/types/dict.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/types/dict.rs b/src/types/dict.rs index 32d0e30199b..970f92e480f 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -431,6 +431,18 @@ enum DictIterImpl { }, } +#[cfg(all(Py_GIL_DISABLED, Py_LIMITED_API))] +impl Drop for DictIterImpl { + fn drop(&mut self) { + match self { + Self::DictIter { iter } => { + // safety: owned value that cannot be null by construction + unsafe { ffi::Py_DECREF(*iter) }; + } + } + } +} + impl DictIterImpl { #[deny(unsafe_op_in_unsafe_fn)] #[inline] From b734e77dc659862961013d0b3a166bb92a95836e Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 8 Apr 2026 09:40:54 -0600 Subject: [PATCH 53/74] fix size hints --- src/types/dict.rs | 77 +++++++++++++++++++++-------------------------- 1 file changed, 35 insertions(+), 42 deletions(-) diff --git a/src/types/dict.rs b/src/types/dict.rs index 970f92e480f..11408db8a05 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -422,9 +422,7 @@ enum DictIterImpl { DictIter { #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] ppos: ffi::Py_ssize_t, - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] di_used: ffi::Py_ssize_t, - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] remaining: ffi::Py_ssize_t, #[cfg(all(Py_GIL_DISABLED, Py_LIMITED_API))] iter: *mut ffi::PyObject, @@ -435,7 +433,7 @@ enum DictIterImpl { impl Drop for DictIterImpl { fn drop(&mut self) { match self { - Self::DictIter { iter } => { + Self::DictIter { iter, .. } => { // safety: owned value that cannot be null by construction unsafe { ffi::Py_DECREF(*iter) }; } @@ -454,9 +452,7 @@ impl DictIterImpl { ) -> Option<(Bound<'py, PyAny>, Bound<'py, PyAny>)> { match self { Self::DictIter { - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] di_used, - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] remaining, #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] ppos, @@ -464,34 +460,34 @@ impl DictIterImpl { iter, .. } => { - #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] - { - let ma_used = dict_len(dict); - - // These checks are similar to what CPython does. - // - // If the dimension of the dict changes e.g. key-value pairs are removed - // or added during iteration, this will panic next time when `next` is called - if *di_used != ma_used { - *di_used = -1; - panic!("dictionary changed size during iteration"); - }; + let ma_used = dict_len(dict); + + // These checks are similar to what CPython does. + // + // If the dimension of the dict changes e.g. key-value pairs are removed + // or added during iteration, this will panic next time when `next` is called + if *di_used != ma_used { + *di_used = -1; + panic!("dictionary changed size during iteration"); + }; - // If the dict is changed in such a way that the length remains constant - // then this will panic at the end of iteration - similar to this: - // - // d = {"a":1, "b":2, "c": 3} - // - // for k, v in d.items(): - // d[f"{k}_"] = 4 - // del d[k] - // print(k) - // - if *remaining == -1 { - *di_used = -1; - panic!("dictionary keys changed during iteration"); - }; + // If the dict is changed in such a way that the length remains constant + // then this will panic at the end of iteration - similar to this: + // + // d = {"a":1, "b":2, "c": 3} + // + // for k, v in d.items(): + // d[f"{k}_"] = 4 + // del d[k] + // print(k) + // + if *remaining == -1 { + *di_used = -1; + panic!("dictionary keys changed during iteration"); + }; + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] + { let mut key: *mut ffi::PyObject = std::ptr::null_mut(); let mut value: *mut ffi::PyObject = std::ptr::null_mut(); @@ -528,6 +524,8 @@ impl DictIterImpl { panic!("Iterating over dictionary failed with error '{}'", e) } }; + dbg!(*remaining); + *remaining -= 1; Some((key, value)) } } @@ -571,14 +569,10 @@ impl<'py> Iterator for BoundDictIterator<'py> { #[inline] fn size_hint(&self) -> (usize, Option) { - #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] let len = self.len(); - #[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))] - let len = 0; (len, Some(len)) } - #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] #[inline] fn count(self) -> usize where @@ -714,7 +708,6 @@ impl<'py> Iterator for BoundDictIterator<'py> { } } -#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] impl ExactSizeIterator for BoundDictIterator<'_> { fn len(&self) -> usize { match self.inner { @@ -725,6 +718,7 @@ impl ExactSizeIterator for BoundDictIterator<'_> { impl<'py> BoundDictIterator<'py> { fn new(dict: Bound<'py, PyDict>) -> Self { + let di_used = dict_len(&dict); #[cfg(all(Py_GIL_DISABLED, Py_LIMITED_API))] let iter = { let new_iter = unsafe { ffi::PyObject_GetIter(dict.as_ptr()) }; @@ -737,14 +731,14 @@ impl<'py> BoundDictIterator<'py> { }; Self { dict, - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] inner: DictIterImpl::DictIter { + #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] ppos: 0, - di_used: dict_len(&dict), - remaining, + di_used, + remaining: di_used, + #[cfg(all(Py_GIL_DISABLED, Py_LIMITED_API))] + iter, }, - #[cfg(all(Py_GIL_DISABLED, Py_LIMITED_API))] - inner: DictIterImpl::DictIter { iter }, } } } @@ -1294,7 +1288,6 @@ mod tests { } #[test] - #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] fn test_iter_size_hint() { Python::attach(|py| { let mut v = HashMap::new(); From a6c8710834443457192b7848d97819a1cb2cd85d Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 8 Apr 2026 10:09:47 -0600 Subject: [PATCH 54/74] delete debug statement --- src/types/dict.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/types/dict.rs b/src/types/dict.rs index 11408db8a05..3d11ea5b383 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -524,7 +524,6 @@ impl DictIterImpl { panic!("Iterating over dictionary failed with error '{}'", e) } }; - dbg!(*remaining); *remaining -= 1; Some((key, value)) } From a827be945d14513a6e284c8a89bdb03190af3490 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 8 Apr 2026 10:14:48 -0600 Subject: [PATCH 55/74] Use Py_TARGET_ABI3T --- .../python-from-rust/calling-existing-code.md | 4 +- pyo3-build-config/src/impl_.rs | 3 +- pyo3-build-config/src/lib.rs | 2 +- pyo3-ffi/src/modsupport.rs | 2 +- pyo3-ffi/src/moduleobject.rs | 10 ++--- pyo3-ffi/src/object.rs | 16 +++---- pyo3-ffi/src/refcount.rs | 2 +- src/conversions/std/num.rs | 12 +++--- src/impl_/pyclass.rs | 6 +-- src/impl_/pymodule.rs | 14 +++---- src/pybacked.rs | 37 ++++++++-------- src/pycell/impl_.rs | 8 ++-- src/sync.rs | 6 +-- src/sync/critical_section.rs | 22 +++++----- src/types/any.rs | 6 +-- src/types/bytearray.rs | 22 +++++----- src/types/dict.rs | 42 +++++++++---------- src/types/list.rs | 14 +++---- tests/test_append_to_inittab.rs | 2 +- 19 files changed, 114 insertions(+), 116 deletions(-) diff --git a/guide/src/python-from-rust/calling-existing-code.md b/guide/src/python-from-rust/calling-existing-code.md index 986498a2338..576caa49532 100644 --- a/guide/src/python-from-rust/calling-existing-code.md +++ b/guide/src/python-from-rust/calling-existing-code.md @@ -154,12 +154,12 @@ mod foo { } } -# #[cfg(not(_Py_OPAQUE_PYOBJECT))] +# #[cfg(not(Py_TARGET_ABI3T))] fn main() -> PyResult<()> { pyo3::append_to_inittab!(foo); Python::attach(|py| Python::run(py, c"import foo; foo.add_one(6)", None, None)) } -# #[cfg(_Py_OPAQUE_PYOBJECT)] +# #[cfg(Py_TARGET_ABI3T)] # fn main() -> () {} ``` diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index bae43d55fee..eb2d417b7e0 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -249,10 +249,11 @@ impl InterpreterConfig { } CPythonABI::ABI3t => { out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); + // emitted immediately below if the host interpreter is free-threaded if !self.is_free_threaded() { out.push("cargo:rustc-cfg=Py_GIL_DISABLED".to_owned()); } - out.push("cargo:rustc-cfg=_Py_OPAQUE_PYOBJECT".to_owned()); + out.push("cargo:rustc-cfg=Py_TARGET_ABI3T".to_owned()); } CPythonABI::VersionSpecific => {} } diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index cf0d8790b7b..a64f9332a2e 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -264,7 +264,7 @@ pub fn print_expected_cfgs() { println!("cargo:rustc-check-cfg=cfg(Py_LIMITED_API)"); println!("cargo:rustc-check-cfg=cfg(Py_GIL_DISABLED)"); - println!("cargo:rustc-check-cfg=cfg(_Py_OPAQUE_PYOBJECT)"); + println!("cargo:rustc-check-cfg=cfg(Py_TARGET_ABI3T)"); println!("cargo:rustc-check-cfg=cfg(PyPy)"); println!("cargo:rustc-check-cfg=cfg(GraalPy)"); println!("cargo:rustc-check-cfg=cfg(py_sys_config, values(\"Py_DEBUG\", \"Py_REF_DEBUG\", \"Py_TRACE_REFS\", \"COUNT_ALLOCS\"))"); diff --git a/pyo3-ffi/src/modsupport.rs b/pyo3-ffi/src/modsupport.rs index b7e9ebddd47..197c297ac2f 100644 --- a/pyo3-ffi/src/modsupport.rs +++ b/pyo3-ffi/src/modsupport.rs @@ -157,7 +157,7 @@ const _PyABIInfo_DEFAULT_FLAG_STABLE: u16 = 0; // skipped PyABIInfo_DEFAULT_ABI_VERSION: depends on Py_VERSION_HEX -#[cfg(all(Py_3_15, Py_LIMITED_API, _Py_OPAQUE_PYOBJECT))] +#[cfg(all(Py_3_15, Py_LIMITED_API, Py_TARGET_ABI3T))] const _PyABIInfo_DEFAULT_FLAG_FT: u16 = PyABIInfo_FREETHREADING_AGNOSTIC; #[cfg(all(Py_3_15, Py_GIL_DISABLED, not(Py_LIMITED_API)))] const _PyABIInfo_DEFAULT_FLAG_FT: u16 = PyABIInfo_FREETHREADED; diff --git a/pyo3-ffi/src/moduleobject.rs b/pyo3-ffi/src/moduleobject.rs index ea6a3b5a04f..eac9329f5e4 100644 --- a/pyo3-ffi/src/moduleobject.rs +++ b/pyo3-ffi/src/moduleobject.rs @@ -1,4 +1,4 @@ -#[cfg(not(_Py_OPAQUE_PYOBJECT))] +#[cfg(not(Py_TARGET_ABI3T))] use crate::methodobject::PyMethodDef; use crate::object::*; use crate::pyport::Py_ssize_t; @@ -50,7 +50,7 @@ extern_libpython! { pub static mut PyModuleDef_Type: PyTypeObject; } -#[cfg(not(_Py_OPAQUE_PYOBJECT))] +#[cfg(not(Py_TARGET_ABI3T))] #[repr(C)] pub struct PyModuleDef_Base { pub ob_base: PyObject, @@ -60,7 +60,7 @@ pub struct PyModuleDef_Base { pub m_copy: *mut PyObject, } -#[cfg(not(_Py_OPAQUE_PYOBJECT))] +#[cfg(not(Py_TARGET_ABI3T))] #[allow( clippy::declare_interior_mutable_const, reason = "contains atomic refcount on free-threaded builds" @@ -151,7 +151,7 @@ extern_libpython! { pub fn PyModule_GetToken(module: *mut PyObject, result: *mut *mut c_void) -> c_int; } -#[cfg(not(_Py_OPAQUE_PYOBJECT))] +#[cfg(not(Py_TARGET_ABI3T))] #[repr(C)] pub struct PyModuleDef { pub m_base: PyModuleDef_Base, @@ -167,5 +167,5 @@ pub struct PyModuleDef { } // from pytypedefs.h -#[cfg(_Py_OPAQUE_PYOBJECT)] +#[cfg(Py_TARGET_ABI3T)] opaque_struct!(pub PyModuleDef); diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index 26434c1c935..96b325cbb76 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -1,12 +1,12 @@ use crate::pyport::{Py_hash_t, Py_ssize_t}; -#[cfg(not(_Py_OPAQUE_PYOBJECT))] +#[cfg(not(Py_TARGET_ABI3T))] #[cfg(Py_GIL_DISABLED)] use crate::refcount; #[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API)))] use crate::PyMutex; use std::ffi::{c_char, c_int, c_uint, c_ulong, c_void}; use std::mem; -#[cfg(not(_Py_OPAQUE_PYOBJECT))] +#[cfg(not(Py_TARGET_ABI3T))] #[cfg(Py_GIL_DISABLED)] use std::sync::atomic::{AtomicIsize, AtomicU32}; @@ -94,7 +94,7 @@ const _PyObject_MIN_ALIGNMENT: usize = 4; // not currently possible to use constant variables with repr(align()), see // https://github.com/rust-lang/rust/issues/52840 -#[cfg(not(_Py_OPAQUE_PYOBJECT))] +#[cfg(not(Py_TARGET_ABI3T))] #[cfg_attr(not(all(Py_3_15, Py_GIL_DISABLED)), repr(C))] #[cfg_attr(all(Py_3_15, Py_GIL_DISABLED), repr(C, align(4)))] #[derive(Debug)] @@ -120,10 +120,10 @@ pub struct PyObject { pub ob_type: *mut PyTypeObject, } -#[cfg(not(_Py_OPAQUE_PYOBJECT))] +#[cfg(not(Py_TARGET_ABI3T))] const _: () = assert!(std::mem::align_of::() >= _PyObject_MIN_ALIGNMENT); -#[cfg(not(_Py_OPAQUE_PYOBJECT))] +#[cfg(not(Py_TARGET_ABI3T))] #[allow( clippy::declare_interior_mutable_const, reason = "contains atomic refcount on free-threaded builds" @@ -155,14 +155,14 @@ pub const PyObject_HEAD_INIT: PyObject = PyObject { }; // from pytypedefs.h -#[cfg(_Py_OPAQUE_PYOBJECT)] +#[cfg(Py_TARGET_ABI3T)] opaque_struct!(pub PyObject); // skipped _Py_UNOWNED_TID // skipped _PyObject_CAST -#[cfg(not(_Py_OPAQUE_PYOBJECT))] +#[cfg(not(Py_TARGET_ABI3T))] #[repr(C)] #[derive(Debug)] pub struct PyVarObject { @@ -175,7 +175,7 @@ pub struct PyVarObject { } // from pytypedefs.h -#[cfg(_Py_OPAQUE_PYOBJECT)] +#[cfg(Py_TARGET_ABI3T)] opaque_struct!(pub PyVarObject); // skipped private _PyVarObject_CAST diff --git a/pyo3-ffi/src/refcount.rs b/pyo3-ffi/src/refcount.rs index 8750da84ef3..d637e8fa9cf 100644 --- a/pyo3-ffi/src/refcount.rs +++ b/pyo3-ffi/src/refcount.rs @@ -116,7 +116,7 @@ pub unsafe fn Py_REFCNT(ob: *mut PyObject) -> Py_ssize_t { } } -#[cfg(not(_Py_OPAQUE_PYOBJECT))] +#[cfg(not(Py_TARGET_ABI3T))] #[cfg(Py_3_12)] #[inline(always)] unsafe fn _Py_IsImmortal(op: *mut PyObject) -> c_int { diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index e0f65e10e99..12722e06e2f 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -6,7 +6,7 @@ use crate::inspect::PyStaticExpr; use crate::py_result_ext::PyResultExt; #[cfg(feature = "experimental-inspect")] use crate::type_object::PyTypeInfo; -#[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] +#[cfg(not(Py_TARGET_ABI3T))] use crate::types::{PyByteArray, PyByteArrayMethods}; use crate::types::{PyBytes, PyInt}; use crate::{exceptions, ffi, Borrowed, Bound, FromPyObject, PyAny, PyErr, PyResult, Python}; @@ -257,7 +257,7 @@ impl<'py> FromPyObject<'_, 'py> for u8 { obj: Borrowed<'_, 'py, PyAny>, _: crate::conversion::private::Token, ) -> Option> { - #[cfg(all(Py_GIL_DISABLED, Py_LIMITED_API))] + #[cfg(Py_TARGET_ABI3T)] { if let Ok(bytes) = obj.cast::() { Some(BytesSequenceExtractor::Bytes(bytes)) @@ -265,7 +265,7 @@ impl<'py> FromPyObject<'_, 'py> for u8 { None } } - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] { if let Ok(bytes) = obj.cast::() { Some(BytesSequenceExtractor::Bytes(bytes)) @@ -280,7 +280,7 @@ impl<'py> FromPyObject<'_, 'py> for u8 { pub(crate) enum BytesSequenceExtractor<'a, 'py> { Bytes(Borrowed<'a, 'py, PyBytes>), - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] ByteArray(Borrowed<'a, 'py, PyByteArray>), } @@ -299,7 +299,7 @@ impl BytesSequenceExtractor<'_, '_> { match self { BytesSequenceExtractor::Bytes(b) => copy_slice(b.as_bytes()), - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] BytesSequenceExtractor::ByteArray(b) => { crate::sync::critical_section::with_critical_section(b, || { // Safety: b is protected by a critical section @@ -316,7 +316,7 @@ impl FromPyObjectSequence for BytesSequenceExtractor<'_, '_> { fn to_vec(&self) -> Vec { match self { BytesSequenceExtractor::Bytes(b) => b.as_bytes().to_vec(), - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] BytesSequenceExtractor::ByteArray(b) => b.to_vec(), } } diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 1e60a9a30ec..b7cb2e44cf5 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1424,7 +1424,7 @@ pub trait ExtractPyClassWithClone {} #[cfg(test)] #[cfg(feature = "macros")] mod tests { - #[cfg(not(_Py_OPAQUE_PYOBJECT))] + #[cfg(not(Py_TARGET_ABI3T))] use crate::pycell::impl_::PyClassObjectContents; use super::*; @@ -1453,13 +1453,13 @@ mod tests { Some(PyMethodDefType::StructMember(member)) => { assert_eq!(unsafe { CStr::from_ptr(member.name) }, c"value"); assert_eq!(member.type_code, ffi::Py_T_OBJECT_EX); - #[cfg(not(_Py_OPAQUE_PYOBJECT))] + #[cfg(not(Py_TARGET_ABI3T))] #[repr(C)] struct ExpectedLayout { ob_base: ffi::PyObject, contents: PyClassObjectContents, } - #[cfg(not(_Py_OPAQUE_PYOBJECT))] + #[cfg(not(Py_TARGET_ABI3T))] assert_eq!( member.offset, (offset_of!(ExpectedLayout, contents) + offset_of!(FrozenClass, value)) diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 8665c86ee3c..bc60c4f8aa8 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -48,7 +48,7 @@ use crate::{ /// `Sync` wrapper of `ffi::PyModuleDef`. pub struct ModuleDef { // wrapped in UnsafeCell so that Rust compiler treats this as interior mutability - #[cfg(not(_Py_OPAQUE_PYOBJECT))] + #[cfg(not(Py_TARGET_ABI3T))] ffi_def: UnsafeCell, #[cfg(Py_3_15)] name: &'static CStr, @@ -77,7 +77,7 @@ impl ModuleDef { ) -> 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. - #[cfg(not(_Py_OPAQUE_PYOBJECT))] + #[cfg(not(Py_TARGET_ABI3T))] #[allow(clippy::declare_interior_mutable_const)] const INIT: ffi::PyModuleDef = ffi::PyModuleDef { m_base: ffi::PyModuleDef_HEAD_INIT, @@ -91,7 +91,7 @@ impl ModuleDef { m_free: None, }; - #[cfg(not(_Py_OPAQUE_PYOBJECT))] + #[cfg(not(Py_TARGET_ABI3T))] let ffi_def = UnsafeCell::new(ffi::PyModuleDef { m_name: name.as_ptr(), m_doc: doc.as_ptr(), @@ -102,7 +102,7 @@ impl ModuleDef { }); ModuleDef { - #[cfg(not(_Py_OPAQUE_PYOBJECT))] + #[cfg(not(Py_TARGET_ABI3T))] ffi_def, #[cfg(Py_3_15)] name, @@ -121,11 +121,11 @@ impl ModuleDef { } pub fn init_multi_phase(&'static self) -> *mut ffi::PyObject { - #[cfg(not(_Py_OPAQUE_PYOBJECT))] + #[cfg(not(Py_TARGET_ABI3T))] unsafe { ffi::PyModuleDef_Init(self.ffi_def.get()) } - #[cfg(_Py_OPAQUE_PYOBJECT)] + #[cfg(Py_TARGET_ABI3T)] panic!("TODO: fix this panic"); } @@ -512,7 +512,7 @@ mod tests { let module_def: ModuleDef = ModuleDef::new(NAME, DOC, &SLOTS); - #[cfg(not(_Py_OPAQUE_PYOBJECT))] + #[cfg(not(Py_TARGET_ABI3T))] unsafe { assert_eq!((*module_def.ffi_def.get()).m_slots, SLOTS.0.get().cast()); } diff --git a/src/pybacked.rs b/src/pybacked.rs index 0fc0bf4b875..ae7b99a1d91 100644 --- a/src/pybacked.rs +++ b/src/pybacked.rs @@ -4,18 +4,15 @@ use crate::inspect::PyStaticExpr; #[cfg(feature = "experimental-inspect")] use crate::type_hint_union; -#[cfg(any( - feature = "experimental-inspect", - not(all(Py_LIMITED_API, Py_GIL_DISABLED)) -))] +#[cfg(any(feature = "experimental-inspect", not(Py_TARGET_ABI3T)))] use crate::types::bytearray::PyByteArray; -#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] +#[cfg(not(Py_TARGET_ABI3T))] use crate::types::bytearray::PyByteArrayMethods; use crate::{ types::{bytes::PyBytesMethods, string::PyStringMethods, PyBytes, PyString, PyTuple}, Borrowed, Bound, CastError, FromPyObject, IntoPyObject, Py, PyAny, PyErr, PyTypeInfo, Python, }; -#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] +#[cfg(not(Py_TARGET_ABI3T))] use std::sync::Arc; use std::{borrow::Borrow, convert::Infallible, ops::Deref, ptr::NonNull}; @@ -195,7 +192,7 @@ pub struct PyBackedBytes { #[cfg_attr(feature = "py-clone", derive(Clone))] enum PyBackedBytesStorage { Python(Py), - #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] + #[cfg(not(Py_TARGET_ABI3T))] Rust(Arc<[u8]>), } @@ -209,7 +206,7 @@ impl PyBackedBytes { PyBackedBytesStorage::Python(bytes) => { PyBackedBytesStorage::Python(bytes.clone_ref(py)) } - #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] + #[cfg(not(Py_TARGET_ABI3T))] PyBackedBytesStorage::Rust(bytes) => PyBackedBytesStorage::Rust(bytes.clone()), }, data: self.data, @@ -273,7 +270,7 @@ impl From> for PyBackedBytes { } } -#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] +#[cfg(not(Py_TARGET_ABI3T))] impl From> for PyBackedBytes { fn from(py_bytearray: Bound<'_, PyByteArray>) -> Self { let s = Arc::<[u8]>::from(py_bytearray.to_vec()); @@ -292,7 +289,7 @@ impl<'a, 'py> FromPyObject<'a, 'py> for PyBackedBytes { const INPUT_TYPE: PyStaticExpr = type_hint_union!(PyBytes::TYPE_HINT, PyByteArray::TYPE_HINT); fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { - #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] + #[cfg(not(Py_TARGET_ABI3T))] { if let Ok(bytes) = obj.cast::() { Ok(Self::from(bytes.to_owned())) @@ -313,7 +310,7 @@ impl<'a, 'py> FromPyObject<'a, 'py> for PyBackedBytes { )) } } - #[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))] + #[cfg(Py_TARGET_ABI3T)] { if let Ok(bytes) = obj.cast::() { Ok(Self::from(bytes.to_owned())) @@ -340,7 +337,7 @@ impl<'py> IntoPyObject<'py> for PyBackedBytes { fn into_pyobject(self, py: Python<'py>) -> Result { match self.storage { PyBackedBytesStorage::Python(bytes) => Ok(bytes.into_bound(py)), - #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] + #[cfg(not(Py_TARGET_ABI3T))] PyBackedBytesStorage::Rust(bytes) => Ok(PyBytes::new(py, &bytes)), } } @@ -357,7 +354,7 @@ impl<'py> IntoPyObject<'py> for &PyBackedBytes { fn into_pyobject(self, py: Python<'py>) -> Result { match &self.storage { PyBackedBytesStorage::Python(bytes) => Ok(bytes.bind(py).clone()), - #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] + #[cfg(not(Py_TARGET_ABI3T))] PyBackedBytesStorage::Rust(bytes) => Ok(PyBytes::new(py, bytes)), } } @@ -523,7 +520,7 @@ mod test { } #[test] - #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] + #[cfg(not(Py_TARGET_ABI3T))] fn py_backed_bytes_from_bytearray() { Python::attach(|py| { let b = PyByteArray::new(py, b"abcde"); @@ -545,7 +542,7 @@ mod test { } #[test] - #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] + #[cfg(not(Py_TARGET_ABI3T))] fn rust_backed_bytes_into_pyobject() { Python::attach(|py| { let orig_bytes = PyByteArray::new(py, b"abcde"); @@ -697,7 +694,7 @@ mod test { let b1: PyBackedBytes = PyBytes::new(py, b"abcde").into(); let b2 = b1.clone_ref(py); assert_eq!(b1, b2); - #[cfg_attr(all(Py_GIL_DISABLED, Py_LIMITED_API), allow(irrefutable_let_patterns))] + #[cfg_attr(Py_TARGET_ABI3T, allow(irrefutable_let_patterns))] let (PyBackedBytesStorage::Python(s1), PyBackedBytesStorage::Python(s2)) = (&b1.storage, &b2.storage) else { @@ -711,7 +708,7 @@ mod test { } #[cfg(feature = "py-clone")] - #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] + #[cfg(not(Py_TARGET_ABI3T))] #[test] fn test_backed_bytes_from_bytearray_clone() { Python::attach(|py| { @@ -724,7 +721,7 @@ mod test { }); } - #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] + #[cfg(not(Py_TARGET_ABI3T))] #[test] fn test_backed_bytes_from_bytearray_clone_ref() { Python::attach(|py| { @@ -744,7 +741,7 @@ mod test { } #[test] - #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] + #[cfg(not(Py_TARGET_ABI3T))] fn test_backed_bytes_eq() { Python::attach(|py| { let b1: PyBackedBytes = PyBytes::new(py, b"abcde").into(); @@ -776,7 +773,7 @@ mod test { }; assert_eq!(h, h1); - #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] + #[cfg(not(Py_TARGET_ABI3T))] { let b2: PyBackedBytes = PyByteArray::new(py, b"abcde").into(); let h2 = { diff --git a/src/pycell/impl_.rs b/src/pycell/impl_.rs index 0496c78f94f..aea9900a1e1 100644 --- a/src/pycell/impl_.rs +++ b/src/pycell/impl_.rs @@ -638,16 +638,16 @@ mod tests { #[test] fn test_inherited_size() { - #[cfg(_Py_OPAQUE_PYOBJECT)] + #[cfg(Py_TARGET_ABI3T)] type ClassObject = PyVariableClassObject; - #[cfg(not(_Py_OPAQUE_PYOBJECT))] + #[cfg(not(Py_TARGET_ABI3T))] type ClassObject = PyStaticClassObject; let base_without_data_size = ClassObject::::BASIC_SIZE; let base_with_data_size = ClassObject::::BASIC_SIZE; let child_without_data_size = ClassObject::::BASIC_SIZE; let child_with_data_size = ClassObject::::BASIC_SIZE; - #[cfg(_Py_OPAQUE_PYOBJECT)] + #[cfg(Py_TARGET_ABI3T)] { assert!(base_without_data_size < 0); // negative indicates variable sized assert!(base_with_data_size < base_without_data_size); @@ -657,7 +657,7 @@ mod tests { child_with_data_size ); } - #[cfg(not(_Py_OPAQUE_PYOBJECT))] + #[cfg(not(Py_TARGET_ABI3T))] { assert!(base_without_data_size > 0); assert!(base_with_data_size > base_without_data_size); diff --git a/src/sync.rs b/src/sync.rs index fbe6c0437e3..06e8326b15b 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -9,7 +9,7 @@ //! interpreter. //! //! This module provides synchronization primitives which are able to synchronize under these conditions. -#[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] +#[cfg(not(Py_TARGET_ABI3T))] use crate::types::PyAny; use crate::{internal::state::SuspendAttach, sealed::Sealed, types::PyString, Bound, Py, Python}; use std::{ @@ -27,7 +27,7 @@ pub(crate) mod once_lock; since = "0.28.0", note = "use pyo3::sync::critical_section::with_critical_section instead" )] -#[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] +#[cfg(not(Py_TARGET_ABI3T))] pub fn with_critical_section(object: &Bound<'_, PyAny>, f: F) -> R where F: FnOnce() -> R, @@ -40,7 +40,7 @@ where since = "0.28.0", note = "use pyo3::sync::critical_section::with_critical_section2 instead" )] -#[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] +#[cfg(not(Py_TARGET_ABI3T))] pub fn with_critical_section2(a: &Bound<'_, PyAny>, b: &Bound<'_, PyAny>, f: F) -> R where F: FnOnce() -> R, diff --git a/src/sync/critical_section.rs b/src/sync/critical_section.rs index 7fe91058fa3..3353c57b3ad 100644 --- a/src/sync/critical_section.rs +++ b/src/sync/critical_section.rs @@ -42,7 +42,7 @@ use crate::types::PyMutex; #[cfg(all(Py_3_14, not(Py_LIMITED_API)))] use crate::Python; -#[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] +#[cfg(not(Py_TARGET_ABI3T))] use crate::{types::PyAny, Bound}; #[cfg(all(Py_3_14, not(Py_LIMITED_API)))] use std::cell::UnsafeCell; @@ -277,34 +277,34 @@ mod tests { use super::{with_critical_section_mutex, with_critical_section_mutex2}; #[cfg(all(not(Py_LIMITED_API), Py_3_14))] use crate::types::PyMutex; - #[cfg(all(not(all(Py_LIMITED_API, Py_GIL_DISABLED)), feature = "macros"))] + #[cfg(all(not(Py_TARGET_ABI3T), feature = "macros"))] use std::sync::atomic::{AtomicBool, Ordering}; #[cfg(all( - not(all(Py_LIMITED_API, Py_GIL_DISABLED)), + not(Py_TARGET_ABI3T), any(feature = "macros", all(not(Py_LIMITED_API), Py_3_14)) ))] use std::sync::Barrier; - #[cfg(all(not(all(Py_LIMITED_API, Py_GIL_DISABLED)), feature = "macros"))] + #[cfg(all(not(Py_TARGET_ABI3T), feature = "macros"))] use crate::Py; #[cfg(all( - not(all(Py_LIMITED_API, Py_GIL_DISABLED)), + not(Py_TARGET_ABI3T), any(feature = "macros", all(not(Py_LIMITED_API), Py_3_14)) ))] use crate::Python; - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] #[cfg(feature = "macros")] #[crate::pyclass(crate = "crate")] struct VecWrapper(Vec); - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] #[cfg(feature = "macros")] #[crate::pyclass(crate = "crate")] struct BoolWrapper(AtomicBool); #[cfg(feature = "macros")] - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] #[test] fn test_critical_section() { let barrier = Barrier::new(2); @@ -370,7 +370,7 @@ mod tests { #[cfg(feature = "macros")] #[test] - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] fn test_critical_section2() { let barrier = Barrier::new(3); @@ -453,7 +453,7 @@ mod tests { #[cfg(feature = "macros")] #[test] - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] fn test_critical_section2_same_object_no_deadlock() { let barrier = Barrier::new(2); @@ -519,7 +519,7 @@ mod tests { #[cfg(feature = "macros")] #[test] - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] fn test_critical_section2_two_containers() { let (vec1, vec2) = Python::attach(|py| { ( diff --git a/src/types/any.rs b/src/types/any.rs index a6a5025d4a5..d9dd344a808 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -4,7 +4,7 @@ use crate::conversion::{FromPyObject, IntoPyObject}; use crate::err::{PyErr, PyResult}; use crate::exceptions::{PyAttributeError, PyTypeError}; use crate::ffi_ptr_ext::FfiPtrExt; -#[cfg(not(_Py_OPAQUE_PYOBJECT))] +#[cfg(not(Py_TARGET_ABI3T))] use crate::impl_::pycell::{PyClassObjectBase, PyStaticClassObject}; use crate::instance::Bound; use crate::internal::get_slot::TP_DESCR_GET; @@ -54,7 +54,7 @@ pyobject_native_type_info!( pyobject_native_type_sized!(PyAny, ffi::PyObject); // We could use pyobject_subclassable_native_type here, but for now only on // opaque PyObject builds to not introduce behavior changes on older Python releases -#[cfg(not(_Py_OPAQUE_PYOBJECT))] +#[cfg(not(Py_TARGET_ABI3T))] impl crate::impl_::pyclass::PyClassBaseType for PyAny { type LayoutAsBase = PyClassObjectBase; type BaseNativeType = PyAny; @@ -63,7 +63,7 @@ impl crate::impl_::pyclass::PyClassBaseType for PyAny { type Layout = PyStaticClassObject; } -#[cfg(_Py_OPAQUE_PYOBJECT)] +#[cfg(Py_TARGET_ABI3T)] pyobject_subclassable_native_type!(PyAny, ffi::PyObject); /// This trait represents the Python APIs which are usable on all Python objects. diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index ebb6d712bd7..6d403d4d8ed 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -2,7 +2,7 @@ use crate::err::{PyErr, PyResult}; use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::{Borrowed, Bound}; use crate::py_result_ext::PyResultExt; -#[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] +#[cfg(not(Py_TARGET_ABI3T))] use crate::sync::critical_section::with_critical_section; use crate::{ffi, PyAny, Python}; use std::slice; @@ -132,14 +132,14 @@ pub trait PyByteArrayMethods<'py>: crate::sealed::Sealed { /// /// ```rust /// use pyo3::prelude::*; - /// # #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] + /// # #[cfg(not(Py_TARGET_ABI3T))] /// use pyo3::exceptions::PyRuntimeError; - /// # #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] + /// # #[cfg(not(Py_TARGET_ABI3T))] /// use pyo3::sync::critical_section::with_critical_section; - /// # #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] + /// # #[cfg(not(Py_TARGET_ABI3T))] /// use pyo3::types::PyByteArray; /// - /// # #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] + /// # #[cfg(not(Py_TARGET_ABI3T))] /// #[pyfunction] /// fn a_valid_function(bytes: &Bound<'_, PyByteArray>) -> PyResult<()> { /// let section = with_critical_section(bytes, || { @@ -160,9 +160,9 @@ pub trait PyByteArrayMethods<'py>: crate::sealed::Sealed { /// /// Ok(()) /// } - /// # #[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))] + /// # #[cfg(Py_TARGET_ABI3T)] /// # fn main() -> () {} - /// # #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] + /// # #[cfg(not(Py_TARGET_ABI3T))] /// # fn main() -> PyResult<()> { /// # Python::attach(|py| -> PyResult<()> { /// # let fun = wrap_pyfunction!(a_valid_function, py)?; @@ -244,7 +244,7 @@ pub trait PyByteArrayMethods<'py>: crate::sealed::Sealed { /// pyo3::py_run!(py, bytearray, "assert bytearray == b'Hello World.'"); /// # }); /// ``` - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] fn to_vec(&self) -> Vec; /// Resizes the bytearray object to the new length `len`. @@ -277,7 +277,7 @@ impl<'py> PyByteArrayMethods<'py> for Bound<'py, PyByteArray> { unsafe { self.as_borrowed().as_bytes_mut() } } - #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] + #[cfg(not(Py_TARGET_ABI3T))] fn to_vec(&self) -> Vec { with_critical_section(self, || { // SAFETY: @@ -368,7 +368,7 @@ mod tests { } #[test] - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] fn test_to_vec() { Python::attach(|py| { let src = b"Hello Python"; @@ -470,7 +470,7 @@ mod tests { any(Py_3_14, not(all(Py_3_13, Py_GIL_DISABLED))) ))] #[test] - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] fn test_data_integrity_in_critical_section() { use crate::instance::Py; use crate::sync::{critical_section::with_critical_section, MutexExt}; diff --git a/src/types/dict.rs b/src/types/dict.rs index 3d11ea5b383..ce3de845993 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -182,7 +182,7 @@ pub trait PyDictMethods<'py>: crate::sealed::Sealed { /// nightly feature is not enabled because we cannot implement an optimised version of /// `iter().try_fold()` on stable yet. If your iteration is infallible then this method has the /// same performance as `.iter().for_each()`. - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] fn locked_for_each(&self, closure: F) -> PyResult<()> where F: Fn(Bound<'py, PyAny>, Bound<'py, PyAny>) -> PyResult<()>; @@ -348,7 +348,7 @@ impl<'py> PyDictMethods<'py> for Bound<'py, PyDict> { BoundDictIterator::new(self.clone()) } - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] fn locked_for_each(&self, f: F) -> PyResult<()> where F: Fn(Bound<'py, PyAny>, Bound<'py, PyAny>) -> PyResult<()>, @@ -420,16 +420,16 @@ pub struct BoundDictIterator<'py> { enum DictIterImpl { DictIter { - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] ppos: ffi::Py_ssize_t, di_used: ffi::Py_ssize_t, remaining: ffi::Py_ssize_t, - #[cfg(all(Py_GIL_DISABLED, Py_LIMITED_API))] + #[cfg(Py_TARGET_ABI3T)] iter: *mut ffi::PyObject, }, } -#[cfg(all(Py_GIL_DISABLED, Py_LIMITED_API))] +#[cfg(Py_TARGET_ABI3T)] impl Drop for DictIterImpl { fn drop(&mut self) { match self { @@ -454,9 +454,9 @@ impl DictIterImpl { Self::DictIter { di_used, remaining, - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] ppos, - #[cfg(all(Py_GIL_DISABLED, Py_LIMITED_API))] + #[cfg(Py_TARGET_ABI3T)] iter, .. } => { @@ -486,7 +486,7 @@ impl DictIterImpl { panic!("dictionary keys changed during iteration"); }; - #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] + #[cfg(not(Py_TARGET_ABI3T))] { let mut key: *mut ffi::PyObject = std::ptr::null_mut(); let mut value: *mut ffi::PyObject = std::ptr::null_mut(); @@ -505,7 +505,7 @@ impl DictIterImpl { None } } - #[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))] + #[cfg(Py_TARGET_ABI3T)] { let py = dict.py(); let mut key: *mut ffi::PyObject = std::ptr::null_mut(); @@ -531,7 +531,7 @@ impl DictIterImpl { } } - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] #[cfg(Py_GIL_DISABLED)] #[inline] fn with_critical_section(&mut self, dict: &Bound<'_, PyDict>, f: F) -> R @@ -551,7 +551,7 @@ impl<'py> Iterator for BoundDictIterator<'py> { #[inline] fn next(&mut self) -> Option { - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] #[cfg(Py_GIL_DISABLED)] { self.inner @@ -559,7 +559,7 @@ impl<'py> Iterator for BoundDictIterator<'py> { inner.next_unchecked(&self.dict) }) } - #[cfg(any(all(Py_GIL_DISABLED, Py_LIMITED_API), not(Py_GIL_DISABLED)))] + #[cfg(any(Py_TARGET_ABI3T, not(Py_GIL_DISABLED)))] { // FIXME: Unsafe with Py_GIL_DISABLED, but no critical sections in the stable ABI unsafe { self.inner.next_unchecked(&self.dict) } @@ -582,7 +582,7 @@ impl<'py> Iterator for BoundDictIterator<'py> { #[inline] #[cfg(Py_GIL_DISABLED)] - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] fn fold(mut self, init: B, mut f: F) -> B where Self: Sized, @@ -615,7 +615,7 @@ impl<'py> Iterator for BoundDictIterator<'py> { } #[inline] - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] fn all(&mut self, mut f: F) -> bool where @@ -633,7 +633,7 @@ impl<'py> Iterator for BoundDictIterator<'py> { } #[inline] - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] fn any(&mut self, mut f: F) -> bool where @@ -651,7 +651,7 @@ impl<'py> Iterator for BoundDictIterator<'py> { } #[inline] - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] fn find

(&mut self, mut predicate: P) -> Option where @@ -669,7 +669,7 @@ impl<'py> Iterator for BoundDictIterator<'py> { } #[inline] - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] fn find_map(&mut self, mut f: F) -> Option where @@ -687,7 +687,7 @@ impl<'py> Iterator for BoundDictIterator<'py> { } #[inline] - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] fn position

(&mut self, mut predicate: P) -> Option where @@ -718,7 +718,7 @@ impl ExactSizeIterator for BoundDictIterator<'_> { impl<'py> BoundDictIterator<'py> { fn new(dict: Bound<'py, PyDict>) -> Self { let di_used = dict_len(&dict); - #[cfg(all(Py_GIL_DISABLED, Py_LIMITED_API))] + #[cfg(Py_TARGET_ABI3T)] let iter = { let new_iter = unsafe { ffi::PyObject_GetIter(dict.as_ptr()) }; assert!( @@ -731,11 +731,11 @@ impl<'py> BoundDictIterator<'py> { Self { dict, inner: DictIterImpl::DictIter { - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] ppos: 0, di_used, remaining: di_used, - #[cfg(all(Py_GIL_DISABLED, Py_LIMITED_API))] + #[cfg(Py_TARGET_ABI3T)] iter, }, } diff --git a/src/types/list.rs b/src/types/list.rs index ffadd0521cf..ce5ef7b68ab 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -211,7 +211,7 @@ pub trait PyListMethods<'py>: crate::sealed::Sealed { /// iterator. Otherwise, the list will not be modified during iteration. /// /// This is equivalent to for_each if the GIL is enabled. - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] fn locked_for_each(&self, closure: F) -> PyResult<()> where F: Fn(Bound<'py, PyAny>) -> PyResult<()>; @@ -421,7 +421,7 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> { } /// Iterates over a list while holding a critical section, calling a closure on each item - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] fn locked_for_each(&self, closure: F) -> PyResult<()> where F: Fn(Bound<'py, PyAny>) -> PyResult<()>, @@ -519,7 +519,7 @@ impl<'py> BoundListIterator<'py> { #[inline] #[cfg(not(feature = "nightly"))] - #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] + #[cfg(not(Py_TARGET_ABI3T))] fn nth( index: &mut Index, length: &mut Length, @@ -591,7 +591,7 @@ impl<'py> BoundListIterator<'py> { #[inline] #[cfg(not(feature = "nightly"))] - #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] + #[cfg(not(Py_TARGET_ABI3T))] fn nth_back( index: &mut Index, length: &mut Length, @@ -620,7 +620,7 @@ impl<'py> BoundListIterator<'py> { } #[allow(dead_code)] - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] fn with_critical_section( &mut self, f: impl FnOnce(&mut Index, &mut Length, &Bound<'py, PyList>) -> R, @@ -658,7 +658,7 @@ impl<'py> Iterator for BoundListIterator<'py> { #[inline] #[cfg(not(feature = "nightly"))] - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] fn nth(&mut self, n: usize) -> Option { self.with_critical_section(|index, length, list| Self::nth(index, length, list, n)) } @@ -854,7 +854,7 @@ impl DoubleEndedIterator for BoundListIterator<'_> { #[inline] #[cfg(not(feature = "nightly"))] - #[cfg(not(all(Py_GIL_DISABLED, Py_LIMITED_API)))] + #[cfg(not(Py_TARGET_ABI3T))] fn nth_back(&mut self, n: usize) -> Option { self.with_critical_section(|index, length, list| Self::nth_back(index, length, list, n)) } diff --git a/tests/test_append_to_inittab.rs b/tests/test_append_to_inittab.rs index ba28a6fde68..ddb693090f2 100644 --- a/tests/test_append_to_inittab.rs +++ b/tests/test_append_to_inittab.rs @@ -19,7 +19,7 @@ mod module_mod_with_functions { use super::foo; } -#[cfg(not(any(PyPy, GraalPy, _Py_OPAQUE_PYOBJECT)))] +#[cfg(not(any(PyPy, GraalPy, Py_TARGET_ABI3T)))] #[test] fn test_module_append_to_inittab() { use pyo3::append_to_inittab; From 0ceda48363319e33700663be17cf178e51c7e07a Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 8 Apr 2026 10:17:15 -0600 Subject: [PATCH 56/74] run formatter --- pyo3-ffi/src/structmember.rs | 6 +++--- src/pyclass/create_type_object.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyo3-ffi/src/structmember.rs b/pyo3-ffi/src/structmember.rs index 4aa6c24db4f..2d9dd5a4a1f 100644 --- a/pyo3-ffi/src/structmember.rs +++ b/pyo3-ffi/src/structmember.rs @@ -2,8 +2,6 @@ use std::ffi::c_int; pub use crate::PyMemberDef; -#[allow(deprecated)] -pub use crate::_Py_T_OBJECT as T_OBJECT; pub use crate::Py_T_BOOL as T_BOOL; pub use crate::Py_T_BYTE as T_BYTE; pub use crate::Py_T_CHAR as T_CHAR; @@ -21,10 +19,12 @@ pub use crate::Py_T_UINT as T_UINT; pub use crate::Py_T_ULONG as T_ULONG; pub use crate::Py_T_ULONGLONG as T_ULONGLONG; pub use crate::Py_T_USHORT as T_USHORT; +#[allow(deprecated)] +pub use crate::_Py_T_OBJECT as T_OBJECT; +pub use crate::Py_T_PYSSIZET as T_PYSSIZET; #[allow(deprecated)] pub use crate::_Py_T_NONE as T_NONE; -pub use crate::Py_T_PYSSIZET as T_PYSSIZET; /* Flags */ pub use crate::Py_READONLY as READONLY; diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index 063b615c8a4..9c72a9b4d1a 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -11,7 +11,7 @@ use crate::{ assign_sequence_item_from_mapping, get_sequence_item_from_mapping, tp_dealloc, tp_dealloc_with_gc, PyClassImpl, PyClassItemsIter, PyObjectOffset, }, - pymethods::{_call_clear, Getter, PyGetterDef, PyMethodDefType, PySetterDef, Setter}, + pymethods::{Getter, PyGetterDef, PyMethodDefType, PySetterDef, Setter, _call_clear}, trampoline::trampoline, }, pycell::impl_::PyClassObjectLayout, From d1ef669d5f8140a8a45dce1e39e8fae20eb8c11e Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 8 Apr 2026 10:30:22 -0600 Subject: [PATCH 57/74] fix check-feature-powerset --- noxfile.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/noxfile.py b/noxfile.py index ff14dc67f31..f54b1a081eb 100644 --- a/noxfile.py +++ b/noxfile.py @@ -86,6 +86,7 @@ def _supported_interpreter_versions( PY_VERSIONS = _supported_interpreter_versions("cpython") ABI3_PY_VERSIONS = [p for p in PY_VERSIONS if not p.endswith("t")] +ABI3T_PY_VERSIONS = [p for p in PY_VERSIONS if int(p.split(".")[0]) > 14] PYPY_VERSIONS = _supported_interpreter_versions("pypy") @@ -1349,6 +1350,10 @@ def check_feature_powerset(session: nox.Session): f"abi3-py3{ver.split('.')[1]}" for ver in ABI3_PY_VERSIONS } + EXPECTED_ABI3T_FEATURES = { + f"abi3-py3{ver.split('.')[1]}" for ver in ABI3T_PY_VERSIONS + } + EXCLUDED_FROM_FULL = { "nightly", "extension-module", @@ -1362,20 +1367,27 @@ def check_feature_powerset(session: nox.Session): features = cargo_toml["features"] full_feature = set(features["full"]) - abi3_features = {feature for feature in features if feature.startswith("abi3")} + abi3_features = {feature for feature in features if feature.startswith("abi3") and not feature.startswith("abi3t")} abi3_version_features = abi3_features - {"abi3"} - unexpected_abi3_features = abi3_version_features - EXPECTED_ABI3_FEATURES - if unexpected_abi3_features: + abi3t_features = {feature for feature in features if feature.startswith("abi3t")} + abi3t_version_features = abi3_features - {"abi3t"} + + unexpected_stable_abi_features = abi3_version_features - EXPECTED_ABI3_FEATURES - EXPECTED_ABI3T_FEATURES + if unexpected_stable_abi_features: session.error( - f"unexpected `abi3` features found in Cargo.toml: {unexpected_abi3_features}" + f"unexpected `abi3` or `abi3t` features found in Cargo.toml: {unexpected_stable_abi_features}" ) missing_abi3_features = EXPECTED_ABI3_FEATURES - abi3_version_features if missing_abi3_features: session.error(f"missing `abi3` features in Cargo.toml: {missing_abi3_features}") - expected_full_feature = features.keys() - EXCLUDED_FROM_FULL - abi3_features + missing_abi3t_features = EXPECTED_ABI3T_FEATURES - abi3t_version_features + if missing_abi3t_features: + session.error(f"missing `abi3t` features in Cargo.toml: {missing_abi3t_features}") + + expected_full_feature = features.keys() - EXCLUDED_FROM_FULL - abi3_features - abi3t_features uncovered_features = expected_full_feature - full_feature if uncovered_features: @@ -1404,6 +1416,7 @@ def check_feature_powerset(session: nox.Session): features_to_skip = [ *(EXCLUDED_FROM_FULL), *abi3_version_features, + *abi3t_version_features, ] # deny warnings From 26c7dc021f53b16860055a4c6f0c874b8c7087c7 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 8 Apr 2026 10:31:47 -0600 Subject: [PATCH 58/74] increment SUPPORTED_VERSIONS_CPYTHON.max --- pyo3-ffi/build.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 3b6d2bced52..25926b21f01 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -18,7 +18,7 @@ const SUPPORTED_VERSIONS_CPYTHON: SupportedVersions = SupportedVersions { min: PythonVersion { major: 3, minor: 8 }, max: PythonVersion { major: 3, - minor: 14, + minor: 15, }, }; From 95ca7d279e6ac440dd41399bf72a8e280f9feea2 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 8 Apr 2026 10:33:46 -0600 Subject: [PATCH 59/74] fix conditional compilation for critical section API --- src/sync/critical_section.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sync/critical_section.rs b/src/sync/critical_section.rs index 3353c57b3ad..b3cf50554ca 100644 --- a/src/sync/critical_section.rs +++ b/src/sync/critical_section.rs @@ -126,7 +126,7 @@ impl EnteredCriticalSection<'_, T> { /// /// This is structurally equivalent to the use of the paired Py_BEGIN_CRITICAL_SECTION and /// Py_END_CRITICAL_SECTION C-API macros. -#[cfg(not(Py_LIMITED_API))] +#[cfg(not(Py_TARGET_ABI3T))] #[cfg_attr(not(Py_GIL_DISABLED), allow(unused_variables))] pub fn with_critical_section(object: &Bound<'_, PyAny>, f: F) -> R where @@ -154,7 +154,7 @@ where /// /// This is structurally equivalent to the use of the paired /// Py_BEGIN_CRITICAL_SECTION2 and Py_END_CRITICAL_SECTION2 C-API macros. -#[cfg(not(Py_LIMITED_API))] +#[cfg(not(Py_TARGET_ABI3T))] #[cfg_attr(not(Py_GIL_DISABLED), allow(unused_variables))] pub fn with_critical_section2(a: &Bound<'_, PyAny>, b: &Bound<'_, PyAny>, f: F) -> R where From b5968853315596954bd878176c4e1ebc0604236b Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 8 Apr 2026 10:35:59 -0600 Subject: [PATCH 60/74] fix ruff --- noxfile.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/noxfile.py b/noxfile.py index f54b1a081eb..f3578ded883 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1367,13 +1367,19 @@ def check_feature_powerset(session: nox.Session): features = cargo_toml["features"] full_feature = set(features["full"]) - abi3_features = {feature for feature in features if feature.startswith("abi3") and not feature.startswith("abi3t")} + abi3_features = { + feature + for feature in features + if feature.startswith("abi3") and not feature.startswith("abi3t") + } abi3_version_features = abi3_features - {"abi3"} abi3t_features = {feature for feature in features if feature.startswith("abi3t")} abi3t_version_features = abi3_features - {"abi3t"} - unexpected_stable_abi_features = abi3_version_features - EXPECTED_ABI3_FEATURES - EXPECTED_ABI3T_FEATURES + unexpected_stable_abi_features = ( + abi3_version_features - EXPECTED_ABI3_FEATURES - EXPECTED_ABI3T_FEATURES + ) if unexpected_stable_abi_features: session.error( f"unexpected `abi3` or `abi3t` features found in Cargo.toml: {unexpected_stable_abi_features}" @@ -1385,9 +1391,13 @@ def check_feature_powerset(session: nox.Session): missing_abi3t_features = EXPECTED_ABI3T_FEATURES - abi3t_version_features if missing_abi3t_features: - session.error(f"missing `abi3t` features in Cargo.toml: {missing_abi3t_features}") + session.error( + f"missing `abi3t` features in Cargo.toml: {missing_abi3t_features}" + ) - expected_full_feature = features.keys() - EXCLUDED_FROM_FULL - abi3_features - abi3t_features + expected_full_feature = ( + features.keys() - EXCLUDED_FROM_FULL - abi3_features - abi3t_features + ) uncovered_features = expected_full_feature - full_feature if uncovered_features: From 22a5332c7dc8c29dd9e243d88aacff9ca3dcda63 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 8 Apr 2026 13:38:58 -0600 Subject: [PATCH 61/74] fix incorrect conditional compilation guargs --- pyo3-ffi/src/modsupport.rs | 6 +++--- src/sync/critical_section.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyo3-ffi/src/modsupport.rs b/pyo3-ffi/src/modsupport.rs index 197c297ac2f..73edc4d75b4 100644 --- a/pyo3-ffi/src/modsupport.rs +++ b/pyo3-ffi/src/modsupport.rs @@ -157,11 +157,11 @@ const _PyABIInfo_DEFAULT_FLAG_STABLE: u16 = 0; // skipped PyABIInfo_DEFAULT_ABI_VERSION: depends on Py_VERSION_HEX -#[cfg(all(Py_3_15, Py_LIMITED_API, Py_TARGET_ABI3T))] +#[cfg(all(Py_3_15, Py_TARGET_ABI3T))] const _PyABIInfo_DEFAULT_FLAG_FT: u16 = PyABIInfo_FREETHREADING_AGNOSTIC; -#[cfg(all(Py_3_15, Py_GIL_DISABLED, not(Py_LIMITED_API)))] +#[cfg(all(Py_3_15, Py_GIL_DISABLED, not(Py_TARGET_ABI3T)))] const _PyABIInfo_DEFAULT_FLAG_FT: u16 = PyABIInfo_FREETHREADED; -#[cfg(all(Py_3_15, not(Py_GIL_DISABLED), not(Py_LIMITED_API)))] +#[cfg(all(Py_3_15, not(Py_GIL_DISABLED), not(Py_TARGET_ABI3T)))] const _PyABIInfo_DEFAULT_FLAG_FT: u16 = PyABIInfo_GIL; #[cfg(Py_3_15)] diff --git a/src/sync/critical_section.rs b/src/sync/critical_section.rs index b3cf50554ca..06ff92cdd51 100644 --- a/src/sync/critical_section.rs +++ b/src/sync/critical_section.rs @@ -271,7 +271,7 @@ where #[cfg(test)] mod tests { #[cfg(feature = "macros")] - #[cfg(not(Py_LIMITED_API))] + #[cfg(not(Py_TARGET_ABI3T))] use super::{with_critical_section, with_critical_section2}; #[cfg(all(not(Py_LIMITED_API), Py_3_14))] use super::{with_critical_section_mutex, with_critical_section_mutex2}; From ec1269a48116e594b935526fa164c8dc5256ead2 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 8 Apr 2026 14:08:19 -0600 Subject: [PATCH 62/74] fix noxfile and ban abi3t builds on 3.14 and older --- noxfile.py | 6 ++++-- pyo3-build-config/src/impl_.rs | 7 +++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/noxfile.py b/noxfile.py index f3578ded883..5bc3a9f03a8 100644 --- a/noxfile.py +++ b/noxfile.py @@ -142,7 +142,7 @@ def test_rust(session: nox.Session): if ( feature_set - and "abi3" in feature_set + and "abi3t" in feature_set and "full" in feature_set and sys.version_info >= (3, 16) ): @@ -1545,7 +1545,9 @@ def _get_feature_sets() -> Tuple[Optional[str], ...]: return (None, features) # do fewer abi3t builds? - return (None, "abi3", "abi3t", features, f"abi3,{features}", f"abi3t,{features}") + if sys.version_info >= (3, 15): + return (None, "abi3", "abi3t", features, f"abi3,{features}", f"abi3t,{features}") + return (None, "abi3", features, f"abi3,{features}") _RELEASE_LINE_START = "release: " diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index eb2d417b7e0..4a17d5f353c 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -371,6 +371,13 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let stable_abi = CPythonABI::from_build_env()?; + if let CPythonABI::ABI3t = stable_abi { + ensure!( + version.minor > 14, + "abi3t is supported on Python 3.15 and newer but build is for Python {version}" + ); + } + let implementation = map["implementation"].parse()?; let gil_disabled = match map["gil_disabled"].as_str() { From 04eb8bb94d79806cb74f5ba5a3647bf8e4082c41 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 8 Apr 2026 14:10:22 -0600 Subject: [PATCH 63/74] ruff format --- noxfile.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 5bc3a9f03a8..7be4080e35b 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1546,7 +1546,14 @@ def _get_feature_sets() -> Tuple[Optional[str], ...]: # do fewer abi3t builds? if sys.version_info >= (3, 15): - return (None, "abi3", "abi3t", features, f"abi3,{features}", f"abi3t,{features}") + return ( + None, + "abi3", + "abi3t", + features, + f"abi3,{features}", + f"abi3t,{features}", + ) return (None, "abi3", features, f"abi3,{features}") From 101949cb4dae02379284afe7a7f16e00606e7495 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 8 Apr 2026 14:17:07 -0600 Subject: [PATCH 64/74] attempt to fix semver-checks --- .github/workflows/ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38a8e2bddee..83a083ab81a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,6 +64,14 @@ jobs: - uses: obi1kenobi/cargo-semver-checks-action@v2 with: baseline-rev: ${{ steps.fetch_merge_base.outputs.merge_base }} + feature-group: "only-explicit-features" + features: "full" + - uses: obi1kenobi/cargo-semver-checks-action@v2 + with: + baseline-rev: ${{ steps.fetch_merge_base.outputs.merge_base }} + feature-group: "only-explicit-features" + features: "full,abi3" + check-msrv: needs: [fmt, resolve] From 828afddc8f0efd984bda71c95e4868b021f04020 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 8 Apr 2026 14:44:36 -0600 Subject: [PATCH 65/74] fix test-version-limits --- noxfile.py | 32 +++++++++++++++++++++++++------- pyo3-build-config/src/impl_.rs | 7 ++++--- pyo3-ffi/build.rs | 2 +- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/noxfile.py b/noxfile.py index 7be4080e35b..e78df76066d 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1136,22 +1136,23 @@ def test_version_limits(session: nox.Session): with _config_file() as config_file: env["PYO3_CONFIG_FILE"] = config_file.name - assert "3.6" not in PY_VERSIONS + assert "3.7" not in PY_VERSIONS config_file.set("CPython", "3.6") _run_cargo(session, "check", env=env, expect_error=True) - assert "3.16" not in PY_VERSIONS - config_file.set("CPython", "3.16") + assert "3.17" not in PY_VERSIONS + config_file.set("CPython", "3.17") _run_cargo(session, "check", env=env, expect_error=True) - # 3.16 CPython should build if abi3 is explicitly requested + # 3.17 CPython should build if abi3 is explicitly requested _run_cargo(session, "check", "--features=pyo3/abi3", env=env) - # 3.15 CPython should build with forward compatibility - # TODO: check on 3.16 when adding abi3-py315 support - config_file.set("CPython", "3.15") + # 3.16 CPython should build with forward compatibility + # TODO: check on 3.17 when adding abi3-py316 support + config_file.set("CPython", "3.16") env["PYO3_USE_ABI3_FORWARD_COMPATIBILITY"] = "1" _run_cargo(session, "check", env=env) + del env["PYO3_USE_ABI3_FORWARD_COMPATIBILITY"] assert "3.10" not in PYPY_VERSIONS config_file.set("PyPy", "3.10") @@ -1165,6 +1166,23 @@ def test_version_limits(session: nox.Session): config_file.set("CPython", "3.14t") _run_cargo(session, "check", env=env) + # 3.15t is PyO3's maximum version of free-threaded Python + config_file.set("CPython", "3.15t") + _run_cargo(session, "check", env=env) + + # 3.16t should build with abi3t forward compatibility + config_file.set("CPython", "3.16t") + env["PYO3_USE_ABI3T_FORWARD_COMPATIBILITY"] = "1" + _run_cargo(session, "check", env=env) + del env["PYO3_USE_ABI3T_FORWARD_COMPATIBILITY"] + + # 3.17t isn't supported + config_file.set("CPython", "3.17t") + _run_cargo(session, "check", env=env, expect_error=True) + + # 3.17t CPython should build if abi3 is explicitly requested + _run_cargo(session, "check", "--features=pyo3/abi3t", env=env) + # attempt to build with latest version and check that abi3 version # configured matches the feature max_minor_version = max(int(v.split(".")[1]) for v in ABI3_PY_VERSIONS) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 4a17d5f353c..fbc257d65c5 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -774,7 +774,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) return Ok(()); } - self.fixup_for_stable_abi_version(abi3_version, is_abi3)?; + self.fixup_for_stable_abi_version(abi3_version, is_abi3, "abi3")?; Ok(()) } @@ -787,7 +787,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) return Ok(()); } - self.fixup_for_stable_abi_version(abi3t_version, is_abi3t)?; + self.fixup_for_stable_abi_version(abi3t_version, is_abi3t, "abi3t")?; Ok(()) } @@ -797,6 +797,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) &mut self, abi_version: Option, abi_check: impl Fn() -> bool, + abi_name: &str, ) -> Result<()> { if let Some(version) = abi_version { ensure!( @@ -809,7 +810,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) ); self.version = version; } else if abi_check() && self.version.minor > STABLE_ABI_MAX_MINOR { - warn!("Automatically falling back to abi3-py3{STABLE_ABI_MAX_MINOR} because current Python is higher than the maximum supported"); + warn!("Automatically falling back to {abi_name}-py3{STABLE_ABI_MAX_MINOR} because current Python is higher than the maximum supported"); self.version.minor = STABLE_ABI_MAX_MINOR; } diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 25926b21f01..7e52735e4cd 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -69,7 +69,7 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { let mut error = MaximumVersionExceeded::new(interpreter_config, versions.max); let major = interpreter_config.version.major; let minor = interpreter_config.version.minor; - if interpreter_config.is_free_threaded() && interpreter_config.version.minor >= 15 { + if interpreter_config.is_free_threaded() && interpreter_config.version.minor < 15 { error.add_help(&format!( "the free-threaded build of CPython {major}{minor} does not support the limited API so this check cannot be suppressed.", )); From cd78153e12682b3d268825589366e744cb7a5fcb Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 8 Apr 2026 15:10:36 -0600 Subject: [PATCH 66/74] fix issues spotted by claude --- noxfile.py | 2 +- pyo3-build-config/src/impl_.rs | 5 +++-- src/conversions/bytes.rs | 1 + src/impl_/pymodule.rs | 2 +- src/types/dict.rs | 3 ++- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/noxfile.py b/noxfile.py index e78df76066d..522d4636a27 100644 --- a/noxfile.py +++ b/noxfile.py @@ -86,7 +86,7 @@ def _supported_interpreter_versions( PY_VERSIONS = _supported_interpreter_versions("cpython") ABI3_PY_VERSIONS = [p for p in PY_VERSIONS if not p.endswith("t")] -ABI3T_PY_VERSIONS = [p for p in PY_VERSIONS if int(p.split(".")[0]) > 14] +ABI3T_PY_VERSIONS = [p for p in PY_VERSIONS if int(p.split(".")[1]) > 14] PYPY_VERSIONS = _supported_interpreter_versions("pypy") diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index fbc257d65c5..1dd4b1bcf67 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -782,7 +782,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) /// Updates configured ABI to build for to the requested abi3t version /// This is a no-op for platforms where abi3t is not supported fn fixup_for_abi3t_version(&mut self, abi3t_version: Option) -> Result<()> { - // PyPy, GraalPy, and the free-threaded build don't support abi3; don't adjust the version + // PyPy, GraalPy, and the free-threaded build don't support abi3t; don't adjust the version if self.implementation.is_pypy() || self.implementation.is_graalpy() { return Ok(()); } @@ -2026,6 +2026,7 @@ pub fn make_cross_compile_config() -> Result> { let interpreter_config = if let Some(cross_config) = cross_compiling_from_cargo_env()? { let mut interpreter_config = load_cross_compile_config(cross_config)?; interpreter_config.fixup_for_abi3_version(get_abi3_version())?; + interpreter_config.fixup_for_abi3t_version(get_abi3t_version())?; Some(interpreter_config) } else { None @@ -2044,7 +2045,7 @@ pub fn make_interpreter_config() -> Result { ensure!( !(abi3_version.is_some() && abi3t_version.is_some()), - "Cannot enable abi3 and abit3t features" + "Cannot simultaneously enable abi3 and abi3t features" ); // See if we can safely skip the Python interpreter configuration detection. diff --git a/src/conversions/bytes.rs b/src/conversions/bytes.rs index 78979d6be68..bbd7e1d67ac 100644 --- a/src/conversions/bytes.rs +++ b/src/conversions/bytes.rs @@ -130,6 +130,7 @@ mod tests { } #[test] + #[cfg(not(Py_TARGET_ABI3T))] fn test_bytearray() { Python::attach(|py| { let py_bytearray = PyByteArray::new(py, b"foobar"); diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index bc60c4f8aa8..17f3ede45c0 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -126,7 +126,7 @@ impl ModuleDef { ffi::PyModuleDef_Init(self.ffi_def.get()) } #[cfg(Py_TARGET_ABI3T)] - panic!("TODO: fix this panic"); + panic!("Legacy module initialization cannot work under abi3t"); } /// Builds a module object directly. Used for [`#[pymodule]`][crate::pymodule] submodules. diff --git a/src/types/dict.rs b/src/types/dict.rs index ce3de845993..876bd7a9893 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -561,7 +561,8 @@ impl<'py> Iterator for BoundDictIterator<'py> { } #[cfg(any(Py_TARGET_ABI3T, not(Py_GIL_DISABLED)))] { - // FIXME: Unsafe with Py_GIL_DISABLED, but no critical sections in the stable ABI + // SAFETY: next_unchecked always owns strong references to + // items in the dict under Py_TARGET_ABI3T unsafe { self.inner.next_unchecked(&self.dict) } } } From 5e5671ea3709293a92b9d2ddef6d2504ca6c88a2 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 8 Apr 2026 15:31:04 -0600 Subject: [PATCH 67/74] strip 't' for version parsing --- noxfile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/noxfile.py b/noxfile.py index 522d4636a27..86b3ffbe7ce 100644 --- a/noxfile.py +++ b/noxfile.py @@ -86,7 +86,7 @@ def _supported_interpreter_versions( PY_VERSIONS = _supported_interpreter_versions("cpython") ABI3_PY_VERSIONS = [p for p in PY_VERSIONS if not p.endswith("t")] -ABI3T_PY_VERSIONS = [p for p in PY_VERSIONS if int(p.split(".")[1]) > 14] +ABI3T_PY_VERSIONS = [p for p in PY_VERSIONS if int(p.split(".")[1].strip("t")) > 14] PYPY_VERSIONS = _supported_interpreter_versions("pypy") @@ -126,7 +126,7 @@ def test_rust(session: nox.Session): # so that it can be used in the test code # (e.g. for `#[cfg(feature = "abi3-py38")]`) _run_cargo_test(session, features=feature_set, extra_flags=flags) - + breakpoint() if ( feature_set and "abi3" in feature_set From 266527f869ea1b3562afcfb2ab805f70bc5aa6aa Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 8 Apr 2026 15:31:12 -0600 Subject: [PATCH 68/74] Adjust comments and error messages --- src/impl_/pymodule.rs | 2 +- src/types/dict.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 17f3ede45c0..8ad6088e686 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -126,7 +126,7 @@ impl ModuleDef { ffi::PyModuleDef_Init(self.ffi_def.get()) } #[cfg(Py_TARGET_ABI3T)] - panic!("Legacy module initialization cannot work under abi3t"); + panic!("Legacy module initialization cannot work under abi3t. Use the PyModExport slots-based initialization hook instead."); } /// Builds a module object directly. Used for [`#[pymodule]`][crate::pymodule] submodules. diff --git a/src/types/dict.rs b/src/types/dict.rs index 876bd7a9893..cb4a2ca4c01 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -562,7 +562,9 @@ impl<'py> Iterator for BoundDictIterator<'py> { #[cfg(any(Py_TARGET_ABI3T, not(Py_GIL_DISABLED)))] { // SAFETY: next_unchecked always owns strong references to - // items in the dict under Py_TARGET_ABI3T + // items in the dict under Py_TARGET_ABI3T. Iteration + // uses an iterator object, relying on CPython guarantees + // that the PyDict and PyIter APIs are safe. unsafe { self.inner.next_unchecked(&self.dict) } } } From 294a4f0df6d4ec9520aa180776abf7f7e4510426 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 8 Apr 2026 15:54:15 -0600 Subject: [PATCH 69/74] fix compiler warning --- src/conversions/bytes.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/conversions/bytes.rs b/src/conversions/bytes.rs index bbd7e1d67ac..eec2d26c3e2 100644 --- a/src/conversions/bytes.rs +++ b/src/conversions/bytes.rs @@ -114,7 +114,9 @@ impl<'py> IntoPyObject<'py> for &Bytes { #[cfg(test)] mod tests { use super::*; - use crate::types::{PyAnyMethods, PyByteArray, PyByteArrayMethods, PyBytes}; + use crate::types::{PyAnyMethods, PyBytes}; + #[cfg(not(Py_TARGET_ABI3T))] + use crate::types::{PyByteArray, PyByteArrayMethods}; use crate::Python; #[test] From b37445d3887b0021ec7a35c630ea9af70ff5d3f6 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 8 Apr 2026 15:54:50 -0600 Subject: [PATCH 70/74] more noxfile fixes --- noxfile.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/noxfile.py b/noxfile.py index 86b3ffbe7ce..3570e60d427 100644 --- a/noxfile.py +++ b/noxfile.py @@ -126,10 +126,10 @@ def test_rust(session: nox.Session): # so that it can be used in the test code # (e.g. for `#[cfg(feature = "abi3-py38")]`) _run_cargo_test(session, features=feature_set, extra_flags=flags) - breakpoint() if ( feature_set and "abi3" in feature_set + and "abi3t" not in feature_set and "full" in feature_set and sys.version_info >= (3, 9) ): @@ -1369,7 +1369,7 @@ def check_feature_powerset(session: nox.Session): } EXPECTED_ABI3T_FEATURES = { - f"abi3-py3{ver.split('.')[1]}" for ver in ABI3T_PY_VERSIONS + f"abi3t-py3{ver.split('.')[1].strip("t")}" for ver in ABI3T_PY_VERSIONS } EXCLUDED_FROM_FULL = { @@ -1393,7 +1393,7 @@ def check_feature_powerset(session: nox.Session): abi3_version_features = abi3_features - {"abi3"} abi3t_features = {feature for feature in features if feature.startswith("abi3t")} - abi3t_version_features = abi3_features - {"abi3t"} + abi3t_version_features = abi3t_features - {"abi3t"} unexpected_stable_abi_features = ( abi3_version_features - EXPECTED_ABI3_FEATURES - EXPECTED_ABI3T_FEATURES @@ -1457,17 +1457,18 @@ def check_feature_powerset(session: nox.Session): subcommand = "minimal-versions" comma_join = ",".join - _run_cargo( - session, - subcommand, - "--feature-powerset", - '--optional-deps=""', - f'--skip="{comma_join(features_to_skip)}"', - *(f"--group-features={comma_join(group)}" for group in features_to_group), - "check", - "--all-targets", - env=env, - ) + for abi_name in ["abi3", "abi3t"]: + _run_cargo( + session, + subcommand, + "--feature-powerset", + '--optional-deps=""', + f'--skip="{comma_join(features_to_skip + [abi_name])}"', + *(f"--group-features={comma_join(group)}" for group in features_to_group), + "check", + "--all-targets", + env=env, + ) @nox.session(name="update-ui-tests", venv_backend="none") From a2584f5f5e260e4f59abfc70e72cb26976cacdbb Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 8 Apr 2026 15:56:15 -0600 Subject: [PATCH 71/74] fix ruff --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 3570e60d427..da38e5d5d67 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1369,7 +1369,7 @@ def check_feature_powerset(session: nox.Session): } EXPECTED_ABI3T_FEATURES = { - f"abi3t-py3{ver.split('.')[1].strip("t")}" for ver in ABI3T_PY_VERSIONS + f"abi3t-py3{ver.split('.')[1].strip('t')}" for ver in ABI3T_PY_VERSIONS } EXCLUDED_FROM_FULL = { From 1b5ec7a346e3854b1949f3a269f79724772f8bf9 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 8 Apr 2026 16:02:47 -0600 Subject: [PATCH 72/74] use a 3.15 interpreter to run the feature-powerset tests --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83a083ab81a..956a1883782 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -620,7 +620,7 @@ jobs: - uses: actions/checkout@v6.0.2 - uses: actions/setup-python@v6 with: - python-version: "3.14" + python-version: "3.15-dev" - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} From 553ef54ba86e24535a48fb92595b2983b2b6b65e Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 8 Apr 2026 16:31:45 -0600 Subject: [PATCH 73/74] fix a few more issues caught by claude --- noxfile.py | 4 +++- src/impl_/pyclass.rs | 4 ++++ src/types/dict.rs | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/noxfile.py b/noxfile.py index da38e5d5d67..88a24bfbe53 100644 --- a/noxfile.py +++ b/noxfile.py @@ -86,7 +86,9 @@ def _supported_interpreter_versions( PY_VERSIONS = _supported_interpreter_versions("cpython") ABI3_PY_VERSIONS = [p for p in PY_VERSIONS if not p.endswith("t")] -ABI3T_PY_VERSIONS = [p for p in PY_VERSIONS if int(p.split(".")[1].strip("t")) > 14] +ABI3T_PY_VERSIONS = [ + p for p in PY_VERSIONS if p.endswith("t") and int(p.split(".")[1].strip("t")) > 14 +] PYPY_VERSIONS = _supported_interpreter_versions("pypy") diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index b7cb2e44cf5..05e79c30ab4 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1465,6 +1465,10 @@ mod tests { (offset_of!(ExpectedLayout, contents) + offset_of!(FrozenClass, value)) as ffi::Py_ssize_t ); + #[cfg(not(Py_TARGET_ABI3T))] + assert_eq!(member.flags, ffi::Py_READONLY); + #[cfg(Py_TARGET_ABI3T)] + // ABI3T builds set other flags besides READONLY assert_eq!(member.flags & ffi::Py_READONLY, ffi::Py_READONLY); } _ => panic!("Expected a StructMember"), diff --git a/src/types/dict.rs b/src/types/dict.rs index cb4a2ca4c01..90df4c22360 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -518,13 +518,13 @@ impl DictIterImpl { 1 => unsafe { key.assume_owned_unchecked(py) }, x => panic!("Unknown return value from PyIter_NextItem: {}", x), }; + *remaining -= 1; let value = match dict.get_item(&key) { Ok(value) => value?, Err(e) => { panic!("Iterating over dictionary failed with error '{}'", e) } }; - *remaining -= 1; Some((key, value)) } } From 2f755ae133596cd9df652154bdea28df38ec756d Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 8 Apr 2026 16:33:25 -0600 Subject: [PATCH 74/74] add comment --- src/types/dict.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/types/dict.rs b/src/types/dict.rs index 90df4c22360..d828011917b 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -518,6 +518,9 @@ impl DictIterImpl { 1 => unsafe { key.assume_owned_unchecked(py) }, x => panic!("Unknown return value from PyIter_NextItem: {}", x), }; + // get_item can only fail if another thread concurrently + // removed the key, so we know that remaining definitely + // needs to be decremented no matter what. *remaining -= 1; let value = match dict.get_item(&key) { Ok(value) => value?,