From eb53c2d8d04fcb2cee44dcc2deb243d3de243e85 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 26 Jan 2026 14:15:03 -0700 Subject: [PATCH 001/195] 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 002/195] 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 003/195] 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 004/195] 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 005/195] 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 006/195] 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 007/195] 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 008/195] 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 009/195] 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 010/195] 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 011/195] 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 012/195] 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 013/195] 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 014/195] 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 015/195] 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 016/195] 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 017/195] 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 018/195] 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 019/195] 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 020/195] 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 021/195] 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 022/195] 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 023/195] 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 024/195] 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 025/195] 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 026/195] 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 027/195] 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 028/195] 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 029/195] 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 030/195] 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 031/195] 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 032/195] 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 033/195] 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 034/195] 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 035/195] 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 036/195] 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 037/195] 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 038/195] 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 039/195] 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 040/195] 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 041/195] 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 042/195] 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 043/195] 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 044/195] 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 045/195] 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 046/195] 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 047/195] 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 048/195] 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 049/195] 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 050/195] 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 051/195] 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 052/195] 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 053/195] 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 054/195] 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 055/195] 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 056/195] 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 057/195] 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 058/195] 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 059/195] 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 060/195] 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 061/195] 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 062/195] 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 063/195] 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 064/195] 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 065/195] 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 066/195] 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 067/195] 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 068/195] 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 069/195] 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 070/195] 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 071/195] 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 072/195] 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 073/195] 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 074/195] 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?, From 710e83534b1b745e1109777abbf5b0d645631c6c Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 16 Apr 2026 14:25:44 -0600 Subject: [PATCH 075/195] use correct windows library name for abi3t --- pyo3-build-config/src/impl_.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 1dd4b1bcf67..c5f76bb8cde 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -1835,11 +1835,15 @@ fn default_lib_name_windows( || (stable_abi == CPythonABI::ABI3t && !(implementation.is_pypy() || implementation.is_graalpy())) { - if debug { - Ok(WINDOWS_STABLE_ABI_DEBUG_LIB_NAME.to_owned()) + let mut lib_name = if debug { + WINDOWS_STABLE_ABI_DEBUG_LIB_NAME.to_owned() } else { - Ok(WINDOWS_STABLE_ABI_LIB_NAME.to_owned()) + WINDOWS_STABLE_ABI_LIB_NAME.to_owned() + }; + if stable_abi == CPythonABI::ABI3t { + lib_name.push('t'); } + Ok(lib_name) } else if mingw { ensure!( !gil_disabled, From 861edd80e4769811c4893f21641d19add5472d9a Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 16 Apr 2026 15:16:27 -0600 Subject: [PATCH 076/195] try to fix linking --- noxfile.py | 3 +++ pyo3-build-config/src/lib.rs | 7 ++++++- pyo3-ffi/src/impl_/macros.rs | 2 ++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index c79cc538989..c037f0308e7 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1219,6 +1219,9 @@ def _check_raw_dylib_macro(session: nox.Session): if minor >= 13: expected_dlls.add(f"python3{minor}t") expected_dlls.add(f"python3{minor}t_d") + if minor >= 15: + expected_dlls.add(f"python3t") + expected_dlls.add(f"python3t_d") # PyPy DLL names (libpypy3.X-c.dll) pypy_min, pypy_max = _parse_supported_interpreter_version("pypy") diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index cdbe5419df7..f97e91d8cec 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -273,7 +273,12 @@ pub fn print_expected_cfgs() { } // pyo3_dll cfg for raw-dylib linking on Windows - let mut dll_names = vec!["python3".to_string(), "python3_d".to_string()]; + let mut dll_names = vec![ + "python3".to_string(), + "python3_d".to_string(), + "python3t".to_string(), + "python3t_d".to_string(), + ]; 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")); diff --git a/pyo3-ffi/src/impl_/macros.rs b/pyo3-ffi/src/impl_/macros.rs index 466fbb8f4de..5e95db1c930 100644 --- a/pyo3-ffi/src/impl_/macros.rs +++ b/pyo3-ffi/src/impl_/macros.rs @@ -324,6 +324,8 @@ macro_rules! extern_libpython { extern_libpython!(@impl $abi { $($body)* } // abi3 "python3", "python3_d", + // abi3t + "python3t", "python3t_d", // Python 3.8 - 3.15 "python38", "python38_d", "python39", "python39_d", From c8e40b9c5060464224703eefd65c13940ca9fb14 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 30 Mar 2026 12:14:53 -0600 Subject: [PATCH 077/195] add CPythonABI enum for pyo3-build-config InterpreterConfig --- pyo3-build-config/src/impl_.rs | 244 +++++++++++++++++++++------------ pyo3-build-config/src/lib.rs | 10 +- pyo3-ffi/build.rs | 14 +- 3 files changed, 166 insertions(+), 102 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 3d64cb34e4c..9a51bc50ae3 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 = 14; +pub(crate) const STABLE_ABI_MAX_MINOR: u8 = 14; #[cfg(test)] thread_local! { @@ -83,6 +83,42 @@ pub fn target_triple_from_env() -> Triple { .expect("Unrecognized TARGET environment variable value") } +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum CPythonABI { + ABI3, + VersionSpecific, +} + +impl CPythonABI { + fn from_build_env() -> Result { + match is_abi3() { + true => Ok(CPythonABI::ABI3), + false => 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::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), + "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 +145,8 @@ pub struct InterpreterConfig { /// Whether linking against the stable/limited Python 3 API. /// - /// Serialized to `abi3`. - pub abi3: bool, + /// Serialized to `stable_abi`. + pub stable_abi: CPythonABI, /// The name of the link library defining Python. /// @@ -194,9 +230,13 @@ impl InterpreterConfig { PythonImplementation::RustPython => out.push("cargo:rustc-cfg=RustPython".to_owned()), } - // If Py_GIL_DISABLED is set, do not build with limited API support - if self.abi3 && !self.is_free_threaded() { - out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); + match self.stable_abi { + CPythonABI::ABI3 => { + if !self.is_free_threaded() { + out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); + } + } + CPythonABI::VersionSpecific => {} } for flag in &self.build_flags.0 { @@ -310,7 +350,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()?; @@ -327,7 +367,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 @@ -339,7 +379,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, @@ -366,7 +406,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(), @@ -421,11 +461,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, @@ -439,7 +479,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) implementation, version, shared: shared || framework, - abi3, + stable_abi, lib_dir, lib_name, executable: None, @@ -473,7 +513,7 @@ 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())?; Ok(config) @@ -516,7 +556,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; @@ -541,7 +581,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), @@ -560,14 +600,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, @@ -590,7 +630,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, )); @@ -645,7 +685,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)?; @@ -707,7 +747,18 @@ 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(()) + } + + /// 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 {} \ @@ -716,11 +767,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(()) @@ -853,7 +903,7 @@ fn is_abi3() -> bool { /// /// 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 }) } @@ -1568,7 +1618,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 +1681,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 +1761,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 +1775,13 @@ 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::VersionSpecific + && !(gil_disabled || 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 +1807,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 +1816,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 && stable_abi != CPythonABI::VersionSpecific { Ok("python3".to_string()) } else if gil_disabled { ensure!(version >= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", version.major, version.minor); @@ -1922,7 +1982,7 @@ pub fn make_interpreter_config() -> Result { let abi3_version = get_abi3_version(); // See if we can safely skip the Python interpreter configuration detection. - // Unix "abi3" extension modules can usually be built without any interpreter. + // Unix stable ABI extension modules can usually be built without any interpreter. let need_interpreter = abi3_version.is_none() || require_libdir_for_target(&host); if have_python_interpreter() { @@ -1931,7 +1991,7 @@ pub fn make_interpreter_config() -> Result { // 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."); } @@ -1990,7 +2050,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()), @@ -2011,7 +2071,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); @@ -2041,7 +2101,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()), @@ -2071,7 +2131,7 @@ mod tests { version: PythonVersion { major: 3, minor: 8 }, implementation: PythonImplementation::CPython, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: None, lib_dir: None, executable: None, @@ -2094,7 +2154,7 @@ mod tests { version: PythonVersion { major: 3, minor: 8 }, implementation: PythonImplementation::CPython, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: None, lib_dir: None, executable: None, @@ -2194,7 +2254,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, @@ -2224,7 +2284,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, @@ -2251,7 +2311,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, @@ -2278,7 +2338,7 @@ mod tests { implementation: PythonImplementation::CPython, version: PythonVersion { major: 3, minor: 8 }, shared: true, - abi3: true, + stable_abi: CPythonABI::ABI3, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -2302,7 +2362,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, @@ -2337,7 +2397,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("C:\\some\\path".into()), executable: None, @@ -2372,7 +2432,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, @@ -2407,7 +2467,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, @@ -2444,7 +2504,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, @@ -2459,12 +2519,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, @@ -2475,7 +2536,7 @@ mod tests { assert!(super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, CPython, - false, + VersionSpecific, false, false, true, @@ -2485,7 +2546,7 @@ mod tests { super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, CPython, - true, + ABI3, false, false, false, @@ -2497,7 +2558,7 @@ mod tests { super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, CPython, - false, + VersionSpecific, true, false, false, @@ -2509,7 +2570,7 @@ mod tests { super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, CPython, - true, + ABI3, true, false, false, @@ -2521,7 +2582,7 @@ mod tests { super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, PyPy, - true, + ABI3, false, false, false, @@ -2536,7 +2597,7 @@ mod tests { minor: 11 }, PyPy, - false, + ABI3, false, false, false, @@ -2548,7 +2609,7 @@ mod tests { super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, CPython, - false, + ABI3, false, true, false, @@ -2562,7 +2623,7 @@ mod tests { super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, CPython, - true, + ABI3, false, true, false, @@ -2577,7 +2638,7 @@ mod tests { minor: 10 }, CPython, - true, + ABI3, false, true, false, @@ -2592,7 +2653,7 @@ mod tests { minor: 12, }, CPython, - false, + VersionSpecific, false, false, true, @@ -2605,7 +2666,7 @@ mod tests { minor: 12, }, CPython, - false, + VersionSpecific, true, false, true, @@ -2618,7 +2679,7 @@ mod tests { minor: 13 }, CPython, - false, + VersionSpecific, false, false, true, @@ -2633,7 +2694,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, @@ -2648,7 +2709,7 @@ mod tests { minor: 13 }, CPython, - false, + VersionSpecific, false, true, true, @@ -2660,13 +2721,14 @@ mod tests { #[test] fn default_lib_name_unix() { + use CPythonABI::*; use PythonImplementation::*; // Defaults to pythonX.Y for CPython 3.8+ assert_eq!( super::default_lib_name_unix( PythonVersion { major: 3, minor: 8 }, CPython, - false, + VersionSpecific, false, None, false @@ -2678,7 +2740,7 @@ mod tests { super::default_lib_name_unix( PythonVersion { major: 3, minor: 9 }, CPython, - false, + VersionSpecific, false, None, false @@ -2691,7 +2753,7 @@ mod tests { super::default_lib_name_unix( PythonVersion { major: 3, minor: 9 }, CPython, - false, + VersionSpecific, false, Some("3.8d"), false @@ -2708,7 +2770,7 @@ mod tests { minor: 11 }, PyPy, - false, + VersionSpecific, false, None, false @@ -2721,7 +2783,7 @@ mod tests { super::default_lib_name_unix( PythonVersion { major: 3, minor: 9 }, PyPy, - false, + VersionSpecific, false, Some("3.11d"), false @@ -2738,7 +2800,7 @@ mod tests { minor: 13 }, CPython, - false, + VersionSpecific, false, None, true @@ -2753,7 +2815,7 @@ mod tests { minor: 12, }, CPython, - false, + VersionSpecific, false, None, true, @@ -2767,7 +2829,7 @@ mod tests { minor: 13 }, CPython, - true, + ABI3, true, None, false @@ -2831,7 +2893,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, @@ -2855,7 +2917,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, @@ -2920,7 +2982,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, @@ -3058,7 +3120,7 @@ mod tests { minor: 11, }, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -3100,7 +3162,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, @@ -3146,7 +3208,7 @@ mod tests { minor: 13, }, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -3179,7 +3241,7 @@ mod tests { implementation: PythonImplementation::CPython, version: PythonVersion { major: 3, minor: 8 }, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -3234,7 +3296,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, @@ -3297,7 +3359,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 9bc9a9c8cc9..e0a1d5070f5 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -268,13 +268,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 { @@ -311,7 +311,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`. @@ -491,7 +491,7 @@ mod tests { minor: 13, }, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: None, lib_dir: None, executable: None, @@ -534,7 +534,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/build.rs b/pyo3-ffi/build.rs index 7c9be0e4cb0..eeacbb31d15 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); + let major = interpreter_config.version.major; + let minor = interpreter_config.version.minor; 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.", - ); + 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()); } @@ -125,12 +127,12 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { PythonImplementation::RustPython => {} } - if interpreter_config.abi3 { + if let CPythonABI::ABI3 = interpreter_config.stable_abi { match interpreter_config.implementation { PythonImplementation::CPython => { if interpreter_config.is_free_threaded() { 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." ) } } From 45d5cc107625727336b1cce49b2f040d9b20c53e Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 30 Mar 2026 12:24:07 -0600 Subject: [PATCH 078/195] add release note --- newsfragments/5924.changed.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 newsfragments/5924.changed.md diff --git a/newsfragments/5924.changed.md b/newsfragments/5924.changed.md new file mode 100644 index 00000000000..07d2d59c8f6 --- /dev/null +++ b/newsfragments/5924.changed.md @@ -0,0 +1,2 @@ +The boolean abi3 field of pyo3_build_config::impl_::InterpreterConfig is now a +variant of a new CPythonABI enum. From e3dc13345d82d1150493a38a95fff8a1975087a3 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 15 Apr 2026 16:35:25 -0600 Subject: [PATCH 079/195] store ABI details in a new PythonAbi struct --- pyo3-build-config/src/impl_.rs | 918 +++++++++++++++----------------- pyo3-build-config/src/lib.rs | 24 +- pyo3-ffi-check/macro/src/lib.rs | 5 +- pyo3-ffi/build.rs | 45 +- 4 files changed, 465 insertions(+), 527 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 9a51bc50ae3..32c1b942ada 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -83,42 +83,6 @@ pub fn target_triple_from_env() -> Triple { .expect("Unrecognized TARGET environment variable value") } -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum CPythonABI { - ABI3, - VersionSpecific, -} - -impl CPythonABI { - fn from_build_env() -> Result { - match is_abi3() { - true => Ok(CPythonABI::ABI3), - false => 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::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), - "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 @@ -128,26 +92,17 @@ impl FromStr for CPythonABI { /// strategies are used to populate this type. #[cfg_attr(test, derive(Debug, PartialEq, Eq))] pub struct InterpreterConfig { - /// The Python implementation flavor. - /// - /// Serialized to `implementation`. - pub implementation: PythonImplementation, - - /// Python `X.Y` version. e.g. `3.9`. + /// Which abi the build is configured to link against /// - /// Serialized to `version`. - pub version: PythonVersion, + /// Serialized to `abi`. + /// See the documentation for the PythonAbi enum for more details. + pub abi: PythonAbi, /// Whether link library is shared. /// /// Serialized to `shared`. pub shared: bool, - /// Whether linking against the stable/limited Python 3 API. - /// - /// Serialized to `stable_abi`. - pub stable_abi: CPythonABI, - /// The name of the link library defining Python. /// /// This effectively controls the `cargo:rustc-link-lib=` value to @@ -215,28 +170,28 @@ impl InterpreterConfig { #[doc(hidden)] pub fn build_script_outputs(&self) -> Vec { // This should have been checked during pyo3-build-config build time. - assert!(self.version >= MINIMUM_SUPPORTED_VERSION); + assert!(self.abi.version >= MINIMUM_SUPPORTED_VERSION); let mut out = vec![]; - for i in MINIMUM_SUPPORTED_VERSION.minor..=self.version.minor { + for i in MINIMUM_SUPPORTED_VERSION.minor..=self.abi.version.minor { out.push(format!("cargo:rustc-cfg=Py_3_{i}")); } - match self.implementation { + match self.abi.implementation { PythonImplementation::CPython => {} PythonImplementation::PyPy => out.push("cargo:rustc-cfg=PyPy".to_owned()), PythonImplementation::GraalPy => out.push("cargo:rustc-cfg=GraalPy".to_owned()), PythonImplementation::RustPython => out.push("cargo:rustc-cfg=RustPython".to_owned()), } - match self.stable_abi { - CPythonABI::ABI3 => { - if !self.is_free_threaded() { + match self.abi.kind { + PythonAbiKind::Abi3 => { + if !self.abi.kind.is_free_threaded() { out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); } } - CPythonABI::VersionSpecific => {} + PythonAbiKind::VersionSpecific(_) => {} } for flag in &self.build_flags.0 { @@ -252,7 +207,10 @@ impl InterpreterConfig { } #[doc(hidden)] - pub fn from_interpreter(interpreter: impl AsRef) -> Result { + pub fn from_interpreter( + interpreter: impl AsRef, + abi3_version: Option, + ) -> Result { const SCRIPT: &str = r#" # Allow the script to run on Python 2, so that nicer error can be printed later. from __future__ import print_function @@ -350,8 +308,6 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .context("failed to parse minor version")?, }; - let stable_abi = CPythonABI::from_build_env()?; - let implementation = map["implementation"].parse()?; let gil_disabled = match map["gil_disabled"].as_str() { @@ -361,29 +317,28 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) _ => panic!("Unknown Py_GIL_DISABLED value"), }; + let mut abi_builder = + PythonAbiBuilder::new(implementation, version).adjust_from_build_env(abi3_version)?; + + if gil_disabled { + abi_builder = abi_builder.free_threaded()?; + } + + let abi = abi_builder.finalize(); + let cygwin = map["cygwin"].as_str() == "True"; let lib_name = if cfg!(windows) { default_lib_name_windows( - version, - implementation, - stable_abi, + 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 // `_d.cp312-win_amd64.pyd` for 3.12 debug build map["ext_suffix"].starts_with("_d."), - gil_disabled, )? } else { - default_lib_name_unix( - version, - implementation, - stable_abi, - cygwin, - map.get("ld_version").map(String::as_str), - gil_disabled, - )? + default_lib_name_unix(abi, cygwin, map.get("ld_version").map(String::as_str))? }; let lib_dir = if cfg!(windows) { @@ -403,10 +358,8 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .context("failed to parse calcsize_pointer")?; Ok(InterpreterConfig { - version, - implementation, shared, - stable_abi, + abi, lib_name: Some(lib_name), lib_dir, executable: map.get("executable").cloned(), @@ -461,14 +414,16 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) None => false, }; let cygwin = soabi.ends_with("cygwin"); - let stable_abi = CPythonABI::from_build_env()?; + let mut abi_builder = + PythonAbiBuilder::new(implementation, version).adjust_from_build_env(None)?; + if gil_disabled { + abi_builder = abi_builder.free_threaded()?; + } + let abi = abi_builder.finalize(); let lib_name = Some(default_lib_name_unix( - version, - implementation, - stable_abi, + abi, cygwin, sysconfigdata.get_value("LDVERSION"), - gil_disabled, )?); let pointer_width = parse_key!(sysconfigdata, "SIZEOF_VOID_P") .map(|bytes_width: u32| bytes_width * 8) @@ -476,10 +431,8 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let build_flags = BuildFlags::from_sysconfigdata(sysconfigdata); Ok(InterpreterConfig { - implementation, - version, + abi, shared: shared || framework, - stable_abi, lib_dir, lib_name, executable: None, @@ -510,11 +463,13 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .context("failed to parse contents of PYO3_CONFIG_FILE")?; // If the abi3 feature is enabled, the minimum Python version is constrained by the abi3 // feature. - // - // TODO: abi3 is a property of the build mode, not the interpreter. Should this be - // removed from `InterpreterConfig`? - config.stable_abi = CPythonABI::from_build_env()?; - config.fixup_for_abi3_version(get_abi3_version())?; + let mut abi_builder = + PythonAbiBuilder::new(config.abi.implementation, config.abi.version) + .adjust_from_build_env(get_abi3_version())?; + if config.abi.kind.is_free_threaded() { + abi_builder = abi_builder.free_threaded()?; + } + config.abi = abi_builder.finalize(); Ok(config) }) @@ -553,10 +508,8 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) }; } - let mut implementation = None; - let mut version = None; + let mut abi = None; let mut shared = None; - let mut stable_abi = None; let mut lib_name = None; let mut lib_dir = None; let mut executable = None; @@ -578,10 +531,8 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .ok_or_else(|| format!("expected key=value pair on line {}", i + 1))?, ); match key { - "implementation" => parse_value!(implementation, value), - "version" => parse_value!(version, value), + "abi" => parse_value!(abi, value), "shared" => parse_value!(shared, 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), @@ -598,16 +549,11 @@ 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 stable_abi = stable_abi.unwrap_or(CPythonABI::VersionSpecific); let build_flags = build_flags.unwrap_or_default(); Ok(InterpreterConfig { - implementation, - version, + abi: abi.unwrap(), shared: shared.unwrap_or(true), - stable_abi, lib_name, lib_dir, executable, @@ -627,13 +573,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) #[cfg(any(test, feature = "resolve-config"))] pub(crate) fn apply_default_lib_name_to_config_file(&mut self, target: &Triple) { if self.lib_name.is_none() { - self.lib_name = Some(default_lib_name_for_target( - self.version, - self.implementation, - self.stable_abi, - self.is_free_threaded(), - target, - )); + self.lib_name = Some(default_lib_name_for_target(self.abi, target)); } } @@ -682,10 +622,8 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) }; } - write_line!(implementation)?; - write_line!(version)?; + write_line!(abi)?; write_line!(shared)?; - write_line!(stable_abi)?; write_option_line!(lib_name)?; write_option_line!(lib_dir)?; write_option_line!(executable)?; @@ -731,49 +669,181 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) envs, ) } +} - pub fn is_free_threaded(&self) -> bool { - self.build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) - } - - /// Updates configured ABI to build for to the requested abi3 version - /// This is a no-op for platforms where abi3 is not supported - fn fixup_for_abi3_version(&mut self, abi3_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() - || self.is_free_threaded() - { - return Ok(()); - } - - self.fixup_for_stable_abi_version(abi3_version, is_abi3)?; +#[derive(Debug)] +pub struct PythonAbiBuilder { + implementation: PythonImplementation, + version: PythonVersion, + kind: Option, +} - Ok(()) +impl PythonAbiBuilder { + pub fn new(implementation: PythonImplementation, version: PythonVersion) -> PythonAbiBuilder { + PythonAbiBuilder { + implementation, + version, + kind: None, + } } - /// 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 { + pub fn abi3(self, abi3_version: Option) -> Result { + if self.kind.is_some() { + bail!("Target ABI already chosen!") + } + + // PyPy and GraalPy don't support abi3; don't adjust the version + if self.implementation.is_pypy() || self.implementation.is_graalpy() { + return Ok(PythonAbiBuilder { + implementation: self.implementation, + version: self.version, + kind: self.kind, + }); + } + let mut build_version = self.version; + if let Some(version) = abi3_version { ensure!( version <= self.version, "cannot set a minimum Python version {} higher than the interpreter version {} \ - (the minimum Python version is implied by the abi3-py3{} feature)", + (the minimum Python version is implied by the abi3-py3{} feature)", version, self.version, version.minor, ); - self.version = version; - } else if abi_check() && self.version.minor > STABLE_ABI_MAX_MINOR { + build_version = version; + } else if 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; + build_version.minor = STABLE_ABI_MAX_MINOR; } - Ok(()) + Ok(PythonAbiBuilder { + kind: Some(PythonAbiKind::Abi3), + version: build_version, + ..self + }) + } + + pub fn adjust_from_build_env( + self, + abi3_version: Option, + ) -> Result { + if is_abi3() { + self.abi3(abi3_version) + } else { + Ok(PythonAbiBuilder { ..self }) + } + } + + pub fn free_threaded(self) -> Result { + if self.kind.is_some() { + bail!("Target ABI already chosen!") + } + if self.version < PythonVersion::PY313 { + let version = self.version; + bail!( + "Free-threaded builds on Python versions before 3.13, tried to build for {version}" + ) + } + Ok(PythonAbiBuilder { + kind: Some(PythonAbiKind::VersionSpecific(true)), + ..self + }) + } + + pub fn finalize(self) -> PythonAbi { + // default to GIL-enabled version-specific ABI + let kind = self.kind.unwrap_or(PythonAbiKind::VersionSpecific(false)); + PythonAbi { + implementation: self.implementation, + kind, + version: self.version, + } + } +} + +#[non_exhaustive] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct PythonAbi { + /// The Python implementation flavor. + /// + /// Serialized to `implementation`. + pub implementation: PythonImplementation, + + /// The ABI flavor + /// + /// Serialized to `kind` + pub kind: PythonAbiKind, + + /// Python `X.Y` version. e.g. `3.9`. + /// + /// Serialized to `version`. + pub version: PythonVersion, +} + +impl Display for PythonAbi { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let implementation = self.implementation; + let kind = self.kind; + let version = self.version; + write!(f, "{implementation}-{kind}-{version}") + } +} + +impl FromStr for PythonAbi { + type Err = crate::errors::Error; + + fn from_str(value: &str) -> Result { + let parts: Vec<&str> = value.split("-").collect(); + let implementation = parts[0].parse()?; + let kind = parts[1].parse()?; + let version: PythonVersion = parts[2].parse()?; + Ok(PythonAbi { + implementation, + kind, + version, + }) + } +} + +/// The "kind" of stable ABI. Either abi3 or abi3t currently. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PythonAbiKind { + /// The original stable ABI, supporting Python 3.2 and up + Abi3, + /// Version specific ABI, which may be different on the free-threaded build (true) or gil-enabled build (false) + VersionSpecific(bool), +} + +impl Display for PythonAbiKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PythonAbiKind::Abi3 => write!(f, "abi3"), + PythonAbiKind::VersionSpecific(gil_disabled) => { + write!(f, "version_specific({gil_disabled})") + } + } + } +} + +impl FromStr for PythonAbiKind { + type Err = crate::errors::Error; + + fn from_str(value: &str) -> Result { + match value { + "abi3" => Ok(PythonAbiKind::Abi3), + "version_specific(true)" => Ok(PythonAbiKind::VersionSpecific(true)), + "version_specific(false)" => Ok(PythonAbiKind::VersionSpecific(false)), + _ => Err(format!("Unrecognized ABI name: {value}").into()), + } + } +} + +impl PythonAbiKind { + pub fn is_free_threaded(&self) -> bool { + match self { + PythonAbiKind::VersionSpecific(gil_disabled) => *gil_disabled, + PythonAbiKind::Abi3 => false, + } } } @@ -788,7 +858,7 @@ impl PythonVersion { major: 3, minor: 15, }; - pub const PY313: Self = PythonVersion { + pub(crate) const PY313: Self = PythonVersion { major: 3, minor: 13, }; @@ -796,10 +866,16 @@ impl PythonVersion { major: 3, minor: 12, }; - const PY310: Self = PythonVersion { + pub const PY311: Self = PythonVersion { + major: 3, + minor: 11, + }; + pub const PY310: Self = PythonVersion { major: 3, minor: 10, }; + pub const PY39: Self = PythonVersion { major: 3, minor: 9 }; + pub const PY38: Self = PythonVersion { major: 3, minor: 8 }; } impl Display for PythonVersion { @@ -1618,27 +1694,24 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result Result { // FIXME: PyPy & GraalPy do not support the Stable ABI. let implementation = PythonImplementation::CPython; - let stable_abi = CPythonABI::ABI3; + let abi_builder = PythonAbiBuilder::new(implementation, version).abi3(Some(version))?; + let abi = abi_builder.finalize(); let lib_name = if host.operating_system == OperatingSystem::Windows { - Some(default_lib_name_windows( - version, - implementation, - stable_abi, - false, - false, - false, - )?) + Some(default_lib_name_windows(abi, false, false)?) } else { None }; Ok(InterpreterConfig { - implementation, - version, + abi, shared: true, - stable_abi, lib_name, lib_dir: None, executable: None, @@ -1728,56 +1793,36 @@ const WINDOWS_STABLE_ABI_DEBUG_LIB_NAME: &str = "python3_d"; /// Generates the default library name for the target platform. #[allow(dead_code)] -fn default_lib_name_for_target( - version: PythonVersion, - implementation: PythonImplementation, - stable_abi: CPythonABI, - gil_disabled: bool, - target: &Triple, -) -> String { +fn default_lib_name_for_target(abi: PythonAbi, target: &Triple) -> String { if target.operating_system == OperatingSystem::Windows { - default_lib_name_windows( - version, - implementation, - stable_abi, - false, - false, - gil_disabled, - ) - .unwrap() + default_lib_name_windows(abi, false, false).unwrap() } else { default_lib_name_unix( - version, - implementation, - stable_abi, + abi, target.operating_system == OperatingSystem::Cygwin, None, - gil_disabled, ) .unwrap() } } -fn default_lib_name_windows( - version: PythonVersion, - implementation: PythonImplementation, - stable_abi: CPythonABI, - mingw: bool, - debug: bool, - gil_disabled: bool, -) -> Result { - if implementation.is_pypy() { +fn default_lib_name_windows(abi: PythonAbi, mingw: bool, debug: bool) -> Result { + if abi.implementation.is_pypy() { // PyPy on Windows ships `libpypy3.X-c.dll` (e.g. `libpypy3.11-c.dll`), // not CPython's `pythonXY.dll`. With raw-dylib linking we need the real // DLL name rather than the import-library alias. - Ok(format!("libpypy{}.{}-c", version.major, version.minor)) - } else if debug && version < PythonVersion::PY310 { + Ok(format!( + "libpypy{}.{}-c", + abi.version.major, abi.version.minor + )) + } else if debug && abi.version < PythonVersion::PY310 { // 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 stable_abi != CPythonABI::VersionSpecific - && !(gil_disabled || implementation.is_pypy() || implementation.is_graalpy()) - { + Ok(format!( + "python{}{}_d", + abi.version.major, abi.version.minor + )) + } else if matches!(abi.kind, PythonAbiKind::Abi3) && !abi.implementation.is_graalpy() { if debug { Ok(WINDOWS_STABLE_ABI_DEBUG_LIB_NAME.to_owned()) } else { @@ -1785,50 +1830,52 @@ fn default_lib_name_windows( } } else if mingw { ensure!( - !gil_disabled, + !abi.kind.is_free_threaded(), "MinGW free-threaded builds are not currently tested or supported" ); // https://packages.msys2.org/base/mingw-w64-python - Ok(format!("python{}.{}", version.major, version.minor)) - } else if gil_disabled { - ensure!(version >= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", version.major, version.minor); + Ok(format!("python{}.{}", abi.version.major, abi.version.minor)) + } else if abi.kind.is_free_threaded() { + ensure!(abi.version >= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", abi.version.major, abi.version.minor); if debug { - Ok(format!("python{}{}t_d", version.major, version.minor)) + Ok(format!( + "python{}{}t_d", + abi.version.major, abi.version.minor + )) } else { - Ok(format!("python{}{}t", version.major, version.minor)) + Ok(format!("python{}{}t", abi.version.major, abi.version.minor)) } } else if debug { - Ok(format!("python{}{}_d", version.major, version.minor)) + Ok(format!( + "python{}{}_d", + abi.version.major, abi.version.minor + )) } else { - Ok(format!("python{}{}", version.major, version.minor)) + Ok(format!("python{}{}", abi.version.major, abi.version.minor)) } } -fn default_lib_name_unix( - version: PythonVersion, - implementation: PythonImplementation, - stable_abi: CPythonABI, - cygwin: bool, - ld_version: Option<&str>, - gil_disabled: bool, -) -> Result { - match implementation { +fn default_lib_name_unix(abi: PythonAbi, cygwin: bool, ld_version: Option<&str>) -> Result { + match abi.implementation { PythonImplementation::CPython => match ld_version { Some(ld_version) => Ok(format!("python{ld_version}")), None => { - if cygwin && stable_abi != CPythonABI::VersionSpecific { + if cygwin && matches!(abi.kind, PythonAbiKind::Abi3) { Ok("python3".to_string()) - } else if gil_disabled { - ensure!(version >= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", version.major, version.minor); - Ok(format!("python{}.{}t", version.major, version.minor)) + } else if abi.kind.is_free_threaded() { + ensure!(abi.version >= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", abi.version.major, abi.version.minor); + Ok(format!( + "python{}.{}t", + abi.version.major, abi.version.minor + )) } else { - Ok(format!("python{}.{}", version.major, version.minor)) + Ok(format!("python{}.{}", abi.version.major, abi.version.minor)) } } }, PythonImplementation::PyPy => match ld_version { Some(ld_version) => Ok(format!("pypy{ld_version}-c")), - None => Ok(format!("pypy{}.{}-c", version.major, version.minor)), + None => Ok(format!("pypy{}.{}-c", abi.version.major, abi.version.minor)), }, PythonImplementation::GraalPy => Ok("python-native".to_string()), @@ -1951,8 +1998,7 @@ pub fn find_interpreter() -> Result { fn get_host_interpreter(abi3_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)?; + let interpreter_config = InterpreterConfig::from_interpreter(interpreter_path, abi3_version)?; Ok(interpreter_config) } @@ -1964,9 +2010,15 @@ fn get_host_interpreter(abi3_version: Option) -> Result 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())?; - Some(interpreter_config) + let mut config = load_cross_compile_config(cross_config)?; + let mut abi_builder = PythonAbiBuilder::new(config.abi.implementation, config.abi.version) + .adjust_from_build_env(get_abi3_version())?; + if config.abi.kind.is_free_threaded() { + abi_builder = abi_builder.free_threaded()?; + } + config.abi = abi_builder.finalize(); + + Some(config) } else { None }; @@ -2049,16 +2101,18 @@ mod tests { #[test] fn test_config_file_roundtrip() { + let abi_builder = + PythonAbiBuilder::new(PythonImplementation::CPython, MINIMUM_SUPPORTED_VERSION) + .abi3(None) + .unwrap(); let config = InterpreterConfig { - stable_abi: CPythonABI::ABI3, + abi: abi_builder.finalize(), build_flags: BuildFlags::default(), pointer_width: Some(32), executable: Some("executable".into()), - implementation: PythonImplementation::CPython, lib_name: Some("lib_name".into()), lib_dir: Some("lib_dir".into()), shared: true, - version: MINIMUM_SUPPORTED_VERSION, suppress_build_script_link_lines: true, extra_build_script_lines: vec!["cargo:test1".to_string(), "cargo:test2".to_string()], python_framework_prefix: None, @@ -2069,9 +2123,9 @@ mod tests { assert_eq!(config, InterpreterConfig::from_reader(&*buf).unwrap()); // And some different options, for variety - + let abi_builder = PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY310); let config = InterpreterConfig { - stable_abi: CPythonABI::VersionSpecific, + abi: abi_builder.finalize(), build_flags: { let mut flags = HashSet::new(); flags.insert(BuildFlag::Py_DEBUG); @@ -2080,14 +2134,9 @@ mod tests { }, pointer_width: None, executable: None, - implementation: PythonImplementation::PyPy, lib_dir: None, lib_name: None, shared: true, - version: PythonVersion { - major: 3, - minor: 10, - }, suppress_build_script_link_lines: false, extra_build_script_lines: vec![], python_framework_prefix: None, @@ -2101,15 +2150,16 @@ mod tests { #[test] fn test_config_file_roundtrip_with_escaping() { let config = InterpreterConfig { - stable_abi: CPythonABI::VersionSpecific, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, MINIMUM_SUPPORTED_VERSION) + .abi3(None) + .unwrap() + .finalize(), build_flags: BuildFlags::default(), pointer_width: Some(32), executable: Some("executable".into()), - implementation: PythonImplementation::CPython, lib_name: Some("lib_name".into()), lib_dir: Some("lib_dir\\n".into()), shared: true, - version: MINIMUM_SUPPORTED_VERSION, suppress_build_script_link_lines: true, extra_build_script_lines: vec!["cargo:test1".to_string(), "cargo:test2".to_string()], python_framework_prefix: None, @@ -2126,12 +2176,12 @@ mod tests { fn test_config_file_defaults() { // Only version is required assert_eq!( - InterpreterConfig::from_reader("version=3.8".as_bytes()).unwrap(), + InterpreterConfig::from_reader("abi=CPython-version_specific(false)-3.8".as_bytes()) + .unwrap(), InterpreterConfig { - version: PythonVersion { major: 3, minor: 8 }, - implementation: PythonImplementation::CPython, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) + .finalize(), shared: true, - stable_abi: CPythonABI::VersionSpecific, lib_name: None, lib_dir: None, executable: None, @@ -2148,13 +2198,14 @@ mod tests { fn test_config_file_unknown_keys() { // ext_suffix is unknown to pyo3-build-config, but it shouldn't error assert_eq!( - InterpreterConfig::from_reader("version=3.8\next_suffix=.python38.so".as_bytes()) - .unwrap(), + InterpreterConfig::from_reader( + "abi=CPython-version_specific(false)-3.8\next_suffix=.python38.so".as_bytes() + ) + .unwrap(), InterpreterConfig { - version: PythonVersion { major: 3, minor: 8 }, - implementation: PythonImplementation::CPython, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) + .finalize(), shared: true, - stable_abi: CPythonABI::VersionSpecific, lib_name: None, lib_dir: None, executable: None, @@ -2254,15 +2305,14 @@ mod tests { assert_eq!( InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), InterpreterConfig { - stable_abi: CPythonABI::VersionSpecific, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) + .finalize(), build_flags: BuildFlags::from_sysconfigdata(&sysconfigdata), pointer_width: Some(64), executable: None, - implementation: PythonImplementation::CPython, lib_dir: Some("/usr/lib".into()), lib_name: Some("python3.8".into()), shared: true, - version: PythonVersion { major: 3, minor: 8 }, suppress_build_script_link_lines: false, extra_build_script_lines: vec![], python_framework_prefix: None, @@ -2284,15 +2334,14 @@ mod tests { assert_eq!( InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), InterpreterConfig { - stable_abi: CPythonABI::VersionSpecific, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) + .finalize(), build_flags: BuildFlags::from_sysconfigdata(&sysconfigdata), pointer_width: Some(64), executable: None, - implementation: PythonImplementation::CPython, lib_dir: Some("/usr/lib".into()), lib_name: Some("python3.8".into()), shared: true, - version: PythonVersion { major: 3, minor: 8 }, suppress_build_script_link_lines: false, extra_build_script_lines: vec![], python_framework_prefix: None, @@ -2311,15 +2360,14 @@ mod tests { assert_eq!( InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), InterpreterConfig { - stable_abi: CPythonABI::VersionSpecific, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) + .finalize(), build_flags: BuildFlags::from_sysconfigdata(&sysconfigdata), pointer_width: Some(64), executable: None, - implementation: PythonImplementation::CPython, lib_dir: Some("/usr/lib".into()), lib_name: Some("python3.8".into()), shared: false, - version: PythonVersion { major: 3, minor: 8 }, suppress_build_script_link_lines: false, extra_build_script_lines: vec![], python_framework_prefix: None, @@ -2335,10 +2383,11 @@ mod tests { assert_eq!( default_abi3_config(&host, min_version).unwrap(), InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 8 }, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) + .abi3(None) + .unwrap() + .finalize(), shared: true, - stable_abi: CPythonABI::ABI3, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -2359,10 +2408,11 @@ mod tests { assert_eq!( default_abi3_config(&host, min_version).unwrap(), InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 9 }, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .abi3(None) + .unwrap() + .finalize(), shared: true, - stable_abi: CPythonABI::ABI3, lib_name: None, lib_dir: None, executable: None, @@ -2394,10 +2444,9 @@ mod tests { assert_eq!( default_cross_compile(&cross_config).unwrap(), InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 8 }, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) + .finalize(), shared: true, - stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python38".into()), lib_dir: Some("C:\\some\\path".into()), executable: None, @@ -2429,10 +2478,9 @@ mod tests { assert_eq!( default_cross_compile(&cross_config).unwrap(), InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 8 }, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) + .finalize(), shared: true, - stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python38".into()), lib_dir: Some("/usr/lib/mingw".into()), executable: None, @@ -2464,10 +2512,9 @@ mod tests { assert_eq!( default_cross_compile(&cross_config).unwrap(), InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 9 }, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .finalize(), shared: true, - stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python3.9".into()), lib_dir: Some("/usr/arm64/lib".into()), executable: None, @@ -2498,13 +2545,9 @@ mod tests { assert_eq!( default_cross_compile(&cross_config).unwrap(), InterpreterConfig { - implementation: PythonImplementation::PyPy, - version: PythonVersion { - major: 3, - minor: 11 - }, + abi: PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY311) + .finalize(), shared: true, - stable_abi: CPythonABI::VersionSpecific, lib_name: Some("pypy3.11-c".into()), lib_dir: None, executable: None, @@ -2519,35 +2562,28 @@ 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, - VersionSpecific, - false, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .finalize(), false, false, ) .unwrap(), "python39", ); - assert!(super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - CPython, - VersionSpecific, - false, - false, - true, - ) - .is_err()); + // free-threaded Python 3.9 builds should be impossible + assert!( + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .free_threaded() + .is_err() + ); assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - CPython, - ABI3, - false, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .abi3(None) + .unwrap() + .finalize(), false, false, ) @@ -2556,34 +2592,32 @@ mod tests { ); assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - CPython, - VersionSpecific, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .finalize(), true, false, - false, ) .unwrap(), "python3.9", ); assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - CPython, - ABI3, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .abi3(None) + .unwrap() + .finalize(), true, false, - false, ) .unwrap(), "python3", ); assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - PyPy, - ABI3, - false, + PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY39) + .abi3(None) + .unwrap() + .finalize(), false, false, ) @@ -2592,13 +2626,10 @@ mod tests { ); assert_eq!( super::default_lib_name_windows( - PythonVersion { - major: 3, - minor: 11 - }, - PyPy, - ABI3, - false, + PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY311) + .abi3(None) + .unwrap() + .finalize(), false, false, ) @@ -2607,12 +2638,12 @@ mod tests { ); assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - CPython, - ABI3, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .abi3(None) + .unwrap() + .finalize(), false, true, - false, ) .unwrap(), "python39_d", @@ -2621,129 +2652,83 @@ mod tests { // to workaround https://github.com/python/cpython/issues/101614 assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - CPython, - ABI3, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .abi3(None) + .unwrap() + .finalize(), false, true, - false, ) .unwrap(), "python39_d", ); assert_eq!( super::default_lib_name_windows( - PythonVersion { - major: 3, - minor: 10 - }, - CPython, - ABI3, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY310) + .abi3(None) + .unwrap() + .finalize(), false, true, - false, ) .unwrap(), "python3_d", ); - // Python versions older than 3.13 don't support gil_disabled - assert!(super::default_lib_name_windows( - PythonVersion { - major: 3, - minor: 12, - }, - CPython, - VersionSpecific, - false, - false, - true, - ) - .is_err()); // mingw and free-threading are incompatible (until someone adds support) assert!(super::default_lib_name_windows( - PythonVersion { - major: 3, - minor: 12, - }, - CPython, - VersionSpecific, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) + .free_threaded() + .unwrap() + .finalize(), true, false, - true, ) .is_err()); assert_eq!( super::default_lib_name_windows( - PythonVersion { - major: 3, - minor: 13 - }, - CPython, - VersionSpecific, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) + .free_threaded() + .unwrap() + .finalize(), false, false, - true, ) .unwrap(), "python313t", ); assert_eq!( super::default_lib_name_windows( - PythonVersion { - major: 3, - minor: 13 - }, - CPython, - ABI3, // abi3 true should not affect the free-threaded lib name - false, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) + .free_threaded() + .unwrap() + .finalize(), false, true, ) .unwrap(), - "python313t", - ); - assert_eq!( - super::default_lib_name_windows( - PythonVersion { - major: 3, - minor: 13 - }, - CPython, - VersionSpecific, - false, - true, - true, - ) - .unwrap(), "python313t_d", ); } #[test] fn default_lib_name_unix() { - use CPythonABI::*; - use PythonImplementation::*; // Defaults to pythonX.Y for CPython 3.8+ assert_eq!( super::default_lib_name_unix( - PythonVersion { major: 3, minor: 8 }, - CPython, - VersionSpecific, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) + .finalize(), false, None, - false ) .unwrap(), "python3.8", ); assert_eq!( super::default_lib_name_unix( - PythonVersion { major: 3, minor: 9 }, - CPython, - VersionSpecific, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .finalize(), false, None, - false ) .unwrap(), "python3.9", @@ -2751,12 +2736,10 @@ mod tests { // Can use ldversion to override for CPython assert_eq!( super::default_lib_name_unix( - PythonVersion { major: 3, minor: 9 }, - CPython, - VersionSpecific, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .finalize(), false, Some("3.8d"), - false ) .unwrap(), "python3.8d", @@ -2765,15 +2748,9 @@ mod tests { // PyPy 3.11 includes ldversion assert_eq!( super::default_lib_name_unix( - PythonVersion { - major: 3, - minor: 11 - }, - PyPy, - VersionSpecific, + PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY311).finalize(), false, None, - false ) .unwrap(), "pypy3.11-c", @@ -2781,12 +2758,9 @@ mod tests { assert_eq!( super::default_lib_name_unix( - PythonVersion { major: 3, minor: 9 }, - PyPy, - VersionSpecific, + PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY39).finalize(), false, Some("3.11d"), - false ) .unwrap(), "pypy3.11d-c", @@ -2795,44 +2769,25 @@ mod tests { // free-threading adds a t suffix assert_eq!( super::default_lib_name_unix( - PythonVersion { - major: 3, - minor: 13 - }, - CPython, - VersionSpecific, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) + .free_threaded() + .unwrap() + .finalize(), false, None, - true ) .unwrap(), "python3.13t", ); - // 3.12 and older are incompatible with gil_disabled - assert!(super::default_lib_name_unix( - PythonVersion { - major: 3, - minor: 12, - }, - CPython, - VersionSpecific, - false, - None, - true, - ) - .is_err()); // cygwin abi3 links to unversioned libpython assert_eq!( super::default_lib_name_unix( - PythonVersion { - major: 3, - minor: 13 - }, - CPython, - ABI3, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) + .abi3(None) + .unwrap() + .finalize(), true, None, - false ) .unwrap(), "python3", @@ -2892,47 +2847,34 @@ mod tests { #[test] fn interpreter_version_reduced_to_abi3() { - let mut config = InterpreterConfig { - stable_abi: CPythonABI::ABI3, + let config = InterpreterConfig { + abi: PythonAbiBuilder::new( + PythonImplementation::CPython, + // Make this greater than the target abi3 version to reduce to below + PythonVersion { major: 3, minor: 9 }, + ) + .abi3(Some(PythonVersion::PY38)) + .unwrap() + .finalize(), build_flags: BuildFlags::default(), pointer_width: None, executable: None, - implementation: PythonImplementation::CPython, lib_dir: None, lib_name: None, shared: true, - // Make this greater than the target abi3 version to reduce to below - version: PythonVersion { major: 3, minor: 9 }, suppress_build_script_link_lines: false, extra_build_script_lines: vec![], python_framework_prefix: None, }; - config - .fixup_for_abi3_version(Some(PythonVersion { major: 3, minor: 8 })) - .unwrap(); - assert_eq!(config.version, PythonVersion { major: 3, minor: 8 }); + assert_eq!(config.abi.version, PythonVersion { major: 3, minor: 8 }); } #[test] fn abi3_version_cannot_be_higher_than_interpreter() { - let mut config = InterpreterConfig { - stable_abi: CPythonABI::ABI3, - build_flags: BuildFlags::new(), - pointer_width: None, - executable: None, - implementation: PythonImplementation::CPython, - lib_dir: None, - lib_name: None, - shared: true, - version: PythonVersion { major: 3, minor: 8 }, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - }; - - assert!(config - .fixup_for_abi3_version(Some(PythonVersion { major: 3, minor: 9 })) + let builder = PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38); + assert!(builder + .abi3(Some(PythonVersion { major: 3, minor: 9 })) .unwrap_err() .to_string() .contains( @@ -3114,13 +3056,9 @@ mod tests { #[test] fn test_build_script_outputs_base() { let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { - major: 3, - minor: 11, - }, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY311) + .finalize(), shared: true, - stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -3141,7 +3079,10 @@ mod tests { ); let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::PyPy, + abi: PythonAbi { + implementation: PythonImplementation::PyPy, + ..interpreter_config.abi + }, ..interpreter_config }; assert_eq!( @@ -3159,10 +3100,11 @@ mod tests { #[test] fn test_build_script_outputs_abi3() { let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 9 }, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .abi3(None) + .unwrap() + .finalize(), shared: true, - stable_abi: CPythonABI::ABI3, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -3183,7 +3125,10 @@ mod tests { ); let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::PyPy, + abi: PythonAbi { + implementation: PythonImplementation::PyPy, + ..interpreter_config.abi + }, ..interpreter_config }; assert_eq!( @@ -3202,13 +3147,11 @@ mod tests { let mut build_flags = BuildFlags::default(); build_flags.0.insert(BuildFlag::Py_GIL_DISABLED); let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { - major: 3, - minor: 13, - }, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) + .free_threaded() + .unwrap() + .finalize(), shared: true, - stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -3238,10 +3181,9 @@ mod tests { let mut build_flags = BuildFlags::default(); build_flags.0.insert(BuildFlag::Py_DEBUG); let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 8 }, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) + .finalize(), shared: true, - stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -3293,10 +3235,9 @@ mod tests { #[test] fn test_apply_default_lib_name_to_config_file() { let mut config = InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 9 }, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .finalize(), shared: true, - stable_abi: CPythonABI::VersionSpecific, lib_name: None, lib_dir: None, executable: None, @@ -3323,8 +3264,8 @@ mod tests { assert_eq!(config.lib_name, Some("python39".into())); // PyPy - config.implementation = PythonImplementation::PyPy; - config.version = PythonVersion { + config.abi.implementation = PythonImplementation::PyPy; + config.abi.version = PythonVersion { major: 3, minor: 11, }; @@ -3336,11 +3277,11 @@ mod tests { config.apply_default_lib_name_to_config_file(&win_x64); assert_eq!(config.lib_name, Some("libpypy3.11-c".into())); - config.implementation = PythonImplementation::CPython; + config.abi.implementation = PythonImplementation::CPython; // Free-threaded - config.build_flags.0.insert(BuildFlag::Py_GIL_DISABLED); - config.version = PythonVersion { + config.abi.kind = PythonAbiKind::VersionSpecific(true); + config.abi.version = PythonVersion { major: 3, minor: 13, }; @@ -3359,7 +3300,10 @@ mod tests { config.build_flags.0.remove(&BuildFlag::Py_GIL_DISABLED); // abi3 - config.stable_abi = CPythonABI::ABI3; + config.abi = PythonAbi { + kind: PythonAbiKind::Abi3, + ..config.abi + }; 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 e0a1d5070f5..457e1aa793d 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -20,7 +20,7 @@ use std::{env, process::Command, str::FromStr, sync::OnceLock}; pub use impl_::{ cross_compiling_from_to, find_all_sysconfigdata, parse_sysconfigdata, BuildFlag, BuildFlags, - CrossCompileConfig, InterpreterConfig, PythonImplementation, PythonVersion, Triple, + CrossCompileConfig, InterpreterConfig, PythonAbi, PythonImplementation, PythonVersion, Triple, }; use target_lexicon::OperatingSystem; @@ -311,7 +311,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, CPythonABI, InterpreterConfig, PythonVersion, + target_triple_from_env, InterpreterConfig, PythonAbi, PythonAbiKind, PythonVersion, }; pub enum BuildConfigSource { /// Config was provided by `PYO3_CONFIG_FILE`. @@ -390,13 +390,13 @@ pub mod pyo3_build_script_impl { interpreter_config: &InterpreterConfig, supported_version: PythonVersion, ) -> Self { - let implementation = match interpreter_config.implementation { + let implementation = match interpreter_config.abi.implementation { PythonImplementation::CPython => "Python", PythonImplementation::PyPy => "PyPy", PythonImplementation::GraalPy => "GraalPy", PythonImplementation::RustPython => "RustPython", }; - let version = &interpreter_config.version; + let version = &interpreter_config.abi.version; let message = format!( "the configured {implementation} version ({version}) is newer than PyO3's maximum supported version ({supported_version})\n\ = help: this package is being built with PyO3 version {current_version}\n\ @@ -485,13 +485,9 @@ mod tests { let mut buf = Vec::new(); let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { - major: 3, - minor: 13, - }, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) + .finalize(), shared: true, - stable_abi: CPythonABI::VersionSpecific, lib_name: None, lib_dir: None, executable: None, @@ -528,13 +524,9 @@ mod tests { #[cfg(feature = "resolve-config")] fn test_maximum_version_exceeded_formatting() { let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { - major: 3, - minor: 13, - }, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) + .finalize(), shared: true, - stable_abi: CPythonABI::VersionSpecific, lib_name: None, lib_dir: None, executable: None, diff --git a/pyo3-ffi-check/macro/src/lib.rs b/pyo3-ffi-check/macro/src/lib.rs index 7434f4dbc2b..b749594111e 100644 --- a/pyo3-ffi-check/macro/src/lib.rs +++ b/pyo3-ffi-check/macro/src/lib.rs @@ -49,7 +49,8 @@ pub fn for_all_structs(input: proc_macro::TokenStream) -> proc_macro::TokenStrea .strip_suffix(".html") .unwrap(); - if pyo3_build_config::get().version < PythonVersion::PY315 && struct_name == "PyBytesWriter" + if pyo3_build_config::get().abi.version < PythonVersion::PY315 + && struct_name == "PyBytesWriter" { // PyBytesWriter was added in Python 3.15 continue; @@ -170,7 +171,7 @@ pub fn for_all_fields(input: proc_macro::TokenStream) -> proc_macro::TokenStream let field_ident = Ident::new(&field_name, Span::call_site()); - let bindgen_field_ident = if (pyo3_build_config::get().version >= PythonVersion::PY312) + let bindgen_field_ident = if (pyo3_build_config::get().abi.version >= PythonVersion::PY312) && struct_name == "PyObject" && field_name == "ob_refcnt" { diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index eeacbb31d15..65cddd23bcb 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -2,8 +2,8 @@ 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, CPythonABI, - InterpreterConfig, MaximumVersionExceeded, PythonVersion, + resolve_build_config, target_triple_from_env, BuildConfig, BuildConfigSource, + InterpreterConfig, MaximumVersionExceeded, PythonAbiKind, PythonVersion, }, warn, PythonImplementation, }; @@ -45,31 +45,32 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { return Ok(()); } - match interpreter_config.implementation { + match interpreter_config.abi.implementation { PythonImplementation::CPython => { let versions = SUPPORTED_VERSIONS_CPYTHON; + let interp_version = interpreter_config.abi.version; ensure!( - interpreter_config.version >= versions.min, + interp_version >= versions.min, "the configured Python interpreter version ({}) is lower than PyO3's minimum supported version ({})", - interpreter_config.version, + interp_version, versions.min, ); let v_plus_1 = PythonVersion { major: versions.max.major, minor: versions.max.minor + 1, }; - if interpreter_config.version == v_plus_1 { + if interp_version == v_plus_1 { warn!( "Using experimental support for the Python {}.{} ABI. \ Build artifacts may not be compatible with the final release of CPython, \ so do not distribute them.", v_plus_1.major, v_plus_1.minor, ); - } else if interpreter_config.version > v_plus_1 { + } else if interp_version > v_plus_1 { 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() { + let major = interp_version.major; + let minor = interp_version.minor; + if interpreter_config.abi.kind.is_free_threaded() { error.add_help(&format!( "the free-threaded build of CPython {major}{minor} does not support the limited API so this check cannot be suppressed.", )); @@ -83,29 +84,29 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { } } - if interpreter_config.is_free_threaded() { + if interpreter_config.abi.kind.is_free_threaded() { let min_free_threaded_version = PythonVersion { major: 3, minor: 14, }; ensure!( - interpreter_config.version >= min_free_threaded_version, + interpreter_config.abi.version >= min_free_threaded_version, "PyO3 does not support the free-threaded build of CPython versions below {}, the selected Python version is {}", min_free_threaded_version, - interpreter_config.version, + interpreter_config.abi.version, ); } } PythonImplementation::PyPy => { let versions = SUPPORTED_VERSIONS_PYPY; ensure!( - interpreter_config.version >= versions.min, + interpreter_config.abi.version >= versions.min, "the configured PyPy interpreter version ({}) is lower than PyO3's minimum supported version ({})", - interpreter_config.version, + interpreter_config.abi.version, versions.min, ); // PyO3 does not support abi3, so we cannot offer forward compatibility - if interpreter_config.version > versions.max { + if interpreter_config.abi.version > versions.max { let error = MaximumVersionExceeded::new(interpreter_config, versions.max); return Err(error.finish().into()); } @@ -113,13 +114,13 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { PythonImplementation::GraalPy => { let versions = SUPPORTED_VERSIONS_GRAALPY; ensure!( - interpreter_config.version >= versions.min, + interpreter_config.abi.version >= versions.min, "the configured GraalPy interpreter version ({}) is lower than PyO3's minimum supported version ({})", - interpreter_config.version, + interpreter_config.abi.version, versions.min, ); // GraalPy does not support abi3, so we cannot offer forward compatibility - if interpreter_config.version > versions.max { + if interpreter_config.abi.version > versions.max { let error = MaximumVersionExceeded::new(interpreter_config, versions.max); return Err(error.finish().into()); } @@ -127,10 +128,10 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { PythonImplementation::RustPython => {} } - if let CPythonABI::ABI3 = interpreter_config.stable_abi { - match interpreter_config.implementation { + if let PythonAbiKind::Abi3 = interpreter_config.abi.kind { + match interpreter_config.abi.implementation { PythonImplementation::CPython => { - if interpreter_config.is_free_threaded() { + if interpreter_config.abi.kind.is_free_threaded() { warn!( "The free-threaded build of CPython does not support abi3 so the build artifacts will be version-specific." ) From 966a82fdd1ba025aeb5c302d90719954b0fde3c9 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 15 Apr 2026 16:47:55 -0600 Subject: [PATCH 080/195] fix nox config file --- noxfile.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/noxfile.py b/noxfile.py index b7130b30d4c..bd91e7544b6 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1640,8 +1640,7 @@ def set( self._config_file.truncate(0) self._config_file.write( f"""\ -implementation={implementation} -version={version} +abi={implementation}-version_specific(false)-{version} build_flags={",".join(build_flags)} suppress_build_script_link_lines=true """ From 9d9a0e2b7bd3e0634264e920f8d843e2b758d5fd Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 17 Apr 2026 15:22:06 -0600 Subject: [PATCH 081/195] fix ffi-check --- pyo3-ffi-check/macro/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyo3-ffi-check/macro/src/lib.rs b/pyo3-ffi-check/macro/src/lib.rs index b749594111e..3c6eb29cfb9 100644 --- a/pyo3-ffi-check/macro/src/lib.rs +++ b/pyo3-ffi-check/macro/src/lib.rs @@ -153,7 +153,8 @@ pub fn for_all_fields(input: proc_macro::TokenStream) -> proc_macro::TokenStream if struct_name == "PyMemberDef" { // bindgen picked `type_` as the field name to avoid the `type` keyword, but PyO3 uses `type_code` all_fields.remove("type_"); - } else if struct_name == "PyObject" && pyo3_build_config::get().version >= PythonVersion::PY312 + } else if struct_name == "PyObject" + && pyo3_build_config::get().abi.version >= PythonVersion::PY312 { // bindgen picked `__bindgen_anon_1` as the field name for the anonymous union containing ob_refcnt, // PyO3 uses ob_refcnt directly From f52f1d89028af65e0c72d1ad87dc43194c59ab8b Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 24 Apr 2026 14:13:34 -0600 Subject: [PATCH 082/195] Refactor to use the builder pattern for InterpreterConfig --- pyo3-build-config/src/impl_.rs | 1145 ++++++++++++++++++------------- pyo3-build-config/src/lib.rs | 26 +- pyo3-ffi-check/macro/src/lib.rs | 7 +- pyo3-ffi/build.rs | 30 +- 4 files changed, 689 insertions(+), 519 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 32c1b942ada..c1b84a7c12a 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -92,17 +92,39 @@ pub fn target_triple_from_env() -> Triple { /// strategies are used to populate this type. #[cfg_attr(test, derive(Debug, PartialEq, Eq))] pub struct InterpreterConfig { - /// Which abi the build is configured to link against + /// The host Python implementation flavor. /// - /// Serialized to `abi`. - /// See the documentation for the PythonAbi enum for more details. - pub abi: PythonAbi, + /// Serialized to `implementation`. + pub implementation: PythonImplementation, + + /// The host Python `X.Y` version. e.g. `3.9`. + /// + /// Serialized to `version`. + pub version: PythonVersion, /// Whether link library is shared. /// /// Serialized to `shared`. pub shared: bool, + /// The ABI to use for the compilation target. + /// + /// Serialized to `target_abi`. + /// See the documentation for the PythonAbi enum for more details. + pub target_abi: PythonAbi, + + /// Deprecated field used to indicate an abi3 target. + /// + /// Creating an InterpreterConfig struct with `abi3` set to `True`, + /// `interpreter` set to `PythonImplementation::CPython` and `version` set + /// to `PythonVersion {major: 3, minor: 9}` is equivalent to setting `abi` + /// to `PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion + /// {major: 3, minor: 9).abi3().finalize()`. + /// + /// Serialized to `abi3`. + #[deprecated] + pub abi3: bool, + /// The name of the link library defining Python. /// /// This effectively controls the `cargo:rustc-link-lib=` value to @@ -170,24 +192,24 @@ impl InterpreterConfig { #[doc(hidden)] pub fn build_script_outputs(&self) -> Vec { // This should have been checked during pyo3-build-config build time. - assert!(self.abi.version >= MINIMUM_SUPPORTED_VERSION); + assert!(self.target_abi.version >= MINIMUM_SUPPORTED_VERSION); let mut out = vec![]; - for i in MINIMUM_SUPPORTED_VERSION.minor..=self.abi.version.minor { + for i in MINIMUM_SUPPORTED_VERSION.minor..=self.target_abi.version.minor { out.push(format!("cargo:rustc-cfg=Py_3_{i}")); } - match self.abi.implementation { + match self.target_abi.implementation { PythonImplementation::CPython => {} PythonImplementation::PyPy => out.push("cargo:rustc-cfg=PyPy".to_owned()), PythonImplementation::GraalPy => out.push("cargo:rustc-cfg=GraalPy".to_owned()), PythonImplementation::RustPython => out.push("cargo:rustc-cfg=RustPython".to_owned()), } - match self.abi.kind { + match self.target_abi.kind { PythonAbiKind::Abi3 => { - if !self.abi.kind.is_free_threaded() { + if !self.target_abi.kind.is_free_threaded() { out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); } } @@ -317,20 +339,33 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) _ => panic!("Unknown Py_GIL_DISABLED value"), }; - let mut abi_builder = - PythonAbiBuilder::new(implementation, version).adjust_from_build_env(abi3_version)?; + let target_version = if let Some(min_version) = abi3_version { + ensure!( + min_version <= version, + "cannot set a minimum Python version {} higher than the interpreter version {} \ + (the minimum Python version is implied by the abi3-py3{} feature)", + min_version, + version, + min_version.minor + ); + min_version + } else { + version + }; + + let mut abi_builder = PythonAbiBuilder::new(implementation, target_version); if gil_disabled { abi_builder = abi_builder.free_threaded()?; } - let abi = abi_builder.finalize(); + let target_abi = abi_builder.finalize(); let cygwin = map["cygwin"].as_str() == "True"; let lib_name = if cfg!(windows) { default_lib_name_windows( - abi, + target_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 @@ -338,7 +373,11 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) map["ext_suffix"].starts_with("_d."), )? } else { - default_lib_name_unix(abi, cygwin, map.get("ld_version").map(String::as_str))? + default_lib_name_unix( + target_abi, + cygwin, + map.get("ld_version").map(String::as_str), + )? }; let lib_dir = if cfg!(windows) { @@ -357,18 +396,16 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .parse() .context("failed to parse calcsize_pointer")?; - Ok(InterpreterConfig { - shared, - abi, - lib_name: Some(lib_name), - lib_dir, - executable: map.get("executable").cloned(), - pointer_width: Some(calcsize_pointer * 8), - build_flags: BuildFlags::from_interpreter(interpreter)?, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix, - }) + Ok(InterpreterConfigBuilder::new(implementation, version) + .target_abi(target_abi)? + .shared(shared) + .lib_name(Some(lib_name)) + .lib_dir(lib_dir) + .executable(map.get("executable").cloned()) + .pointer_width(calcsize_pointer * 8) + .build_flags(BuildFlags::from_interpreter(interpreter)?)? + .python_framework_prefix(python_framework_prefix) + .finalize()) } /// Generate from parsed sysconfigdata file @@ -414,34 +451,29 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) None => false, }; let cygwin = soabi.ends_with("cygwin"); - let mut abi_builder = - PythonAbiBuilder::new(implementation, version).adjust_from_build_env(None)?; + let mut abi_builder = PythonAbiBuilder::from_build_env(implementation, version)?; if gil_disabled { abi_builder = abi_builder.free_threaded()?; } - let abi = abi_builder.finalize(); + let target_abi = abi_builder.finalize(); let lib_name = Some(default_lib_name_unix( - abi, + target_abi, cygwin, sysconfigdata.get_value("LDVERSION"), )?); - let pointer_width = parse_key!(sysconfigdata, "SIZEOF_VOID_P") - .map(|bytes_width: u32| bytes_width * 8) - .ok(); + let pointer_width = + parse_key!(sysconfigdata, "SIZEOF_VOID_P").map(|bytes_width: u32| bytes_width * 8)?; let build_flags = BuildFlags::from_sysconfigdata(sysconfigdata); - Ok(InterpreterConfig { - abi, - shared: shared || framework, - lib_dir, - lib_name, - executable: None, - pointer_width, - build_flags, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix, - }) + Ok(InterpreterConfigBuilder::new(implementation, version) + .target_abi(target_abi)? + .shared(shared || framework) + .lib_dir(lib_dir) + .lib_name(lib_name) + .pointer_width(pointer_width) + .build_flags(build_flags)? + .python_framework_prefix(python_framework_prefix) + .finalize()) } /// Import an externally-provided config file. @@ -463,13 +495,14 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .context("failed to parse contents of PYO3_CONFIG_FILE")?; // If the abi3 feature is enabled, the minimum Python version is constrained by the abi3 // feature. - let mut abi_builder = - PythonAbiBuilder::new(config.abi.implementation, config.abi.version) - .adjust_from_build_env(get_abi3_version())?; - if config.abi.kind.is_free_threaded() { + let mut abi_builder = PythonAbiBuilder::from_build_env( + config.target_abi.implementation, + get_abi3_version().unwrap_or(config.target_abi.version), + )?; + if config.target_abi.kind.is_free_threaded() { abi_builder = abi_builder.free_threaded()?; } - config.abi = abi_builder.finalize(); + config.target_abi = abi_builder.finalize(); Ok(config) }) @@ -508,8 +541,12 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) }; } - let mut abi = None; + let mut implementation = None; + let mut version: Option = None; let mut shared = None; + let mut target_abi = None; + // deprecated in the struct but we still allow it to support old config files + let mut abi3: Option = None; let mut lib_name = None; let mut lib_dir = None; let mut executable = None; @@ -531,8 +568,11 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .ok_or_else(|| format!("expected key=value pair on line {}", i + 1))?, ); match key { - "abi" => parse_value!(abi, value), + "implementation" => parse_value!(implementation, value), + "version" => parse_value!(version, value), "shared" => parse_value!(shared, value), + "target_abi" => parse_value!(target_abi, value), + "abi3" => parse_value!(abi3, value), "lib_name" => parse_value!(lib_name, value), "lib_dir" => parse_value!(lib_dir, value), "executable" => parse_value!(executable, value), @@ -549,20 +589,44 @@ 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 target_abi = if !(target_abi.is_some() || abi3.is_some()) { + PythonAbiBuilder::new(implementation, version).finalize() + } else if abi3.is_some() && abi3.unwrap() { + // should we produce a user-visible warning about this? + PythonAbiBuilder::new(implementation, version) + .abi3() + .unwrap() + .finalize() + } else { + ensure!( + !(target_abi.is_some() && abi3.is_some()), + "Invalid config that sets both target_abi and abi3." + ); + target_abi.unwrap() + }; + let build_flags = build_flags.unwrap_or_default(); + let builder = InterpreterConfigBuilder::new(implementation, version) + .target_abi(target_abi)? + .shared(shared.unwrap_or(true)) + .lib_name(lib_name) + .lib_dir(lib_dir) + .executable(executable) + .build_flags(build_flags)? + .suppress_build_script_link_lines(suppress_build_script_link_lines) + .extra_build_script_lines(extra_build_script_lines) + .python_framework_prefix(python_framework_prefix); + + let builder = if let Some(pointer_width) = pointer_width { + builder.pointer_width(pointer_width) + } else { + builder + }; - Ok(InterpreterConfig { - abi: abi.unwrap(), - shared: shared.unwrap_or(true), - lib_name, - lib_dir, - executable, - pointer_width, - build_flags, - suppress_build_script_link_lines: suppress_build_script_link_lines.unwrap_or(false), - extra_build_script_lines, - python_framework_prefix, - }) + Ok(builder.finalize()) } /// Helper function to apply a default lib_name if none is set in `PYO3_CONFIG_FILE`. @@ -573,7 +637,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) #[cfg(any(test, feature = "resolve-config"))] pub(crate) fn apply_default_lib_name_to_config_file(&mut self, target: &Triple) { if self.lib_name.is_none() { - self.lib_name = Some(default_lib_name_for_target(self.abi, target)); + self.lib_name = Some(default_lib_name_for_target(self.target_abi, target)); } } @@ -622,8 +686,10 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) }; } - write_line!(abi)?; + write_line!(implementation)?; + write_line!(version)?; write_line!(shared)?; + write_line!(target_abi)?; write_option_line!(lib_name)?; write_option_line!(lib_dir)?; write_option_line!(executable)?; @@ -671,6 +737,181 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) } } +#[cfg_attr(test, derive(Debug))] +pub struct InterpreterConfigBuilder { + implementation: PythonImplementation, + version: PythonVersion, + shared: Option, + target_abi: Option, + lib_name: Option, + lib_dir: Option, + executable: Option, + pointer_width: Option, + build_flags: Option, + suppress_build_script_link_lines: Option, + extra_build_script_lines: Option>, + python_framework_prefix: Option, +} + +impl InterpreterConfigBuilder { + pub fn new( + implementation: PythonImplementation, + version: PythonVersion, + ) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { + implementation, + version, + shared: None, + target_abi: None, + lib_name: None, + lib_dir: None, + executable: None, + pointer_width: None, + build_flags: None, + suppress_build_script_link_lines: None, + extra_build_script_lines: None, + python_framework_prefix: None, + } + } + + pub fn target_abi(self, target_abi: PythonAbi) -> Result { + ensure!(self.target_abi.is_none(), "Target ABI already set!"); + Ok(InterpreterConfigBuilder { + target_abi: Some(target_abi), + ..self + }) + } + + pub fn abi3(self) -> Result { + let implementation = self.implementation; + let version = self.version; + self.target_abi( + PythonAbiBuilder::new(implementation, version) + .abi3() + // this can't panic because abi3() is caleld on a builder with no chosen ABI + .unwrap() + .finalize(), + ) + } + + pub fn free_threaded(self) -> Result { + let implementation: PythonImplementation = self.implementation; + let version: PythonVersion = self.version; + self.target_abi( + PythonAbiBuilder::new(implementation, version) + .free_threaded() + // this can't panic because abi3() is called on a builder with no chosen ABI + .unwrap() + .finalize(), + )? + .build_flags(BuildFlags::default()) + } + + pub fn lib_name(self, lib_name: Option) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { lib_name, ..self } + } + + pub fn pointer_width(self, pointer_width: u32) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { + pointer_width: Some(pointer_width), + ..self + } + } + + pub fn executable(self, executable: Option) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { executable, ..self } + } + + pub fn suppress_build_script_link_lines( + self, + suppress_build_script_link_lines: Option, + ) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { + suppress_build_script_link_lines, + ..self + } + } + + pub fn extra_build_script_lines( + self, + extra_build_script_lines: Vec, + ) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { + extra_build_script_lines: Some(extra_build_script_lines), + ..self + } + } + + pub fn lib_dir(self, lib_dir: Option) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { lib_dir, ..self } + } + + pub fn shared(self, shared: bool) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { + shared: Some(shared), + ..self + } + } + + pub fn build_flags(self, build_flags: BuildFlags) -> Result { + ensure!(self.build_flags.is_none(), "Build flags already set!"); + let build_flags = if build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { + ensure!(self.target_abi.is_some(), "Must target a free-threaded ABI if build flags contain Py_GIL_DISABLED but no target_abi is set"); + ensure!( + self.target_abi.unwrap().kind + == PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded), + "build_flags contains Py_GIL_DISABLED but target ABI is not free-threaded" + ); + build_flags + } else if let Some(target_abi) = self.target_abi { + let mut flags = build_flags.clone(); + if target_abi.kind.is_free_threaded() { + flags.0.insert(BuildFlag::Py_GIL_DISABLED); + } + flags + } else { + build_flags + }; + Ok(InterpreterConfigBuilder { + build_flags: Some(build_flags), + ..self + }) + } + + pub fn python_framework_prefix( + self, + python_framework_prefix: Option, + ) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { + python_framework_prefix, + ..self + } + } + + pub fn finalize(self) -> InterpreterConfig { + #[allow(deprecated)] + InterpreterConfig { + implementation: self.implementation, + version: self.version, + shared: self.shared.unwrap_or(true), + target_abi: self + .target_abi + .unwrap_or(PythonAbiBuilder::new(self.implementation, self.version).finalize()), + abi3: false, + lib_name: self.lib_name, + lib_dir: self.lib_dir, + executable: self.executable, + pointer_width: self.pointer_width, + build_flags: self.build_flags.unwrap_or_default(), + suppress_build_script_link_lines: self + .suppress_build_script_link_lines + .unwrap_or(false), + extra_build_script_lines: self.extra_build_script_lines.unwrap_or(vec![]), + python_framework_prefix: self.python_framework_prefix, + } + } +} + #[derive(Debug)] pub struct PythonAbiBuilder { implementation: PythonImplementation, @@ -687,7 +928,23 @@ impl PythonAbiBuilder { } } - pub fn abi3(self, abi3_version: Option) -> Result { + pub fn from_build_env( + implementation: PythonImplementation, + version: PythonVersion, + ) -> Result { + let builder = PythonAbiBuilder { + implementation, + version, + kind: None, + }; + if is_abi3() { + builder.abi3() + } else { + Ok(builder) + } + } + + pub fn abi3(self) -> Result { if self.kind.is_some() { bail!("Target ABI already chosen!") } @@ -701,17 +958,7 @@ impl PythonAbiBuilder { }); } let mut build_version = self.version; - if let Some(version) = abi3_version { - ensure!( - version <= self.version, - "cannot set a minimum Python version {} higher than the interpreter version {} \ - (the minimum Python version is implied by the abi3-py3{} feature)", - version, - self.version, - version.minor, - ); - build_version = version; - } else if self.version.minor > STABLE_ABI_MAX_MINOR { + if 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"); build_version.minor = STABLE_ABI_MAX_MINOR; } @@ -723,17 +970,6 @@ impl PythonAbiBuilder { }) } - pub fn adjust_from_build_env( - self, - abi3_version: Option, - ) -> Result { - if is_abi3() { - self.abi3(abi3_version) - } else { - Ok(PythonAbiBuilder { ..self }) - } - } - pub fn free_threaded(self) -> Result { if self.kind.is_some() { bail!("Target ABI already chosen!") @@ -741,18 +977,20 @@ impl PythonAbiBuilder { if self.version < PythonVersion::PY313 { let version = self.version; bail!( - "Free-threaded builds on Python versions before 3.13, tried to build for {version}" + "Cannot target free-threaded builds for Python versions before 3.13, tried to build for {version}" ) } Ok(PythonAbiBuilder { - kind: Some(PythonAbiKind::VersionSpecific(true)), + kind: Some(PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)), ..self }) } pub fn finalize(self) -> PythonAbi { // default to GIL-enabled version-specific ABI - let kind = self.kind.unwrap_or(PythonAbiKind::VersionSpecific(false)); + let kind = self + .kind + .unwrap_or(PythonAbiKind::VersionSpecific(GilUsed::GilEnabled)); PythonAbi { implementation: self.implementation, kind, @@ -810,8 +1048,8 @@ impl FromStr for PythonAbi { pub enum PythonAbiKind { /// The original stable ABI, supporting Python 3.2 and up Abi3, - /// Version specific ABI, which may be different on the free-threaded build (true) or gil-enabled build (false) - VersionSpecific(bool), + /// Version specific ABI, which is different on the free-threaded build + VersionSpecific(GilUsed), } impl Display for PythonAbiKind { @@ -831,8 +1069,12 @@ impl FromStr for PythonAbiKind { fn from_str(value: &str) -> Result { match value { "abi3" => Ok(PythonAbiKind::Abi3), - "version_specific(true)" => Ok(PythonAbiKind::VersionSpecific(true)), - "version_specific(false)" => Ok(PythonAbiKind::VersionSpecific(false)), + "version_specific(free_threaded)" => { + Ok(PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)) + } + "version_specific(gil_enabled)" => { + Ok(PythonAbiKind::VersionSpecific(GilUsed::GilEnabled)) + } _ => Err(format!("Unrecognized ABI name: {value}").into()), } } @@ -841,10 +1083,44 @@ impl FromStr for PythonAbiKind { impl PythonAbiKind { pub fn is_free_threaded(&self) -> bool { match self { - PythonAbiKind::VersionSpecific(gil_disabled) => *gil_disabled, + PythonAbiKind::VersionSpecific(gil_disabled) => *gil_disabled == GilUsed::FreeThreaded, PythonAbiKind::Abi3 => false, } } + + pub fn is_abi3(&self) -> bool { + matches!(self, PythonAbiKind::Abi3) + } +} + +/// Whether the ABI is for the GIL-enabled or free-threaded build. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum GilUsed { + /// The original PyObject layout + GilEnabled, + /// The free-threaded PyObject layout + FreeThreaded, +} + +impl Display for GilUsed { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GilUsed::GilEnabled => write!(f, "gil_enabled"), + GilUsed::FreeThreaded => write!(f, "free_threaded"), + } + } +} + +impl FromStr for GilUsed { + type Err = crate::errors::Error; + + fn from_str(value: &str) -> Result { + match value { + "gil_enabled" => Ok(GilUsed::GilEnabled), + "free_threaded" => Ok(GilUsed::FreeThreaded), + _ => Err(format!("Unrecognized ABI name: {value}").into()), + } + } } #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] @@ -858,7 +1134,11 @@ impl PythonVersion { major: 3, minor: 15, }; - pub(crate) const PY313: Self = PythonVersion { + pub const PY314: Self = PythonVersion { + major: 3, + minor: 14, + }; + pub const PY313: Self = PythonVersion { major: 3, minor: 13, }; @@ -1698,29 +1978,21 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result Result { // FIXME: PyPy & GraalPy do not support the Stable ABI. - let implementation = PythonImplementation::CPython; - let abi_builder = PythonAbiBuilder::new(implementation, version).abi3(Some(version))?; - let abi = abi_builder.finalize(); - - let lib_name = if host.operating_system == OperatingSystem::Windows { - Some(default_lib_name_windows(abi, false, false)?) + let builder = InterpreterConfigBuilder::new(PythonImplementation::CPython, version) + .abi3() + .unwrap(); + // abi3() sets the target_abi on the builder struct so unwrapping is safe + let target_abi = builder.target_abi.unwrap(); + Ok(if host.operating_system == OperatingSystem::Windows { + builder.lib_name(Some(default_lib_name_windows(target_abi, false, false)?)) } else { - None - }; - - Ok(InterpreterConfig { - abi, - shared: true, - lib_name, - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - }) + builder + } + .finalize()) } /// Detects the cross compilation target interpreter configuration from all @@ -2011,12 +2272,14 @@ fn get_host_interpreter(abi3_version: Option) -> Result Result> { let interpreter_config = if let Some(cross_config) = cross_compiling_from_cargo_env()? { let mut config = load_cross_compile_config(cross_config)?; - let mut abi_builder = PythonAbiBuilder::new(config.abi.implementation, config.abi.version) - .adjust_from_build_env(get_abi3_version())?; - if config.abi.kind.is_free_threaded() { + let mut abi_builder = PythonAbiBuilder::from_build_env( + config.target_abi.implementation, + get_abi3_version().unwrap_or(config.target_abi.version), + )?; + if config.target_abi.kind.is_free_threaded() { abi_builder = abi_builder.free_threaded()?; } - config.abi = abi_builder.finalize(); + config.target_abi = abi_builder.finalize(); Some(config) } else { @@ -2101,46 +2364,36 @@ mod tests { #[test] fn test_config_file_roundtrip() { - let abi_builder = - PythonAbiBuilder::new(PythonImplementation::CPython, MINIMUM_SUPPORTED_VERSION) - .abi3(None) - .unwrap(); - let config = InterpreterConfig { - abi: abi_builder.finalize(), - build_flags: BuildFlags::default(), - pointer_width: Some(32), - executable: Some("executable".into()), - lib_name: Some("lib_name".into()), - lib_dir: Some("lib_dir".into()), - shared: true, - suppress_build_script_link_lines: true, - extra_build_script_lines: vec!["cargo:test1".to_string(), "cargo:test2".to_string()], - python_framework_prefix: None, - }; + let implementation = PythonImplementation::CPython; + let version = MINIMUM_SUPPORTED_VERSION; + let config = InterpreterConfigBuilder::new(implementation, version) + .abi3() + .unwrap() + .pointer_width(32) + .executable(Some("executable".into())) + .lib_dir(Some("lib_name".into())) + .lib_name(Some("lib_name".into())) + .extra_build_script_lines(vec!["cargo:test1".to_string(), "cargo:test2".to_string()]) + .finalize(); let mut buf: Vec = Vec::new(); config.to_writer(&mut buf).unwrap(); assert_eq!(config, InterpreterConfig::from_reader(&*buf).unwrap()); // And some different options, for variety - let abi_builder = PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY310); - let config = InterpreterConfig { - abi: abi_builder.finalize(), - build_flags: { - let mut flags = HashSet::new(); - flags.insert(BuildFlag::Py_DEBUG); - flags.insert(BuildFlag::Other(String::from("Py_SOME_FLAG"))); - BuildFlags(flags) - }, - pointer_width: None, - executable: None, - lib_dir: None, - lib_name: None, - shared: true, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, + let version = PythonVersion::PY310; + let implementation = PythonImplementation::PyPy; + let build_flags = { + let mut flags = HashSet::new(); + flags.insert(BuildFlag::Py_DEBUG); + flags.insert(BuildFlag::Other(String::from("Py_SOME_FLAG"))); + BuildFlags(flags) }; + let config = InterpreterConfigBuilder::new(implementation, version) + .build_flags(build_flags) + .unwrap() + .finalize(); + let mut buf: Vec = Vec::new(); config.to_writer(&mut buf).unwrap(); @@ -2149,21 +2402,17 @@ mod tests { #[test] fn test_config_file_roundtrip_with_escaping() { - let config = InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, MINIMUM_SUPPORTED_VERSION) - .abi3(None) - .unwrap() - .finalize(), - build_flags: BuildFlags::default(), - pointer_width: Some(32), - executable: Some("executable".into()), - lib_name: Some("lib_name".into()), - lib_dir: Some("lib_dir\\n".into()), - shared: true, - suppress_build_script_link_lines: true, - extra_build_script_lines: vec!["cargo:test1".to_string(), "cargo:test2".to_string()], - python_framework_prefix: None, - }; + let implementation = PythonImplementation::CPython; + let version = MINIMUM_SUPPORTED_VERSION; + let config = InterpreterConfigBuilder::new(implementation, version) + .abi3() + .unwrap() + .pointer_width(32) + .executable(Some("executable".into())) + .lib_name(Some("lib_name".into())) + .lib_dir(Some("lib_dir\\n".into())) + .extra_build_script_lines(vec!["cargo:test1".to_string(), "cargo:test2".to_string()]) + .finalize(); let mut buf: Vec = Vec::new(); config.to_writer(&mut buf).unwrap(); @@ -2175,46 +2424,23 @@ mod tests { #[test] fn test_config_file_defaults() { // Only version is required + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY38; assert_eq!( - InterpreterConfig::from_reader("abi=CPython-version_specific(false)-3.8".as_bytes()) - .unwrap(), - InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) - .finalize(), - shared: true, - lib_name: None, - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } + InterpreterConfig::from_reader("version=3.8".as_bytes()).unwrap(), + InterpreterConfigBuilder::new(implementation, version,).finalize() ) } #[test] fn test_config_file_unknown_keys() { // ext_suffix is unknown to pyo3-build-config, but it shouldn't error + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY38; assert_eq!( - InterpreterConfig::from_reader( - "abi=CPython-version_specific(false)-3.8\next_suffix=.python38.so".as_bytes() - ) - .unwrap(), - InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) - .finalize(), - shared: true, - lib_name: None, - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } + InterpreterConfig::from_reader("version=3.8\next_suffix=.python38.so".as_bytes()) + .unwrap(), + InterpreterConfigBuilder::new(implementation, version,).finalize() ) } @@ -2302,21 +2528,17 @@ mod tests { sysconfigdata.insert("LIBDIR", "/usr/lib"); sysconfigdata.insert("LDVERSION", "3.8"); sysconfigdata.insert("SIZEOF_VOID_P", "8"); + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY38; assert_eq!( InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), - InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) - .finalize(), - build_flags: BuildFlags::from_sysconfigdata(&sysconfigdata), - pointer_width: Some(64), - executable: None, - lib_dir: Some("/usr/lib".into()), - lib_name: Some("python3.8".into()), - shared: true, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } + InterpreterConfigBuilder::new(implementation, version,) + .build_flags(BuildFlags::from_sysconfigdata(&sysconfigdata)) + .unwrap() + .lib_dir(Some("/usr/lib".into())) + .lib_name(Some("python3.8".into())) + .pointer_width(64) + .finalize() ); } @@ -2331,21 +2553,17 @@ mod tests { sysconfigdata.insert("LIBDIR", "/usr/lib"); sysconfigdata.insert("LDVERSION", "3.8"); sysconfigdata.insert("SIZEOF_VOID_P", "8"); + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY38; assert_eq!( InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), - InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) - .finalize(), - build_flags: BuildFlags::from_sysconfigdata(&sysconfigdata), - pointer_width: Some(64), - executable: None, - lib_dir: Some("/usr/lib".into()), - lib_name: Some("python3.8".into()), - shared: true, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } + InterpreterConfigBuilder::new(implementation, version,) + .build_flags(BuildFlags::from_sysconfigdata(&sysconfigdata)) + .unwrap() + .lib_dir(Some("/usr/lib".into())) + .lib_name(Some("python3.8".into())) + .pointer_width(64) + .finalize() ); sysconfigdata = Sysconfigdata::new(); @@ -2357,21 +2575,18 @@ mod tests { sysconfigdata.insert("LIBDIR", "/usr/lib"); sysconfigdata.insert("LDVERSION", "3.8"); sysconfigdata.insert("SIZEOF_VOID_P", "8"); + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY38; assert_eq!( InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), - InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) - .finalize(), - build_flags: BuildFlags::from_sysconfigdata(&sysconfigdata), - pointer_width: Some(64), - executable: None, - lib_dir: Some("/usr/lib".into()), - lib_name: Some("python3.8".into()), - shared: false, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } + InterpreterConfigBuilder::new(implementation, version,) + .build_flags(BuildFlags::from_sysconfigdata(&sysconfigdata)) + .unwrap() + .lib_dir(Some("/usr/lib".into())) + .lib_name(Some("python3.8".into())) + .pointer_width(64) + .shared(false) + .finalize() ); } @@ -2380,49 +2595,27 @@ mod tests { let host = triple!("x86_64-pc-windows-msvc"); let min_version = "3.8".parse().unwrap(); - assert_eq!( - default_abi3_config(&host, min_version).unwrap(), - InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) - .abi3(None) - .unwrap() - .finalize(), - shared: true, - lib_name: Some("python3".into()), - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } - ); + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY38; + let config = InterpreterConfigBuilder::new(implementation, version) + .abi3() + .unwrap() + .lib_name(Some("python3".into())) + .finalize(); + assert_eq!(default_abi3_config(&host, min_version).unwrap(), config); } #[test] fn unix_hardcoded_abi3_compile() { let host = triple!("x86_64-unknown-linux-gnu"); let min_version = "3.9".parse().unwrap(); - - assert_eq!( - default_abi3_config(&host, min_version).unwrap(), - InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .abi3(None) - .unwrap() - .finalize(), - shared: true, - lib_name: None, - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } - ); + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY39; + let config = InterpreterConfigBuilder::new(implementation, version) + .abi3() + .unwrap() + .finalize(); + assert_eq!(default_abi3_config(&host, min_version).unwrap(), config); } #[test] @@ -2441,22 +2634,13 @@ mod tests { .unwrap() .unwrap(); - assert_eq!( - default_cross_compile(&cross_config).unwrap(), - InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) - .finalize(), - shared: true, - lib_name: Some("python38".into()), - lib_dir: Some("C:\\some\\path".into()), - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } - ); + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY38; + let config = InterpreterConfigBuilder::new(implementation, version) + .lib_name(Some("python38".into())) + .lib_dir(Some("C:\\some\\path".into())) + .finalize(); + assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } #[test] @@ -2475,22 +2659,13 @@ mod tests { .unwrap() .unwrap(); - assert_eq!( - default_cross_compile(&cross_config).unwrap(), - InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) - .finalize(), - shared: true, - lib_name: Some("python38".into()), - lib_dir: Some("/usr/lib/mingw".into()), - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } - ); + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY38; + let config = InterpreterConfigBuilder::new(implementation, version) + .lib_name(Some("python38".into())) + .lib_dir(Some("/usr/lib/mingw".into())) + .finalize(); + assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } #[test] @@ -2509,22 +2684,13 @@ mod tests { .unwrap() .unwrap(); - assert_eq!( - default_cross_compile(&cross_config).unwrap(), - InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .finalize(), - shared: true, - lib_name: Some("python3.9".into()), - lib_dir: Some("/usr/arm64/lib".into()), - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } - ); + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY39; + let config = InterpreterConfigBuilder::new(implementation, version) + .lib_name(Some("python3.9".into())) + .lib_dir(Some("/usr/arm64/lib".into())) + .finalize(); + assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } #[test] @@ -2542,22 +2708,12 @@ mod tests { .unwrap() .unwrap(); - assert_eq!( - default_cross_compile(&cross_config).unwrap(), - InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY311) - .finalize(), - shared: true, - lib_name: Some("pypy3.11-c".into()), - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } - ); + let implementation = PythonImplementation::PyPy; + let version = PythonVersion::PY311; + let config = InterpreterConfigBuilder::new(implementation, version) + .lib_name(Some("pypy3.11-c".into())) + .finalize(); + assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } #[test] @@ -2581,7 +2737,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .abi3(None) + .abi3() .unwrap() .finalize(), false, @@ -2603,7 +2759,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .abi3(None) + .abi3() .unwrap() .finalize(), true, @@ -2615,7 +2771,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY39) - .abi3(None) + .abi3() .unwrap() .finalize(), false, @@ -2627,7 +2783,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY311) - .abi3(None) + .abi3() .unwrap() .finalize(), false, @@ -2639,7 +2795,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .abi3(None) + .abi3() .unwrap() .finalize(), false, @@ -2653,7 +2809,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .abi3(None) + .abi3() .unwrap() .finalize(), false, @@ -2665,7 +2821,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY310) - .abi3(None) + .abi3() .unwrap() .finalize(), false, @@ -2783,7 +2939,7 @@ mod tests { assert_eq!( super::default_lib_name_unix( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) - .abi3(None) + .abi3() .unwrap() .finalize(), true, @@ -2794,6 +2950,25 @@ mod tests { ); } + #[test] + fn abi_builder_error_paths() { + let builder = PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY314) + .free_threaded() + .unwrap() + .abi3(); + assert!(builder.is_err()); + assert!(builder + .unwrap_err() + .to_string() + .contains("ABI already chosen!")); + + let builder = PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .free_threaded(); + + assert!(builder.is_err()); + assert!(builder.unwrap_err().to_string().contains("Cannot target")); + } + #[test] fn parse_cross_python_version() { let env_vars = CrossCompileEnvVars { @@ -2846,40 +3021,36 @@ mod tests { } #[test] - fn interpreter_version_reduced_to_abi3() { - let config = InterpreterConfig { - abi: PythonAbiBuilder::new( - PythonImplementation::CPython, - // Make this greater than the target abi3 version to reduce to below - PythonVersion { major: 3, minor: 9 }, + fn target_abi3_version_different_from_host() { + let implementation = PythonImplementation::CPython; + let host_version = PythonVersion::PY39; + let target_version = PythonVersion::PY38; + let config = InterpreterConfigBuilder::new(implementation, host_version) + .target_abi( + PythonAbiBuilder::new(implementation, target_version) + .abi3() + .unwrap() + .finalize(), ) - .abi3(Some(PythonVersion::PY38)) .unwrap() - .finalize(), - build_flags: BuildFlags::default(), - pointer_width: None, - executable: None, - lib_dir: None, - lib_name: None, - shared: true, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - }; - - assert_eq!(config.abi.version, PythonVersion { major: 3, minor: 8 }); + .finalize(); + assert_eq!(config.target_abi.version, target_version); + assert_eq!(config.version, host_version); } #[test] fn abi3_version_cannot_be_higher_than_interpreter() { - let builder = PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38); - assert!(builder - .abi3(Some(PythonVersion { major: 3, minor: 9 })) - .unwrap_err() - .to_string() - .contains( - "cannot set a minimum Python version 3.9 higher than the interpreter version 3.8" - )); + if !have_python_interpreter() { + return; + } + + let interpreter = get_host_interpreter(Some(PythonVersion { + major: 3, + minor: 45, + })); + assert!(interpreter.unwrap_err().to_string().contains( + "cannot set a minimum Python version 3.45 higher than the interpreter version" + )); } #[test] @@ -2920,23 +3091,21 @@ mod tests { }; let sysconfigdata = super::parse_sysconfigdata(sysconfigdata_path).unwrap(); let parsed_config = InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(); + let implementation = PythonImplementation::CPython; assert_eq!( parsed_config, - InterpreterConfig { - stable_abi: CPythonABI::VersionSpecific, - build_flags: BuildFlags(interpreter_config.build_flags.0.clone()), - pointer_width: Some(64), - executable: None, - implementation: PythonImplementation::CPython, - lib_dir: interpreter_config.lib_dir.to_owned(), - lib_name: interpreter_config.lib_name.to_owned(), - shared: true, - version: interpreter_config.version, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } + InterpreterConfigBuilder::new( + implementation, + interpreter_config.version, + PythonAbiBuilder::new(implementation, interpreter_config.version).finalize() + ) + .build_flags(interpreter_config.build_flags.0.clone()) + .pointer_width(64) + .lib_dir(interpreter_config.lib_dir.to_owned()) + .lib_name(interpreter_config.lib_name.to_owned()) + .finalize() + .unwrap() ) } @@ -3055,19 +3224,11 @@ mod tests { #[test] fn test_build_script_outputs_base() { - let interpreter_config = InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY311) - .finalize(), - shared: true, - lib_name: Some("python3".into()), - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - }; + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY311; + let interpreter_config = InterpreterConfigBuilder::new(implementation, version) + .lib_name(Some("python3".into())) + .finalize(); assert_eq!( interpreter_config.build_script_outputs(), [ @@ -3079,9 +3240,9 @@ mod tests { ); let interpreter_config = InterpreterConfig { - abi: PythonAbi { + target_abi: PythonAbi { implementation: PythonImplementation::PyPy, - ..interpreter_config.abi + ..interpreter_config.target_abi }, ..interpreter_config }; @@ -3099,21 +3260,13 @@ mod tests { #[test] fn test_build_script_outputs_abi3() { - let interpreter_config = InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .abi3(None) - .unwrap() - .finalize(), - shared: true, - lib_name: Some("python3".into()), - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - }; + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY39; + let interpreter_config = InterpreterConfigBuilder::new(implementation, version) + .abi3() + .unwrap() + .lib_name(Some("python3".into())) + .finalize(); assert_eq!( interpreter_config.build_script_outputs(), @@ -3125,9 +3278,9 @@ mod tests { ); let interpreter_config = InterpreterConfig { - abi: PythonAbi { + target_abi: PythonAbi { implementation: PythonImplementation::PyPy, - ..interpreter_config.abi + ..interpreter_config.target_abi }, ..interpreter_config }; @@ -3144,24 +3297,13 @@ mod tests { #[test] fn test_build_script_outputs_gil_disabled() { - let mut build_flags = BuildFlags::default(); - build_flags.0.insert(BuildFlag::Py_GIL_DISABLED); - let interpreter_config = InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) - .free_threaded() - .unwrap() - .finalize(), - shared: true, - lib_name: Some("python3".into()), - lib_dir: None, - executable: None, - pointer_width: None, - build_flags, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - }; - + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY313; + let interpreter_config = InterpreterConfigBuilder::new(implementation, version) + .free_threaded() + .unwrap() + .lib_name(Some("python3".into())) + .finalize(); assert_eq!( interpreter_config.build_script_outputs(), [ @@ -3176,24 +3318,51 @@ mod tests { ); } + #[test] + fn test_interpreter_config_builder_gil_disabled_flag() { + let builder = + InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); + let mut flags = BuildFlags::new(); + flags.0.insert(BuildFlag::Py_GIL_DISABLED); + assert!(builder + .build_flags(flags) + .unwrap_err() + .to_string() + .contains("Must target a free-threaded ABI")); + + let builder = + InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); + let mut flags = BuildFlags::new(); + flags.0.insert(BuildFlag::Py_GIL_DISABLED); + assert!(builder + .abi3() + .unwrap() + .build_flags(flags) + .unwrap_err() + .to_string() + .contains("target ABI is not free-threaded")); + + let builder = + InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); + let config = builder.free_threaded().unwrap(); + assert!(config + .build_flags + .unwrap() + .0 + .contains(&BuildFlag::Py_GIL_DISABLED)) + } + #[test] fn test_build_script_outputs_debug() { let mut build_flags = BuildFlags::default(); build_flags.0.insert(BuildFlag::Py_DEBUG); - let interpreter_config = InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) - .finalize(), - shared: true, - lib_name: Some("python3".into()), - lib_dir: None, - executable: None, - pointer_width: None, - build_flags, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - }; - + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY38; + let interpreter_config = InterpreterConfigBuilder::new(implementation, version) + .lib_name(Some("python3".into())) + .build_flags(build_flags) + .unwrap() + .finalize(); assert_eq!( interpreter_config.build_script_outputs(), [ @@ -3234,19 +3403,9 @@ mod tests { #[test] fn test_apply_default_lib_name_to_config_file() { - let mut config = InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .finalize(), - shared: true, - lib_name: None, - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - }; + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY39; + let mut config = InterpreterConfigBuilder::new(implementation, version).finalize(); let unix = Triple::from_str("x86_64-unknown-linux-gnu").unwrap(); let win_x64 = Triple::from_str("x86_64-pc-windows-msvc").unwrap(); @@ -3264,8 +3423,8 @@ mod tests { assert_eq!(config.lib_name, Some("python39".into())); // PyPy - config.abi.implementation = PythonImplementation::PyPy; - config.abi.version = PythonVersion { + config.target_abi.implementation = PythonImplementation::PyPy; + config.target_abi.version = PythonVersion { major: 3, minor: 11, }; @@ -3277,11 +3436,11 @@ mod tests { config.apply_default_lib_name_to_config_file(&win_x64); assert_eq!(config.lib_name, Some("libpypy3.11-c".into())); - config.abi.implementation = PythonImplementation::CPython; + config.target_abi.implementation = PythonImplementation::CPython; // Free-threaded - config.abi.kind = PythonAbiKind::VersionSpecific(true); - config.abi.version = PythonVersion { + config.target_abi.kind = PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded); + config.target_abi.version = PythonVersion { major: 3, minor: 13, }; @@ -3300,9 +3459,9 @@ mod tests { config.build_flags.0.remove(&BuildFlag::Py_GIL_DISABLED); // abi3 - config.abi = PythonAbi { + config.target_abi = PythonAbi { kind: PythonAbiKind::Abi3, - ..config.abi + ..config.target_abi }; config.lib_name = None; config.apply_default_lib_name_to_config_file(&unix); diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 457e1aa793d..a41eb442b56 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -20,7 +20,8 @@ use std::{env, process::Command, str::FromStr, sync::OnceLock}; pub use impl_::{ cross_compiling_from_to, find_all_sysconfigdata, parse_sysconfigdata, BuildFlag, BuildFlags, - CrossCompileConfig, InterpreterConfig, PythonAbi, PythonImplementation, PythonVersion, Triple, + CrossCompileConfig, InterpreterConfig, InterpreterConfigBuilder, PythonAbi, + PythonImplementation, PythonVersion, Triple, }; use target_lexicon::OperatingSystem; @@ -390,13 +391,13 @@ pub mod pyo3_build_script_impl { interpreter_config: &InterpreterConfig, supported_version: PythonVersion, ) -> Self { - let implementation = match interpreter_config.abi.implementation { + let implementation = match interpreter_config.target_abi.implementation { PythonImplementation::CPython => "Python", PythonImplementation::PyPy => "PyPy", PythonImplementation::GraalPy => "GraalPy", PythonImplementation::RustPython => "RustPython", }; - let version = &interpreter_config.abi.version; + let version = &interpreter_config.target_abi.version; let message = format!( "the configured {implementation} version ({version}) is newer than PyO3's maximum supported version ({supported_version})\n\ = help: this package is being built with PyO3 version {current_version}\n\ @@ -483,10 +484,14 @@ mod tests { #[test] fn python_framework_link_args() { let mut buf = Vec::new(); - + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY313; + let target_abi = PythonAbiBuilder::new(implementation, version).finalize(); let interpreter_config = InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) - .finalize(), + implementation, + version, + target_abi, + abi3: false, shared: true, lib_name: None, lib_dir: None, @@ -523,9 +528,14 @@ mod tests { #[test] #[cfg(feature = "resolve-config")] fn test_maximum_version_exceeded_formatting() { + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY313; + let target_abi = PythonAbiBuilder::new(implementation, version).finalize(); let interpreter_config = InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) - .finalize(), + implementation, + version, + target_abi, + abi3: false, shared: true, lib_name: None, lib_dir: None, diff --git a/pyo3-ffi-check/macro/src/lib.rs b/pyo3-ffi-check/macro/src/lib.rs index 3c6eb29cfb9..82a90bfcfd3 100644 --- a/pyo3-ffi-check/macro/src/lib.rs +++ b/pyo3-ffi-check/macro/src/lib.rs @@ -49,7 +49,7 @@ pub fn for_all_structs(input: proc_macro::TokenStream) -> proc_macro::TokenStrea .strip_suffix(".html") .unwrap(); - if pyo3_build_config::get().abi.version < PythonVersion::PY315 + if pyo3_build_config::get().target_abi.version < PythonVersion::PY315 && struct_name == "PyBytesWriter" { // PyBytesWriter was added in Python 3.15 @@ -154,7 +154,7 @@ pub fn for_all_fields(input: proc_macro::TokenStream) -> proc_macro::TokenStream // bindgen picked `type_` as the field name to avoid the `type` keyword, but PyO3 uses `type_code` all_fields.remove("type_"); } else if struct_name == "PyObject" - && pyo3_build_config::get().abi.version >= PythonVersion::PY312 + && pyo3_build_config::get().target_abi.version >= PythonVersion::PY312 { // bindgen picked `__bindgen_anon_1` as the field name for the anonymous union containing ob_refcnt, // PyO3 uses ob_refcnt directly @@ -172,7 +172,8 @@ pub fn for_all_fields(input: proc_macro::TokenStream) -> proc_macro::TokenStream let field_ident = Ident::new(&field_name, Span::call_site()); - let bindgen_field_ident = if (pyo3_build_config::get().abi.version >= PythonVersion::PY312) + let bindgen_field_ident = if (pyo3_build_config::get().target_abi.version + >= PythonVersion::PY312) && struct_name == "PyObject" && field_name == "ob_refcnt" { diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 65cddd23bcb..34300893461 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -45,10 +45,10 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { return Ok(()); } - match interpreter_config.abi.implementation { + match interpreter_config.target_abi.implementation { PythonImplementation::CPython => { let versions = SUPPORTED_VERSIONS_CPYTHON; - let interp_version = interpreter_config.abi.version; + let interp_version = interpreter_config.target_abi.version; ensure!( interp_version >= versions.min, "the configured Python interpreter version ({}) is lower than PyO3's minimum supported version ({})", @@ -70,7 +70,7 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { let mut error = MaximumVersionExceeded::new(interpreter_config, versions.max); let major = interp_version.major; let minor = interp_version.minor; - if interpreter_config.abi.kind.is_free_threaded() { + if interpreter_config.target_abi.kind.is_free_threaded() { error.add_help(&format!( "the free-threaded build of CPython {major}{minor} does not support the limited API so this check cannot be suppressed.", )); @@ -84,29 +84,29 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { } } - if interpreter_config.abi.kind.is_free_threaded() { + if interpreter_config.target_abi.kind.is_free_threaded() { let min_free_threaded_version = PythonVersion { major: 3, minor: 14, }; ensure!( - interpreter_config.abi.version >= min_free_threaded_version, + interpreter_config.target_abi.version >= min_free_threaded_version, "PyO3 does not support the free-threaded build of CPython versions below {}, the selected Python version is {}", min_free_threaded_version, - interpreter_config.abi.version, + interpreter_config.target_abi.version, ); } } PythonImplementation::PyPy => { let versions = SUPPORTED_VERSIONS_PYPY; ensure!( - interpreter_config.abi.version >= versions.min, + interpreter_config.target_abi.version >= versions.min, "the configured PyPy interpreter version ({}) is lower than PyO3's minimum supported version ({})", - interpreter_config.abi.version, + interpreter_config.target_abi.version, versions.min, ); // PyO3 does not support abi3, so we cannot offer forward compatibility - if interpreter_config.abi.version > versions.max { + if interpreter_config.target_abi.version > versions.max { let error = MaximumVersionExceeded::new(interpreter_config, versions.max); return Err(error.finish().into()); } @@ -114,13 +114,13 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { PythonImplementation::GraalPy => { let versions = SUPPORTED_VERSIONS_GRAALPY; ensure!( - interpreter_config.abi.version >= versions.min, + interpreter_config.target_abi.version >= versions.min, "the configured GraalPy interpreter version ({}) is lower than PyO3's minimum supported version ({})", - interpreter_config.abi.version, + interpreter_config.target_abi.version, versions.min, ); // GraalPy does not support abi3, so we cannot offer forward compatibility - if interpreter_config.abi.version > versions.max { + if interpreter_config.target_abi.version > versions.max { let error = MaximumVersionExceeded::new(interpreter_config, versions.max); return Err(error.finish().into()); } @@ -128,10 +128,10 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { PythonImplementation::RustPython => {} } - if let PythonAbiKind::Abi3 = interpreter_config.abi.kind { - match interpreter_config.abi.implementation { + if let PythonAbiKind::Abi3 = interpreter_config.target_abi.kind { + match interpreter_config.target_abi.implementation { PythonImplementation::CPython => { - if interpreter_config.abi.kind.is_free_threaded() { + if interpreter_config.target_abi.kind.is_free_threaded() { warn!( "The free-threaded build of CPython does not support abi3 so the build artifacts will be version-specific." ) From 77039d27c2a3bf3198526008828280631ed23a57 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 24 Apr 2026 14:17:42 -0600 Subject: [PATCH 083/195] revert noxfile change --- noxfile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index bd91e7544b6..b7130b30d4c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1640,7 +1640,8 @@ def set( self._config_file.truncate(0) self._config_file.write( f"""\ -abi={implementation}-version_specific(false)-{version} +implementation={implementation} +version={version} build_flags={",".join(build_flags)} suppress_build_script_link_lines=true """ From 0765953e0c4c98090fa61072daa6290bacaecc85 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 24 Apr 2026 15:27:22 -0600 Subject: [PATCH 084/195] fix clippy-all --- pyo3-build-config/src/impl_.rs | 51 +++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index c1b84a7c12a..8f06734b9e3 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -452,7 +452,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) }; let cygwin = soabi.ends_with("cygwin"); let mut abi_builder = PythonAbiBuilder::from_build_env(implementation, version)?; - if gil_disabled { + if gil_disabled && abi_builder.kind.is_none() { abi_builder = abi_builder.free_threaded()?; } let target_abi = abi_builder.finalize(); @@ -499,7 +499,8 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) config.target_abi.implementation, get_abi3_version().unwrap_or(config.target_abi.version), )?; - if config.target_abi.kind.is_free_threaded() { + // only allow free-threaded builds if the build environment didn't force an abi3 build + if config.target_abi.kind.is_free_threaded() && abi_builder.kind.is_none() { abi_builder = abi_builder.free_threaded()?; } config.target_abi = abi_builder.finalize(); @@ -591,15 +592,28 @@ 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 target_abi = if !(target_abi.is_some() || abi3.is_some()) { + let target_abi = if !(target_abi.is_some() || abi3.is_some() || build_flags.is_some()) { PythonAbiBuilder::new(implementation, version).finalize() } else if abi3.is_some() && abi3.unwrap() { - // should we produce a user-visible warning about this? + warn!("abi3 configuration file option is deprecated, set target_abi instead"); PythonAbiBuilder::new(implementation, version) .abi3() .unwrap() .finalize() + } else if let Some(ref flags) = build_flags { + if flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { + PythonAbiBuilder::new(implementation, version) + .free_threaded() + .unwrap() + .finalize() + } else { + // we could avoid this branch with if let chains + ensure!( + !(target_abi.is_some() && abi3.is_some()), + "Invalid config that sets both target_abi and abi3." + ); + target_abi.unwrap_or(PythonAbiBuilder::new(implementation, version).finalize()) + } } else { ensure!( !(target_abi.is_some() && abi3.is_some()), @@ -737,7 +751,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) } } -#[cfg_attr(test, derive(Debug))] +#[derive(Debug)] pub struct InterpreterConfigBuilder { implementation: PythonImplementation, version: PythonVersion, @@ -775,7 +789,11 @@ impl InterpreterConfigBuilder { } pub fn target_abi(self, target_abi: PythonAbi) -> Result { - ensure!(self.target_abi.is_none(), "Target ABI already set!"); + ensure!( + self.target_abi.is_none(), + "Target ABI already set to {:?}", + target_abi + ); Ok(InterpreterConfigBuilder { target_abi: Some(target_abi), ..self @@ -946,7 +964,10 @@ impl PythonAbiBuilder { pub fn abi3(self) -> Result { if self.kind.is_some() { - bail!("Target ABI already chosen!") + bail!( + "ABI kind already set to {:?}, cannot set to abi3", + self.kind + ) } // PyPy and GraalPy don't support abi3; don't adjust the version @@ -972,7 +993,10 @@ impl PythonAbiBuilder { pub fn free_threaded(self) -> Result { if self.kind.is_some() { - bail!("Target ABI already chosen!") + bail!( + "Target ABI already set to {:?}, cannot set to free-threaded", + self.kind + ) } if self.version < PythonVersion::PY313 { let version = self.version; @@ -1569,8 +1593,8 @@ impl FromStr for BuildFlag { /// is the equivalent of `#ifdef {varname}` in C. /// /// see Misc/SpecialBuilds.txt in the python source for what these mean. -#[cfg_attr(test, derive(Debug, PartialEq, Eq))] -#[derive(Clone, Default)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[derive(Debug, Clone, Default)] pub struct BuildFlags(pub HashSet); impl BuildFlags { @@ -1979,7 +2003,8 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result> { config.target_abi.implementation, get_abi3_version().unwrap_or(config.target_abi.version), )?; - if config.target_abi.kind.is_free_threaded() { + if config.target_abi.kind.is_free_threaded() && abi_builder.kind.is_none() { abi_builder = abi_builder.free_threaded()?; } config.target_abi = abi_builder.finalize(); From cde688c79da8096dbb248978210e7806fcad0adb Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 24 Apr 2026 15:32:22 -0600 Subject: [PATCH 085/195] fix build-config test --- 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 8f06734b9e3..98ae8db8afd 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -2985,7 +2985,7 @@ mod tests { assert!(builder .unwrap_err() .to_string() - .contains("ABI already chosen!")); + .contains("ABI kind already set to")); let builder = PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) .free_threaded(); From b11b262d5a193216af0f43c7550708466ad3a2f8 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 24 Apr 2026 16:16:07 -0600 Subject: [PATCH 086/195] Fix issues linking against onld stable ABI versions --- pyo3-build-config/src/impl_.rs | 76 ++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 32 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 98ae8db8afd..5ad6c6dbe20 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -451,7 +451,11 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) None => false, }; let cygwin = soabi.ends_with("cygwin"); - let mut abi_builder = PythonAbiBuilder::from_build_env(implementation, version)?; + let mut abi_builder = PythonAbiBuilder::from_build_env( + implementation, + version, + if is_abi3() { Some(version) } else { None }, + )?; if gil_disabled && abi_builder.kind.is_none() { abi_builder = abi_builder.free_threaded()?; } @@ -495,9 +499,11 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .context("failed to parse contents of PYO3_CONFIG_FILE")?; // If the abi3 feature is enabled, the minimum Python version is constrained by the abi3 // feature. + let abi3_version = get_abi3_version(); let mut abi_builder = PythonAbiBuilder::from_build_env( - config.target_abi.implementation, - get_abi3_version().unwrap_or(config.target_abi.version), + config.implementation, + config.version, + abi3_version, )?; // only allow free-threaded builds if the build environment didn't force an abi3 build if config.target_abi.kind.is_free_threaded() && abi_builder.kind.is_none() { @@ -592,35 +598,38 @@ 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 target_abi = if !(target_abi.is_some() || abi3.is_some() || build_flags.is_some()) { - PythonAbiBuilder::new(implementation, version).finalize() - } else if abi3.is_some() && abi3.unwrap() { - warn!("abi3 configuration file option is deprecated, set target_abi instead"); - PythonAbiBuilder::new(implementation, version) - .abi3() - .unwrap() - .finalize() - } else if let Some(ref flags) = build_flags { - if flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { + let target_abi = + if !(is_abi3() || target_abi.is_some() || abi3.is_some() || build_flags.is_some()) { + PythonAbiBuilder::new(implementation, version).finalize() + } else if (abi3.is_some() && abi3.unwrap()) || is_abi3() { + if abi3.is_some() && abi3.unwrap() { + warn!("abi3 configuration file option is deprecated, set target_abi instead"); + } PythonAbiBuilder::new(implementation, version) - .free_threaded() + .abi3() .unwrap() .finalize() + } else if let Some(ref flags) = build_flags { + if flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { + PythonAbiBuilder::new(implementation, version) + .free_threaded() + .unwrap() + .finalize() + } else { + // we could avoid this branch with if let chains + ensure!( + !(target_abi.is_some() && abi3.is_some()), + "Invalid config that sets both target_abi and abi3." + ); + target_abi.unwrap_or(PythonAbiBuilder::new(implementation, version).finalize()) + } } else { - // we could avoid this branch with if let chains ensure!( !(target_abi.is_some() && abi3.is_some()), "Invalid config that sets both target_abi and abi3." ); - target_abi.unwrap_or(PythonAbiBuilder::new(implementation, version).finalize()) - } - } else { - ensure!( - !(target_abi.is_some() && abi3.is_some()), - "Invalid config that sets both target_abi and abi3." - ); - target_abi.unwrap() - }; + target_abi.unwrap() + }; let build_flags = build_flags.unwrap_or_default(); let builder = InterpreterConfigBuilder::new(implementation, version) @@ -949,13 +958,14 @@ impl PythonAbiBuilder { pub fn from_build_env( implementation: PythonImplementation, version: PythonVersion, + abi3_version: Option, ) -> Result { let builder = PythonAbiBuilder { implementation, - version, + version: abi3_version.unwrap_or(version), kind: None, }; - if is_abi3() { + if abi3_version.is_some() && is_abi3() { builder.abi3() } else { Ok(builder) @@ -1271,9 +1281,6 @@ fn have_python_interpreter() -> bool { env_var("PYO3_NO_PYTHON").is_none() } -/// Checks if `abi3` or any of the `abi3-py3*` features is enabled for the PyO3 crate. -/// -/// Must be called from a PyO3 crate build script. fn is_abi3() -> bool { cargo_env_var("CARGO_FEATURE_ABI3").is_some() || env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY").is_some_and(|os_str| os_str == "1") @@ -2002,7 +2009,11 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result> { let interpreter_config = if let Some(cross_config) = cross_compiling_from_cargo_env()? { let mut config = load_cross_compile_config(cross_config)?; let mut abi_builder = PythonAbiBuilder::from_build_env( - config.target_abi.implementation, - get_abi3_version().unwrap_or(config.target_abi.version), + config.implementation, + config.version, + get_abi3_version(), )?; if config.target_abi.kind.is_free_threaded() && abi_builder.kind.is_none() { abi_builder = abi_builder.free_threaded()?; From 9cb420ac5fa5224fdc4edcf168229c83889b5c07 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 28 Apr 2026 14:40:51 -0600 Subject: [PATCH 087/195] remove load-bearing debug impls --- pyo3-build-config/src/impl_.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 5ad6c6dbe20..81f5c22063f 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -760,7 +760,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) } } -#[derive(Debug)] +#[cfg_attr(test, derive(Debug))] pub struct InterpreterConfigBuilder { implementation: PythonImplementation, version: PythonVersion, @@ -800,7 +800,7 @@ impl InterpreterConfigBuilder { pub fn target_abi(self, target_abi: PythonAbi) -> Result { ensure!( self.target_abi.is_none(), - "Target ABI already set to {:?}", + "Target ABI already set to {}", target_abi ); Ok(InterpreterConfigBuilder { @@ -939,7 +939,7 @@ impl InterpreterConfigBuilder { } } -#[derive(Debug)] +#[cfg_attr(test, derive(Debug))] pub struct PythonAbiBuilder { implementation: PythonImplementation, version: PythonVersion, @@ -975,8 +975,8 @@ impl PythonAbiBuilder { pub fn abi3(self) -> Result { if self.kind.is_some() { bail!( - "ABI kind already set to {:?}, cannot set to abi3", - self.kind + "ABI kind already set to {}, cannot set to abi3", + self.kind.unwrap() ) } @@ -1004,8 +1004,8 @@ impl PythonAbiBuilder { pub fn free_threaded(self) -> Result { if self.kind.is_some() { bail!( - "Target ABI already set to {:?}, cannot set to free-threaded", - self.kind + "Target ABI already set to {}, cannot set to free-threaded", + self.kind.unwrap() ) } if self.version < PythonVersion::PY313 { @@ -1034,7 +1034,8 @@ impl PythonAbiBuilder { } #[non_exhaustive] -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Copy, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(Debug))] pub struct PythonAbi { /// The Python implementation flavor. /// @@ -1078,7 +1079,8 @@ impl FromStr for PythonAbi { } /// The "kind" of stable ABI. Either abi3 or abi3t currently. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(test, derive(Debug))] pub enum PythonAbiKind { /// The original stable ABI, supporting Python 3.2 and up Abi3, @@ -1128,7 +1130,8 @@ impl PythonAbiKind { } /// Whether the ABI is for the GIL-enabled or free-threaded build. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(test, derive(Debug))] pub enum GilUsed { /// The original PyObject layout GilEnabled, From 7fe98abccc040f0f9cde8a8fe9a1f8a63787e87d Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 28 Apr 2026 15:05:22 -0600 Subject: [PATCH 088/195] fix issue with InterpreterConfig::from_interpreter never returning an abi3 config --- pyo3-build-config/src/impl_.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 81f5c22063f..91e62764c9e 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -355,6 +355,10 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let mut abi_builder = PythonAbiBuilder::new(implementation, target_version); + if is_abi3() || abi3_version.is_some() { + abi_builder = abi_builder.abi3()?; + } + if gil_disabled { abi_builder = abi_builder.free_threaded()?; } From 41be8375c7628094ca6432dc229159c267e94293 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 28 Apr 2026 15:10:19 -0600 Subject: [PATCH 089/195] fix clippy lint --- pyo3-build-config/src/impl_.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 91e62764c9e..58607ad841c 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -977,11 +977,8 @@ impl PythonAbiBuilder { } pub fn abi3(self) -> Result { - if self.kind.is_some() { - bail!( - "ABI kind already set to {}, cannot set to abi3", - self.kind.unwrap() - ) + if let Some(kind) = self.kind { + bail!("ABI kind already set to {kind}, cannot set to abi3",) } // PyPy and GraalPy don't support abi3; don't adjust the version @@ -1006,11 +1003,8 @@ impl PythonAbiBuilder { } pub fn free_threaded(self) -> Result { - if self.kind.is_some() { - bail!( - "Target ABI already set to {}, cannot set to free-threaded", - self.kind.unwrap() - ) + if let Some(kind) = self.kind { + bail!("Target ABI already set to {kind}, cannot set to free-threaded",) } if self.version < PythonVersion::PY313 { let version = self.version; From 58162a4805d041274240d2efb210814ef1c22609 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 28 Apr 2026 15:34:32 -0600 Subject: [PATCH 090/195] fix PythonAbiBuilder::from_build_env --- pyo3-build-config/src/impl_.rs | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 58607ad841c..d7c6fa75733 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -353,11 +353,8 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) version }; - let mut abi_builder = PythonAbiBuilder::new(implementation, target_version); - - if is_abi3() || abi3_version.is_some() { - abi_builder = abi_builder.abi3()?; - } + let mut abi_builder = + PythonAbiBuilder::from_build_env(implementation, target_version, abi3_version)?; if gil_disabled { abi_builder = abi_builder.free_threaded()?; @@ -455,11 +452,8 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) None => false, }; let cygwin = soabi.ends_with("cygwin"); - let mut abi_builder = PythonAbiBuilder::from_build_env( - implementation, - version, - if is_abi3() { Some(version) } else { None }, - )?; + let mut abi_builder = + PythonAbiBuilder::from_build_env(implementation, version, Some(version))?; if gil_disabled && abi_builder.kind.is_none() { abi_builder = abi_builder.free_threaded()?; } @@ -969,7 +963,7 @@ impl PythonAbiBuilder { version: abi3_version.unwrap_or(version), kind: None, }; - if abi3_version.is_some() && is_abi3() { + if is_abi3() { builder.abi3() } else { Ok(builder) @@ -2010,11 +2004,7 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Date: Tue, 28 Apr 2026 16:30:06 -0600 Subject: [PATCH 091/195] Only target abi3 for non-free-threaded builds --- pyo3-build-config/src/impl_.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index d7c6fa75733..5e244d19d35 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -353,8 +353,11 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) version }; - let mut abi_builder = - PythonAbiBuilder::from_build_env(implementation, target_version, abi3_version)?; + let mut abi_builder = if gil_disabled { + PythonAbiBuilder::new(implementation, target_version) + } else { + PythonAbiBuilder::from_build_env(implementation, target_version, abi3_version)? + }; if gil_disabled { abi_builder = abi_builder.free_threaded()?; @@ -599,14 +602,6 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let target_abi = if !(is_abi3() || target_abi.is_some() || abi3.is_some() || build_flags.is_some()) { PythonAbiBuilder::new(implementation, version).finalize() - } else if (abi3.is_some() && abi3.unwrap()) || is_abi3() { - if abi3.is_some() && abi3.unwrap() { - warn!("abi3 configuration file option is deprecated, set target_abi instead"); - } - PythonAbiBuilder::new(implementation, version) - .abi3() - .unwrap() - .finalize() } else if let Some(ref flags) = build_flags { if flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { PythonAbiBuilder::new(implementation, version) @@ -621,6 +616,14 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) ); target_abi.unwrap_or(PythonAbiBuilder::new(implementation, version).finalize()) } + } else if (abi3.is_some() && abi3.unwrap()) || is_abi3() { + if abi3.is_some() && abi3.unwrap() { + warn!("abi3 configuration file option is deprecated, set target_abi instead"); + } + PythonAbiBuilder::new(implementation, version) + .abi3() + .unwrap() + .finalize() } else { ensure!( !(target_abi.is_some() && abi3.is_some()), From c8993904e6a76c13298c958f11aa34e93920c3c2 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 28 Apr 2026 16:37:34 -0600 Subject: [PATCH 092/195] fix logic error in InterpreterConfig::from_reader --- pyo3-build-config/src/impl_.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 5e244d19d35..a44b1cfc6b0 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -455,12 +455,12 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) None => false, }; let cygwin = soabi.ends_with("cygwin"); - let mut abi_builder = - PythonAbiBuilder::from_build_env(implementation, version, Some(version))?; - if gil_disabled && abi_builder.kind.is_none() { - abi_builder = abi_builder.free_threaded()?; + let target_abi = if gil_disabled { + PythonAbiBuilder::new(implementation, version).free_threaded()? + } else { + PythonAbiBuilder::from_build_env(implementation, version, Some(version))? } - let target_abi = abi_builder.finalize(); + .finalize(); let lib_name = Some(default_lib_name_unix( target_abi, cygwin, From 942ecc97a86ab77e968883124cab862565f2837f Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 30 Apr 2026 12:46:10 -0600 Subject: [PATCH 093/195] smooth over merge --- pyo3-build-config/src/impl_.rs | 269 +++++++++++++++------------------ pyo3-ffi/build.rs | 19 +++ 2 files changed, 143 insertions(+), 145 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 3435cd53903..55e9861993c 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -83,54 +83,6 @@ 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 { - Ok(CPythonABI::ABI3) - } else if abi3t { - Ok(CPythonABI::ABI3t) - } else { - 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 @@ -256,23 +208,20 @@ impl InterpreterConfig { } match self.target_abi.kind { - PythonAbiKind::Abi3 => { - if !self.target_abi.kind.is_free_threaded() { - out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); + PythonAbiKind::Stable(kind) => { + out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); + if kind == StableAbi::Abi3t { + out.push("cargo:rustc-cfg=Py_GIL_DISABLED".to_owned()); + out.push("cargo:rustc-cfg=Py_TARGET_ABI3T".to_owned()); } } - PythonAbiKind::VersionSpecific(_) => {} - } - - for flag in &self.build_flags.0 { - match flag { - BuildFlag::Py_GIL_DISABLED => { - out.push("cargo:rustc-cfg=Py_GIL_DISABLED".to_owned()) + PythonAbiKind::VersionSpecific(kind) => match kind { + GilUsed::FreeThreaded => { + out.push("cargo:rustc-cfg=Py_GIL_DISABLED".to_owned()); } - flag => out.push(format!("cargo:rustc-cfg=py_sys_config=\"{flag}\"")), - } + GilUsed::GilEnabled => {} + }, } - out } @@ -647,38 +596,47 @@ 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 target_abi = - if !(is_abi3() || target_abi.is_some() || abi3.is_some() || build_flags.is_some()) { - PythonAbiBuilder::new(implementation, version).finalize() - } else if let Some(ref flags) = build_flags { - if flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { - PythonAbiBuilder::new(implementation, version) - .free_threaded() - .unwrap() - .finalize() - } else { - // we could avoid this branch with if let chains - ensure!( - !(target_abi.is_some() && abi3.is_some()), - "Invalid config that sets both target_abi and abi3." - ); - target_abi.unwrap_or(PythonAbiBuilder::new(implementation, version).finalize()) - } - } else if (abi3.is_some() && abi3.unwrap()) || is_abi3() { - if abi3.is_some() && abi3.unwrap() { - warn!("abi3 configuration file option is deprecated, set target_abi instead"); - } + let target_abi = if !(is_abi3() + || is_abi3t() + || target_abi.is_some() + || abi3.is_some() + || build_flags.is_some()) + { + PythonAbiBuilder::new(implementation, version).finalize() + } else if is_abi3t() { + PythonAbiBuilder::new(implementation, version) + .stable_abi(StableAbi::Abi3t) + .unwrap() + .finalize() + } else if let Some(ref flags) = build_flags { + if flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { PythonAbiBuilder::new(implementation, version) - .abi3() + .free_threaded() .unwrap() .finalize() } else { + // we could avoid this branch with if let chains ensure!( !(target_abi.is_some() && abi3.is_some()), "Invalid config that sets both target_abi and abi3." ); - target_abi.unwrap() - }; + target_abi.unwrap_or(PythonAbiBuilder::new(implementation, version).finalize()) + } + } else if (abi3.is_some() && abi3.unwrap()) || is_abi3() { + if abi3.is_some() && abi3.unwrap() { + warn!("abi3 configuration file option is deprecated, set target_abi instead"); + } + PythonAbiBuilder::new(implementation, version) + .stable_abi(StableAbi::Abi3) + .unwrap() + .finalize() + } else { + ensure!( + !(target_abi.is_some() && abi3.is_some()), + "Invalid config that sets both target_abi and abi3." + ); + target_abi.unwrap() + }; let build_flags = build_flags.unwrap_or_default(); let builder = InterpreterConfigBuilder::new(implementation, version) @@ -858,13 +816,13 @@ impl InterpreterConfigBuilder { }) } - pub fn abi3(self) -> Result { + pub fn stable_abi(self, kind: StableAbi) -> Result { let implementation = self.implementation; let version = self.version; self.target_abi( PythonAbiBuilder::new(implementation, version) - .abi3() - // this can't panic because abi3() is caleld on a builder with no chosen ABI + .stable_abi(kind) + // this can't panic because stable_abi() is called on a builder with no chosen ABI .unwrap() .finalize(), ) @@ -933,11 +891,13 @@ impl InterpreterConfigBuilder { ensure!(self.build_flags.is_none(), "Build flags already set!"); let build_flags = if build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { ensure!(self.target_abi.is_some(), "Must target a free-threaded ABI if build flags contain Py_GIL_DISABLED but no target_abi is set"); - ensure!( - self.target_abi.unwrap().kind - == PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded), - "build_flags contains Py_GIL_DISABLED but target ABI is not free-threaded" - ); + if let Some(target_abi) = self.target_abi { + ensure!( + target_abi.kind == PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded) + || target_abi.kind == PythonAbiKind::Stable(StableAbi::Abi3t), + "build_flags contains Py_GIL_DISABLED but target ABI '{target_abi}' is not free-threaded" + ); + } build_flags } else if let Some(target_abi) = self.target_abi { let mut flags = build_flags.clone(); @@ -1015,33 +975,31 @@ impl PythonAbiBuilder { kind: None, }; if is_abi3() { - builder.abi3() + builder.stable_abi(StableAbi::Abi3) } else { Ok(builder) } } - pub fn abi3(self) -> Result { - if let Some(kind) = self.kind { - bail!("ABI kind already set to {kind}, cannot set to abi3",) - } - - // PyPy and GraalPy don't support abi3; don't adjust the version - if self.implementation.is_pypy() || self.implementation.is_graalpy() { - return Ok(PythonAbiBuilder { - implementation: self.implementation, - version: self.version, - kind: self.kind, - }); - } + pub fn stable_abi(self, kind: StableAbi) -> Result { + ensure!( + self.kind.is_none(), + "ABI kind already set to {}, cannot set to {}", + self.kind.unwrap(), + kind + ); + ensure!( + kind == StableAbi::Abi3 || kind == StableAbi::Abi3t, + "Cannot set a stable abi build with a version-specific ABI kind '{kind}'" + ); let mut build_version = self.version; if 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 {kind}-py3{STABLE_ABI_MAX_MINOR} because current Python is higher than the maximum supported"); build_version.minor = STABLE_ABI_MAX_MINOR; } Ok(PythonAbiBuilder { - kind: Some(PythonAbiKind::Abi3), + kind: Some(PythonAbiKind::Stable(kind)), version: build_version, ..self }) @@ -1125,8 +1083,8 @@ impl FromStr for PythonAbi { #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(test, derive(Debug))] pub enum PythonAbiKind { - /// The original stable ABI, supporting Python 3.2 and up - Abi3, + /// One of the stable ABIs, which supports multiple Python versions + Stable(StableAbi), /// Version specific ABI, which is different on the free-threaded build VersionSpecific(GilUsed), } @@ -1134,7 +1092,7 @@ pub enum PythonAbiKind { impl Display for PythonAbiKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - PythonAbiKind::Abi3 => write!(f, "abi3"), + PythonAbiKind::Stable(stable_abi) => write!(f, "{stable_abi}"), PythonAbiKind::VersionSpecific(gil_disabled) => { write!(f, "version_specific({gil_disabled})") } @@ -1147,7 +1105,8 @@ impl FromStr for PythonAbiKind { fn from_str(value: &str) -> Result { match value { - "abi3" => Ok(PythonAbiKind::Abi3), + "abi3" => Ok(PythonAbiKind::Stable(StableAbi::Abi3)), + "abi3t" => Ok(PythonAbiKind::Stable(StableAbi::Abi3t)), "version_specific(free_threaded)" => { Ok(PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)) } @@ -1163,15 +1122,30 @@ impl PythonAbiKind { pub fn is_free_threaded(&self) -> bool { match self { PythonAbiKind::VersionSpecific(gil_disabled) => *gil_disabled == GilUsed::FreeThreaded, - PythonAbiKind::Abi3 => false, + PythonAbiKind::Stable(StableAbi::Abi3) => false, + PythonAbiKind::Stable(StableAbi::Abi3t) => true, } } +} - pub fn is_abi3(&self) -> bool { - matches!(self, PythonAbiKind::Abi3) - } +/// Whether the ABI is for the GIL-enabled or free-threaded build. +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(test, derive(Debug))] +pub enum StableAbi { + /// The original stable ABI, supporting Python 3.2 and up + Abi3, + /// The free-threaded stable ABI, supporting Python 3.15 and up + Abi3t, } +impl Display for StableAbi { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + StableAbi::Abi3 => write!(f, "abi3"), + StableAbi::Abi3t => write!(f, "abi3t"), + } + } +} /// Whether the ABI is for the GIL-enabled or free-threaded build. #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(test, derive(Debug))] @@ -2102,7 +2076,7 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result { // FIXME: PyPy & GraalPy do not support the Stable ABI. let builder = InterpreterConfigBuilder::new(PythonImplementation::CPython, version) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap(); // abi3() sets the target_abi on the builder struct so unwrapping is safe let target_abi = builder.target_abi.unwrap(); @@ -2178,11 +2152,16 @@ fn default_lib_name_windows(abi: PythonAbi, mingw: bool, debug: bool) -> Result< "python{}{}_d", abi.version.major, abi.version.minor )) - } else if matches!(abi.kind, PythonAbiKind::Abi3) && !abi.implementation.is_graalpy() { - if debug { - Ok(WINDOWS_STABLE_ABI_DEBUG_LIB_NAME.to_owned()) + } else if abi.kind == PythonAbiKind::Stable(StableAbi::Abi3) + || abi.kind == PythonAbiKind::Stable(StableAbi::Abi3t) + { + let mut lib_name = if debug { + WINDOWS_STABLE_ABI_DEBUG_LIB_NAME.to_owned() } else { - Ok(WINDOWS_STABLE_ABI_LIB_NAME.to_owned()) + WINDOWS_STABLE_ABI_LIB_NAME.to_owned() + }; + if abi.kind == PythonAbiKind::Stable(StableAbi::Abi3t) { + lib_name.push('t'); } Ok(lib_name) } else if mingw { @@ -2217,8 +2196,10 @@ fn default_lib_name_unix(abi: PythonAbi, cygwin: bool, ld_version: Option<&str>) PythonImplementation::CPython => match ld_version { Some(ld_version) => Ok(format!("python{ld_version}")), None => { - if cygwin && matches!(abi.kind, PythonAbiKind::Abi3) { + if cygwin && matches!(abi.kind, PythonAbiKind::Stable(StableAbi::Abi3)) { Ok("python3".to_string()) + } else if cygwin && matches!(abi.kind, PythonAbiKind::Stable(StableAbi::Abi3t)) { + Ok("python3t".to_string()) } else if abi.kind.is_free_threaded() { ensure!(abi.version >= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", abi.version.major, abi.version.minor); Ok(format!( @@ -2352,13 +2333,11 @@ 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, - abi3t_version: Option, -) -> Result { +fn get_host_interpreter(stable_abi_version: Option) -> Result { let interpreter_path = find_interpreter()?; - let interpreter_config = InterpreterConfig::from_interpreter(interpreter_path, abi3_version)?; + let interpreter_config = + InterpreterConfig::from_interpreter(interpreter_path, stable_abi_version)?; Ok(interpreter_config) } @@ -2408,7 +2387,7 @@ pub fn make_interpreter_config() -> Result { (abi3_version.is_none() && abi3t_version.is_none()) || require_libdir_for_target(&host); if have_python_interpreter() { - match get_host_interpreter(abi3_version, abi3t_version) { + match get_host_interpreter(abi3_version.or(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), @@ -2474,7 +2453,7 @@ mod tests { let implementation = PythonImplementation::CPython; let version = MINIMUM_SUPPORTED_VERSION; let config = InterpreterConfigBuilder::new(implementation, version) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .pointer_width(32) .executable(Some("executable".into())) @@ -2512,7 +2491,7 @@ mod tests { let implementation = PythonImplementation::CPython; let version = MINIMUM_SUPPORTED_VERSION; let config = InterpreterConfigBuilder::new(implementation, version) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .pointer_width(32) .executable(Some("executable".into())) @@ -2705,7 +2684,7 @@ mod tests { let implementation = PythonImplementation::CPython; let version = PythonVersion::PY38; let config = InterpreterConfigBuilder::new(implementation, version) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .lib_name(Some("python3".into())) .finalize(); @@ -2719,7 +2698,7 @@ mod tests { let implementation = PythonImplementation::CPython; let version = PythonVersion::PY39; let config = InterpreterConfigBuilder::new(implementation, version) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(); assert_eq!(default_abi3_config(&host, min_version).unwrap(), config); @@ -2844,7 +2823,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(), false, @@ -2866,7 +2845,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(), true, @@ -2878,7 +2857,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY39) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(), false, @@ -2890,7 +2869,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY311) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(), false, @@ -2902,7 +2881,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(), false, @@ -2916,7 +2895,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(), false, @@ -2928,7 +2907,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY310) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(), false, @@ -3046,7 +3025,7 @@ mod tests { assert_eq!( super::default_lib_name_unix( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(), true, @@ -3062,7 +3041,7 @@ mod tests { let builder = PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY314) .free_threaded() .unwrap() - .abi3(); + .stable_abi(StableAbi::Abi3); assert!(builder.is_err()); assert!(builder .unwrap_err() @@ -3135,7 +3114,7 @@ mod tests { let config = InterpreterConfigBuilder::new(implementation, host_version) .target_abi( PythonAbiBuilder::new(implementation, target_version) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(), ) @@ -3370,7 +3349,7 @@ mod tests { let implementation = PythonImplementation::CPython; let version = PythonVersion::PY39; let interpreter_config = InterpreterConfigBuilder::new(implementation, version) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .lib_name(Some("python3".into())) .finalize(); @@ -3465,7 +3444,7 @@ mod tests { let mut flags = BuildFlags::new(); flags.0.insert(BuildFlag::Py_GIL_DISABLED); assert!(builder - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .build_flags(flags) .unwrap_err() diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index bfd1b9defb0..f6393606b8a 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -128,6 +128,25 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { PythonImplementation::RustPython => {} } + if let PythonAbiKind::Abi3t = interpreter_config.target_abi.kind { + match interpreter_config.target_abi.implementation { + PythonImplementation::CPython => { + ensure!( + interpreter_config.target_abi.version >= PythonVersion::PY315, + "Abi3t builds are not supported on CPython targets before Python 3.15" + ) + } + PythonImplementation::PyPy => warn!( + "PyPy does not yet support abi3t so the build artifacts will be version-specific. \ + See https://github.com/pypy/pypy/issues/3397 for more information." + ), + PythonImplementation::GraalPy => warn!( + "GraalPy does not support abi3t so the build artifacts will be version-specific." + ), + PythonImplementation::RustPython => {} + } + } + if let PythonAbiKind::Abi3 = interpreter_config.target_abi.kind { match interpreter_config.target_abi.implementation { PythonImplementation::CPython => { From 90bef3c03065b9c8611dd7a7404c63302e27a388 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 30 Apr 2026 13:27:12 -0600 Subject: [PATCH 094/195] tests pass --- pyo3-build-config/src/impl_.rs | 19 +++++++++----- pyo3-build-config/src/lib.rs | 1 + pyo3-ffi/build.rs | 47 +++++++++++++--------------------- pyo3-ffi/src/object.rs | 5 ---- 4 files changed, 32 insertions(+), 40 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 55e9861993c..1b3325a6630 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -222,6 +222,13 @@ impl InterpreterConfig { GilUsed::GilEnabled => {} }, } + for flag in &self.build_flags.0 { + match flag { + // already handled by target ABI logic above + BuildFlag::Py_GIL_DISABLED => continue, + flag => out.push(format!("cargo:rustc-cfg=py_sys_config=\"{flag}\"")), + } + } out } @@ -3381,10 +3388,10 @@ mod tests { ); let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { - major: 3, - minor: 15, + target_abi: PythonAbi { + implementation: PythonImplementation::CPython, + version: PythonVersion::PY315, + ..interpreter_config.target_abi }, ..interpreter_config }; @@ -3449,7 +3456,7 @@ mod tests { .build_flags(flags) .unwrap_err() .to_string() - .contains("target ABI is not free-threaded")); + .contains("is not free-threaded")); let builder = InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); @@ -3569,7 +3576,7 @@ mod tests { // abi3 config.target_abi = PythonAbi { - kind: PythonAbiKind::Abi3, + kind: PythonAbiKind::Stable(StableAbi::Abi3), ..config.target_abi }; config.lib_name = None; diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index a6524d58344..3f226989661 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -319,6 +319,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, PythonAbi, PythonAbiKind, PythonVersion, + StableAbi, }; pub enum BuildConfigSource { /// Config was provided by `PYO3_CONFIG_FILE`. diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index f6393606b8a..a6f4c4aa45f 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -3,7 +3,7 @@ use pyo3_build_config::{ 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, - InterpreterConfig, MaximumVersionExceeded, PythonAbiKind, PythonVersion, + InterpreterConfig, MaximumVersionExceeded, PythonAbiKind, PythonVersion, StableAbi, }, warn, PythonImplementation, }; @@ -128,40 +128,29 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { PythonImplementation::RustPython => {} } - if let PythonAbiKind::Abi3t = interpreter_config.target_abi.kind { + if let PythonAbiKind::Stable(abi) = interpreter_config.target_abi.kind { match interpreter_config.target_abi.implementation { - PythonImplementation::CPython => { - ensure!( - interpreter_config.target_abi.version >= PythonVersion::PY315, - "Abi3t builds are not supported on CPython targets before Python 3.15" - ) - } - PythonImplementation::PyPy => warn!( - "PyPy does not yet support abi3t so the build artifacts will be version-specific. \ - See https://github.com/pypy/pypy/issues/3397 for more information." - ), - PythonImplementation::GraalPy => warn!( - "GraalPy does not support abi3t so the build artifacts will be version-specific." - ), - PythonImplementation::RustPython => {} - } - } - - if let PythonAbiKind::Abi3 = interpreter_config.target_abi.kind { - match interpreter_config.target_abi.implementation { - PythonImplementation::CPython => { - if interpreter_config.target_abi.kind.is_free_threaded() { - warn!( - "The free-threaded build of CPython does not support abi3 so the build artifacts will be version-specific." + PythonImplementation::CPython => match abi { + StableAbi::Abi3t => { + ensure!( + interpreter_config.target_abi.version >= PythonVersion::PY315, + "Abi3t builds are not supported on CPython targets before Python 3.15" ) } - } + StableAbi::Abi3 => { + if interpreter_config.target_abi.kind.is_free_threaded() { + warn!( + "The free-threaded build of CPython does not support abi3 so the build artifacts will be version-specific." + ) + } + } + }, PythonImplementation::PyPy => warn!( - "PyPy does not yet support abi3 so the build artifacts will be version-specific. \ - See https://github.com/pypy/pypy/issues/3397 for more information." + "PyPy does not yet support {abi} so the build artifacts will be version-specific. \ + See https://github.com/pypy/pypy/issues/3397 for more information." ), PythonImplementation::GraalPy => warn!( - "GraalPy does not support abi3 so the build artifacts will be version-specific." + "GraalPy does not support {abi} so the build artifacts will be version-specific." ), PythonImplementation::RustPython => {} } diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index 5d9201a500f..f51318de5b2 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -256,11 +256,6 @@ pub unsafe fn Py_SIZE(ob: *mut PyObject) -> Py_ssize_t { _Py_SIZE(ob) } -#[cfg(all(Py_LIMITED_API, Py_3_15))] -extern_libpython! { - pub fn Py_IS_TYPE(ob: *mut PyObject, tp: *mut PyTypeObject) -> c_int; -} - #[inline] #[cfg(not(all(Py_LIMITED_API, Py_3_15)))] pub unsafe fn Py_IS_TYPE(ob: *mut PyObject, tp: *mut PyTypeObject) -> c_int { From ea2eab72baab81c909222c3c1c6a56f50ba4a860 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 30 Apr 2026 13:33:38 -0600 Subject: [PATCH 095/195] Add stub abi3t implementation --- pyo3-build-config/src/impl_.rs | 275 +++++++++++++++++++++------------ pyo3-build-config/src/lib.rs | 1 + pyo3-ffi/build.rs | 27 ++-- 3 files changed, 197 insertions(+), 106 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index a44b1cfc6b0..0bedb4a720d 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -208,23 +208,27 @@ impl InterpreterConfig { } match self.target_abi.kind { - PythonAbiKind::Abi3 => { - if !self.target_abi.kind.is_free_threaded() { - out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); + PythonAbiKind::Stable(kind) => { + out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); + if kind == StableAbi::Abi3t { + out.push("cargo:rustc-cfg=Py_GIL_DISABLED".to_owned()); + out.push("cargo:rustc-cfg=Py_TARGET_ABI3T".to_owned()); } } - PythonAbiKind::VersionSpecific(_) => {} + PythonAbiKind::VersionSpecific(kind) => match kind { + GilUsed::FreeThreaded => { + out.push("cargo:rustc-cfg=Py_GIL_DISABLED".to_owned()); + } + GilUsed::GilEnabled => {} + }, } - for flag in &self.build_flags.0 { match flag { - BuildFlag::Py_GIL_DISABLED => { - out.push("cargo:rustc-cfg=Py_GIL_DISABLED".to_owned()) - } + // already handled by target ABI logic above + BuildFlag::Py_GIL_DISABLED => continue, flag => out.push(format!("cargo:rustc-cfg=py_sys_config=\"{flag}\"")), } } - out } @@ -599,38 +603,47 @@ 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 target_abi = - if !(is_abi3() || target_abi.is_some() || abi3.is_some() || build_flags.is_some()) { - PythonAbiBuilder::new(implementation, version).finalize() - } else if let Some(ref flags) = build_flags { - if flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { - PythonAbiBuilder::new(implementation, version) - .free_threaded() - .unwrap() - .finalize() - } else { - // we could avoid this branch with if let chains - ensure!( - !(target_abi.is_some() && abi3.is_some()), - "Invalid config that sets both target_abi and abi3." - ); - target_abi.unwrap_or(PythonAbiBuilder::new(implementation, version).finalize()) - } - } else if (abi3.is_some() && abi3.unwrap()) || is_abi3() { - if abi3.is_some() && abi3.unwrap() { - warn!("abi3 configuration file option is deprecated, set target_abi instead"); - } + let target_abi = if !(is_abi3() + || is_abi3t() + || target_abi.is_some() + || abi3.is_some() + || build_flags.is_some()) + { + PythonAbiBuilder::new(implementation, version).finalize() + } else if is_abi3t() { + PythonAbiBuilder::new(implementation, version) + .stable_abi(StableAbi::Abi3t) + .unwrap() + .finalize() + } else if let Some(ref flags) = build_flags { + if flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { PythonAbiBuilder::new(implementation, version) - .abi3() + .free_threaded() .unwrap() .finalize() } else { + // we could avoid this branch with if let chains ensure!( !(target_abi.is_some() && abi3.is_some()), "Invalid config that sets both target_abi and abi3." ); - target_abi.unwrap() - }; + target_abi.unwrap_or(PythonAbiBuilder::new(implementation, version).finalize()) + } + } else if (abi3.is_some() && abi3.unwrap()) || is_abi3() { + if abi3.is_some() && abi3.unwrap() { + warn!("abi3 configuration file option is deprecated, set target_abi instead"); + } + PythonAbiBuilder::new(implementation, version) + .stable_abi(StableAbi::Abi3) + .unwrap() + .finalize() + } else { + ensure!( + !(target_abi.is_some() && abi3.is_some()), + "Invalid config that sets both target_abi and abi3." + ); + target_abi.unwrap() + }; let build_flags = build_flags.unwrap_or_default(); let builder = InterpreterConfigBuilder::new(implementation, version) @@ -810,13 +823,13 @@ impl InterpreterConfigBuilder { }) } - pub fn abi3(self) -> Result { + pub fn stable_abi(self, kind: StableAbi) -> Result { let implementation = self.implementation; let version = self.version; self.target_abi( PythonAbiBuilder::new(implementation, version) - .abi3() - // this can't panic because abi3() is caleld on a builder with no chosen ABI + .stable_abi(kind) + // this can't panic because stable_abi() is called on a builder with no chosen ABI .unwrap() .finalize(), ) @@ -885,11 +898,13 @@ impl InterpreterConfigBuilder { ensure!(self.build_flags.is_none(), "Build flags already set!"); let build_flags = if build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { ensure!(self.target_abi.is_some(), "Must target a free-threaded ABI if build flags contain Py_GIL_DISABLED but no target_abi is set"); - ensure!( - self.target_abi.unwrap().kind - == PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded), - "build_flags contains Py_GIL_DISABLED but target ABI is not free-threaded" - ); + if let Some(target_abi) = self.target_abi { + ensure!( + target_abi.kind == PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded) + || target_abi.kind == PythonAbiKind::Stable(StableAbi::Abi3t), + "build_flags contains Py_GIL_DISABLED but target ABI '{target_abi}' is not free-threaded" + ); + } build_flags } else if let Some(target_abi) = self.target_abi { let mut flags = build_flags.clone(); @@ -967,33 +982,31 @@ impl PythonAbiBuilder { kind: None, }; if is_abi3() { - builder.abi3() + builder.stable_abi(StableAbi::Abi3) } else { Ok(builder) } } - pub fn abi3(self) -> Result { - if let Some(kind) = self.kind { - bail!("ABI kind already set to {kind}, cannot set to abi3",) - } - - // PyPy and GraalPy don't support abi3; don't adjust the version - if self.implementation.is_pypy() || self.implementation.is_graalpy() { - return Ok(PythonAbiBuilder { - implementation: self.implementation, - version: self.version, - kind: self.kind, - }); - } + pub fn stable_abi(self, kind: StableAbi) -> Result { + ensure!( + self.kind.is_none(), + "ABI kind already set to {}, cannot set to {}", + self.kind.unwrap(), + kind + ); + ensure!( + kind == StableAbi::Abi3 || kind == StableAbi::Abi3t, + "Cannot set a stable abi build with a version-specific ABI kind '{kind}'" + ); let mut build_version = self.version; if 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 {kind}-py3{STABLE_ABI_MAX_MINOR} because current Python is higher than the maximum supported"); build_version.minor = STABLE_ABI_MAX_MINOR; } Ok(PythonAbiBuilder { - kind: Some(PythonAbiKind::Abi3), + kind: Some(PythonAbiKind::Stable(kind)), version: build_version, ..self }) @@ -1077,8 +1090,8 @@ impl FromStr for PythonAbi { #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(test, derive(Debug))] pub enum PythonAbiKind { - /// The original stable ABI, supporting Python 3.2 and up - Abi3, + /// One of the stable ABIs, which supports multiple Python versions + Stable(StableAbi), /// Version specific ABI, which is different on the free-threaded build VersionSpecific(GilUsed), } @@ -1086,7 +1099,7 @@ pub enum PythonAbiKind { impl Display for PythonAbiKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - PythonAbiKind::Abi3 => write!(f, "abi3"), + PythonAbiKind::Stable(stable_abi) => write!(f, "{stable_abi}"), PythonAbiKind::VersionSpecific(gil_disabled) => { write!(f, "version_specific({gil_disabled})") } @@ -1099,7 +1112,8 @@ impl FromStr for PythonAbiKind { fn from_str(value: &str) -> Result { match value { - "abi3" => Ok(PythonAbiKind::Abi3), + "abi3" => Ok(PythonAbiKind::Stable(StableAbi::Abi3)), + "abi3t" => Ok(PythonAbiKind::Stable(StableAbi::Abi3t)), "version_specific(free_threaded)" => { Ok(PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)) } @@ -1115,15 +1129,30 @@ impl PythonAbiKind { pub fn is_free_threaded(&self) -> bool { match self { PythonAbiKind::VersionSpecific(gil_disabled) => *gil_disabled == GilUsed::FreeThreaded, - PythonAbiKind::Abi3 => false, + PythonAbiKind::Stable(StableAbi::Abi3) => false, + PythonAbiKind::Stable(StableAbi::Abi3t) => true, } } +} - pub fn is_abi3(&self) -> bool { - matches!(self, PythonAbiKind::Abi3) - } +/// Whether the ABI is for the GIL-enabled or free-threaded build. +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(test, derive(Debug))] +pub enum StableAbi { + /// The original stable ABI, supporting Python 3.2 and up + Abi3, + /// The free-threaded stable ABI, supporting Python 3.15 and up + Abi3t, } +impl Display for StableAbi { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + StableAbi::Abi3 => write!(f, "abi3"), + StableAbi::Abi3t => write!(f, "abi3t"), + } + } +} /// Whether the ABI is for the GIL-enabled or free-threaded build. #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(test, derive(Debug))] @@ -1284,6 +1313,14 @@ 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. @@ -1293,6 +1330,15 @@ pub fn get_abi3_version() -> Option { 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: @@ -2037,7 +2083,7 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result { // FIXME: PyPy & GraalPy do not support the Stable ABI. let builder = InterpreterConfigBuilder::new(PythonImplementation::CPython, version) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap(); // abi3() sets the target_abi on the builder struct so unwrapping is safe let target_abi = builder.target_abi.unwrap(); @@ -2113,12 +2159,18 @@ fn default_lib_name_windows(abi: PythonAbi, mingw: bool, debug: bool) -> Result< "python{}{}_d", abi.version.major, abi.version.minor )) - } else if matches!(abi.kind, PythonAbiKind::Abi3) && !abi.implementation.is_graalpy() { - if debug { - Ok(WINDOWS_STABLE_ABI_DEBUG_LIB_NAME.to_owned()) + } else if abi.kind == PythonAbiKind::Stable(StableAbi::Abi3) + || abi.kind == PythonAbiKind::Stable(StableAbi::Abi3t) + { + let mut lib_name = if debug { + WINDOWS_STABLE_ABI_DEBUG_LIB_NAME.to_owned() } else { - Ok(WINDOWS_STABLE_ABI_LIB_NAME.to_owned()) + WINDOWS_STABLE_ABI_LIB_NAME.to_owned() + }; + if abi.kind == PythonAbiKind::Stable(StableAbi::Abi3t) { + lib_name.push('t'); } + Ok(lib_name) } else if mingw { ensure!( !abi.kind.is_free_threaded(), @@ -2151,8 +2203,10 @@ fn default_lib_name_unix(abi: PythonAbi, cygwin: bool, ld_version: Option<&str>) PythonImplementation::CPython => match ld_version { Some(ld_version) => Ok(format!("python{ld_version}")), None => { - if cygwin && matches!(abi.kind, PythonAbiKind::Abi3) { + if cygwin && matches!(abi.kind, PythonAbiKind::Stable(StableAbi::Abi3)) { Ok("python3".to_string()) + } else if cygwin && matches!(abi.kind, PythonAbiKind::Stable(StableAbi::Abi3t)) { + Ok("python3t".to_string()) } else if abi.kind.is_free_threaded() { ensure!(abi.version >= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", abi.version.major, abi.version.minor); Ok(format!( @@ -2286,10 +2340,11 @@ 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(stable_abi_version: Option) -> Result { let interpreter_path = find_interpreter()?; - let interpreter_config = InterpreterConfig::from_interpreter(interpreter_path, abi3_version)?; + let interpreter_config = + InterpreterConfig::from_interpreter(interpreter_path, stable_abi_version)?; Ok(interpreter_config) } @@ -2326,13 +2381,20 @@ 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 simultaneously enable abi3 and abi3t features" + ); // See if we can safely skip the Python interpreter configuration detection. // Unix stable ABI extension modules can usually be built without any interpreter. - let need_interpreter = abi3_version.is_none() || require_libdir_for_target(&host); + 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.or(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), @@ -2344,8 +2406,8 @@ pub fn make_interpreter_config() -> Result { } } 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." ); }; @@ -2398,7 +2460,7 @@ mod tests { let implementation = PythonImplementation::CPython; let version = MINIMUM_SUPPORTED_VERSION; let config = InterpreterConfigBuilder::new(implementation, version) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .pointer_width(32) .executable(Some("executable".into())) @@ -2436,7 +2498,7 @@ mod tests { let implementation = PythonImplementation::CPython; let version = MINIMUM_SUPPORTED_VERSION; let config = InterpreterConfigBuilder::new(implementation, version) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .pointer_width(32) .executable(Some("executable".into())) @@ -2629,7 +2691,7 @@ mod tests { let implementation = PythonImplementation::CPython; let version = PythonVersion::PY38; let config = InterpreterConfigBuilder::new(implementation, version) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .lib_name(Some("python3".into())) .finalize(); @@ -2643,7 +2705,7 @@ mod tests { let implementation = PythonImplementation::CPython; let version = PythonVersion::PY39; let config = InterpreterConfigBuilder::new(implementation, version) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(); assert_eq!(default_abi3_config(&host, min_version).unwrap(), config); @@ -2768,7 +2830,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(), false, @@ -2790,7 +2852,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(), true, @@ -2802,7 +2864,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY39) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(), false, @@ -2814,7 +2876,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY311) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(), false, @@ -2826,7 +2888,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(), false, @@ -2840,7 +2902,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(), false, @@ -2852,7 +2914,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY310) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(), false, @@ -2970,7 +3032,7 @@ mod tests { assert_eq!( super::default_lib_name_unix( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(), true, @@ -2986,7 +3048,7 @@ mod tests { let builder = PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY314) .free_threaded() .unwrap() - .abi3(); + .stable_abi(StableAbi::Abi3); assert!(builder.is_err()); assert!(builder .unwrap_err() @@ -3059,7 +3121,7 @@ mod tests { let config = InterpreterConfigBuilder::new(implementation, host_version) .target_abi( PythonAbiBuilder::new(implementation, target_version) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(), ) @@ -3294,7 +3356,7 @@ mod tests { let implementation = PythonImplementation::CPython; let version = PythonVersion::PY39; let interpreter_config = InterpreterConfigBuilder::new(implementation, version) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .lib_name(Some("python3".into())) .finalize(); @@ -3324,6 +3386,29 @@ mod tests { "cargo:rustc-cfg=Py_LIMITED_API".to_owned(), ] ); + + let interpreter_config = InterpreterConfig { + target_abi: PythonAbi { + implementation: PythonImplementation::CPython, + version: PythonVersion::PY315, + ..interpreter_config.target_abi + }, + ..interpreter_config + }; + assert_eq!( + interpreter_config.build_script_outputs(), + [ + "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(), + ] + ); } #[test] @@ -3366,12 +3451,12 @@ mod tests { let mut flags = BuildFlags::new(); flags.0.insert(BuildFlag::Py_GIL_DISABLED); assert!(builder - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .build_flags(flags) .unwrap_err() .to_string() - .contains("target ABI is not free-threaded")); + .contains("is not free-threaded")); let builder = InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); @@ -3491,7 +3576,7 @@ mod tests { // abi3 config.target_abi = PythonAbi { - kind: PythonAbiKind::Abi3, + kind: PythonAbiKind::Stable(StableAbi::Abi3), ..config.target_abi }; config.lib_name = None; diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index a41eb442b56..db652952e5c 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -313,6 +313,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, PythonAbi, PythonAbiKind, PythonVersion, + StableAbi, }; pub enum BuildConfigSource { /// Config was provided by `PYO3_CONFIG_FILE`. diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 34300893461..b1df86fab6a 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -3,7 +3,7 @@ use pyo3_build_config::{ 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, - InterpreterConfig, MaximumVersionExceeded, PythonAbiKind, PythonVersion, + InterpreterConfig, MaximumVersionExceeded, PythonAbiKind, PythonVersion, StableAbi, }, warn, PythonImplementation, }; @@ -128,21 +128,26 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { PythonImplementation::RustPython => {} } - if let PythonAbiKind::Abi3 = interpreter_config.target_abi.kind { + if let PythonAbiKind::Stable(abi) = interpreter_config.target_abi.kind { match interpreter_config.target_abi.implementation { - PythonImplementation::CPython => { - if interpreter_config.target_abi.kind.is_free_threaded() { - warn!( - "The free-threaded build of CPython does not support abi3 so the build artifacts will be version-specific." - ) + PythonImplementation::CPython => match abi { + StableAbi::Abi3t => { + bail!("Abi3t builds are not yet supported") } - } + StableAbi::Abi3 => { + if interpreter_config.target_abi.kind.is_free_threaded() { + warn!( + "The free-threaded build of CPython does not support abi3 so the build artifacts will be version-specific." + ) + } + } + }, PythonImplementation::PyPy => warn!( - "PyPy does not yet support abi3 so the build artifacts will be version-specific. \ - See https://github.com/pypy/pypy/issues/3397 for more information." + "PyPy does not yet support {abi} so the build artifacts will be version-specific. \ + See https://github.com/pypy/pypy/issues/3397 for more information." ), PythonImplementation::GraalPy => warn!( - "GraalPy does not support abi3 so the build artifacts will be version-specific." + "GraalPy does not support {abi} so the build artifacts will be version-specific." ), PythonImplementation::RustPython => {} } From 8354ef7cf11d54c3ceaca1a025bd0c849af01a79 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 30 Apr 2026 14:50:09 -0600 Subject: [PATCH 096/195] Improve test coverage --- pyo3-build-config/src/impl_.rs | 182 ++++++++++++++++++++++----------- 1 file changed, 125 insertions(+), 57 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 0bedb4a720d..2cfa4f15da0 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -360,7 +360,8 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let mut abi_builder = if gil_disabled { PythonAbiBuilder::new(implementation, target_version) } else { - PythonAbiBuilder::from_build_env(implementation, target_version, abi3_version)? + // we already + PythonAbiBuilder::from_build_env(implementation, target_version, abi3_version) }; if gil_disabled { @@ -460,9 +461,12 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) }; let cygwin = soabi.ends_with("cygwin"); let target_abi = if gil_disabled { - PythonAbiBuilder::new(implementation, version).free_threaded()? + // unwrap is safe because this is a freshly created PythonAbiBuilder with no ABI kind set + PythonAbiBuilder::new(implementation, version) + .free_threaded() + .unwrap() } else { - PythonAbiBuilder::from_build_env(implementation, version, Some(version))? + PythonAbiBuilder::from_build_env(implementation, version, Some(version)) } .finalize(); let lib_name = Some(default_lib_name_unix( @@ -509,7 +513,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) config.implementation, config.version, abi3_version, - )?; + ); // only allow free-threaded builds if the build environment didn't force an abi3 build if config.target_abi.kind.is_free_threaded() && abi_builder.kind.is_none() { abi_builder = abi_builder.free_threaded()?; @@ -603,32 +607,27 @@ 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 target_abi = if !(is_abi3() - || is_abi3t() - || target_abi.is_some() - || abi3.is_some() - || build_flags.is_some()) - { - PythonAbiBuilder::new(implementation, version).finalize() + let flags_contains_free_threaded = if let Some(ref flags) = build_flags { + flags.0.contains(&BuildFlag::Py_GIL_DISABLED) + } else { + false + }; + let target_abi = if target_abi.is_some() { + ensure!( + !(target_abi.is_some() && abi3.is_some()), + "Invalid config that sets both target_abi and abi3." + ); + target_abi.unwrap() } else if is_abi3t() { PythonAbiBuilder::new(implementation, version) .stable_abi(StableAbi::Abi3t) .unwrap() .finalize() - } else if let Some(ref flags) = build_flags { - if flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { - PythonAbiBuilder::new(implementation, version) - .free_threaded() - .unwrap() - .finalize() - } else { - // we could avoid this branch with if let chains - ensure!( - !(target_abi.is_some() && abi3.is_some()), - "Invalid config that sets both target_abi and abi3." - ); - target_abi.unwrap_or(PythonAbiBuilder::new(implementation, version).finalize()) - } + } else if flags_contains_free_threaded { + PythonAbiBuilder::new(implementation, version) + .free_threaded() + .unwrap() + .finalize() } else if (abi3.is_some() && abi3.unwrap()) || is_abi3() { if abi3.is_some() && abi3.unwrap() { warn!("abi3 configuration file option is deprecated, set target_abi instead"); @@ -638,16 +637,14 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .unwrap() .finalize() } else { - ensure!( - !(target_abi.is_some() && abi3.is_some()), - "Invalid config that sets both target_abi and abi3." - ); - target_abi.unwrap() + PythonAbiBuilder::new(implementation, version).finalize() }; let build_flags = build_flags.unwrap_or_default(); let builder = InterpreterConfigBuilder::new(implementation, version) - .target_abi(target_abi)? + .target_abi(target_abi) + // cannot fail because this is a newly-created InterpreterConfigBuilder with no target ABI set + .unwrap() .shared(shared.unwrap_or(true)) .lib_name(lib_name) .lib_dir(lib_dir) @@ -844,7 +841,9 @@ impl InterpreterConfigBuilder { // this can't panic because abi3() is called on a builder with no chosen ABI .unwrap() .finalize(), - )? + ) + // this can't panic because abi3() is called on a builder with no chosen ABI + .unwrap() .build_flags(BuildFlags::default()) } @@ -897,11 +896,9 @@ impl InterpreterConfigBuilder { pub fn build_flags(self, build_flags: BuildFlags) -> Result { ensure!(self.build_flags.is_none(), "Build flags already set!"); let build_flags = if build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { - ensure!(self.target_abi.is_some(), "Must target a free-threaded ABI if build flags contain Py_GIL_DISABLED but no target_abi is set"); if let Some(target_abi) = self.target_abi { ensure!( - target_abi.kind == PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded) - || target_abi.kind == PythonAbiKind::Stable(StableAbi::Abi3t), + target_abi.kind.is_free_threaded(), "build_flags contains Py_GIL_DISABLED but target ABI '{target_abi}' is not free-threaded" ); } @@ -975,16 +972,17 @@ impl PythonAbiBuilder { implementation: PythonImplementation, version: PythonVersion, abi3_version: Option, - ) -> Result { + ) -> PythonAbiBuilder { let builder = PythonAbiBuilder { implementation, version: abi3_version.unwrap_or(version), kind: None, }; if is_abi3() { - builder.stable_abi(StableAbi::Abi3) + // freshly created builder without an ABI kind so this can't fail + builder.stable_abi(StableAbi::Abi3).unwrap() } else { - Ok(builder) + builder } } @@ -1172,18 +1170,6 @@ impl Display for GilUsed { } } -impl FromStr for GilUsed { - type Err = crate::errors::Error; - - fn from_str(value: &str) -> Result { - match value { - "gil_enabled" => Ok(GilUsed::GilEnabled), - "free_threaded" => Ok(GilUsed::FreeThreaded), - _ => Err(format!("Unrecognized ABI name: {value}").into()), - } - } -} - #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct PythonVersion { pub major: u8, @@ -2053,7 +2039,7 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result> { config.implementation, config.version, get_abi3_version(), - )?; + ); if config.target_abi.kind.is_free_threaded() && abi_builder.kind.is_none() { abi_builder = abi_builder.free_threaded()?; } @@ -2537,6 +2523,70 @@ mod tests { ) } + #[test] + fn test_config_file_invalid_keys() { + assert!( + InterpreterConfig::from_reader("version=3.14\ntarget_abi=foo-bar-baz".as_bytes()) + .is_err() + ); + assert!(InterpreterConfig::from_reader( + "version=3.14\ntarget_abi=CPython-bar-baz".as_bytes() + ) + .is_err()); + assert!(InterpreterConfig::from_reader( + "version=3.14\ntarget_abi=CPython-abi3-baz".as_bytes() + ) + .is_err()); + } + + #[test] + fn gil_disabled_config_file_corner_cases() { + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY313; + assert_eq!( + InterpreterConfig::from_reader("version=3.13\nbuild_flags=Py_GIL_DISABLED".as_bytes()) + .unwrap(), + InterpreterConfigBuilder::new(implementation, version) + .free_threaded() + .unwrap() + .finalize() + ); + assert_eq!( + InterpreterConfig::from_reader( + "version=3.13\ntarget_abi=CPython-version_specific(free_threaded)-3.13".as_bytes() + ) + .unwrap(), + InterpreterConfigBuilder::new(implementation, version) + .free_threaded() + .unwrap() + .finalize() + ); + assert!(InterpreterConfig::from_reader("version=3.13\ntarget_abi=CPython-version_specific(gil_enabled)-3.13\nbuild_flags=Py_GIL_DISABLED".as_bytes()).unwrap_err().to_string().contains("is not free-threaded")) + } + + #[test] + fn abi3_from_old_config_file() { + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY313; + assert_eq!( + InterpreterConfig::from_reader("version=3.13\nabi3=true".as_bytes()).unwrap(), + InterpreterConfigBuilder::new(implementation, version) + .stable_abi(StableAbi::Abi3) + .unwrap() + .finalize() + ); + } + + #[test] + fn test_target_abi_and_abi3() { + assert!(InterpreterConfig::from_reader( + "version=3.13\nabi3=true\ntarget_abi=CPython-abi3-3.13".as_bytes() + ) + .unwrap_err() + .to_string() + .contains("Invalid config"),); + } + #[test] fn build_flags_default() { assert_eq!(BuildFlags::default(), BuildFlags::new()); @@ -3060,6 +3110,22 @@ mod tests { assert!(builder.is_err()); assert!(builder.unwrap_err().to_string().contains("Cannot target")); + + assert_eq!( + PythonAbiBuilder::new( + PythonImplementation::CPython, + PythonVersion { + major: 3, + minor: 16, + }, + ) + .stable_abi(StableAbi::Abi3) + .unwrap() + .finalize() + .version + .minor, + STABLE_ABI_MAX_MINOR + ); } #[test] @@ -3144,6 +3210,12 @@ mod tests { assert!(interpreter.unwrap_err().to_string().contains( "cannot set a minimum Python version 3.45 higher than the interpreter version" )); + + let interpreter = get_host_interpreter(Some(PythonVersion::PY313)); + assert_eq!( + interpreter.unwrap().target_abi.version, + PythonVersion::PY313 + ); } #[test] @@ -3440,11 +3512,7 @@ mod tests { InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); let mut flags = BuildFlags::new(); flags.0.insert(BuildFlag::Py_GIL_DISABLED); - assert!(builder - .build_flags(flags) - .unwrap_err() - .to_string() - .contains("Must target a free-threaded ABI")); + assert!(builder.build_flags(flags).unwrap().target_abi.is_none()); let builder = InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); From 6854756c258fe8ae7b7efeb74da4420c561920e6 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 30 Apr 2026 15:05:30 -0600 Subject: [PATCH 097/195] fix clippy --- pyo3-build-config/src/impl_.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 2cfa4f15da0..c6d5c53a9db 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -612,12 +612,12 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) } else { false }; - let target_abi = if target_abi.is_some() { + let target_abi = if let Some(target_abi) = target_abi { ensure!( - !(target_abi.is_some() && abi3.is_some()), + abi3.is_none(), "Invalid config that sets both target_abi and abi3." ); - target_abi.unwrap() + target_abi } else if is_abi3t() { PythonAbiBuilder::new(implementation, version) .stable_abi(StableAbi::Abi3t) From 3a2166545a69da5b147ec9c054ba7475e989ae5c Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 30 Apr 2026 15:20:13 -0600 Subject: [PATCH 098/195] fix 3.14t abi3 builds being a no-op --- pyo3-build-config/src/impl_.rs | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index c6d5c53a9db..1e8e596f74c 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -897,10 +897,11 @@ impl InterpreterConfigBuilder { ensure!(self.build_flags.is_none(), "Build flags already set!"); let build_flags = if build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { if let Some(target_abi) = self.target_abi { - ensure!( - target_abi.kind.is_free_threaded(), - "build_flags contains Py_GIL_DISABLED but target ABI '{target_abi}' is not free-threaded" - ); + if !target_abi.kind.is_free_threaded() { + warn!( + "build_flags contains Py_GIL_DISABLED but target ABI '{target_abi}' is not free-threaded" + ); + } } build_flags } else if let Some(target_abi) = self.target_abi { @@ -2561,7 +2562,9 @@ mod tests { .unwrap() .finalize() ); - assert!(InterpreterConfig::from_reader("version=3.13\ntarget_abi=CPython-version_specific(gil_enabled)-3.13\nbuild_flags=Py_GIL_DISABLED".as_bytes()).unwrap_err().to_string().contains("is not free-threaded")) + let mut flags = BuildFlags::default(); + flags.0.insert(BuildFlag::Py_GIL_DISABLED); + assert_eq!(InterpreterConfig::from_reader("version=3.13\ntarget_abi=CPython-version_specific(gil_enabled)-3.13\nbuild_flags=Py_GIL_DISABLED".as_bytes()).unwrap(), InterpreterConfigBuilder::new(implementation, version).build_flags(flags).unwrap().finalize()); } #[test] @@ -3518,13 +3521,17 @@ mod tests { InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); let mut flags = BuildFlags::new(); flags.0.insert(BuildFlag::Py_GIL_DISABLED); - assert!(builder - .stable_abi(StableAbi::Abi3) - .unwrap() - .build_flags(flags) - .unwrap_err() - .to_string() - .contains("is not free-threaded")); + assert!( + builder + .stable_abi(StableAbi::Abi3) + .unwrap() + .build_flags(flags) + .unwrap() + .finalize() + .target_abi + .kind + == PythonAbiKind::Stable(StableAbi::Abi3) + ); let builder = InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); From 97087aa8aca109f1a01f897210c7ddc0eca8b2c8 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 1 May 2026 21:55:24 -0500 Subject: [PATCH 099/195] fix ffi-check to account for private _tp_iteritem PyTypeObject slot --- pyo3-ffi/src/cpython/object.rs | 3 +++ pyo3-ffi/src/object.rs | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/pyo3-ffi/src/cpython/object.rs b/pyo3-ffi/src/cpython/object.rs index dcde54bd90b..1aff7835a57 100644 --- a/pyo3-ffi/src/cpython/object.rs +++ b/pyo3-ffi/src/cpython/object.rs @@ -6,6 +6,7 @@ use std::mem; // skipped private _Py_NewReference // skipped private _Py_NewReferenceNoTotal // skipped private _Py_ResurrectReference +// skipped private _Py_ForgetReference // skipped private _Py_GetGlobalRefTotal // skipped private _Py_GetRefTotal @@ -277,6 +278,8 @@ pub struct PyTypeObject { pub tp_next: *mut PyTypeObject, #[cfg(Py_3_13)] pub tp_versions_used: u16, + #[cfg(Py_3_15)] + pub _tp_iteritem: Option, } #[cfg(Py_3_11)] diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index 8e712c23dc1..b8266f1e611 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -275,6 +275,12 @@ pub type hashfunc = unsafe extern "C" fn(*mut PyObject) -> Py_hash_t; pub type richcmpfunc = unsafe extern "C" fn(*mut PyObject, *mut PyObject, c_int) -> *mut PyObject; pub type getiterfunc = unsafe extern "C" fn(*mut PyObject) -> *mut PyObject; pub type iternextfunc = unsafe extern "C" fn(*mut PyObject) -> *mut PyObject; +#[repr(C)] +pub struct _PyObjectIndexPair { + pub object: *mut PyObject, + pub index: Py_ssize_t, +} +pub type _Py_iteritemfunc = unsafe extern "C" fn(*mut PyObject, Py_ssize_t) -> _PyObjectIndexPair; pub type descrgetfunc = unsafe extern "C" fn(*mut PyObject, *mut PyObject, *mut PyObject) -> *mut PyObject; pub type descrsetfunc = unsafe extern "C" fn(*mut PyObject, *mut PyObject, *mut PyObject) -> c_int; From 80a591cec7eef662bbd1a861c4ac67242d90e95e Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Sun, 3 May 2026 11:49:41 -0500 Subject: [PATCH 100/195] expose the critical section API --- pyo3-ffi/src/cpython/critical_section.rs | 29 +------------------- pyo3-ffi/src/cpython/mod.rs | 6 ++--- pyo3-ffi/src/critical_section.rs | 34 ++++++++++++++++++++++++ pyo3-ffi/src/lib.rs | 2 ++ 4 files changed, 40 insertions(+), 31 deletions(-) create mode 100644 pyo3-ffi/src/critical_section.rs diff --git a/pyo3-ffi/src/cpython/critical_section.rs b/pyo3-ffi/src/cpython/critical_section.rs index 5db205b9840..4862a24f281 100644 --- a/pyo3-ffi/src/cpython/critical_section.rs +++ b/pyo3-ffi/src/cpython/critical_section.rs @@ -1,38 +1,11 @@ -#[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 c8215212b37..de4da5afe0c 100644 --- a/pyo3-ffi/src/cpython/mod.rs +++ b/pyo3-ffi/src/cpython/mod.rs @@ -7,7 +7,7 @@ pub(crate) mod ceval; pub(crate) mod code; pub(crate) mod compile; pub(crate) mod complexobject; -#[cfg(Py_3_13)] +#[cfg(Py_3_14)] pub(crate) mod critical_section; pub(crate) mod descrobject; pub(crate) mod dictobject; @@ -56,8 +56,8 @@ 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(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..3efc1ebfd79 --- /dev/null +++ b/pyo3-ffi/src/critical_section.rs @@ -0,0 +1,34 @@ +#[cfg(not(Py_LIMITED_API))] +use crate::PyMutex; +use crate::PyObject; + +#[cfg(all(Py_LIMITED_API, Py_3_15))] +opaque_struct!(pub PyMutex); + +#[cfg(any(all(Py_GIL_DISABLED, Py_3_13, not(Py_LIMITED_API)), Py_3_15,))] +#[repr(C)] +#[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)), Py_3_15,))] +#[repr(C)] +pub struct PyCriticalSection2 { + _cs_base: PyCriticalSection, + _cs_mutex2: *mut PyMutex, +} + +#[cfg(all(not(Py_GIL_DISABLED), Py_3_13, not(Py_3_15), not(Py_LIMITED_API)))] +opaque_struct!(pub PyCriticalSection); +#[cfg(all(not(Py_GIL_DISABLED), Py_3_13, not(Py_3_15), not(Py_LIMITED_API)))] +opaque_struct!(pub PyCriticalSection2); + +#[cfg(any(all(Py_GIL_DISABLED, Py_3_13), Py_3_15))] +extern_libpython! { + 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 df72ac7b23e..5b9235e857f 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -439,6 +439,7 @@ 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::*; @@ -504,6 +505,7 @@ mod compile; mod complexobject; #[cfg(not(Py_LIMITED_API))] mod context; +mod critical_section; #[cfg(not(Py_LIMITED_API))] pub(crate) mod datetime; mod descrobject; From be1b77f7063b30476662d6531c36f1f8a4c3ce0c Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Sun, 3 May 2026 16:12:23 -0500 Subject: [PATCH 101/195] Add initial support for Python 3.15.0b1 --- pyo3-ffi/src/critical_section.rs | 2 +- pyo3-ffi/src/lib.rs | 5 ++ pyo3-ffi/src/moduleobject.rs | 31 +------- pyo3-ffi/src/object.rs | 10 +++ pyo3-ffi/src/slots.rs | 123 ++++++++++++++++++++++++++++++ pyo3-ffi/src/slots_generated.rs | 56 ++++++++++++++ pyo3-ffi/src/typeslots.rs | 4 - pyo3-macros-backend/src/module.rs | 2 +- src/impl_/pymodule.rs | 72 ++++++++++++++--- 9 files changed, 262 insertions(+), 43 deletions(-) create mode 100644 pyo3-ffi/src/slots.rs create mode 100644 pyo3-ffi/src/slots_generated.rs diff --git a/pyo3-ffi/src/critical_section.rs b/pyo3-ffi/src/critical_section.rs index 3efc1ebfd79..f42aac51e93 100644 --- a/pyo3-ffi/src/critical_section.rs +++ b/pyo3-ffi/src/critical_section.rs @@ -1,5 +1,6 @@ #[cfg(not(Py_LIMITED_API))] use crate::PyMutex; +#[cfg(any(all(Py_GIL_DISABLED, Py_3_13), Py_3_15))] use crate::PyObject; #[cfg(all(Py_LIMITED_API, Py_3_15))] @@ -7,7 +8,6 @@ opaque_struct!(pub PyMutex); #[cfg(any(all(Py_GIL_DISABLED, Py_3_13, not(Py_LIMITED_API)), Py_3_15,))] #[repr(C)] -#[repr(C)] pub struct PyCriticalSection { _cs_prev: usize, _cs_mutex: *mut PyMutex, diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index 5b9235e857f..15ca57c74d8 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -439,6 +439,7 @@ pub use self::compile::*; pub use self::complexobject::*; #[cfg(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::*; @@ -481,6 +482,8 @@ pub use self::rangeobject::*; pub use self::refcount::*; pub use self::setobject::*; pub use self::sliceobject::*; +pub use self::slots::*; +pub use self::slots_generated::*; pub use self::structseq::*; pub use self::sysmodule::*; pub use self::traceback::*; @@ -572,6 +575,8 @@ mod rangeobject; mod refcount; mod setobject; mod sliceobject; +mod slots; +mod slots_generated; mod structseq; mod sysmodule; mod traceback; diff --git a/pyo3-ffi/src/moduleobject.rs b/pyo3-ffi/src/moduleobject.rs index 391c5a8518e..c9105448319 100644 --- a/pyo3-ffi/src/moduleobject.rs +++ b/pyo3-ffi/src/moduleobject.rs @@ -1,6 +1,8 @@ use crate::methodobject::PyMethodDef; use crate::object::*; use crate::pyport::Py_ssize_t; +#[cfg(Py_3_15)] +use crate::slots::PySlot; use std::ffi::{c_char, c_int, c_void}; extern_libpython! { @@ -85,33 +87,6 @@ impl Default for PyModuleDef_Slot { } } -pub const Py_mod_create: c_int = 1; -pub const Py_mod_exec: c_int = 2; -#[cfg(Py_3_12)] -pub const Py_mod_multiple_interpreters: c_int = 3; -#[cfg(Py_3_13)] -pub const Py_mod_gil: c_int = 4; -#[cfg(Py_3_15)] -pub const Py_mod_abi: c_int = 5; -#[cfg(Py_3_15)] -pub const Py_mod_name: c_int = 6; -#[cfg(Py_3_15)] -pub const Py_mod_doc: c_int = 7; -#[cfg(Py_3_15)] -pub const Py_mod_state_size: c_int = 8; -#[cfg(Py_3_15)] -pub const Py_mod_methods: c_int = 9; -#[cfg(Py_3_15)] -pub const Py_mod_state_traverse: c_int = 10; -#[cfg(Py_3_15)] -pub const Py_mod_state_clear: c_int = 11; -#[cfg(Py_3_15)] -pub const Py_mod_state_free: c_int = 12; -#[cfg(Py_3_15)] -pub const Py_mod_token: c_int = 13; - -// skipped private _Py_mod_LAST_SLOT - #[cfg(Py_3_12)] #[allow( clippy::zero_ptr, @@ -140,7 +115,7 @@ extern_libpython! { #[cfg(Py_3_15)] extern_libpython! { pub fn PyModule_FromSlotsAndSpec( - slots: *const PyModuleDef_Slot, + slots: *const PySlot, spec: *mut PyObject, ) -> *mut PyObject; pub fn PyModule_Exec(_mod: *mut PyObject) -> c_int; diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index b8266f1e611..4c4015577a7 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -3,6 +3,8 @@ use crate::pyport::{Py_hash_t, Py_ssize_t}; use crate::refcount; #[cfg(Py_GIL_DISABLED)] use crate::PyMutex; +#[cfg(Py_3_15)] +use crate::PySlot; use std::ffi::{c_char, c_int, c_uint, c_ulong, c_void}; use std::mem; #[cfg(Py_GIL_DISABLED)] @@ -383,6 +385,14 @@ extern_libpython! { #[cfg_attr(PyPy, link_name = "PyPyType_GetTypeDataSize")] pub fn PyType_GetTypeDataSize(cls: *mut PyTypeObject) -> Py_ssize_t; + #[cfg(Py_3_14)] + #[cfg_attr(PyPy, link_name = "PyPyType_GetBaseByToken")] + pub fn PyType_GetBaseByToken(type_: *mut PyTypeObject, token: *mut c_void, result: *mut *mut PyTypeObject) -> c_int; + + #[cfg(Py_3_15)] + #[cfg_attr(PyPy, link_name = "PyPyType_FromSlot")] + pub fn PyType_FromSlots(slots: *mut PySlot) -> *mut PyObject; + #[cfg_attr(PyPy, link_name = "PyPyType_IsSubtype")] pub fn PyType_IsSubtype(a: *mut PyTypeObject, b: *mut PyTypeObject) -> c_int; } diff --git a/pyo3-ffi/src/slots.rs b/pyo3-ffi/src/slots.rs new file mode 100644 index 00000000000..734479a5fed --- /dev/null +++ b/pyo3-ffi/src/slots.rs @@ -0,0 +1,123 @@ +use crate::Py_ssize_t; +use std::ffi::c_void; + +pub type _Py_funcptr_t = unsafe extern "C" fn(); + +#[derive(Copy, Clone)] +#[repr(C)] +pub union _anon_union_32b { + pub sl_reserved: u32, +} + +#[derive(Copy, Clone)] +#[repr(C)] +pub union _anon_union_64b { + pub sl_ptr: *mut c_void, + pub sl_func: _Py_funcptr_t, + pub sl_size: Py_ssize_t, + pub sl_int64: i64, + pub sl_uint64: u64, +} + +#[derive(Copy, Clone)] +#[repr(C)] +pub struct PySlot { + pub sl_id: u16, + pub sl_flags: u16, + pub anon1: _anon_union_32b, + pub anon2: _anon_union_64b, +} + +impl PartialEq for PySlot { + fn eq(&self, other: &Self) -> bool { + unsafe { + // memcmp returns 0 if the memory blocks are identical + libc::memcmp( + self as *const Self as *const c_void, + other as *const Self as *const c_void, + std::mem::size_of::(), + ) == 0 + } + } +} + +pub const PySlot_OPTIONAL: u16 = 0x01; +pub const PySlot_STATIC: u16 = 0x02; +pub const PySlot_INTPTR: u16 = 0x04; +pub const Py_slot_invalid: u16 = 0xffff; + +pub const fn PySlot_DATA(NAME: u16, VALUE: *mut c_void) -> PySlot { + PySlot { + sl_id: NAME, + sl_flags: PySlot_INTPTR, + anon1: _anon_union_32b { sl_reserved: 0 }, + anon2: _anon_union_64b { sl_ptr: VALUE }, + } +} + +pub const fn PySlot_FUNC(NAME: u16, VALUE: _Py_funcptr_t) -> PySlot { + PySlot { + sl_id: NAME, + sl_flags: 0, + anon1: _anon_union_32b { sl_reserved: 0 }, + anon2: _anon_union_64b { sl_func: VALUE }, + } +} + +pub const fn PySlot_SIZE(NAME: u16, VALUE: Py_ssize_t) -> PySlot { + PySlot { + sl_id: NAME, + sl_flags: 0, + anon1: _anon_union_32b { sl_reserved: 0 }, + anon2: _anon_union_64b { sl_size: VALUE }, + } +} + +pub const fn PySlot_INT64(NAME: u16, VALUE: i64) -> PySlot { + PySlot { + sl_id: NAME, + sl_flags: 0, + anon1: _anon_union_32b { sl_reserved: 0 }, + anon2: _anon_union_64b { sl_int64: VALUE }, + } +} + +pub const fn PySlot_UINT64(NAME: u16, VALUE: u64) -> PySlot { + PySlot { + sl_id: NAME, + sl_flags: 0, + anon1: _anon_union_32b { sl_reserved: 0 }, + anon2: _anon_union_64b { sl_uint64: VALUE }, + } +} + +pub const fn PySlot_STATIC_DATA(NAME: u16, VALUE: *mut c_void) -> PySlot { + PySlot { + sl_id: NAME, + sl_flags: PySlot_STATIC, + anon1: _anon_union_32b { sl_reserved: 0 }, + anon2: _anon_union_64b { sl_ptr: VALUE }, + } +} + +pub const fn PySplot_PTR(NAME: u16, VALUE: *mut c_void) -> PySlot { + PySlot { + sl_id: NAME, + sl_flags: PySlot_INTPTR, + anon1: _anon_union_32b { sl_reserved: 0 }, + anon2: _anon_union_64b { sl_ptr: VALUE }, + } +} + +pub const fn PySplot_PTR_STATIC(NAME: u16, VALUE: *mut c_void) -> PySlot { + PySlot { + sl_id: NAME, + sl_flags: PySlot_INTPTR | PySlot_STATIC, + anon1: _anon_union_32b { sl_reserved: 0 }, + anon2: _anon_union_64b { sl_ptr: VALUE }, + } +} + +pub const fn PySlot_END() -> PySlot { + unsafe { std::mem::zeroed() } +} diff --git a/pyo3-ffi/src/slots_generated.rs b/pyo3-ffi/src/slots_generated.rs new file mode 100644 index 00000000000..ad7ab55734a --- /dev/null +++ b/pyo3-ffi/src/slots_generated.rs @@ -0,0 +1,56 @@ +use std::ffi::c_int; + +#[allow(unused_variables)] +const fn _Py_SLOT_COMPAT_VALUE(OLD: u16, NEW: u16) -> u16 { + #[cfg(Py_3_15)] + { + NEW + } + + #[cfg(not(Py_3_15))] + { + OLD + } +} + +#[allow(unused_variables)] +const fn _Py_SLOT_COMPAT_VALUE_int(OLD: c_int, NEW: c_int) -> c_int { + #[cfg(Py_3_15)] + { + NEW + } + + #[cfg(not(Py_3_15))] + { + OLD + } +} + +pub const Py_slot_end: u16 = 0; +pub const Py_mod_create: u16 = _Py_SLOT_COMPAT_VALUE(1, 84); +pub const Py_mod_exec: u16 = _Py_SLOT_COMPAT_VALUE(2, 85); +pub const Py_mod_multiple_interpreters: u16 = _Py_SLOT_COMPAT_VALUE(3, 86); +pub const Py_mod_gil: u16 = _Py_SLOT_COMPAT_VALUE(4, 87); +pub const Py_bf_getbuffer: c_int = _Py_SLOT_COMPAT_VALUE_int(1, 88); +pub const Py_bf_releasebuffer: c_int = _Py_SLOT_COMPAT_VALUE_int(2, 89); +pub const Py_mp_ass_subscript: c_int = _Py_SLOT_COMPAT_VALUE_int(3, 90); +pub const Py_mp_length: c_int = _Py_SLOT_COMPAT_VALUE_int(4, 91); +pub const Py_slot_subslots: u16 = 92; +pub const Py_tp_slots: u16 = 93; +pub const Py_mod_slots: u16 = 94; +pub const Py_tp_name: u16 = 95; +pub const Py_tp_basicsize: u16 = 96; +pub const Py_tp_extra_basicsize: u16 = 97; +pub const Py_tp_itemsize: u16 = 98; +pub const Py_tp_flags: u16 = 99; +pub const Py_mod_name: u16 = 100; +pub const Py_mod_doc: u16 = 101; +pub const Py_mod_state_size: u16 = 102; +pub const Py_mod_methods: u16 = 103; +pub const Py_mod_state_traverse: u16 = 104; +pub const Py_mod_state_clear: u16 = 105; +pub const Py_mod_state_free: u16 = 106; +pub const Py_tp_metaclass: u16 = 107; +pub const Py_tp_module: u16 = 108; +pub const Py_mod_abi: u16 = 109; +pub const Py_mod_token: u16 = 110; diff --git a/pyo3-ffi/src/typeslots.rs b/pyo3-ffi/src/typeslots.rs index fd0c3b77a74..6d9bad0a83e 100644 --- a/pyo3-ffi/src/typeslots.rs +++ b/pyo3-ffi/src/typeslots.rs @@ -1,9 +1,5 @@ use std::ffi::c_int; -pub const Py_bf_getbuffer: c_int = 1; -pub const Py_bf_releasebuffer: c_int = 2; -pub const Py_mp_ass_subscript: c_int = 3; -pub const Py_mp_length: c_int = 4; pub const Py_mp_subscript: c_int = 5; pub const Py_nb_absolute: c_int = 6; pub const Py_nb_add: c_int = 7; diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index b3a127a4ba5..6bea2f3a4a3 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -573,7 +573,7 @@ fn module_initialization( /// 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 { + pub unsafe extern "C" fn __pyo3_export() -> *mut #pyo3_path::ffi::PySlot { _PYO3_DEF.get_slots() } }); diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 4a99b1b6fb9..d7bef2a8100 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -227,8 +227,8 @@ impl ModuleDef { .map(|py_module| py_module.clone_ref(py)) } } - pub fn get_slots(&'static self) -> *mut ffi::PyModuleDef_Slot { - self.slots.0.get() as *mut ffi::PyModuleDef_Slot + pub fn get_slots(&'static self) -> *mut ffi::PySlot { + self.slots.0.get() as *mut ffi::PySlot } } @@ -249,7 +249,10 @@ const MAX_SLOTS_WITH_TRAILING_NULL: usize = MAX_SLOTS + 1; /// actual slots pushed due to the need to have a zeroed element on the end. pub struct PyModuleSlotsBuilder { // values (initially all zeroed) + #[cfg(not(Py_3_15))] values: [ffi::PyModuleDef_Slot; MAX_SLOTS_WITH_TRAILING_NULL], + #[cfg(Py_3_15)] + values: [ffi::PySlot; MAX_SLOTS_WITH_TRAILING_NULL], // current length len: usize, } @@ -270,11 +273,23 @@ impl PyModuleSlotsBuilder { } pub const fn with_mod_exec(self, exec: ModuleExecSlot) -> Self { - self.push(ffi::Py_mod_exec, exec as *mut c_void) + #[cfg(not(Py_3_15))] + { + self.push(ffi::Py_mod_exec, exec as *mut c_void) + } + #[cfg(Py_3_15)] + { + self.push_value(ffi::PySlot_FUNC(ffi::Py_mod_exec, unsafe { + std::mem::transmute::< + unsafe extern "C" fn(*mut pyo3_ffi::PyObject) -> i32, + ffi::_Py_funcptr_t, + >(exec) + })) + } } pub const fn with_gil_used(self, gil_used: bool) -> Self { - #[cfg(Py_3_13)] + #[cfg(all(Py_3_13, not(Py_3_15)))] { self.push( ffi::Py_mod_gil, @@ -286,6 +301,18 @@ impl PyModuleSlotsBuilder { ) } + #[cfg(Py_3_15)] + { + self.push_value(ffi::PySlot_DATA( + ffi::Py_mod_gil, + if gil_used { + ffi::Py_MOD_GIL_USED + } else { + ffi::Py_MOD_GIL_NOT_USED + }, + )) + } + #[cfg(not(Py_3_13))] { // Silence unused variable warning @@ -297,7 +324,10 @@ impl PyModuleSlotsBuilder { 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) + self.push_value(ffi::PySlot_STATIC_DATA( + ffi::Py_mod_name, + name.as_ptr() as *mut c_void, + )) } #[cfg(not(Py_3_15))] @@ -312,7 +342,10 @@ impl PyModuleSlotsBuilder { #[cfg(Py_3_15)] { ffi::PyABIInfo_VAR!(ABI_INFO); - self.push(ffi::Py_mod_abi, std::ptr::addr_of_mut!(ABI_INFO).cast()) + self.push_value(ffi::PySlot_STATIC_DATA( + ffi::Py_mod_abi, + std::ptr::addr_of_mut!(ABI_INFO).cast(), + )) } #[cfg(not(Py_3_15))] @@ -324,7 +357,10 @@ 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) + self.push_value(ffi::PySlot_STATIC_DATA( + ffi::Py_mod_doc, + doc.as_ptr() as *mut c_void, + )) } #[cfg(not(Py_3_15))] @@ -339,21 +375,39 @@ impl PyModuleSlotsBuilder { PyModuleSlots(UnsafeCell::new(self.values)) } - const fn push(mut self, slot: c_int, value: *mut c_void) -> Self { + #[cfg(not(Py_3_15))] + const fn push(mut self, slot: u16, value: *mut c_void) -> Self { // Required to guarantee there's still a zeroed element // at the end assert!( self.len < MAX_SLOTS, "Cannot add more than MAX_SLOTS slots to a PyModuleSlots", ); - self.values[self.len] = ffi::PyModuleDef_Slot { slot, value }; + self.values[self.len] = ffi::PyModuleDef_Slot { + slot: slot as c_int, + value, + }; + self.len += 1; + self + } + + #[cfg(Py_3_15)] + const fn push_value(mut self, value: ffi::PySlot) -> Self { + assert!( + self.len < MAX_SLOTS, + "Cannot add more than MAX_SLOTS slots to a PyModuleSlots", + ); + self.values[self.len] = value; self.len += 1; self } } /// Wrapper to safely store module slots, to be used in a `ModuleDef`. +#[cfg(not(Py_3_15))] pub struct PyModuleSlots(UnsafeCell<[ffi::PyModuleDef_Slot; MAX_SLOTS_WITH_TRAILING_NULL]>); +#[cfg(Py_3_15)] +pub struct PyModuleSlots(UnsafeCell<[ffi::PySlot; MAX_SLOTS_WITH_TRAILING_NULL]>); // It might be possible to avoid this with SyncUnsafeCell in the future // From 090d11da752a5d5ca6fd3a05f22c20d1de624c6a Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Sun, 3 May 2026 16:47:23 -0500 Subject: [PATCH 102/195] fix ffi docstring example --- pyo3-ffi/src/lib.rs | 50 ++++++++++++++++++++------------------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index 15ca57c74d8..d3de3be44f0 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -159,35 +159,11 @@ //! PyMethodDef::zeroed(), //! ]; //! -//! #[cfg(Py_3_15)] -//! PyABIInfo_VAR!(ABI_INFO); -//! +//! #[cfg(not(Py_3_15))] //! const SLOTS_LEN: usize = -//! 1 + cfg!(Py_3_12) as usize + cfg!(Py_GIL_DISABLED) as usize + 4 * (cfg!(Py_3_15) as usize); +//! 1 + cfg!(Py_3_12) as usize + cfg!(Py_GIL_DISABLED) as usize; +//! #[cfg(not(Py_3_15))] //! static mut SLOTS: [PyModuleDef_Slot; SLOTS_LEN] = [ -//! #[cfg(Py_3_15)] -//! PyModuleDef_Slot { -//! slot: Py_mod_abi, -//! value: (&raw mut ABI_INFO).cast(), -//! }, -//! #[cfg(Py_3_15)] -//! PyModuleDef_Slot { -//! slot: Py_mod_name, -//! // safety: Python does not write to this field -//! value: c"string_sum".as_ptr() as *mut c_void, -//! }, -//! #[cfg(Py_3_15)] -//! PyModuleDef_Slot { -//! slot: Py_mod_doc, -//! // safety: Python does not write to this field -//! value: c"A Python module written in Rust.".as_ptr() as *mut c_void, -//! }, -//! #[cfg(Py_3_15)] -//! PyModuleDef_Slot { -//! slot: Py_mod_methods, -//! value: (&raw mut METHODS).cast(), -//! }, -//! #[cfg(Py_3_12)] //! PyModuleDef_Slot { //! slot: Py_mod_multiple_interpreters, //! value: Py_MOD_PER_INTERPRETER_GIL_SUPPORTED, @@ -203,6 +179,24 @@ //! }, //! ]; //! +//! #[cfg(Py_3_15)] +//! PyABIInfo_VAR!(ABI_INFO); +//! +//! #[cfg(Py_3_15)] +//! const SLOTS_LEN: usize = +//! 1 + cfg!(Py_3_12) as usize + cfg!(Py_GIL_DISABLED) as usize + 4 * (cfg!(Py_3_15) as usize); +//! #[cfg(Py_3_15)] +//! static mut SLOTS: [PySlot; SLOTS_LEN] = [ +//! PySlot_STATIC_DATA(Py_mod_abi, std::ptr::addr_of_mut!(ABI_INFO).cast()), +//! PySlot_STATIC_DATA(Py_mod_name, c"string_sum".as_ptr() as *mut c_void), +//! PySlot_STATIC_DATA(Py_mod_name, c"A Python module written in Rust.".as_ptr() as *mut c_void), +//! PySlot_STATIC_DATA(Py_mod_methods, (&raw mut METHODS).cast()), +//! PySlot_DATA(Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED), +//! #[cfg(Py_GIL_DISABLED)] +//! PySlot_DATA(Py_mod_gil, Py_MOD_GIL_NOT_USED), +//! PySlot_END(), +//! ]; +//! //! // The module initialization function //! #[cfg(not(Py_3_15))] //! #[allow(non_snake_case, reason = "must be named `PyInit_`")] @@ -214,7 +208,7 @@ //! #[cfg(Py_3_15)] //! #[allow(non_snake_case, reason = "must be named `PyModExport_`")] //! #[no_mangle] -//! pub unsafe extern "C" fn PyModExport_string_sum() -> *mut PyModuleDef_Slot { +//! pub unsafe extern "C" fn PyModExport_string_sum() -> *mut PySlot { //! (&raw mut SLOTS).cast() //! } //! From 81daf343558fd245ac49d0f64174d48e55ffa529 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Sun, 3 May 2026 17:39:42 -0500 Subject: [PATCH 103/195] typo fix --- pyo3-ffi/src/slots.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyo3-ffi/src/slots.rs b/pyo3-ffi/src/slots.rs index 734479a5fed..e6a4853acff 100644 --- a/pyo3-ffi/src/slots.rs +++ b/pyo3-ffi/src/slots.rs @@ -100,7 +100,7 @@ pub const fn PySlot_STATIC_DATA(NAME: u16, VALUE: *mut c_void) -> PySlot { } } -pub const fn PySplot_PTR(NAME: u16, VALUE: *mut c_void) -> PySlot { +pub const fn PySlot_PTR(NAME: u16, VALUE: *mut c_void) -> PySlot { PySlot { sl_id: NAME, sl_flags: PySlot_INTPTR, @@ -109,7 +109,7 @@ pub const fn PySplot_PTR(NAME: u16, VALUE: *mut c_void) -> PySlot { } } -pub const fn PySplot_PTR_STATIC(NAME: u16, VALUE: *mut c_void) -> PySlot { +pub const fn PySlot_PTR_STATIC(NAME: u16, VALUE: *mut c_void) -> PySlot { PySlot { sl_id: NAME, sl_flags: PySlot_INTPTR | PySlot_STATIC, From 3ce92757867bb083dc38da1d45d708fc6e6906c7 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Sun, 3 May 2026 19:22:05 -0500 Subject: [PATCH 104/195] fix ffi-check macro --- pyo3-ffi-check/macro/src/lib.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyo3-ffi-check/macro/src/lib.rs b/pyo3-ffi-check/macro/src/lib.rs index 7434f4dbc2b..a14319d6ecf 100644 --- a/pyo3-ffi-check/macro/src/lib.rs +++ b/pyo3-ffi-check/macro/src/lib.rs @@ -181,6 +181,13 @@ pub fn for_all_fields(input: proc_macro::TokenStream) -> proc_macro::TokenStream // the field name in the C API is `type`, but that's a keyword in Rust // so PyO3 picked type_code, bindgen picked type_ Ident::new("type_", Span::call_site()) + } else if struct_name == "PySlot" && field_name == "anon1" { + // PySlot has two anonymous unions (since they aren't allowed in Rust, + // PyO3 invented names for them); bindgen names the first one __bindgen_anon_1 + Ident::new("__bindgen_anon_1", Span::call_site()) + } else if struct_name == "PySlot" && field_name == "anon2" { + // ...and the second one __bindgen_anon_2 + Ident::new("__bindgen_anon_2", Span::call_site()) } else { field_ident.clone() }; From 4763179f13fe6f58b81bdaf27f9a15724f5bb537 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Sun, 3 May 2026 20:14:34 -0500 Subject: [PATCH 105/195] fix ffi examples --- pyo3-ffi/examples/sequential/src/lib.rs | 2 +- pyo3-ffi/examples/sequential/src/module.rs | 78 ++++++++++------------ pyo3-ffi/examples/string-sum/src/lib.rs | 41 ++++++------ 3 files changed, 56 insertions(+), 65 deletions(-) diff --git a/pyo3-ffi/examples/sequential/src/lib.rs b/pyo3-ffi/examples/sequential/src/lib.rs index 3c08e6a7c42..499e2877e87 100644 --- a/pyo3-ffi/examples/sequential/src/lib.rs +++ b/pyo3-ffi/examples/sequential/src/lib.rs @@ -17,6 +17,6 @@ pub unsafe extern "C" fn PyInit_sequential() -> *mut PyObject { #[cfg(Py_3_15)] #[allow(non_snake_case, reason = "must be named `PyModExport_`")] #[no_mangle] -pub unsafe extern "C" fn PyModExport_sequential() -> *mut PyModuleDef_Slot { +pub unsafe extern "C" fn PyModExport_sequential() -> *mut PySlot { (&raw mut SEQUENTIAL_SLOTS).cast() } diff --git a/pyo3-ffi/examples/sequential/src/module.rs b/pyo3-ffi/examples/sequential/src/module.rs index a7d47f259a8..df1138eb9fb 100644 --- a/pyo3-ffi/examples/sequential/src/module.rs +++ b/pyo3-ffi/examples/sequential/src/module.rs @@ -20,44 +20,42 @@ PyABIInfo_VAR!(ABI_INFO); const SEQUENTIAL_SLOTS_LEN: usize = 2 + cfg!(Py_3_12) as usize + cfg!(Py_GIL_DISABLED) as usize + 7 * (cfg!(Py_3_15) as usize); +#[cfg(Py_3_15)] +pub static mut SEQUENTIAL_SLOTS: [PySlot; SEQUENTIAL_SLOTS_LEN] = [ + PySlot_STATIC_DATA(Py_mod_abi, (&raw mut ABI_INFO).cast()), + PySlot_STATIC_DATA(Py_mod_name, c"sequential".as_ptr() as *mut c_void), + PySlot_STATIC_DATA( + Py_mod_doc, + c"A library for generating sequential ids, written in Rust.".as_ptr() as *mut c_void, + ), + PySlot_SIZE( + Py_mod_state_size, + mem::size_of::() as Py_ssize_t, + ), + PySlot_FUNC(Py_mod_state_traverse, unsafe { + std::mem::transmute::(sequential_traverse) + }), + PySlot_FUNC(Py_mod_state_clear, unsafe { + std::mem::transmute::(sequential_clear) + }), + PySlot_FUNC(Py_mod_state_free, unsafe { + std::mem::transmute::(sequential_free) + }), + PySlot_FUNC(Py_mod_exec, unsafe { + std::mem::transmute:: c_int, _Py_funcptr_t>( + sequential_exec, + ) + }), + PySlot_DATA( + Py_mod_multiple_interpreters, + Py_MOD_PER_INTERPRETER_GIL_SUPPORTED, + ), + #[cfg(Py_GIL_DISABLED)] + PySlot_DATA(Py_mod_gil, Py_MOD_GIL_NOT_USED), + PySlot_END(), +]; +#[cfg(not(Py_3_15))] pub static mut SEQUENTIAL_SLOTS: [PyModuleDef_Slot; SEQUENTIAL_SLOTS_LEN] = [ - #[cfg(Py_3_15)] - PyModuleDef_Slot { - slot: Py_mod_abi, - value: (&raw mut ABI_INFO).cast(), - }, - #[cfg(Py_3_15)] - PyModuleDef_Slot { - slot: Py_mod_name, - // safety: Python does not write to this field - value: c"sequential".as_ptr() as *mut c_void, - }, - #[cfg(Py_3_15)] - PyModuleDef_Slot { - slot: Py_mod_doc, - // safety: Python does not write to this field - value: c"A library for generating sequential ids, written in Rust.".as_ptr() as *mut c_void, - }, - #[cfg(Py_3_15)] - PyModuleDef_Slot { - slot: Py_mod_state_size, - value: mem::size_of::() as *mut c_void, - }, - #[cfg(Py_3_15)] - PyModuleDef_Slot { - slot: Py_mod_state_traverse, - value: sequential_traverse as *mut c_void, - }, - #[cfg(Py_3_15)] - PyModuleDef_Slot { - slot: Py_mod_state_clear, - value: sequential_clear as *mut c_void, - }, - #[cfg(Py_3_15)] - PyModuleDef_Slot { - slot: Py_mod_state_free, - value: sequential_free as *mut c_void, - }, PyModuleDef_Slot { slot: Py_mod_exec, value: sequential_exec as *mut c_void, @@ -70,11 +68,7 @@ pub static mut SEQUENTIAL_SLOTS: [PyModuleDef_Slot; SEQUENTIAL_SLOTS_LEN] = [ #[cfg(Py_GIL_DISABLED)] PyModuleDef_Slot { slot: Py_mod_gil, - value: Py_MOD_GIL_NOT_USED, - }, - PyModuleDef_Slot { - slot: 0, - value: ptr::null_mut(), + vale: Py_MOD_GIL_NOT_USED, }, ]; diff --git a/pyo3-ffi/examples/string-sum/src/lib.rs b/pyo3-ffi/examples/string-sum/src/lib.rs index 5081405b773..be50a9f06b8 100644 --- a/pyo3-ffi/examples/string-sum/src/lib.rs +++ b/pyo3-ffi/examples/string-sum/src/lib.rs @@ -36,29 +36,26 @@ PyABIInfo_VAR!(ABI_INFO); const SLOTS_LEN: usize = 1 + cfg!(Py_3_12) as usize + cfg!(Py_GIL_DISABLED) as usize + 4 * (cfg!(Py_3_15) as usize); +#[cfg(Py_3_15)] +static mut SLOTS: [PySlot; SLOTS_LEN] = [ + PySlot_STATIC_DATA(Py_mod_abi, (&raw mut ABI_INFO).cast()), + // safety: Python does not write to these static fields + PySlot_STATIC_DATA(Py_mod_name, c"string_sum".as_ptr() as *mut c_void), + PySlot_STATIC_DATA( + Py_mod_doc, + c"A Python module written in Rust.".as_ptr() as *mut c_void, + ), + PySlot_STATIC_DATA(Py_mod_methods, (&raw mut METHODS).cast()), + PySlot_DATA( + Py_mod_multiple_interpreters, + Py_MOD_PER_INTERPRETER_GIL_SUPPORTED, + ), + #[cfg(Py_GIL_DISABLED)] + PySlot_DATA(Py_mod_gil, Py_MOD_GIL_NOT_USED), + PySlot_END(), +]; +#[cfg(not(Py_3_15))] static mut SLOTS: [PyModuleDef_Slot; SLOTS_LEN] = [ - #[cfg(Py_3_15)] - PyModuleDef_Slot { - slot: Py_mod_abi, - value: (&raw mut ABI_INFO).cast(), - }, - #[cfg(Py_3_15)] - PyModuleDef_Slot { - slot: Py_mod_name, - // safety: Python does not write to this field - value: c"string_sum".as_ptr() as *mut c_void, - }, - #[cfg(Py_3_15)] - PyModuleDef_Slot { - slot: Py_mod_doc, - // safety: Python does not write to this field - value: c"A Python module written in Rust.".as_ptr() as *mut c_void, - }, - #[cfg(Py_3_15)] - PyModuleDef_Slot { - slot: Py_mod_methods, - value: (&raw mut METHODS).cast(), - }, #[cfg(Py_3_12)] PyModuleDef_Slot { slot: Py_mod_multiple_interpreters, From b6d6f99075ac28a937cb0ea94bfed5e71ca7bec4 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Sun, 3 May 2026 20:58:06 -0500 Subject: [PATCH 106/195] make PySlot_FUNC a Rust macro --- pyo3-ffi/examples/sequential/src/module.rs | 20 +++++------------ pyo3-ffi/src/lib.rs | 2 +- pyo3-ffi/src/slots.rs | 26 ++++++++++++++++------ src/impl_/pymodule.rs | 7 +----- 4 files changed, 26 insertions(+), 29 deletions(-) diff --git a/pyo3-ffi/examples/sequential/src/module.rs b/pyo3-ffi/examples/sequential/src/module.rs index df1138eb9fb..73c2d714508 100644 --- a/pyo3-ffi/examples/sequential/src/module.rs +++ b/pyo3-ffi/examples/sequential/src/module.rs @@ -32,20 +32,10 @@ pub static mut SEQUENTIAL_SLOTS: [PySlot; SEQUENTIAL_SLOTS_LEN] = [ Py_mod_state_size, mem::size_of::() as Py_ssize_t, ), - PySlot_FUNC(Py_mod_state_traverse, unsafe { - std::mem::transmute::(sequential_traverse) - }), - PySlot_FUNC(Py_mod_state_clear, unsafe { - std::mem::transmute::(sequential_clear) - }), - PySlot_FUNC(Py_mod_state_free, unsafe { - std::mem::transmute::(sequential_free) - }), - PySlot_FUNC(Py_mod_exec, unsafe { - std::mem::transmute:: c_int, _Py_funcptr_t>( - sequential_exec, - ) - }), + PySlot_FUNC!(Py_mod_state_traverse, sequential_traverse), + PySlot_FUNC!(Py_mod_state_clear, sequential_clear), + PySlot_FUNC!(Py_mod_state_free, sequential_free), + PySlot_FUNC!(Py_mod_exec, sequential_exec), PySlot_DATA( Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED, @@ -68,7 +58,7 @@ pub static mut SEQUENTIAL_SLOTS: [PyModuleDef_Slot; SEQUENTIAL_SLOTS_LEN] = [ #[cfg(Py_GIL_DISABLED)] PyModuleDef_Slot { slot: Py_mod_gil, - vale: Py_MOD_GIL_NOT_USED, + value: Py_MOD_GIL_NOT_USED, }, ]; diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index d3de3be44f0..3c989775aa7 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -189,7 +189,7 @@ //! static mut SLOTS: [PySlot; SLOTS_LEN] = [ //! PySlot_STATIC_DATA(Py_mod_abi, std::ptr::addr_of_mut!(ABI_INFO).cast()), //! PySlot_STATIC_DATA(Py_mod_name, c"string_sum".as_ptr() as *mut c_void), -//! PySlot_STATIC_DATA(Py_mod_name, c"A Python module written in Rust.".as_ptr() as *mut c_void), +//! PySlot_STATIC_DATA(Py_mod_doc, c"A Python module written in Rust.".as_ptr() as *mut c_void), //! PySlot_STATIC_DATA(Py_mod_methods, (&raw mut METHODS).cast()), //! PySlot_DATA(Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED), //! #[cfg(Py_GIL_DISABLED)] diff --git a/pyo3-ffi/src/slots.rs b/pyo3-ffi/src/slots.rs index e6a4853acff..3a747ae9d9c 100644 --- a/pyo3-ffi/src/slots.rs +++ b/pyo3-ffi/src/slots.rs @@ -55,13 +55,25 @@ pub const fn PySlot_DATA(NAME: u16, VALUE: *mut c_void) -> PySlot { } } -pub const fn PySlot_FUNC(NAME: u16, VALUE: _Py_funcptr_t) -> PySlot { - PySlot { - sl_id: NAME, - sl_flags: 0, - anon1: _anon_union_32b { sl_reserved: 0 }, - anon2: _anon_union_64b { sl_func: VALUE }, - } +/// # Safety +/// +/// Like the C macro, this performs no signature check on `$value`. The caller +/// must ensure the function pointer is appropriate for the given slot id. +/// (Constructing the `PySlot` is itself sound — all `extern "C"` fn pointers +/// share size and ABI with `_Py_funcptr_t` — but CPython will eventually call +/// the function with a particular signature, and a mismatch is UB.) +#[macro_export] +macro_rules! PySlot_FUNC { + ($name:expr, $value:expr) => { + $crate::PySlot { + sl_id: $name, + sl_flags: 0, + anon1: $crate::_anon_union_32b { sl_reserved: 0 }, + anon2: $crate::_anon_union_64b { + sl_func: unsafe { ::std::mem::transmute::<_, $crate::_Py_funcptr_t>($value) }, + }, + } + }; } pub const fn PySlot_SIZE(NAME: u16, VALUE: Py_ssize_t) -> PySlot { diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index d7bef2a8100..5d0838ac106 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -279,12 +279,7 @@ impl PyModuleSlotsBuilder { } #[cfg(Py_3_15)] { - self.push_value(ffi::PySlot_FUNC(ffi::Py_mod_exec, unsafe { - std::mem::transmute::< - unsafe extern "C" fn(*mut pyo3_ffi::PyObject) -> i32, - ffi::_Py_funcptr_t, - >(exec) - })) + self.push_value(ffi::PySlot_FUNC!(ffi::Py_mod_exec, exec)) } } From 4828759d35cf410478f860ab1f2cf11b0920017a Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Sun, 3 May 2026 21:32:18 -0500 Subject: [PATCH 107/195] fix clippy --- pyo3-ffi/src/cpython/mod.rs | 4 ++-- pyo3-ffi/src/critical_section.rs | 4 ++-- pyo3-ffi/src/lib.rs | 4 ++-- pyo3-ffi/src/object.rs | 2 ++ pyo3-ffi/src/slots.rs | 23 +++++++++++++++++++++++ pyo3-ffi/src/slots_generated.rs | 20 ++++++++++++++++++++ 6 files changed, 51 insertions(+), 6 deletions(-) diff --git a/pyo3-ffi/src/cpython/mod.rs b/pyo3-ffi/src/cpython/mod.rs index de4da5afe0c..3590c9f4d38 100644 --- a/pyo3-ffi/src/cpython/mod.rs +++ b/pyo3-ffi/src/cpython/mod.rs @@ -7,7 +7,7 @@ pub(crate) mod ceval; pub(crate) mod code; pub(crate) mod compile; pub(crate) mod complexobject; -#[cfg(Py_3_14)] +#[cfg(all(Py_3_14, Py_GIL_DISABLED))] pub(crate) mod critical_section; pub(crate) mod descrobject; pub(crate) mod dictobject; @@ -56,7 +56,7 @@ pub use self::ceval::*; pub use self::code::*; pub use self::compile::*; pub use self::complexobject::*; -#[cfg(Py_3_14)] +#[cfg(all(Py_3_14, Py_GIL_DISABLED))] pub use self::critical_section::{PyCriticalSection2_BeginMutex, PyCriticalSection_BeginMutex}; pub use self::descrobject::*; pub use self::dictobject::*; diff --git a/pyo3-ffi/src/critical_section.rs b/pyo3-ffi/src/critical_section.rs index f42aac51e93..7850163084e 100644 --- a/pyo3-ffi/src/critical_section.rs +++ b/pyo3-ffi/src/critical_section.rs @@ -1,9 +1,9 @@ -#[cfg(not(Py_LIMITED_API))] +#[cfg(any(all(Py_GIL_DISABLED, Py_3_13, not(Py_LIMITED_API)), Py_3_15,))] use crate::PyMutex; #[cfg(any(all(Py_GIL_DISABLED, Py_3_13), Py_3_15))] use crate::PyObject; -#[cfg(all(Py_LIMITED_API, Py_3_15))] +#[cfg(all(Py_LIMITED_API, not(Py_3_15)))] opaque_struct!(pub PyMutex); #[cfg(any(all(Py_GIL_DISABLED, Py_3_13, not(Py_LIMITED_API)), Py_3_15,))] diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index 3c989775aa7..db40b741255 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -40,7 +40,7 @@ //! PyO3 uses `rustc`'s `--cfg` flags to enable or disable code used for different Python versions. //! If you want to do this for your own crate, you can do so with the [`pyo3-build-config`] crate. //! -//! - `Py_3_8`, `Py_3_9`, `Py_3_10`, `Py_3_11`, `Py_3_12`, `Py_3_13`, `Py_3_14`: Marks code that is +//! - `Py_3_8`, `Py_3_9`, `Py_3_10`, `Py_3_11`, `Py_3_12`, `Py_3_13`, `Py_3_14`, `Py_3_15`: Marks code that is //! only enabled when compiling for a given minimum Python version. //! - `Py_LIMITED_API`: Marks code enabled when the `abi3` feature flag is enabled. //! - `Py_GIL_DISABLED`: Marks code that runs only in the free-threaded build of CPython. @@ -433,7 +433,7 @@ pub use self::compile::*; pub use self::complexobject::*; #[cfg(not(Py_LIMITED_API))] pub use self::context::*; -#[cfg(Py_3_15)] +#[cfg(Py_3_13)] pub use self::critical_section::*; #[cfg(not(Py_LIMITED_API))] pub use self::datetime::*; diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index 4c4015577a7..a2f7defa63e 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -277,11 +277,13 @@ pub type hashfunc = unsafe extern "C" fn(*mut PyObject) -> Py_hash_t; pub type richcmpfunc = unsafe extern "C" fn(*mut PyObject, *mut PyObject, c_int) -> *mut PyObject; pub type getiterfunc = unsafe extern "C" fn(*mut PyObject) -> *mut PyObject; pub type iternextfunc = unsafe extern "C" fn(*mut PyObject) -> *mut PyObject; +#[cfg(Py_3_15)] #[repr(C)] pub struct _PyObjectIndexPair { pub object: *mut PyObject, pub index: Py_ssize_t, } +#[cfg(Py_3_15)] pub type _Py_iteritemfunc = unsafe extern "C" fn(*mut PyObject, Py_ssize_t) -> _PyObjectIndexPair; pub type descrgetfunc = unsafe extern "C" fn(*mut PyObject, *mut PyObject, *mut PyObject) -> *mut PyObject; diff --git a/pyo3-ffi/src/slots.rs b/pyo3-ffi/src/slots.rs index 3a747ae9d9c..bad37400841 100644 --- a/pyo3-ffi/src/slots.rs +++ b/pyo3-ffi/src/slots.rs @@ -1,16 +1,21 @@ +#[cfg(Py_3_15)] use crate::Py_ssize_t; +#[cfg(Py_3_15)] use std::ffi::c_void; +#[cfg(Py_3_15)] pub type _Py_funcptr_t = unsafe extern "C" fn(); #[derive(Copy, Clone)] #[repr(C)] +#[cfg(Py_3_15)] pub union _anon_union_32b { pub sl_reserved: u32, } #[derive(Copy, Clone)] #[repr(C)] +#[cfg(Py_3_15)] pub union _anon_union_64b { pub sl_ptr: *mut c_void, pub sl_func: _Py_funcptr_t, @@ -21,6 +26,7 @@ pub union _anon_union_64b { #[derive(Copy, Clone)] #[repr(C)] +#[cfg(Py_3_15)] pub struct PySlot { pub sl_id: u16, pub sl_flags: u16, @@ -28,6 +34,10 @@ pub struct PySlot { pub anon2: _anon_union_64b, } +#[cfg(not(Py_3_15))] +opaque_struct!(pub PySlot); + +#[cfg(Py_3_15)] impl PartialEq for PySlot { fn eq(&self, other: &Self) -> bool { unsafe { @@ -41,11 +51,16 @@ impl PartialEq for PySlot { } } +#[cfg(Py_3_15)] pub const PySlot_OPTIONAL: u16 = 0x01; +#[cfg(Py_3_15)] pub const PySlot_STATIC: u16 = 0x02; +#[cfg(Py_3_15)] pub const PySlot_INTPTR: u16 = 0x04; +#[cfg(Py_3_15)] pub const Py_slot_invalid: u16 = 0xffff; +#[cfg(Py_3_15)] pub const fn PySlot_DATA(NAME: u16, VALUE: *mut c_void) -> PySlot { PySlot { sl_id: NAME, @@ -63,6 +78,7 @@ pub const fn PySlot_DATA(NAME: u16, VALUE: *mut c_void) -> PySlot { /// share size and ABI with `_Py_funcptr_t` — but CPython will eventually call /// the function with a particular signature, and a mismatch is UB.) #[macro_export] +#[cfg(Py_3_15)] macro_rules! PySlot_FUNC { ($name:expr, $value:expr) => { $crate::PySlot { @@ -76,6 +92,7 @@ macro_rules! PySlot_FUNC { }; } +#[cfg(Py_3_15)] pub const fn PySlot_SIZE(NAME: u16, VALUE: Py_ssize_t) -> PySlot { PySlot { sl_id: NAME, @@ -85,6 +102,7 @@ pub const fn PySlot_SIZE(NAME: u16, VALUE: Py_ssize_t) -> PySlot { } } +#[cfg(Py_3_15)] pub const fn PySlot_INT64(NAME: u16, VALUE: i64) -> PySlot { PySlot { sl_id: NAME, @@ -94,6 +112,7 @@ pub const fn PySlot_INT64(NAME: u16, VALUE: i64) -> PySlot { } } +#[cfg(Py_3_15)] pub const fn PySlot_UINT64(NAME: u16, VALUE: u64) -> PySlot { PySlot { sl_id: NAME, @@ -103,6 +122,7 @@ pub const fn PySlot_UINT64(NAME: u16, VALUE: u64) -> PySlot { } } +#[cfg(Py_3_15)] pub const fn PySlot_STATIC_DATA(NAME: u16, VALUE: *mut c_void) -> PySlot { PySlot { sl_id: NAME, @@ -112,6 +132,7 @@ pub const fn PySlot_STATIC_DATA(NAME: u16, VALUE: *mut c_void) -> PySlot { } } +#[cfg(Py_3_15)] pub const fn PySlot_PTR(NAME: u16, VALUE: *mut c_void) -> PySlot { PySlot { sl_id: NAME, @@ -121,6 +142,7 @@ pub const fn PySlot_PTR(NAME: u16, VALUE: *mut c_void) -> PySlot { } } +#[cfg(Py_3_15)] pub const fn PySlot_PTR_STATIC(NAME: u16, VALUE: *mut c_void) -> PySlot { PySlot { sl_id: NAME, @@ -130,6 +152,7 @@ pub const fn PySlot_PTR_STATIC(NAME: u16, VALUE: *mut c_void) -> PySlot { } } +#[cfg(Py_3_15)] pub const fn PySlot_END() -> PySlot { unsafe { std::mem::zeroed() } } diff --git a/pyo3-ffi/src/slots_generated.rs b/pyo3-ffi/src/slots_generated.rs index ad7ab55734a..cfe33009a06 100644 --- a/pyo3-ffi/src/slots_generated.rs +++ b/pyo3-ffi/src/slots_generated.rs @@ -26,6 +26,7 @@ const fn _Py_SLOT_COMPAT_VALUE_int(OLD: c_int, NEW: c_int) -> c_int { } } +#[cfg(Py_3_15)] pub const Py_slot_end: u16 = 0; pub const Py_mod_create: u16 = _Py_SLOT_COMPAT_VALUE(1, 84); pub const Py_mod_exec: u16 = _Py_SLOT_COMPAT_VALUE(2, 85); @@ -35,22 +36,41 @@ pub const Py_bf_getbuffer: c_int = _Py_SLOT_COMPAT_VALUE_int(1, 88); pub const Py_bf_releasebuffer: c_int = _Py_SLOT_COMPAT_VALUE_int(2, 89); pub const Py_mp_ass_subscript: c_int = _Py_SLOT_COMPAT_VALUE_int(3, 90); pub const Py_mp_length: c_int = _Py_SLOT_COMPAT_VALUE_int(4, 91); +#[cfg(Py_3_15)] pub const Py_slot_subslots: u16 = 92; +#[cfg(Py_3_15)] pub const Py_tp_slots: u16 = 93; +#[cfg(Py_3_15)] pub const Py_mod_slots: u16 = 94; +#[cfg(Py_3_15)] pub const Py_tp_name: u16 = 95; +#[cfg(Py_3_15)] pub const Py_tp_basicsize: u16 = 96; +#[cfg(Py_3_15)] pub const Py_tp_extra_basicsize: u16 = 97; +#[cfg(Py_3_15)] pub const Py_tp_itemsize: u16 = 98; +#[cfg(Py_3_15)] pub const Py_tp_flags: u16 = 99; +#[cfg(Py_3_15)] pub const Py_mod_name: u16 = 100; +#[cfg(Py_3_15)] pub const Py_mod_doc: u16 = 101; +#[cfg(Py_3_15)] pub const Py_mod_state_size: u16 = 102; +#[cfg(Py_3_15)] pub const Py_mod_methods: u16 = 103; +#[cfg(Py_3_15)] pub const Py_mod_state_traverse: u16 = 104; +#[cfg(Py_3_15)] pub const Py_mod_state_clear: u16 = 105; +#[cfg(Py_3_15)] pub const Py_mod_state_free: u16 = 106; +#[cfg(Py_3_15)] pub const Py_tp_metaclass: u16 = 107; +#[cfg(Py_3_15)] pub const Py_tp_module: u16 = 108; +#[cfg(Py_3_15)] pub const Py_mod_abi: u16 = 109; +#[cfg(Py_3_15)] pub const Py_mod_token: u16 = 110; From 1e2a2c38cbff997954372f6d12ae7e95023381ab Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Sun, 3 May 2026 22:02:20 -0500 Subject: [PATCH 108/195] keep macros version-independent --- pyo3-ffi/src/lib.rs | 1 + pyo3-ffi/src/slots.rs | 3 --- pyo3-macros-backend/src/module.rs | 11 ++++------- src/impl_/pymodule.rs | 29 ++++++++++++++++++++++++++++- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index db40b741255..bb9a6a5b744 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -476,6 +476,7 @@ pub use self::rangeobject::*; pub use self::refcount::*; pub use self::setobject::*; pub use self::sliceobject::*; +#[cfg(Py_3_15)] pub use self::slots::*; pub use self::slots_generated::*; pub use self::structseq::*; diff --git a/pyo3-ffi/src/slots.rs b/pyo3-ffi/src/slots.rs index bad37400841..d0a9d32b26b 100644 --- a/pyo3-ffi/src/slots.rs +++ b/pyo3-ffi/src/slots.rs @@ -34,9 +34,6 @@ pub struct PySlot { pub anon2: _anon_union_64b, } -#[cfg(not(Py_3_15))] -opaque_struct!(pub PySlot); - #[cfg(Py_3_15)] impl PartialEq for PySlot { fn eq(&self, other: &Self) -> bool { diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 6bea2f3a4a3..4195e61faaa 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -569,13 +569,10 @@ fn module_initialization( _PYO3_DEF.init_multi_phase() } - /// 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::PySlot { - _PYO3_DEF.get_slots() - } + // Defines the `PyModExport_` entry point used by Python 3.15 and newer. + // Expands to nothing on older Python versions, so this stays version-agnostic + // and `ffi::PySlot` is only required to exist on 3.15+. + #pyo3_path::__pyo3_pymodexport!(#pymodexport_symbol, _PYO3_DEF); }); } Ok(result) diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 5d0838ac106..c8bb69ef70d 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -49,6 +49,7 @@ pub struct ModuleDef { 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( @@ -101,6 +102,7 @@ impl ModuleDef { name, #[cfg(Py_3_15)] doc, + #[cfg(Py_3_15)] slots, // -1 is never expected to be a valid interpreter ID #[cfg(all( @@ -227,11 +229,37 @@ impl ModuleDef { .map(|py_module| py_module.clone_ref(py)) } } + #[cfg(Py_3_15)] pub fn get_slots(&'static self) -> *mut ffi::PySlot { self.slots.0.get() as *mut ffi::PySlot } } +/// Defines the `PyModExport_` entry point used by Python 3.15 and newer. +/// +/// This is wrapped in a `macro_rules!` so the proc-macro backend can emit a single +/// version-agnostic invocation; the body only expands on Python 3.15+, where +/// `ffi::PySlot` is defined. +#[cfg(Py_3_15)] +#[doc(hidden)] +#[macro_export] +macro_rules! __pyo3_pymodexport { + ($symbol:literal, $def:path) => { + #[doc(hidden)] + #[export_name = $symbol] + pub unsafe extern "C" fn __pyo3_export() -> *mut $crate::ffi::PySlot { + $def.get_slots() + } + }; +} + +#[cfg(not(Py_3_15))] +#[doc(hidden)] +#[macro_export] +macro_rules! __pyo3_pymodexport { + ($symbol:literal, $def:path) => {}; +} + /// Type of the exec slot used to initialise module contents pub type ModuleExecSlot = unsafe extern "C" fn(*mut ffi::PyObject) -> c_int; @@ -557,7 +585,6 @@ mod tests { assert_eq!(module_def.name, NAME); assert_eq!(module_def.doc, DOC); } - assert_eq!(module_def.slots.0.get(), SLOTS.0.get()); } #[test] From ecc365a9f6cc6ace5edc5897533e9bf0fb08d4a9 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 5 May 2026 09:13:34 -0600 Subject: [PATCH 109/195] format extern_libpython blocks --- pyo3-ffi/src/moduleobject.rs | 5 +---- pyo3-ffi/src/object.rs | 6 +++++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pyo3-ffi/src/moduleobject.rs b/pyo3-ffi/src/moduleobject.rs index c9105448319..68767493b48 100644 --- a/pyo3-ffi/src/moduleobject.rs +++ b/pyo3-ffi/src/moduleobject.rs @@ -114,10 +114,7 @@ extern_libpython! { #[cfg(Py_3_15)] extern_libpython! { - pub fn PyModule_FromSlotsAndSpec( - slots: *const PySlot, - spec: *mut PyObject, - ) -> *mut PyObject; + pub fn PyModule_FromSlotsAndSpec(slots: *const PySlot, spec: *mut PyObject) -> *mut PyObject; pub fn PyModule_Exec(_mod: *mut PyObject) -> c_int; pub fn PyModule_GetStateSize(_mod: *mut PyObject, result: *mut Py_ssize_t) -> c_int; pub fn PyModule_GetToken(module: *mut PyObject, result: *mut *mut c_void) -> c_int; diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index a2f7defa63e..d67d5e39c7c 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -389,7 +389,11 @@ extern_libpython! { #[cfg(Py_3_14)] #[cfg_attr(PyPy, link_name = "PyPyType_GetBaseByToken")] - pub fn PyType_GetBaseByToken(type_: *mut PyTypeObject, token: *mut c_void, result: *mut *mut PyTypeObject) -> c_int; + pub fn PyType_GetBaseByToken( + type_: *mut PyTypeObject, + token: *mut c_void, + result: *mut *mut PyTypeObject, + ) -> c_int; #[cfg(Py_3_15)] #[cfg_attr(PyPy, link_name = "PyPyType_FromSlot")] From 14c80dbb4d2e501b7198f6dd8d487cd82a47afe3 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 5 May 2026 11:16:28 -0600 Subject: [PATCH 110/195] use an explicit type in the PySlot_FUNC macro --- pyo3-ffi/examples/sequential/src/module.rs | 12 ++++++++---- pyo3-ffi/src/slots.rs | 12 +++++------- src/impl_/pymodule.rs | 2 +- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/pyo3-ffi/examples/sequential/src/module.rs b/pyo3-ffi/examples/sequential/src/module.rs index 73c2d714508..435b64b6eaf 100644 --- a/pyo3-ffi/examples/sequential/src/module.rs +++ b/pyo3-ffi/examples/sequential/src/module.rs @@ -32,10 +32,14 @@ pub static mut SEQUENTIAL_SLOTS: [PySlot; SEQUENTIAL_SLOTS_LEN] = [ Py_mod_state_size, mem::size_of::() as Py_ssize_t, ), - PySlot_FUNC!(Py_mod_state_traverse, sequential_traverse), - PySlot_FUNC!(Py_mod_state_clear, sequential_clear), - PySlot_FUNC!(Py_mod_state_free, sequential_free), - PySlot_FUNC!(Py_mod_exec, sequential_exec), + PySlot_FUNC!(Py_mod_state_traverse, traverseproc, sequential_traverse), + PySlot_FUNC!(Py_mod_state_clear, inquiry, sequential_clear), + PySlot_FUNC!(Py_mod_state_free, freefunc, sequential_free), + PySlot_FUNC!( + Py_mod_exec, + unsafe extern "C" fn(*mut PyObject) -> c_int, + sequential_exec + ), PySlot_DATA( Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED, diff --git a/pyo3-ffi/src/slots.rs b/pyo3-ffi/src/slots.rs index d0a9d32b26b..b986b14f075 100644 --- a/pyo3-ffi/src/slots.rs +++ b/pyo3-ffi/src/slots.rs @@ -69,21 +69,19 @@ pub const fn PySlot_DATA(NAME: u16, VALUE: *mut c_void) -> PySlot { /// # Safety /// -/// Like the C macro, this performs no signature check on `$value`. The caller -/// must ensure the function pointer is appropriate for the given slot id. -/// (Constructing the `PySlot` is itself sound — all `extern "C"` fn pointers -/// share size and ABI with `_Py_funcptr_t` — but CPython will eventually call -/// the function with a particular signature, and a mismatch is UB.) +/// `$fn_ty$` must be the action found-pointer type of `$value` and must be a +/// valid signature for slot `$name`. A mismatch between the slot and expected signature +/// from CPython's point of view is UB. #[macro_export] #[cfg(Py_3_15)] macro_rules! PySlot_FUNC { - ($name:expr, $value:expr) => { + ($name:expr, $fn_ty:ty, $value:expr) => { $crate::PySlot { sl_id: $name, sl_flags: 0, anon1: $crate::_anon_union_32b { sl_reserved: 0 }, anon2: $crate::_anon_union_64b { - sl_func: unsafe { ::std::mem::transmute::<_, $crate::_Py_funcptr_t>($value) }, + sl_func: unsafe { ::std::mem::transmute::<$fn_ty, $crate::_Py_funcptr_t>($value) }, }, } }; diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index c8bb69ef70d..175058f759e 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -307,7 +307,7 @@ impl PyModuleSlotsBuilder { } #[cfg(Py_3_15)] { - self.push_value(ffi::PySlot_FUNC!(ffi::Py_mod_exec, exec)) + self.push_value(ffi::PySlot_FUNC!(ffi::Py_mod_exec, ModuleExecSlot, exec)) } } From af92c51dd31c88890216c55f205ec156d92f21d4 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 5 May 2026 11:36:22 -0600 Subject: [PATCH 111/195] fix issues seen in CI builds on older Pythons --- pyo3-ffi/src/critical_section.rs | 2 +- pyo3-ffi/src/lib.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pyo3-ffi/src/critical_section.rs b/pyo3-ffi/src/critical_section.rs index 7850163084e..9249df7aad4 100644 --- a/pyo3-ffi/src/critical_section.rs +++ b/pyo3-ffi/src/critical_section.rs @@ -3,7 +3,7 @@ use crate::PyMutex; #[cfg(any(all(Py_GIL_DISABLED, Py_3_13), Py_3_15))] use crate::PyObject; -#[cfg(all(Py_LIMITED_API, not(Py_3_15)))] +#[cfg(all(Py_LIMITED_API, Py_3_13, not(Py_3_15)))] opaque_struct!(pub PyMutex); #[cfg(any(all(Py_GIL_DISABLED, Py_3_13, not(Py_LIMITED_API)), Py_3_15,))] diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index bb9a6a5b744..32cbe7b84b9 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -128,6 +128,8 @@ //! ```rust,no_run //! #[cfg(Py_3_15)] //! use std::ffi::c_void; +//! #[cfg(not(Py_3_15))] +//! use std::ffi::c_int; //! use std::ffi::{c_char, c_long}; //! use std::ptr; //! @@ -165,7 +167,7 @@ //! #[cfg(not(Py_3_15))] //! static mut SLOTS: [PyModuleDef_Slot; SLOTS_LEN] = [ //! PyModuleDef_Slot { -//! slot: Py_mod_multiple_interpreters, +//! slot: Py_mod_multiple_interpreters as c_int, //! value: Py_MOD_PER_INTERPRETER_GIL_SUPPORTED, //! }, //! #[cfg(Py_GIL_DISABLED)] From 17583a89625ccbc6bf7507846e59d4f0fce529d3 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 5 May 2026 11:44:19 -0600 Subject: [PATCH 112/195] fix one more spot in the ffi tests --- pyo3-ffi/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index 32cbe7b84b9..0a80a60b7d0 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -172,7 +172,7 @@ //! }, //! #[cfg(Py_GIL_DISABLED)] //! PyModuleDef_Slot { -//! slot: Py_mod_gil, +//! slot: Py_mod_gil as c_int, //! value: Py_MOD_GIL_NOT_USED, //! }, //! PyModuleDef_Slot { From 1caf6852be0f1f5ab1bd672f6c7859c049f8ddcb Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 5 May 2026 11:54:23 -0600 Subject: [PATCH 113/195] fix type error in string-sum implementation --- pyo3-ffi/examples/string-sum/src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyo3-ffi/examples/string-sum/src/lib.rs b/pyo3-ffi/examples/string-sum/src/lib.rs index be50a9f06b8..2b614cd563a 100644 --- a/pyo3-ffi/examples/string-sum/src/lib.rs +++ b/pyo3-ffi/examples/string-sum/src/lib.rs @@ -1,3 +1,5 @@ +#[cfg(not(Py_3_15))] +use std::ffi::c_int; #[cfg(Py_3_15)] use std::ffi::c_void; use std::ffi::{c_char, c_long}; @@ -58,12 +60,12 @@ static mut SLOTS: [PySlot; SLOTS_LEN] = [ static mut SLOTS: [PyModuleDef_Slot; SLOTS_LEN] = [ #[cfg(Py_3_12)] PyModuleDef_Slot { - slot: Py_mod_multiple_interpreters, + slot: Py_mod_multiple_interpreters as c_int, value: Py_MOD_PER_INTERPRETER_GIL_SUPPORTED, }, #[cfg(Py_GIL_DISABLED)] PyModuleDef_Slot { - slot: Py_mod_gil, + slot: Py_mod_gil as c_int, value: Py_MOD_GIL_NOT_USED, }, PyModuleDef_Slot { From 85bc3022d1a5b7304890116a6e9941720cea06bb Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 5 May 2026 11:54:34 -0600 Subject: [PATCH 114/195] add changelog entries --- newsfragments/6014.added.md | 4 ++++ newsfragments/6014.changed.md | 1 + 2 files changed, 5 insertions(+) create mode 100644 newsfragments/6014.added.md create mode 100644 newsfragments/6014.changed.md diff --git a/newsfragments/6014.added.md b/newsfragments/6014.added.md new file mode 100644 index 00000000000..feb0e2a00bb --- /dev/null +++ b/newsfragments/6014.added.md @@ -0,0 +1,4 @@ +* Added support for Python 3.15.0b1. +* Added FFI bindings for the PEP 820 PySlot C API. +* Added bindings for the critical section API in the limited API on Python 3.15 + and newer. diff --git a/newsfragments/6014.changed.md b/newsfragments/6014.changed.md new file mode 100644 index 00000000000..5ff683a0dd3 --- /dev/null +++ b/newsfragments/6014.changed.md @@ -0,0 +1 @@ +* Updated FFI bindings and module initialization to use PEP 820 PySlot API. From 75b89628d27fff9845662cb52a4d89a40d69d1bc Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 5 May 2026 12:19:44 -0600 Subject: [PATCH 115/195] remove unnecessary PartialEq implementation --- pyo3-ffi/src/slots.rs | 14 -------------- src/impl_/pymodule.rs | 33 ++++++++++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/pyo3-ffi/src/slots.rs b/pyo3-ffi/src/slots.rs index b986b14f075..7770dfae3be 100644 --- a/pyo3-ffi/src/slots.rs +++ b/pyo3-ffi/src/slots.rs @@ -34,20 +34,6 @@ pub struct PySlot { pub anon2: _anon_union_64b, } -#[cfg(Py_3_15)] -impl PartialEq for PySlot { - fn eq(&self, other: &Self) -> bool { - unsafe { - // memcmp returns 0 if the memory blocks are identical - libc::memcmp( - self as *const Self as *const c_void, - other as *const Self as *const c_void, - std::mem::size_of::(), - ) == 0 - } - } -} - #[cfg(Py_3_15)] pub const PySlot_OPTIONAL: u16 = 0x01; #[cfg(Py_3_15)] diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 175058f759e..09928d572d5 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -495,7 +495,11 @@ impl PyAddToModule for ModuleDef { #[cfg(test)] mod tests { - use std::{borrow::Cow, ffi::CStr, os::raw::c_int}; + use std::{ + borrow::Cow, + ffi::CStr, + os::raw::{c_int, c_void}, + }; use crate::{ ffi, @@ -597,8 +601,31 @@ mod tests { .with_gil_used(false) .with_abi_info(); - assert!(builder.values[builder.len] == unsafe { std::mem::zeroed() }); - assert!(builder.values[builder.len - 1] != unsafe { std::mem::zeroed() }); + #[cfg(Py_3_15)] + type SlotType = ffi::PySlot; + #[cfg(not(Py_3_15))] + type SlotType = ffi::PyModuleDef_Slot; + + let zeroed: SlotType = unsafe { std::mem::zeroed() }; + + assert!( + unsafe { + libc::memcmp( + &builder.values[builder.len] as *const SlotType as *const c_void, + &zeroed as *const SlotType as *const c_void, + std::mem::size_of::(), + ) + } == 0 + ); + assert!( + unsafe { + libc::memcmp( + &builder.values[builder.len - 1] as *const SlotType as *const c_void, + &zeroed as *const SlotType as *const c_void, + std::mem::size_of::(), + ) + } != 0 + ); assert!(builder.len == MAX_SLOTS); let result = std::panic::catch_unwind(|| builder.with_mod_exec(module_exec).build()); From cbedefc71c2f1a171a358644709f4409aaf02861 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 5 May 2026 12:30:28 -0600 Subject: [PATCH 116/195] use std::slice::from_raw_parts instead of libc::memcmp --- src/impl_/pymodule.rs | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 09928d572d5..258124fa06b 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -495,11 +495,7 @@ impl PyAddToModule for ModuleDef { #[cfg(test)] mod tests { - use std::{ - borrow::Cow, - ffi::CStr, - os::raw::{c_int, c_void}, - }; + use std::{borrow::Cow, ffi::CStr, os::raw::c_int}; use crate::{ ffi, @@ -608,24 +604,19 @@ mod tests { let zeroed: SlotType = unsafe { std::mem::zeroed() }; - assert!( - unsafe { - libc::memcmp( - &builder.values[builder.len] as *const SlotType as *const c_void, - &zeroed as *const SlotType as *const c_void, - std::mem::size_of::(), - ) - } == 0 - ); - assert!( + fn raw_bytes(inst: &SlotType) -> &[u8] { unsafe { - libc::memcmp( - &builder.values[builder.len - 1] as *const SlotType as *const c_void, - &zeroed as *const SlotType as *const c_void, + std::slice::from_raw_parts( + inst as *const SlotType as *const u8, std::mem::size_of::(), ) - } != 0 - ); + } + } + let second_last_bytes = raw_bytes(&builder.values[builder.len - 1]); + let end_slot_bytes = raw_bytes(&builder.values[builder.len]); + let zeroed_bytes = raw_bytes(&zeroed); + assert_eq!(end_slot_bytes, zeroed_bytes); + assert_ne!(second_last_bytes, zeroed_bytes); assert!(builder.len == MAX_SLOTS); let result = std::panic::catch_unwind(|| builder.with_mod_exec(module_exec).build()); From ec46a200631ceff458e2f0d6b3aafc2f97146a63 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 5 May 2026 12:34:05 -0600 Subject: [PATCH 117/195] re-add incorrectly deleted end slot --- pyo3-ffi/examples/sequential/src/module.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyo3-ffi/examples/sequential/src/module.rs b/pyo3-ffi/examples/sequential/src/module.rs index 435b64b6eaf..f66129fc95a 100644 --- a/pyo3-ffi/examples/sequential/src/module.rs +++ b/pyo3-ffi/examples/sequential/src/module.rs @@ -64,6 +64,10 @@ pub static mut SEQUENTIAL_SLOTS: [PyModuleDef_Slot; SEQUENTIAL_SLOTS_LEN] = [ slot: Py_mod_gil, value: Py_MOD_GIL_NOT_USED, }, + PyModuleDef_Slot { + slot: 0, + value: ptr::null_mut(), + }, ]; unsafe extern "C" fn sequential_exec(module: *mut PyObject) -> c_int { From 63486a3da15af96d9e6266b836082446cbe159fc Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 5 May 2026 13:01:40 -0600 Subject: [PATCH 118/195] Use PartialEq on Python 3.14 and older --- src/impl_/pymodule.rs | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 258124fa06b..77c111745c7 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -596,27 +596,30 @@ mod tests { .with_doc(c"some doc") .with_gil_used(false) .with_abi_info(); + let second_last = builder.values[builder.len - 1]; + let last = builder.values[builder.len]; #[cfg(Py_3_15)] - type SlotType = ffi::PySlot; - #[cfg(not(Py_3_15))] - type SlotType = ffi::PyModuleDef_Slot; - - let zeroed: SlotType = unsafe { std::mem::zeroed() }; - - fn raw_bytes(inst: &SlotType) -> &[u8] { - unsafe { - std::slice::from_raw_parts( - inst as *const SlotType as *const u8, - std::mem::size_of::(), - ) + { + let zeroed = unsafe { std::mem::zeroed() }; + fn raw_bytes(inst: &ffi::PySlot) -> &[u8] { + unsafe { + std::slice::from_raw_parts( + inst as *const ffi::PySlot as *const u8, + std::mem::size_of::(), + ) + } } + let zeroed_bytes = raw_bytes(&zeroed); + assert_eq!(raw_bytes(&last), zeroed_bytes); + assert_ne!(raw_bytes(&second_last), zeroed_bytes); + } + #[cfg(not(Py_3_15))] + { + let zeroed = ffi::PyModuleDef_Slot::default(); + assert_eq!(last, zeroed); + assert_ne!(second_last, zeroed); } - let second_last_bytes = raw_bytes(&builder.values[builder.len - 1]); - let end_slot_bytes = raw_bytes(&builder.values[builder.len]); - let zeroed_bytes = raw_bytes(&zeroed); - assert_eq!(end_slot_bytes, zeroed_bytes); - assert_ne!(second_last_bytes, zeroed_bytes); assert!(builder.len == MAX_SLOTS); let result = std::panic::catch_unwind(|| builder.with_mod_exec(module_exec).build()); From f4e86f7a72e4aad569c2c40cc03e33f45cb6a9e5 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 5 May 2026 13:10:34 -0600 Subject: [PATCH 119/195] fix compiler error due to missing Debug impl --- src/impl_/pymodule.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 77c111745c7..6ec073136d1 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -617,8 +617,8 @@ mod tests { #[cfg(not(Py_3_15))] { let zeroed = ffi::PyModuleDef_Slot::default(); - assert_eq!(last, zeroed); - assert_ne!(second_last, zeroed); + assert!(last == zeroed); + assert!(second_last != zeroed); } assert!(builder.len == MAX_SLOTS); From ee68ff070280d0797951371e83d5d2998c4042fd Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 5 May 2026 13:35:12 -0600 Subject: [PATCH 120/195] Finish updating slots_generated and adjust types for slot IDs --- pyo3-ffi/examples/string-sum/src/lib.rs | 4 +- pyo3-ffi/src/lib.rs | 6 +- pyo3-ffi/src/slots.rs | 44 ++++-- pyo3-ffi/src/slots_generated.rs | 186 ++++++++++++++++-------- pyo3-ffi/src/typeslots.rs | 78 ---------- src/impl_/pymodule.rs | 8 +- 6 files changed, 160 insertions(+), 166 deletions(-) delete mode 100644 pyo3-ffi/src/typeslots.rs diff --git a/pyo3-ffi/examples/string-sum/src/lib.rs b/pyo3-ffi/examples/string-sum/src/lib.rs index 2b614cd563a..8818c9d0fb2 100644 --- a/pyo3-ffi/examples/string-sum/src/lib.rs +++ b/pyo3-ffi/examples/string-sum/src/lib.rs @@ -60,12 +60,12 @@ static mut SLOTS: [PySlot; SLOTS_LEN] = [ static mut SLOTS: [PyModuleDef_Slot; SLOTS_LEN] = [ #[cfg(Py_3_12)] PyModuleDef_Slot { - slot: Py_mod_multiple_interpreters as c_int, + slot: Py_mod_multiple_interpreters, value: Py_MOD_PER_INTERPRETER_GIL_SUPPORTED, }, #[cfg(Py_GIL_DISABLED)] PyModuleDef_Slot { - slot: Py_mod_gil as c_int, + slot: Py_mod_gil, value: Py_MOD_GIL_NOT_USED, }, PyModuleDef_Slot { diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index 0a80a60b7d0..1038b6f254b 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -167,12 +167,12 @@ //! #[cfg(not(Py_3_15))] //! static mut SLOTS: [PyModuleDef_Slot; SLOTS_LEN] = [ //! PyModuleDef_Slot { -//! slot: Py_mod_multiple_interpreters as c_int, +//! slot: Py_mod_multiple_interpreters, //! value: Py_MOD_PER_INTERPRETER_GIL_SUPPORTED, //! }, //! #[cfg(Py_GIL_DISABLED)] //! PyModuleDef_Slot { -//! slot: Py_mod_gil as c_int, +//! slot: Py_mod_gil, //! value: Py_MOD_GIL_NOT_USED, //! }, //! PyModuleDef_Slot { @@ -485,7 +485,6 @@ pub use self::structseq::*; pub use self::sysmodule::*; pub use self::traceback::*; pub use self::tupleobject::*; -pub use self::typeslots::*; pub use self::unicodeobject::*; pub use self::warnings::*; pub use self::weakrefobject::*; @@ -579,7 +578,6 @@ mod sysmodule; mod traceback; // skipped tracemalloc.h mod tupleobject; -mod typeslots; mod unicodeobject; mod warnings; mod weakrefobject; diff --git a/pyo3-ffi/src/slots.rs b/pyo3-ffi/src/slots.rs index 7770dfae3be..c3f7fbaade1 100644 --- a/pyo3-ffi/src/slots.rs +++ b/pyo3-ffi/src/slots.rs @@ -1,7 +1,7 @@ #[cfg(Py_3_15)] use crate::Py_ssize_t; #[cfg(Py_3_15)] -use std::ffi::c_void; +use std::ffi::{c_int, c_void}; #[cfg(Py_3_15)] pub type _Py_funcptr_t = unsafe extern "C" fn(); @@ -43,10 +43,18 @@ pub const PySlot_INTPTR: u16 = 0x04; #[cfg(Py_3_15)] pub const Py_slot_invalid: u16 = 0xffff; +const fn safe_cast_c_int_to_u16(val: i32) -> u16 { + if val >= 0 && val <= u16::MAX as c_int { + val as u16 + } else { + panic!("Slot ID out of range for u16!"); + } +} + #[cfg(Py_3_15)] -pub const fn PySlot_DATA(NAME: u16, VALUE: *mut c_void) -> PySlot { +pub const fn PySlot_DATA(NAME: c_int, VALUE: *mut c_void) -> PySlot { PySlot { - sl_id: NAME, + sl_id: safe_cast_c_int_to_u16(NAME), sl_flags: PySlot_INTPTR, anon1: _anon_union_32b { sl_reserved: 0 }, anon2: _anon_union_64b { sl_ptr: VALUE }, @@ -63,7 +71,11 @@ pub const fn PySlot_DATA(NAME: u16, VALUE: *mut c_void) -> PySlot { macro_rules! PySlot_FUNC { ($name:expr, $fn_ty:ty, $value:expr) => { $crate::PySlot { - sl_id: $name, + sl_id: if $name >= 0 && $name <= u16::MAX as c_int { + $name as u16 + } else { + panic!("Slot ID out of range for u16!"); + }, sl_flags: 0, anon1: $crate::_anon_union_32b { sl_reserved: 0 }, anon2: $crate::_anon_union_64b { @@ -74,9 +86,9 @@ macro_rules! PySlot_FUNC { } #[cfg(Py_3_15)] -pub const fn PySlot_SIZE(NAME: u16, VALUE: Py_ssize_t) -> PySlot { +pub const fn PySlot_SIZE(NAME: c_int, VALUE: Py_ssize_t) -> PySlot { PySlot { - sl_id: NAME, + sl_id: safe_cast_c_int_to_u16(NAME), sl_flags: 0, anon1: _anon_union_32b { sl_reserved: 0 }, anon2: _anon_union_64b { sl_size: VALUE }, @@ -84,9 +96,9 @@ pub const fn PySlot_SIZE(NAME: u16, VALUE: Py_ssize_t) -> PySlot { } #[cfg(Py_3_15)] -pub const fn PySlot_INT64(NAME: u16, VALUE: i64) -> PySlot { +pub const fn PySlot_INT64(NAME: c_int, VALUE: i64) -> PySlot { PySlot { - sl_id: NAME, + sl_id: safe_cast_c_int_to_u16(NAME), sl_flags: 0, anon1: _anon_union_32b { sl_reserved: 0 }, anon2: _anon_union_64b { sl_int64: VALUE }, @@ -94,9 +106,9 @@ pub const fn PySlot_INT64(NAME: u16, VALUE: i64) -> PySlot { } #[cfg(Py_3_15)] -pub const fn PySlot_UINT64(NAME: u16, VALUE: u64) -> PySlot { +pub const fn PySlot_UINT64(NAME: c_int, VALUE: u64) -> PySlot { PySlot { - sl_id: NAME, + sl_id: safe_cast_c_int_to_u16(NAME), sl_flags: 0, anon1: _anon_union_32b { sl_reserved: 0 }, anon2: _anon_union_64b { sl_uint64: VALUE }, @@ -104,9 +116,9 @@ pub const fn PySlot_UINT64(NAME: u16, VALUE: u64) -> PySlot { } #[cfg(Py_3_15)] -pub const fn PySlot_STATIC_DATA(NAME: u16, VALUE: *mut c_void) -> PySlot { +pub const fn PySlot_STATIC_DATA(NAME: c_int, VALUE: *mut c_void) -> PySlot { PySlot { - sl_id: NAME, + sl_id: safe_cast_c_int_to_u16(NAME), sl_flags: PySlot_STATIC, anon1: _anon_union_32b { sl_reserved: 0 }, anon2: _anon_union_64b { sl_ptr: VALUE }, @@ -114,9 +126,9 @@ pub const fn PySlot_STATIC_DATA(NAME: u16, VALUE: *mut c_void) -> PySlot { } #[cfg(Py_3_15)] -pub const fn PySlot_PTR(NAME: u16, VALUE: *mut c_void) -> PySlot { +pub const fn PySlot_PTR(NAME: c_int, VALUE: *mut c_void) -> PySlot { PySlot { - sl_id: NAME, + sl_id: safe_cast_c_int_to_u16(NAME), sl_flags: PySlot_INTPTR, anon1: _anon_union_32b { sl_reserved: 0 }, anon2: _anon_union_64b { sl_ptr: VALUE }, @@ -124,9 +136,9 @@ pub const fn PySlot_PTR(NAME: u16, VALUE: *mut c_void) -> PySlot { } #[cfg(Py_3_15)] -pub const fn PySlot_PTR_STATIC(NAME: u16, VALUE: *mut c_void) -> PySlot { +pub const fn PySlot_PTR_STATIC(NAME: c_int, VALUE: *mut c_void) -> PySlot { PySlot { - sl_id: NAME, + sl_id: safe_cast_c_int_to_u16(NAME), sl_flags: PySlot_INTPTR | PySlot_STATIC, anon1: _anon_union_32b { sl_reserved: 0 }, anon2: _anon_union_64b { sl_ptr: VALUE }, diff --git a/pyo3-ffi/src/slots_generated.rs b/pyo3-ffi/src/slots_generated.rs index cfe33009a06..935a6bb3b23 100644 --- a/pyo3-ffi/src/slots_generated.rs +++ b/pyo3-ffi/src/slots_generated.rs @@ -1,20 +1,7 @@ use std::ffi::c_int; #[allow(unused_variables)] -const fn _Py_SLOT_COMPAT_VALUE(OLD: u16, NEW: u16) -> u16 { - #[cfg(Py_3_15)] - { - NEW - } - - #[cfg(not(Py_3_15))] - { - OLD - } -} - -#[allow(unused_variables)] -const fn _Py_SLOT_COMPAT_VALUE_int(OLD: c_int, NEW: c_int) -> c_int { +const fn _Py_SLOT_COMPAT_VALUE(OLD: c_int, NEW: c_int) -> c_int { #[cfg(Py_3_15)] { NEW @@ -28,49 +15,128 @@ const fn _Py_SLOT_COMPAT_VALUE_int(OLD: c_int, NEW: c_int) -> c_int { #[cfg(Py_3_15)] pub const Py_slot_end: u16 = 0; -pub const Py_mod_create: u16 = _Py_SLOT_COMPAT_VALUE(1, 84); -pub const Py_mod_exec: u16 = _Py_SLOT_COMPAT_VALUE(2, 85); -pub const Py_mod_multiple_interpreters: u16 = _Py_SLOT_COMPAT_VALUE(3, 86); -pub const Py_mod_gil: u16 = _Py_SLOT_COMPAT_VALUE(4, 87); -pub const Py_bf_getbuffer: c_int = _Py_SLOT_COMPAT_VALUE_int(1, 88); -pub const Py_bf_releasebuffer: c_int = _Py_SLOT_COMPAT_VALUE_int(2, 89); -pub const Py_mp_ass_subscript: c_int = _Py_SLOT_COMPAT_VALUE_int(3, 90); -pub const Py_mp_length: c_int = _Py_SLOT_COMPAT_VALUE_int(4, 91); -#[cfg(Py_3_15)] -pub const Py_slot_subslots: u16 = 92; -#[cfg(Py_3_15)] -pub const Py_tp_slots: u16 = 93; -#[cfg(Py_3_15)] -pub const Py_mod_slots: u16 = 94; -#[cfg(Py_3_15)] -pub const Py_tp_name: u16 = 95; -#[cfg(Py_3_15)] -pub const Py_tp_basicsize: u16 = 96; -#[cfg(Py_3_15)] -pub const Py_tp_extra_basicsize: u16 = 97; -#[cfg(Py_3_15)] -pub const Py_tp_itemsize: u16 = 98; -#[cfg(Py_3_15)] -pub const Py_tp_flags: u16 = 99; -#[cfg(Py_3_15)] -pub const Py_mod_name: u16 = 100; -#[cfg(Py_3_15)] -pub const Py_mod_doc: u16 = 101; -#[cfg(Py_3_15)] -pub const Py_mod_state_size: u16 = 102; -#[cfg(Py_3_15)] -pub const Py_mod_methods: u16 = 103; -#[cfg(Py_3_15)] -pub const Py_mod_state_traverse: u16 = 104; -#[cfg(Py_3_15)] -pub const Py_mod_state_clear: u16 = 105; -#[cfg(Py_3_15)] -pub const Py_mod_state_free: u16 = 106; -#[cfg(Py_3_15)] -pub const Py_tp_metaclass: u16 = 107; -#[cfg(Py_3_15)] -pub const Py_tp_module: u16 = 108; -#[cfg(Py_3_15)] -pub const Py_mod_abi: u16 = 109; -#[cfg(Py_3_15)] -pub const Py_mod_token: u16 = 110; +pub const Py_mp_subscript: c_int = 5; +pub const Py_nb_absolute: c_int = 6; +pub const Py_nb_add: c_int = 7; +pub const Py_nb_and: c_int = 8; +pub const Py_nb_bool: c_int = 9; +pub const Py_nb_divmod: c_int = 10; +pub const Py_nb_float: c_int = 11; +pub const Py_nb_floor_divide: c_int = 12; +pub const Py_nb_index: c_int = 13; +pub const Py_nb_inplace_add: c_int = 14; +pub const Py_nb_inplace_and: c_int = 15; +pub const Py_nb_inplace_floor_divide: c_int = 16; +pub const Py_nb_inplace_lshift: c_int = 17; +pub const Py_nb_inplace_multiply: c_int = 18; +pub const Py_nb_inplace_or: c_int = 19; +pub const Py_nb_inplace_power: c_int = 20; +pub const Py_nb_inplace_remainder: c_int = 21; +pub const Py_nb_inplace_rshift: c_int = 22; +pub const Py_nb_inplace_subtract: c_int = 23; +pub const Py_nb_inplace_true_divide: c_int = 24; +pub const Py_nb_inplace_xor: c_int = 25; +pub const Py_nb_int: c_int = 26; +pub const Py_nb_invert: c_int = 27; +pub const Py_nb_lshift: c_int = 28; +pub const Py_nb_multiply: c_int = 29; +pub const Py_nb_negative: c_int = 30; +pub const Py_nb_or: c_int = 31; +pub const Py_nb_positive: c_int = 32; +pub const Py_nb_power: c_int = 33; +pub const Py_nb_remainder: c_int = 34; +pub const Py_nb_rshift: c_int = 35; +pub const Py_nb_subtract: c_int = 36; +pub const Py_nb_true_divide: c_int = 37; +pub const Py_nb_xor: c_int = 38; +pub const Py_sq_ass_item: c_int = 39; +pub const Py_sq_concat: c_int = 40; +pub const Py_sq_contains: c_int = 41; +pub const Py_sq_inplace_concat: c_int = 42; +pub const Py_sq_inplace_repeat: c_int = 43; +pub const Py_sq_item: c_int = 44; +pub const Py_sq_length: c_int = 45; +pub const Py_sq_repeat: c_int = 46; +pub const Py_tp_alloc: c_int = 47; +pub const Py_tp_base: c_int = 48; +pub const Py_tp_bases: c_int = 49; +pub const Py_tp_call: c_int = 50; +pub const Py_tp_clear: c_int = 51; +pub const Py_tp_dealloc: c_int = 52; +pub const Py_tp_del: c_int = 53; +pub const Py_tp_descr_get: c_int = 54; +pub const Py_tp_descr_set: c_int = 55; +pub const Py_tp_doc: c_int = 56; +pub const Py_tp_getattr: c_int = 57; +pub const Py_tp_getattro: c_int = 58; +pub const Py_tp_hash: c_int = 59; +pub const Py_tp_init: c_int = 60; +pub const Py_tp_is_gc: c_int = 61; +pub const Py_tp_iter: c_int = 62; +pub const Py_tp_iternext: c_int = 63; +pub const Py_tp_methods: c_int = 64; +pub const Py_tp_new: c_int = 65; +pub const Py_tp_repr: c_int = 66; +pub const Py_tp_richcompare: c_int = 67; +pub const Py_tp_setattr: c_int = 68; +pub const Py_tp_setattro: c_int = 69; +pub const Py_tp_str: c_int = 70; +pub const Py_tp_traverse: c_int = 71; +pub const Py_tp_members: c_int = 72; +pub const Py_tp_getset: c_int = 73; +pub const Py_tp_free: c_int = 74; +pub const Py_nb_matrix_multiply: c_int = 75; +pub const Py_nb_inplace_matrix_multiply: c_int = 76; +pub const Py_am_await: c_int = 77; +pub const Py_am_aiter: c_int = 78; +pub const Py_am_anext: c_int = 79; +pub const Py_tp_finalize: c_int = 80; +pub const Py_am_send: c_int = 81; +pub const Py_tp_vectorcall: c_int = 82; +pub const Py_tp_token: c_int = 83; +pub const Py_mod_create: c_int = _Py_SLOT_COMPAT_VALUE(1, 84); +pub const Py_mod_exec: c_int = _Py_SLOT_COMPAT_VALUE(2, 85); +pub const Py_mod_multiple_interpreters: c_int = _Py_SLOT_COMPAT_VALUE(3, 86); +pub const Py_mod_gil: c_int = _Py_SLOT_COMPAT_VALUE(4, 87); +pub const Py_bf_getbuffer: c_int = _Py_SLOT_COMPAT_VALUE(1, 88); +pub const Py_bf_releasebuffer: c_int = _Py_SLOT_COMPAT_VALUE(2, 89); +pub const Py_mp_ass_subscript: c_int = _Py_SLOT_COMPAT_VALUE(3, 90); +pub const Py_mp_length: c_int = _Py_SLOT_COMPAT_VALUE(4, 91); +#[cfg(Py_3_15)] +pub const Py_slot_subslots: c_int = 92; +#[cfg(Py_3_15)] +pub const Py_tp_slots: c_int = 93; +#[cfg(Py_3_15)] +pub const Py_mod_slots: c_int = 94; +#[cfg(Py_3_15)] +pub const Py_tp_name: c_int = 95; +#[cfg(Py_3_15)] +pub const Py_tp_basicsize: c_int = 96; +#[cfg(Py_3_15)] +pub const Py_tp_extra_basicsize: c_int = 97; +#[cfg(Py_3_15)] +pub const Py_tp_itemsize: c_int = 98; +#[cfg(Py_3_15)] +pub const Py_tp_flags: c_int = 99; +#[cfg(Py_3_15)] +pub const Py_mod_name: c_int = 100; +#[cfg(Py_3_15)] +pub const Py_mod_doc: c_int = 101; +#[cfg(Py_3_15)] +pub const Py_mod_state_size: c_int = 102; +#[cfg(Py_3_15)] +pub const Py_mod_methods: c_int = 103; +#[cfg(Py_3_15)] +pub const Py_mod_state_traverse: c_int = 104; +#[cfg(Py_3_15)] +pub const Py_mod_state_clear: c_int = 105; +#[cfg(Py_3_15)] +pub const Py_mod_state_free: c_int = 106; +#[cfg(Py_3_15)] +pub const Py_tp_metaclass: c_int = 107; +#[cfg(Py_3_15)] +pub const Py_tp_module: c_int = 108; +#[cfg(Py_3_15)] +pub const Py_mod_abi: c_int = 109; +#[cfg(Py_3_15)] +pub const Py_mod_token: c_int = 110; diff --git a/pyo3-ffi/src/typeslots.rs b/pyo3-ffi/src/typeslots.rs deleted file mode 100644 index 6d9bad0a83e..00000000000 --- a/pyo3-ffi/src/typeslots.rs +++ /dev/null @@ -1,78 +0,0 @@ -use std::ffi::c_int; - -pub const Py_mp_subscript: c_int = 5; -pub const Py_nb_absolute: c_int = 6; -pub const Py_nb_add: c_int = 7; -pub const Py_nb_and: c_int = 8; -pub const Py_nb_bool: c_int = 9; -pub const Py_nb_divmod: c_int = 10; -pub const Py_nb_float: c_int = 11; -pub const Py_nb_floor_divide: c_int = 12; -pub const Py_nb_index: c_int = 13; -pub const Py_nb_inplace_add: c_int = 14; -pub const Py_nb_inplace_and: c_int = 15; -pub const Py_nb_inplace_floor_divide: c_int = 16; -pub const Py_nb_inplace_lshift: c_int = 17; -pub const Py_nb_inplace_multiply: c_int = 18; -pub const Py_nb_inplace_or: c_int = 19; -pub const Py_nb_inplace_power: c_int = 20; -pub const Py_nb_inplace_remainder: c_int = 21; -pub const Py_nb_inplace_rshift: c_int = 22; -pub const Py_nb_inplace_subtract: c_int = 23; -pub const Py_nb_inplace_true_divide: c_int = 24; -pub const Py_nb_inplace_xor: c_int = 25; -pub const Py_nb_int: c_int = 26; -pub const Py_nb_invert: c_int = 27; -pub const Py_nb_lshift: c_int = 28; -pub const Py_nb_multiply: c_int = 29; -pub const Py_nb_negative: c_int = 30; -pub const Py_nb_or: c_int = 31; -pub const Py_nb_positive: c_int = 32; -pub const Py_nb_power: c_int = 33; -pub const Py_nb_remainder: c_int = 34; -pub const Py_nb_rshift: c_int = 35; -pub const Py_nb_subtract: c_int = 36; -pub const Py_nb_true_divide: c_int = 37; -pub const Py_nb_xor: c_int = 38; -pub const Py_sq_ass_item: c_int = 39; -pub const Py_sq_concat: c_int = 40; -pub const Py_sq_contains: c_int = 41; -pub const Py_sq_inplace_concat: c_int = 42; -pub const Py_sq_inplace_repeat: c_int = 43; -pub const Py_sq_item: c_int = 44; -pub const Py_sq_length: c_int = 45; -pub const Py_sq_repeat: c_int = 46; -pub const Py_tp_alloc: c_int = 47; -pub const Py_tp_base: c_int = 48; -pub const Py_tp_bases: c_int = 49; -pub const Py_tp_call: c_int = 50; -pub const Py_tp_clear: c_int = 51; -pub const Py_tp_dealloc: c_int = 52; -pub const Py_tp_del: c_int = 53; -pub const Py_tp_descr_get: c_int = 54; -pub const Py_tp_descr_set: c_int = 55; -pub const Py_tp_doc: c_int = 56; -pub const Py_tp_getattr: c_int = 57; -pub const Py_tp_getattro: c_int = 58; -pub const Py_tp_hash: c_int = 59; -pub const Py_tp_init: c_int = 60; -pub const Py_tp_is_gc: c_int = 61; -pub const Py_tp_iter: c_int = 62; -pub const Py_tp_iternext: c_int = 63; -pub const Py_tp_methods: c_int = 64; -pub const Py_tp_new: c_int = 65; -pub const Py_tp_repr: c_int = 66; -pub const Py_tp_richcompare: c_int = 67; -pub const Py_tp_setattr: c_int = 68; -pub const Py_tp_setattro: c_int = 69; -pub const Py_tp_str: c_int = 70; -pub const Py_tp_traverse: c_int = 71; -pub const Py_tp_members: c_int = 72; -pub const Py_tp_getset: c_int = 73; -pub const Py_tp_free: c_int = 74; -pub const Py_nb_matrix_multiply: c_int = 75; -pub const Py_nb_inplace_matrix_multiply: c_int = 76; -pub const Py_am_await: c_int = 77; -pub const Py_am_aiter: c_int = 78; -pub const Py_am_anext: c_int = 79; -pub const Py_tp_finalize: c_int = 80; diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 6ec073136d1..65ddcd049ed 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -1,5 +1,4 @@ //! Implementation details of `#[pymodule]` which need to be accessible from proc-macro generated code. - use std::{ cell::UnsafeCell, ffi::CStr, @@ -399,17 +398,14 @@ impl PyModuleSlotsBuilder { } #[cfg(not(Py_3_15))] - const fn push(mut self, slot: u16, value: *mut c_void) -> Self { + const fn push(mut self, slot: c_int, value: *mut c_void) -> Self { // Required to guarantee there's still a zeroed element // at the end assert!( self.len < MAX_SLOTS, "Cannot add more than MAX_SLOTS slots to a PyModuleSlots", ); - self.values[self.len] = ffi::PyModuleDef_Slot { - slot: slot as c_int, - value, - }; + self.values[self.len] = ffi::PyModuleDef_Slot { slot, value }; self.len += 1; self } From 816fd560e073e3e790e4d93242d7470fc6df6be8 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 5 May 2026 13:38:20 -0600 Subject: [PATCH 121/195] add missing cfg gate --- pyo3-ffi/src/slots.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/pyo3-ffi/src/slots.rs b/pyo3-ffi/src/slots.rs index c3f7fbaade1..469f1f99e56 100644 --- a/pyo3-ffi/src/slots.rs +++ b/pyo3-ffi/src/slots.rs @@ -43,6 +43,7 @@ pub const PySlot_INTPTR: u16 = 0x04; #[cfg(Py_3_15)] pub const Py_slot_invalid: u16 = 0xffff; +#[cfg(Py_3_15)] const fn safe_cast_c_int_to_u16(val: i32) -> u16 { if val >= 0 && val <= u16::MAX as c_int { val as u16 From 8e045841f8a2f99277c4c5405437245016cf46ba Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 5 May 2026 13:46:56 -0600 Subject: [PATCH 122/195] remove unused import --- pyo3-ffi/examples/string-sum/src/lib.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyo3-ffi/examples/string-sum/src/lib.rs b/pyo3-ffi/examples/string-sum/src/lib.rs index 8818c9d0fb2..be50a9f06b8 100644 --- a/pyo3-ffi/examples/string-sum/src/lib.rs +++ b/pyo3-ffi/examples/string-sum/src/lib.rs @@ -1,5 +1,3 @@ -#[cfg(not(Py_3_15))] -use std::ffi::c_int; #[cfg(Py_3_15)] use std::ffi::c_void; use std::ffi::{c_char, c_long}; From 8fd0acb89ef271bcf4bbe9ee17901d2098eac6b2 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 6 May 2026 09:26:02 -0600 Subject: [PATCH 123/195] make InterpreterConfigBuilder.extra_build_script_lines a Vec --- pyo3-build-config/src/impl_.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 287cdd00b89..4a7a1f220a9 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -783,7 +783,7 @@ pub struct InterpreterConfigBuilder { pointer_width: Option, build_flags: Option, suppress_build_script_link_lines: Option, - extra_build_script_lines: Option>, + extra_build_script_lines: Vec, python_framework_prefix: Option, } @@ -803,7 +803,7 @@ impl InterpreterConfigBuilder { pointer_width: None, build_flags: None, suppress_build_script_link_lines: None, - extra_build_script_lines: None, + extra_build_script_lines: vec![], python_framework_prefix: None, } } @@ -877,7 +877,7 @@ impl InterpreterConfigBuilder { extra_build_script_lines: Vec, ) -> InterpreterConfigBuilder { InterpreterConfigBuilder { - extra_build_script_lines: Some(extra_build_script_lines), + extra_build_script_lines: extra_build_script_lines, ..self } } @@ -947,7 +947,7 @@ impl InterpreterConfigBuilder { suppress_build_script_link_lines: self .suppress_build_script_link_lines .unwrap_or(false), - extra_build_script_lines: self.extra_build_script_lines.unwrap_or(vec![]), + extra_build_script_lines: self.extra_build_script_lines, python_framework_prefix: self.python_framework_prefix, } } From 7d5881c7a15704565c4cc0bca02ff6825b814e7a Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 6 May 2026 10:32:36 -0600 Subject: [PATCH 124/195] Add an abi3 version sanitization function, adjust error handling --- pyo3-build-config/src/impl_.rs | 51 +++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 4a7a1f220a9..3af071d7976 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -83,6 +83,25 @@ pub fn target_triple_from_env() -> Triple { .expect("Unrecognized TARGET environment variable value") } +fn sanitize_abi3_version( + abi3_version: Option, + version: PythonVersion, +) -> Result { + if let Some(min_version) = abi3_version { + ensure!( + min_version <= version, + "cannot set a minimum Python version {} higher than the interpreter version {} \ + (the minimum Python version is implied by the abi3-py3{} feature)", + min_version, + version, + min_version.minor + ); + Ok(min_version) + } else { + Ok(version) + } +} + /// Configuration needed by PyO3 to build for the correct Python implementation. /// /// Usually this is queried directly from the Python interpreter, or overridden using the @@ -343,25 +362,13 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) _ => panic!("Unknown Py_GIL_DISABLED value"), }; - let target_version = if let Some(min_version) = abi3_version { - ensure!( - min_version <= version, - "cannot set a minimum Python version {} higher than the interpreter version {} \ - (the minimum Python version is implied by the abi3-py3{} feature)", - min_version, - version, - min_version.minor - ); - min_version - } else { - version - }; + let target_version = sanitize_abi3_version(abi3_version, version)?; let mut abi_builder = if gil_disabled { PythonAbiBuilder::new(implementation, target_version) } else { // we already - PythonAbiBuilder::from_build_env(implementation, target_version, abi3_version) + PythonAbiBuilder::from_build_env(implementation, target_version, abi3_version)? }; if gil_disabled { @@ -466,7 +473,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .free_threaded() .unwrap() } else { - PythonAbiBuilder::from_build_env(implementation, version, Some(version)) + PythonAbiBuilder::from_build_env(implementation, version, Some(version))? } .finalize(); let lib_name = Some(default_lib_name_unix( @@ -513,7 +520,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) config.implementation, config.version, abi3_version, - ); + )?; // only allow free-threaded builds if the build environment didn't force an abi3 build if config.target_abi.kind.is_free_threaded() && abi_builder.kind.is_none() { abi_builder = abi_builder.free_threaded()?; @@ -973,17 +980,17 @@ impl PythonAbiBuilder { implementation: PythonImplementation, version: PythonVersion, abi3_version: Option, - ) -> PythonAbiBuilder { + ) -> Result { let builder = PythonAbiBuilder { implementation, - version: abi3_version.unwrap_or(version), + version: sanitize_abi3_version(abi3_version, version)?, kind: None, }; if is_abi3() { // freshly created builder without an ABI kind so this can't fail - builder.stable_abi(StableAbi::Abi3).unwrap() + builder.stable_abi(StableAbi::Abi3) } else { - builder + Ok(builder) } } @@ -2040,7 +2047,7 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result> { config.implementation, config.version, get_abi3_version(), - ); + )?; if config.target_abi.kind.is_free_threaded() && abi_builder.kind.is_none() { abi_builder = abi_builder.free_threaded()?; } From 5c735496e6b27aa994bd01a5920b52e7d83828ec Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 6 May 2026 10:40:21 -0600 Subject: [PATCH 125/195] Adjust comment fragment, add TODO --- 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 3af071d7976..f01e91f6561 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -364,10 +364,10 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let target_version = sanitize_abi3_version(abi3_version, version)?; + // TODO: fall back to abi3t builds on 3.15t and newer let mut abi_builder = if gil_disabled { PythonAbiBuilder::new(implementation, target_version) } else { - // we already PythonAbiBuilder::from_build_env(implementation, target_version, abi3_version)? }; From 1e71ed4d53f1ae03aa9936a4a735e182ba9d0104 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 6 May 2026 11:38:39 -0600 Subject: [PATCH 126/195] apply focused comments from self-review --- pyo3-build-config/src/impl_.rs | 222 +++++++++++++++++++++------------ 1 file changed, 141 insertions(+), 81 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index f01e91f6561..6e4e9dce85d 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -414,13 +414,13 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) Ok(InterpreterConfigBuilder::new(implementation, version) .target_abi(target_abi)? - .shared(shared) - .lib_name(Some(lib_name)) - .lib_dir(lib_dir) - .executable(map.get("executable").cloned()) - .pointer_width(calcsize_pointer * 8) + .shared(shared)? + .lib_name(Some(lib_name))? + .lib_dir(lib_dir)? + .executable(map.get("executable").cloned())? + .pointer_width(calcsize_pointer * 8)? .build_flags(BuildFlags::from_interpreter(interpreter)?)? - .python_framework_prefix(python_framework_prefix) + .python_framework_prefix(python_framework_prefix)? .finalize()) } @@ -487,12 +487,12 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) Ok(InterpreterConfigBuilder::new(implementation, version) .target_abi(target_abi)? - .shared(shared || framework) - .lib_dir(lib_dir) - .lib_name(lib_name) - .pointer_width(pointer_width) + .shared(shared || framework)? + .lib_dir(lib_dir)? + .lib_name(lib_name)? + .pointer_width(pointer_width)? .build_flags(build_flags)? - .python_framework_prefix(python_framework_prefix) + .python_framework_prefix(python_framework_prefix)? .finalize()) } @@ -626,17 +626,22 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) ); target_abi } else if is_abi3t() { + ensure!( + !is_abi3(), + "Cannot simultaneously enable features that enable abi3 and abi3t builds" + ); PythonAbiBuilder::new(implementation, version) .stable_abi(StableAbi::Abi3t) .unwrap() .finalize() } else if flags_contains_free_threaded { + // This fires even if is_abi3() is True for backward compatibility reasons PythonAbiBuilder::new(implementation, version) .free_threaded() .unwrap() .finalize() - } else if (abi3.is_some() && abi3.unwrap()) || is_abi3() { - if abi3.is_some() && abi3.unwrap() { + } else if (abi3 == Some(true)) || is_abi3() { + if abi3 == Some(true) { warn!("abi3 configuration file option is deprecated, set target_abi instead"); } PythonAbiBuilder::new(implementation, version) @@ -652,17 +657,17 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .target_abi(target_abi) // cannot fail because this is a newly-created InterpreterConfigBuilder with no target ABI set .unwrap() - .shared(shared.unwrap_or(true)) - .lib_name(lib_name) - .lib_dir(lib_dir) - .executable(executable) + .shared(shared.unwrap_or(true))? + .lib_name(lib_name)? + .lib_dir(lib_dir)? + .executable(executable)? .build_flags(build_flags)? - .suppress_build_script_link_lines(suppress_build_script_link_lines) - .extra_build_script_lines(extra_build_script_lines) - .python_framework_prefix(python_framework_prefix); + .suppress_build_script_link_lines(suppress_build_script_link_lines)? + .extra_build_script_lines(extra_build_script_lines)? + .python_framework_prefix(python_framework_prefix)?; let builder = if let Some(pointer_width) = pointer_width { - builder.pointer_width(pointer_width) + builder.pointer_width(pointer_width)? } else { builder }; @@ -849,77 +854,69 @@ impl InterpreterConfigBuilder { .unwrap() .finalize(), ) - // this can't panic because abi3() is called on a builder with no chosen ABI - .unwrap() - .build_flags(BuildFlags::default()) } - pub fn lib_name(self, lib_name: Option) -> InterpreterConfigBuilder { - InterpreterConfigBuilder { lib_name, ..self } + pub fn lib_name(self, lib_name: Option) -> Result { + ensure!(self.lib_name.is_none(), "lib_name already set!"); + Ok(InterpreterConfigBuilder { lib_name, ..self }) } - pub fn pointer_width(self, pointer_width: u32) -> InterpreterConfigBuilder { - InterpreterConfigBuilder { + pub fn pointer_width(self, pointer_width: u32) -> Result { + ensure!(self.pointer_width.is_none(), "pointer_width already set!"); + Ok(InterpreterConfigBuilder { pointer_width: Some(pointer_width), ..self - } + }) } - pub fn executable(self, executable: Option) -> InterpreterConfigBuilder { - InterpreterConfigBuilder { executable, ..self } + pub fn executable(self, executable: Option) -> Result { + ensure!(self.executable.is_none(), "executable already set!"); + Ok(InterpreterConfigBuilder { executable, ..self }) } pub fn suppress_build_script_link_lines( self, suppress_build_script_link_lines: Option, - ) -> InterpreterConfigBuilder { - InterpreterConfigBuilder { + ) -> Result { + ensure!( + self.suppress_build_script_link_lines.is_none(), + "suppress_build_script_link_lines already set!" + ); + Ok(InterpreterConfigBuilder { suppress_build_script_link_lines, ..self - } + }) } pub fn extra_build_script_lines( self, extra_build_script_lines: Vec, - ) -> InterpreterConfigBuilder { - InterpreterConfigBuilder { + ) -> Result { + ensure!( + self.extra_build_script_lines.len() == 0, + "extra_build_script_lines already set!" + ); + Ok(InterpreterConfigBuilder { extra_build_script_lines: extra_build_script_lines, ..self - } + }) } - pub fn lib_dir(self, lib_dir: Option) -> InterpreterConfigBuilder { - InterpreterConfigBuilder { lib_dir, ..self } + pub fn lib_dir(self, lib_dir: Option) -> Result { + ensure!(self.lib_dir.is_none(), "lib_dir already set!"); + Ok(InterpreterConfigBuilder { lib_dir, ..self }) } - pub fn shared(self, shared: bool) -> InterpreterConfigBuilder { - InterpreterConfigBuilder { + pub fn shared(self, shared: bool) -> Result { + ensure!(self.shared.is_none(), "shared already set!"); + Ok(InterpreterConfigBuilder { shared: Some(shared), ..self - } + }) } pub fn build_flags(self, build_flags: BuildFlags) -> Result { ensure!(self.build_flags.is_none(), "Build flags already set!"); - let build_flags = if build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { - if let Some(target_abi) = self.target_abi { - if !target_abi.kind.is_free_threaded() { - warn!( - "build_flags contains Py_GIL_DISABLED but target ABI '{target_abi}' is not free-threaded" - ); - } - } - build_flags - } else if let Some(target_abi) = self.target_abi { - let mut flags = build_flags.clone(); - if target_abi.kind.is_free_threaded() { - flags.0.insert(BuildFlag::Py_GIL_DISABLED); - } - flags - } else { - build_flags - }; Ok(InterpreterConfigBuilder { build_flags: Some(build_flags), ..self @@ -929,14 +926,47 @@ impl InterpreterConfigBuilder { pub fn python_framework_prefix( self, python_framework_prefix: Option, - ) -> InterpreterConfigBuilder { - InterpreterConfigBuilder { + ) -> Result { + ensure!( + self.python_framework_prefix.is_none(), + "Python_framework_prefix already set!" + ); + Ok(InterpreterConfigBuilder { python_framework_prefix, ..self - } + }) } pub fn finalize(self) -> InterpreterConfig { + let build_flags = if let Some(mut build_flags) = self.build_flags { + if build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { + if let Some(target_abi) = self.target_abi { + if !target_abi.kind.is_free_threaded() { + warn!( + "build_flags contains Py_GIL_DISABLED but target ABI '{target_abi}' is not free-threaded" + ); + } + } + } else if let Some(target_abi) = self.target_abi { + if target_abi.kind.is_free_threaded() { + build_flags.0.insert(BuildFlag::Py_GIL_DISABLED); + } + } + build_flags + } else if let Some(target_abi) = self.target_abi { + let mut flags = if let Some(ref build_flags) = self.build_flags { + build_flags.clone() + } else { + BuildFlags::default() + }; + if target_abi.kind.is_free_threaded() { + flags.0.insert(BuildFlag::Py_GIL_DISABLED); + } + flags + } else { + BuildFlags::default() + }; + dbg!(&build_flags); #[allow(deprecated)] InterpreterConfig { implementation: self.implementation, @@ -945,12 +975,16 @@ impl InterpreterConfigBuilder { target_abi: self .target_abi .unwrap_or(PythonAbiBuilder::new(self.implementation, self.version).finalize()), - abi3: false, + abi3: if let Some(target_abi) = self.target_abi { + matches!(target_abi.kind, PythonAbiKind::Stable(StableAbi::Abi3)) + } else { + false + }, lib_name: self.lib_name, lib_dir: self.lib_dir, executable: self.executable, pointer_width: self.pointer_width, - build_flags: self.build_flags.unwrap_or_default(), + build_flags: build_flags, suppress_build_script_link_lines: self .suppress_build_script_link_lines .unwrap_or(false), @@ -1080,14 +1114,12 @@ impl FromStr for PythonAbi { type Err = crate::errors::Error; fn from_str(value: &str) -> Result { - let parts: Vec<&str> = value.split("-").collect(); - let implementation = parts[0].parse()?; - let kind = parts[1].parse()?; - let version: PythonVersion = parts[2].parse()?; + let mut parts = value.splitn(3, "-"); + let msg = "Invalid ABI string representation: {value}"; Ok(PythonAbi { - implementation, - kind, - version, + implementation: parts.next().ok_or(msg)?.parse()?, + kind: parts.next().ok_or(msg)?.parse()?, + version: parts.next().ok_or(msg)?.parse()?, }) } } @@ -2060,8 +2092,8 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result = Vec::new(); config.to_writer(&mut buf).unwrap(); @@ -2501,10 +2538,15 @@ mod tests { .stable_abi(StableAbi::Abi3) .unwrap() .pointer_width(32) + .unwrap() .executable(Some("executable".into())) + .unwrap() .lib_name(Some("lib_name".into())) + .unwrap() .lib_dir(Some("lib_dir\\n".into())) + .unwrap() .extra_build_script_lines(vec!["cargo:test1".to_string(), "cargo:test2".to_string()]) + .unwrap() .finalize(); let mut buf: Vec = Vec::new(); config.to_writer(&mut buf).unwrap(); @@ -2695,8 +2737,11 @@ mod tests { .build_flags(BuildFlags::from_sysconfigdata(&sysconfigdata)) .unwrap() .lib_dir(Some("/usr/lib".into())) + .unwrap() .lib_name(Some("python3.8".into())) + .unwrap() .pointer_width(64) + .unwrap() .finalize() ); } @@ -2720,8 +2765,11 @@ mod tests { .build_flags(BuildFlags::from_sysconfigdata(&sysconfigdata)) .unwrap() .lib_dir(Some("/usr/lib".into())) + .unwrap() .lib_name(Some("python3.8".into())) + .unwrap() .pointer_width(64) + .unwrap() .finalize() ); @@ -2742,9 +2790,13 @@ mod tests { .build_flags(BuildFlags::from_sysconfigdata(&sysconfigdata)) .unwrap() .lib_dir(Some("/usr/lib".into())) + .unwrap() .lib_name(Some("python3.8".into())) + .unwrap() .pointer_width(64) + .unwrap() .shared(false) + .unwrap() .finalize() ); } @@ -2760,6 +2812,7 @@ mod tests { .stable_abi(StableAbi::Abi3) .unwrap() .lib_name(Some("python3".into())) + .unwrap() .finalize(); assert_eq!(default_abi3_config(&host, min_version).unwrap(), config); } @@ -2797,7 +2850,9 @@ mod tests { let version = PythonVersion::PY38; let config = InterpreterConfigBuilder::new(implementation, version) .lib_name(Some("python38".into())) + .unwrap() .lib_dir(Some("C:\\some\\path".into())) + .unwrap() .finalize(); assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } @@ -2822,7 +2877,9 @@ mod tests { let version = PythonVersion::PY38; let config = InterpreterConfigBuilder::new(implementation, version) .lib_name(Some("python38".into())) + .unwrap() .lib_dir(Some("/usr/lib/mingw".into())) + .unwrap() .finalize(); assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } @@ -2847,7 +2904,9 @@ mod tests { let version = PythonVersion::PY39; let config = InterpreterConfigBuilder::new(implementation, version) .lib_name(Some("python3.9".into())) + .unwrap() .lib_dir(Some("/usr/arm64/lib".into())) + .unwrap() .finalize(); assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } @@ -2871,6 +2930,7 @@ mod tests { let version = PythonVersion::PY311; let config = InterpreterConfigBuilder::new(implementation, version) .lib_name(Some("pypy3.11-c".into())) + .unwrap() .finalize(); assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } @@ -3409,6 +3469,7 @@ mod tests { let version = PythonVersion::PY311; let interpreter_config = InterpreterConfigBuilder::new(implementation, version) .lib_name(Some("python3".into())) + .unwrap() .finalize(); assert_eq!( interpreter_config.build_script_outputs(), @@ -3447,6 +3508,7 @@ mod tests { .stable_abi(StableAbi::Abi3) .unwrap() .lib_name(Some("python3".into())) + .unwrap() .finalize(); assert_eq!( @@ -3507,6 +3569,7 @@ mod tests { .free_threaded() .unwrap() .lib_name(Some("python3".into())) + .unwrap() .finalize(); assert_eq!( interpreter_config.build_script_outputs(), @@ -3548,12 +3611,8 @@ mod tests { let builder = InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); - let config = builder.free_threaded().unwrap(); - assert!(config - .build_flags - .unwrap() - .0 - .contains(&BuildFlag::Py_GIL_DISABLED)) + let config = builder.free_threaded().unwrap().finalize(); + assert!(config.build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED)) } #[test] @@ -3564,6 +3623,7 @@ mod tests { let version = PythonVersion::PY38; let interpreter_config = InterpreterConfigBuilder::new(implementation, version) .lib_name(Some("python3".into())) + .unwrap() .build_flags(build_flags) .unwrap() .finalize(); From b3ef667dec37dd422b74628e9997cc52f3e80a73 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 6 May 2026 14:41:15 -0600 Subject: [PATCH 127/195] simplify modeling of free-threaded builds and Py_GIL_DISABLED build flag --- pyo3-build-config/src/impl_.rs | 194 ++++++++++++++++----------------- pyo3-ffi/build.rs | 6 +- 2 files changed, 96 insertions(+), 104 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 6e4e9dce85d..a4fae544e20 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -83,6 +83,16 @@ pub fn target_triple_from_env() -> Triple { .expect("Unrecognized TARGET environment variable value") } +fn check_gil_disabled_consistency(target_abi: PythonAbi, build_flags: &BuildFlags) -> Result<()> { + let flag_set = build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED); + if flag_set && !target_abi.kind.free_threaded() { + bail!( + "build_flags contains Py_GIL_DISABLED but target ABI '{target_abi}' is not free-threaded" + ); + } + Ok(()) +} + fn sanitize_abi3_version( abi3_version: Option, version: PythonVersion, @@ -362,20 +372,9 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) _ => panic!("Unknown Py_GIL_DISABLED value"), }; - let target_version = sanitize_abi3_version(abi3_version, version)?; - - // TODO: fall back to abi3t builds on 3.15t and newer - let mut abi_builder = if gil_disabled { - PythonAbiBuilder::new(implementation, target_version) - } else { - PythonAbiBuilder::from_build_env(implementation, target_version, abi3_version)? - }; - - if gil_disabled { - abi_builder = abi_builder.free_threaded()?; - } - - let target_abi = abi_builder.finalize(); + let target_abi = + PythonAbiBuilder::from_sysconfig(implementation, version, abi3_version, gil_disabled)? + .finalize(); let cygwin = map["cygwin"].as_str() == "True"; @@ -467,15 +466,9 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) None => false, }; let cygwin = soabi.ends_with("cygwin"); - let target_abi = if gil_disabled { - // unwrap is safe because this is a freshly created PythonAbiBuilder with no ABI kind set - PythonAbiBuilder::new(implementation, version) - .free_threaded() - .unwrap() - } else { - PythonAbiBuilder::from_build_env(implementation, version, Some(version))? - } - .finalize(); + let target_abi = + PythonAbiBuilder::from_sysconfig(implementation, version, None, gil_disabled)? + .finalize(); let lib_name = Some(default_lib_name_unix( target_abi, cygwin, @@ -522,7 +515,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) abi3_version, )?; // only allow free-threaded builds if the build environment didn't force an abi3 build - if config.target_abi.kind.is_free_threaded() && abi_builder.kind.is_none() { + if config.target_abi.kind.free_threaded() && abi_builder.kind.is_none() { abi_builder = abi_builder.free_threaded()?; } config.target_abi = abi_builder.finalize(); @@ -826,6 +819,9 @@ impl InterpreterConfigBuilder { "Target ABI already set to {}", target_abi ); + if let Some(ref build_flags) = self.build_flags { + check_gil_disabled_consistency(target_abi, build_flags)?; + } Ok(InterpreterConfigBuilder { target_abi: Some(target_abi), ..self @@ -917,6 +913,15 @@ impl InterpreterConfigBuilder { pub fn build_flags(self, build_flags: BuildFlags) -> Result { ensure!(self.build_flags.is_none(), "Build flags already set!"); + if build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { + match self.target_abi { + Some(target_abi) => check_gil_disabled_consistency(target_abi, &build_flags)?, + None => bail!( + "build_flags contains Py_GIL_DISABLED but target ABI is not set; \ + call .free_threaded() (or set target_abi) before .build_flags()" + ), + } + } Ok(InterpreterConfigBuilder { build_flags: Some(build_flags), ..self @@ -938,53 +943,25 @@ impl InterpreterConfigBuilder { } pub fn finalize(self) -> InterpreterConfig { - let build_flags = if let Some(mut build_flags) = self.build_flags { - if build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { - if let Some(target_abi) = self.target_abi { - if !target_abi.kind.is_free_threaded() { - warn!( - "build_flags contains Py_GIL_DISABLED but target ABI '{target_abi}' is not free-threaded" - ); - } - } - } else if let Some(target_abi) = self.target_abi { - if target_abi.kind.is_free_threaded() { - build_flags.0.insert(BuildFlag::Py_GIL_DISABLED); - } - } - build_flags - } else if let Some(target_abi) = self.target_abi { - let mut flags = if let Some(ref build_flags) = self.build_flags { - build_flags.clone() - } else { - BuildFlags::default() - }; - if target_abi.kind.is_free_threaded() { - flags.0.insert(BuildFlag::Py_GIL_DISABLED); - } - flags - } else { - BuildFlags::default() - }; - dbg!(&build_flags); + let target_abi = self + .target_abi + .unwrap_or_else(|| PythonAbiBuilder::new(self.implementation, self.version).finalize()); + let mut build_flags = self.build_flags.unwrap_or_default(); + if target_abi.kind.free_threaded() { + build_flags.0.insert(BuildFlag::Py_GIL_DISABLED); + } #[allow(deprecated)] InterpreterConfig { implementation: self.implementation, version: self.version, shared: self.shared.unwrap_or(true), - target_abi: self - .target_abi - .unwrap_or(PythonAbiBuilder::new(self.implementation, self.version).finalize()), - abi3: if let Some(target_abi) = self.target_abi { - matches!(target_abi.kind, PythonAbiKind::Stable(StableAbi::Abi3)) - } else { - false - }, + target_abi, + abi3: matches!(target_abi.kind, PythonAbiKind::Stable(StableAbi::Abi3)), lib_name: self.lib_name, lib_dir: self.lib_dir, executable: self.executable, pointer_width: self.pointer_width, - build_flags: build_flags, + build_flags, suppress_build_script_link_lines: self .suppress_build_script_link_lines .unwrap_or(false), @@ -1028,6 +1005,21 @@ impl PythonAbiBuilder { } } + pub fn from_sysconfig( + implementation: PythonImplementation, + version: PythonVersion, + abi3_version: Option, + gil_disabled: bool, + ) -> Result { + let target_version = sanitize_abi3_version(abi3_version, version)?; + if gil_disabled { + // TODO: fall back to abi3t builds on 3.15t and newer + PythonAbiBuilder::new(implementation, target_version).free_threaded() + } else { + PythonAbiBuilder::from_build_env(implementation, target_version, abi3_version) + } + } + pub fn stable_abi(self, kind: StableAbi) -> Result { ensure!( self.kind.is_none(), @@ -1164,7 +1156,7 @@ impl FromStr for PythonAbiKind { } impl PythonAbiKind { - pub fn is_free_threaded(&self) -> bool { + pub fn free_threaded(&self) -> bool { match self { PythonAbiKind::VersionSpecific(gil_disabled) => *gil_disabled == GilUsed::FreeThreaded, PythonAbiKind::Stable(StableAbi::Abi3) => false, @@ -2199,12 +2191,12 @@ fn default_lib_name_windows(abi: PythonAbi, mingw: bool, debug: bool) -> Result< Ok(lib_name) } else if mingw { ensure!( - !abi.kind.is_free_threaded(), + !abi.kind.free_threaded(), "MinGW free-threaded builds are not currently tested or supported" ); // https://packages.msys2.org/base/mingw-w64-python Ok(format!("python{}.{}", abi.version.major, abi.version.minor)) - } else if abi.kind.is_free_threaded() { + } else if abi.kind.free_threaded() { ensure!(abi.version >= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", abi.version.major, abi.version.minor); if debug { Ok(format!( @@ -2233,7 +2225,7 @@ fn default_lib_name_unix(abi: PythonAbi, cygwin: bool, ld_version: Option<&str>) Ok("python3".to_string()) } else if cygwin && matches!(abi.kind, PythonAbiKind::Stable(StableAbi::Abi3t)) { Ok("python3t".to_string()) - } else if abi.kind.is_free_threaded() { + } else if abi.kind.free_threaded() { ensure!(abi.version >= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", abi.version.major, abi.version.minor); Ok(format!( "python{}.{}t", @@ -2394,7 +2386,7 @@ pub fn make_cross_compile_config() -> Result> { config.version, get_abi3_version(), )?; - if config.target_abi.kind.is_free_threaded() && abi_builder.kind.is_none() { + if config.target_abi.kind.free_threaded() && abi_builder.kind.is_none() { abi_builder = abi_builder.free_threaded()?; } config.target_abi = abi_builder.finalize(); @@ -2599,6 +2591,7 @@ mod tests { fn gil_disabled_config_file_corner_cases() { let implementation = PythonImplementation::CPython; let version = PythonVersion::PY313; + // Legacy: build_flags=Py_GIL_DISABLED with no target_abi infers free-threaded. assert_eq!( InterpreterConfig::from_reader("version=3.13\nbuild_flags=Py_GIL_DISABLED".as_bytes()) .unwrap(), @@ -2607,6 +2600,7 @@ mod tests { .unwrap() .finalize() ); + // Canonical: target_abi=free_threaded. assert_eq!( InterpreterConfig::from_reader( "version=3.13\ntarget_abi=CPython-version_specific(free_threaded)-3.13".as_bytes() @@ -2617,9 +2611,14 @@ mod tests { .unwrap() .finalize() ); + // target_abi=gil_enabled with build_flags=Py_GIL_DISABLED is inconsistent and rejected. + assert!(InterpreterConfig::from_reader("version=3.13\ntarget_abi=CPython-version_specific(gil_enabled)-3.13\nbuild_flags=Py_GIL_DISABLED".as_bytes()).is_err()); + // build_flags=Py_GIL_DISABLED on a builder without target_abi is also rejected. let mut flags = BuildFlags::default(); flags.0.insert(BuildFlag::Py_GIL_DISABLED); - assert_eq!(InterpreterConfig::from_reader("version=3.13\ntarget_abi=CPython-version_specific(gil_enabled)-3.13\nbuild_flags=Py_GIL_DISABLED".as_bytes()).unwrap(), InterpreterConfigBuilder::new(implementation, version).build_flags(flags).unwrap().finalize()); + assert!(InterpreterConfigBuilder::new(implementation, version) + .build_flags(flags) + .is_err()); } #[test] @@ -3287,11 +3286,14 @@ mod tests { "cannot set a minimum Python version 3.45 higher than the interpreter version" )); - let interpreter = get_host_interpreter(Some(PythonVersion::PY313)); - assert_eq!( - interpreter.unwrap().target_abi.version, - PythonVersion::PY313 - ); + let host_version = get_host_interpreter(None).unwrap().version; + if host_version >= PythonVersion::PY313 { + let interpreter = get_host_interpreter(Some(PythonVersion::PY313)); + assert_eq!( + interpreter.unwrap().target_abi.version, + PythonVersion::PY313 + ); + } } #[test] @@ -3318,7 +3320,7 @@ mod tests { version: Some(interpreter_config.version), implementation: Some(interpreter_config.implementation), target: triple!("x86_64-unknown-linux-gnu"), - abiflags: if interpreter_config.is_free_threaded() { + abiflags: if interpreter_config.target_abi.kind.free_threaded() { Some("t".into()) } else { None @@ -3332,22 +3334,17 @@ mod tests { }; let sysconfigdata = super::parse_sysconfigdata(sysconfigdata_path).unwrap(); let parsed_config = InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(); - let implementation = PythonImplementation::CPython; + assert_eq!(parsed_config.implementation, PythonImplementation::CPython); assert_eq!( - parsed_config, - InterpreterConfigBuilder::new( - implementation, - interpreter_config.version, - PythonAbiBuilder::new(implementation, interpreter_config.version).finalize() - ) - .build_flags(interpreter_config.build_flags.0.clone()) - .pointer_width(64) - .lib_dir(interpreter_config.lib_dir.to_owned()) - .lib_name(interpreter_config.lib_name.to_owned()) - .finalize() - .unwrap() - ) + parsed_config.target_abi.implementation, + PythonImplementation::CPython + ); + assert_eq!( + parsed_config.free_threaded(), + interpreter_config.free_threaded() + ); + assert_eq!(parsed_config.pointer_width, Some(64)); } #[test] @@ -3591,28 +3588,23 @@ mod tests { InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); let mut flags = BuildFlags::new(); flags.0.insert(BuildFlag::Py_GIL_DISABLED); - assert!(builder.build_flags(flags).unwrap().target_abi.is_none()); + assert!(builder.build_flags(flags).is_err()); let builder = InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); let mut flags = BuildFlags::new(); flags.0.insert(BuildFlag::Py_GIL_DISABLED); - assert!( - builder - .stable_abi(StableAbi::Abi3) - .unwrap() - .build_flags(flags) - .unwrap() - .finalize() - .target_abi - .kind - == PythonAbiKind::Stable(StableAbi::Abi3) - ); + assert!(builder + .stable_abi(StableAbi::Abi3) + .unwrap() + .build_flags(flags) + .is_err()); let builder = InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); let config = builder.free_threaded().unwrap().finalize(); - assert!(config.build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED)) + assert!(config.target_abi.kind.free_threaded()); + assert!(config.build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED)); } #[test] diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index b1df86fab6a..f0e4f131fff 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -70,7 +70,7 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { let mut error = MaximumVersionExceeded::new(interpreter_config, versions.max); let major = interp_version.major; let minor = interp_version.minor; - if interpreter_config.target_abi.kind.is_free_threaded() { + if interpreter_config.target_abi.kind.free_threaded() { error.add_help(&format!( "the free-threaded build of CPython {major}{minor} does not support the limited API so this check cannot be suppressed.", )); @@ -84,7 +84,7 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { } } - if interpreter_config.target_abi.kind.is_free_threaded() { + if interpreter_config.target_abi.kind.free_threaded() { let min_free_threaded_version = PythonVersion { major: 3, minor: 14, @@ -135,7 +135,7 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { bail!("Abi3t builds are not yet supported") } StableAbi::Abi3 => { - if interpreter_config.target_abi.kind.is_free_threaded() { + if interpreter_config.target_abi.kind.free_threaded() { warn!( "The free-threaded build of CPython does not support abi3 so the build artifacts will be version-specific." ) From df3bf042af00e1f87c43b1bc8c31975e854812bf Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 6 May 2026 14:44:17 -0600 Subject: [PATCH 128/195] fix clippy --- pyo3-build-config/src/impl_.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index a4fae544e20..157891e7139 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -889,11 +889,11 @@ impl InterpreterConfigBuilder { extra_build_script_lines: Vec, ) -> Result { ensure!( - self.extra_build_script_lines.len() == 0, + self.extra_build_script_lines.is_empty(), "extra_build_script_lines already set!" ); Ok(InterpreterConfigBuilder { - extra_build_script_lines: extra_build_script_lines, + extra_build_script_lines, ..self }) } From fbacc26537948bc40b372e3f4f63e34cf2da0890 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 6 May 2026 15:04:12 -0600 Subject: [PATCH 129/195] relax accepting Py_GIL_DISABLED in build_flags if no target ABI is set --- pyo3-build-config/src/impl_.rs | 110 +++++++++++++++++---------------- 1 file changed, 58 insertions(+), 52 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 157891e7139..df273f66f7b 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -83,16 +83,6 @@ pub fn target_triple_from_env() -> Triple { .expect("Unrecognized TARGET environment variable value") } -fn check_gil_disabled_consistency(target_abi: PythonAbi, build_flags: &BuildFlags) -> Result<()> { - let flag_set = build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED); - if flag_set && !target_abi.kind.free_threaded() { - bail!( - "build_flags contains Py_GIL_DISABLED but target ABI '{target_abi}' is not free-threaded" - ); - } - Ok(()) -} - fn sanitize_abi3_version( abi3_version: Option, version: PythonVersion, @@ -411,7 +401,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .parse() .context("failed to parse calcsize_pointer")?; - Ok(InterpreterConfigBuilder::new(implementation, version) + InterpreterConfigBuilder::new(implementation, version) .target_abi(target_abi)? .shared(shared)? .lib_name(Some(lib_name))? @@ -420,7 +410,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .pointer_width(calcsize_pointer * 8)? .build_flags(BuildFlags::from_interpreter(interpreter)?)? .python_framework_prefix(python_framework_prefix)? - .finalize()) + .finalize() } /// Generate from parsed sysconfigdata file @@ -478,7 +468,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) parse_key!(sysconfigdata, "SIZEOF_VOID_P").map(|bytes_width: u32| bytes_width * 8)?; let build_flags = BuildFlags::from_sysconfigdata(sysconfigdata); - Ok(InterpreterConfigBuilder::new(implementation, version) + InterpreterConfigBuilder::new(implementation, version) .target_abi(target_abi)? .shared(shared || framework)? .lib_dir(lib_dir)? @@ -486,7 +476,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .pointer_width(pointer_width)? .build_flags(build_flags)? .python_framework_prefix(python_framework_prefix)? - .finalize()) + .finalize() } /// Import an externally-provided config file. @@ -665,7 +655,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) builder }; - Ok(builder.finalize()) + builder.finalize() } /// Helper function to apply a default lib_name if none is set in `PYO3_CONFIG_FILE`. @@ -819,9 +809,6 @@ impl InterpreterConfigBuilder { "Target ABI already set to {}", target_abi ); - if let Some(ref build_flags) = self.build_flags { - check_gil_disabled_consistency(target_abi, build_flags)?; - } Ok(InterpreterConfigBuilder { target_abi: Some(target_abi), ..self @@ -913,15 +900,6 @@ impl InterpreterConfigBuilder { pub fn build_flags(self, build_flags: BuildFlags) -> Result { ensure!(self.build_flags.is_none(), "Build flags already set!"); - if build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { - match self.target_abi { - Some(target_abi) => check_gil_disabled_consistency(target_abi, &build_flags)?, - None => bail!( - "build_flags contains Py_GIL_DISABLED but target ABI is not set; \ - call .free_threaded() (or set target_abi) before .build_flags()" - ), - } - } Ok(InterpreterConfigBuilder { build_flags: Some(build_flags), ..self @@ -942,16 +920,18 @@ impl InterpreterConfigBuilder { }) } - pub fn finalize(self) -> InterpreterConfig { + pub fn finalize(self) -> Result { let target_abi = self .target_abi .unwrap_or_else(|| PythonAbiBuilder::new(self.implementation, self.version).finalize()); let mut build_flags = self.build_flags.unwrap_or_default(); if target_abi.kind.free_threaded() { build_flags.0.insert(BuildFlag::Py_GIL_DISABLED); + } else { + ensure!(!build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED), "Build flags contains Py_GIL_DISABLED but target ABI '{target_abi} is not free-threaded.") } #[allow(deprecated)] - InterpreterConfig { + Ok(InterpreterConfig { implementation: self.implementation, version: self.version, shared: self.shared.unwrap_or(true), @@ -967,7 +947,7 @@ impl InterpreterConfigBuilder { .unwrap_or(false), extra_build_script_lines: self.extra_build_script_lines, python_framework_prefix: self.python_framework_prefix, - } + }) } } @@ -2082,11 +2062,11 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result = Vec::new(); config.to_writer(&mut buf).unwrap(); @@ -2514,7 +2495,8 @@ mod tests { let config = InterpreterConfigBuilder::new(implementation, version) .build_flags(build_flags) .unwrap() - .finalize(); + .finalize() + .unwrap(); let mut buf: Vec = Vec::new(); config.to_writer(&mut buf).unwrap(); @@ -2539,7 +2521,8 @@ mod tests { .unwrap() .extra_build_script_lines(vec!["cargo:test1".to_string(), "cargo:test2".to_string()]) .unwrap() - .finalize(); + .finalize() + .unwrap(); let mut buf: Vec = Vec::new(); config.to_writer(&mut buf).unwrap(); @@ -2555,7 +2538,9 @@ mod tests { let version = PythonVersion::PY38; assert_eq!( InterpreterConfig::from_reader("version=3.8".as_bytes()).unwrap(), - InterpreterConfigBuilder::new(implementation, version,).finalize() + InterpreterConfigBuilder::new(implementation, version,) + .finalize() + .unwrap() ) } @@ -2567,7 +2552,9 @@ mod tests { assert_eq!( InterpreterConfig::from_reader("version=3.8\next_suffix=.python38.so".as_bytes()) .unwrap(), - InterpreterConfigBuilder::new(implementation, version,).finalize() + InterpreterConfigBuilder::new(implementation, version,) + .finalize() + .unwrap() ) } @@ -2599,6 +2586,7 @@ mod tests { .free_threaded() .unwrap() .finalize() + .unwrap() ); // Canonical: target_abi=free_threaded. assert_eq!( @@ -2610,6 +2598,7 @@ mod tests { .free_threaded() .unwrap() .finalize() + .unwrap() ); // target_abi=gil_enabled with build_flags=Py_GIL_DISABLED is inconsistent and rejected. assert!(InterpreterConfig::from_reader("version=3.13\ntarget_abi=CPython-version_specific(gil_enabled)-3.13\nbuild_flags=Py_GIL_DISABLED".as_bytes()).is_err()); @@ -2631,6 +2620,7 @@ mod tests { .stable_abi(StableAbi::Abi3) .unwrap() .finalize() + .unwrap() ); } @@ -2742,6 +2732,7 @@ mod tests { .pointer_width(64) .unwrap() .finalize() + .unwrap() ); } @@ -2770,6 +2761,7 @@ mod tests { .pointer_width(64) .unwrap() .finalize() + .unwrap() ); sysconfigdata = Sysconfigdata::new(); @@ -2797,6 +2789,7 @@ mod tests { .shared(false) .unwrap() .finalize() + .unwrap() ); } @@ -2812,7 +2805,8 @@ mod tests { .unwrap() .lib_name(Some("python3".into())) .unwrap() - .finalize(); + .finalize() + .unwrap(); assert_eq!(default_abi3_config(&host, min_version).unwrap(), config); } @@ -2825,7 +2819,8 @@ mod tests { let config = InterpreterConfigBuilder::new(implementation, version) .stable_abi(StableAbi::Abi3) .unwrap() - .finalize(); + .finalize() + .unwrap(); assert_eq!(default_abi3_config(&host, min_version).unwrap(), config); } @@ -2852,7 +2847,8 @@ mod tests { .unwrap() .lib_dir(Some("C:\\some\\path".into())) .unwrap() - .finalize(); + .finalize() + .unwrap(); assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } @@ -2879,7 +2875,8 @@ mod tests { .unwrap() .lib_dir(Some("/usr/lib/mingw".into())) .unwrap() - .finalize(); + .finalize() + .unwrap(); assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } @@ -2906,7 +2903,8 @@ mod tests { .unwrap() .lib_dir(Some("/usr/arm64/lib".into())) .unwrap() - .finalize(); + .finalize() + .unwrap(); assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } @@ -2930,7 +2928,8 @@ mod tests { let config = InterpreterConfigBuilder::new(implementation, version) .lib_name(Some("pypy3.11-c".into())) .unwrap() - .finalize(); + .finalize() + .unwrap(); assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } @@ -3267,7 +3266,8 @@ mod tests { .finalize(), ) .unwrap() - .finalize(); + .finalize() + .unwrap(); assert_eq!(config.target_abi.version, target_version); assert_eq!(config.version, host_version); } @@ -3467,7 +3467,8 @@ mod tests { let interpreter_config = InterpreterConfigBuilder::new(implementation, version) .lib_name(Some("python3".into())) .unwrap() - .finalize(); + .finalize() + .unwrap(); assert_eq!( interpreter_config.build_script_outputs(), [ @@ -3506,7 +3507,8 @@ mod tests { .unwrap() .lib_name(Some("python3".into())) .unwrap() - .finalize(); + .finalize() + .unwrap(); assert_eq!( interpreter_config.build_script_outputs(), @@ -3567,7 +3569,8 @@ mod tests { .unwrap() .lib_name(Some("python3".into())) .unwrap() - .finalize(); + .finalize() + .unwrap(); assert_eq!( interpreter_config.build_script_outputs(), [ @@ -3602,7 +3605,7 @@ mod tests { let builder = InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); - let config = builder.free_threaded().unwrap().finalize(); + let config = builder.free_threaded().unwrap().finalize().unwrap(); assert!(config.target_abi.kind.free_threaded()); assert!(config.build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED)); } @@ -3618,7 +3621,8 @@ mod tests { .unwrap() .build_flags(build_flags) .unwrap() - .finalize(); + .finalize() + .unwrap(); assert_eq!( interpreter_config.build_script_outputs(), [ @@ -3661,7 +3665,9 @@ mod tests { fn test_apply_default_lib_name_to_config_file() { let implementation = PythonImplementation::CPython; let version = PythonVersion::PY39; - let mut config = InterpreterConfigBuilder::new(implementation, version).finalize(); + let mut config = InterpreterConfigBuilder::new(implementation, version) + .finalize() + .unwrap(); let unix = Triple::from_str("x86_64-unknown-linux-gnu").unwrap(); let win_x64 = Triple::from_str("x86_64-pc-windows-msvc").unwrap(); From 0efa792ace75e708b013f26afe922ecaa975337f Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 6 May 2026 15:57:55 -0600 Subject: [PATCH 130/195] more fixes and suggestions from claude --- pyo3-build-config/src/impl_.rs | 298 +++++++++++++++------------------ pyo3-build-config/src/lib.rs | 44 ++--- pyo3-ffi/build.rs | 14 +- 3 files changed, 153 insertions(+), 203 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index df273f66f7b..572df63505d 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -102,6 +102,18 @@ fn sanitize_abi3_version( } } +fn apply_build_env_to_config(mut config: InterpreterConfig) -> Result { + let abi3_version = get_abi3_version(); + let mut abi_builder = + PythonAbiBuilder::from_build_env(config.implementation, config.version, abi3_version)?; + // only allow free-threaded builds if the build environment didn't force an abi3 build + if config.target_abi.kind.is_free_threaded() && abi_builder.kind.is_none() { + abi_builder = abi_builder.free_threaded()?; + } + config.target_abi = abi_builder.finalize(); + Ok(config) +} + /// Configuration needed by PyO3 to build for the correct Python implementation. /// /// Usually this is queried directly from the Python interpreter, or overridden using the @@ -135,13 +147,13 @@ pub struct InterpreterConfig { /// Deprecated field used to indicate an abi3 target. /// /// Creating an InterpreterConfig struct with `abi3` set to `True`, - /// `interpreter` set to `PythonImplementation::CPython` and `version` set - /// to `PythonVersion {major: 3, minor: 9}` is equivalent to setting `abi` + /// `implementation` set to `PythonImplementation::CPython` and `version` set + /// to `PythonVersion {major: 3, minor: 9}` is equivalent to setting `target_abi` /// to `PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion - /// {major: 3, minor: 9).abi3().finalize()`. + /// {major: 3, minor: 9}).stable_abi(StableAbi::Abi3)?.finalize()?`. /// /// Serialized to `abi3`. - #[deprecated] + #[deprecated(note = "Set a target_abi that is abi3 instead")] pub abi3: bool, /// The name of the link library defining Python. @@ -403,13 +415,13 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) InterpreterConfigBuilder::new(implementation, version) .target_abi(target_abi)? - .shared(shared)? - .lib_name(Some(lib_name))? - .lib_dir(lib_dir)? - .executable(map.get("executable").cloned())? - .pointer_width(calcsize_pointer * 8)? + .shared(shared) + .lib_name(Some(lib_name)) + .lib_dir(lib_dir) + .executable(map.get("executable").cloned()) + .pointer_width(calcsize_pointer * 8) .build_flags(BuildFlags::from_interpreter(interpreter)?)? - .python_framework_prefix(python_framework_prefix)? + .python_framework_prefix(python_framework_prefix) .finalize() } @@ -470,12 +482,12 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) InterpreterConfigBuilder::new(implementation, version) .target_abi(target_abi)? - .shared(shared || framework)? - .lib_dir(lib_dir)? - .lib_name(lib_name)? - .pointer_width(pointer_width)? + .shared(shared || framework) + .lib_dir(lib_dir) + .lib_name(lib_name) + .pointer_width(pointer_width) .build_flags(build_flags)? - .python_framework_prefix(python_framework_prefix)? + .python_framework_prefix(python_framework_prefix) .finalize() } @@ -494,23 +506,10 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) "PYO3_CONFIG_FILE must be an absolute path" ); - let mut config = InterpreterConfig::from_path(path) - .context("failed to parse contents of PYO3_CONFIG_FILE")?; - // If the abi3 feature is enabled, the minimum Python version is constrained by the abi3 - // feature. - let abi3_version = get_abi3_version(); - let mut abi_builder = PythonAbiBuilder::from_build_env( - config.implementation, - config.version, - abi3_version, - )?; - // only allow free-threaded builds if the build environment didn't force an abi3 build - if config.target_abi.kind.free_threaded() && abi_builder.kind.is_none() { - abi_builder = abi_builder.free_threaded()?; - } - config.target_abi = abi_builder.finalize(); - - Ok(config) + apply_build_env_to_config( + InterpreterConfig::from_path(path) + .context("failed to parse contents of PYO3_CONFIG_FILE")?, + ) }) } @@ -640,17 +639,17 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .target_abi(target_abi) // cannot fail because this is a newly-created InterpreterConfigBuilder with no target ABI set .unwrap() - .shared(shared.unwrap_or(true))? - .lib_name(lib_name)? - .lib_dir(lib_dir)? - .executable(executable)? + .shared(shared.unwrap_or(true)) + .lib_name(lib_name) + .lib_dir(lib_dir) + .executable(executable) .build_flags(build_flags)? - .suppress_build_script_link_lines(suppress_build_script_link_lines)? - .extra_build_script_lines(extra_build_script_lines)? - .python_framework_prefix(python_framework_prefix)?; + .suppress_build_script_link_lines(suppress_build_script_link_lines) + .extra_build_script_lines(extra_build_script_lines) + .python_framework_prefix(python_framework_prefix); let builder = if let Some(pointer_width) = pointer_width { - builder.pointer_width(pointer_width)? + builder.pointer_width(pointer_width) } else { builder }; @@ -839,67 +838,62 @@ impl InterpreterConfigBuilder { ) } - pub fn lib_name(self, lib_name: Option) -> Result { - ensure!(self.lib_name.is_none(), "lib_name already set!"); - Ok(InterpreterConfigBuilder { lib_name, ..self }) + pub fn lib_name(self, lib_name: Option) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { lib_name, ..self } } - pub fn pointer_width(self, pointer_width: u32) -> Result { - ensure!(self.pointer_width.is_none(), "pointer_width already set!"); - Ok(InterpreterConfigBuilder { + pub fn pointer_width(self, pointer_width: u32) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { pointer_width: Some(pointer_width), ..self - }) + } } - pub fn executable(self, executable: Option) -> Result { - ensure!(self.executable.is_none(), "executable already set!"); - Ok(InterpreterConfigBuilder { executable, ..self }) + pub fn executable(self, executable: Option) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { executable, ..self } } pub fn suppress_build_script_link_lines( self, suppress_build_script_link_lines: Option, - ) -> Result { - ensure!( - self.suppress_build_script_link_lines.is_none(), - "suppress_build_script_link_lines already set!" - ); - Ok(InterpreterConfigBuilder { + ) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { suppress_build_script_link_lines, ..self - }) + } } pub fn extra_build_script_lines( self, extra_build_script_lines: Vec, - ) -> Result { - ensure!( - self.extra_build_script_lines.is_empty(), - "extra_build_script_lines already set!" - ); - Ok(InterpreterConfigBuilder { + ) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { extra_build_script_lines, ..self - }) + } } - pub fn lib_dir(self, lib_dir: Option) -> Result { - ensure!(self.lib_dir.is_none(), "lib_dir already set!"); - Ok(InterpreterConfigBuilder { lib_dir, ..self }) + pub fn lib_dir(self, lib_dir: Option) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { lib_dir, ..self } } - pub fn shared(self, shared: bool) -> Result { - ensure!(self.shared.is_none(), "shared already set!"); - Ok(InterpreterConfigBuilder { + pub fn shared(self, shared: bool) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { shared: Some(shared), ..self - }) + } } pub fn build_flags(self, build_flags: BuildFlags) -> Result { ensure!(self.build_flags.is_none(), "Build flags already set!"); + if let Some(target_abi) = self.target_abi { + if build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { + ensure!( + target_abi.kind != PythonAbiKind::Stable(StableAbi::Abi3), + "Targeting an abi3 build so cannot set build_flags containing Py_GIL_DISABLED." + ) + } + } Ok(InterpreterConfigBuilder { build_flags: Some(build_flags), ..self @@ -909,23 +903,29 @@ impl InterpreterConfigBuilder { pub fn python_framework_prefix( self, python_framework_prefix: Option, - ) -> Result { - ensure!( - self.python_framework_prefix.is_none(), - "Python_framework_prefix already set!" - ); - Ok(InterpreterConfigBuilder { + ) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { python_framework_prefix, ..self - }) + } } pub fn finalize(self) -> Result { - let target_abi = self - .target_abi - .unwrap_or_else(|| PythonAbiBuilder::new(self.implementation, self.version).finalize()); let mut build_flags = self.build_flags.unwrap_or_default(); - if target_abi.kind.free_threaded() { + let target_abi = if build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { + self.target_abi.unwrap_or_else(|| { + PythonAbiBuilder::new(self.implementation, self.version) + .free_threaded() + // cannot panic because this is a freshly created PythonAbiBuilder + .unwrap() + .finalize() + }) + } else { + self.target_abi.unwrap_or_else(|| { + PythonAbiBuilder::new(self.implementation, self.version).finalize() + }) + }; + if target_abi.kind.is_free_threaded() { build_flags.0.insert(BuildFlag::Py_GIL_DISABLED); } else { ensure!(!build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED), "Build flags contains Py_GIL_DISABLED but target ABI '{target_abi} is not free-threaded.") @@ -1075,10 +1075,7 @@ pub struct PythonAbi { impl Display for PythonAbi { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let implementation = self.implementation; - let kind = self.kind; - let version = self.version; - write!(f, "{implementation}-{kind}-{version}") + write!(f, "{}-{}-{}", self.implementation, self.kind, self.version) } } @@ -1086,12 +1083,20 @@ impl FromStr for PythonAbi { type Err = crate::errors::Error; fn from_str(value: &str) -> Result { - let mut parts = value.splitn(3, "-"); - let msg = "Invalid ABI string representation: {value}"; + let mut parts = value.splitn(3, '-'); Ok(PythonAbi { - implementation: parts.next().ok_or(msg)?.parse()?, - kind: parts.next().ok_or(msg)?.parse()?, - version: parts.next().ok_or(msg)?.parse()?, + implementation: parts + .next() + .ok_or_else(|| format!("Invalid ABI string representation: {value}"))? + .parse()?, + kind: parts + .next() + .ok_or_else(|| format!("Invalid ABI string representation: {value}"))? + .parse()?, + version: parts + .next() + .ok_or_else(|| format!("Invalid ABI string representation: {value}"))? + .parse()?, }) } } @@ -1110,8 +1115,8 @@ impl Display for PythonAbiKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { PythonAbiKind::Stable(stable_abi) => write!(f, "{stable_abi}"), - PythonAbiKind::VersionSpecific(gil_disabled) => { - write!(f, "version_specific({gil_disabled})") + PythonAbiKind::VersionSpecific(gil_used) => { + write!(f, "version_specific({gil_used})") } } } @@ -1136,9 +1141,9 @@ impl FromStr for PythonAbiKind { } impl PythonAbiKind { - pub fn free_threaded(&self) -> bool { + pub fn is_free_threaded(self) -> bool { match self { - PythonAbiKind::VersionSpecific(gil_disabled) => *gil_disabled == GilUsed::FreeThreaded, + PythonAbiKind::VersionSpecific(gil_disabled) => gil_disabled == GilUsed::FreeThreaded, PythonAbiKind::Stable(StableAbi::Abi3) => false, PythonAbiKind::Stable(StableAbi::Abi3t) => true, } @@ -2064,11 +2069,27 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result, + abi3t_version: Option, +) -> Result { + if abi3_version.is_some() && abi3t_version.is_some() { + bail!("Cannot simultaneously set abi3 and abi3t features") + } else if let Some(version) = abi3_version { + default_abi3_config(host, version) + } else if abi3t_version.is_some() { + bail!("Abi3t support has not yet been implemented") + } else { + bail!("Neither abi3 or abi3t features are enabled") + } +} + /// Generates "default" interpreter configuration when compiling "abi3" extensions /// without a working Python interpreter. /// @@ -2080,13 +2101,13 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result { // FIXME: PyPy & GraalPy do not support the Stable ABI. + let target_abi = PythonAbiBuilder::new(PythonImplementation::CPython, version) + .stable_abi(StableAbi::Abi3)? + .finalize(); let builder = InterpreterConfigBuilder::new(PythonImplementation::CPython, version) - .stable_abi(StableAbi::Abi3) - .unwrap(); - // abi3() sets the target_abi on the builder struct so unwrapping is safe - let target_abi = builder.target_abi.unwrap(); + .target_abi(target_abi)?; if host.operating_system == OperatingSystem::Windows { - builder.lib_name(Some(default_lib_name_windows(target_abi, false, false)?))? + builder.lib_name(Some(default_lib_name_windows(target_abi, false, false)?)) } else { builder } @@ -2171,12 +2192,12 @@ fn default_lib_name_windows(abi: PythonAbi, mingw: bool, debug: bool) -> Result< Ok(lib_name) } else if mingw { ensure!( - !abi.kind.free_threaded(), + !abi.kind.is_free_threaded(), "MinGW free-threaded builds are not currently tested or supported" ); // https://packages.msys2.org/base/mingw-w64-python Ok(format!("python{}.{}", abi.version.major, abi.version.minor)) - } else if abi.kind.free_threaded() { + } else if abi.kind.is_free_threaded() { ensure!(abi.version >= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", abi.version.major, abi.version.minor); if debug { Ok(format!( @@ -2205,7 +2226,7 @@ fn default_lib_name_unix(abi: PythonAbi, cygwin: bool, ld_version: Option<&str>) Ok("python3".to_string()) } else if cygwin && matches!(abi.kind, PythonAbiKind::Stable(StableAbi::Abi3t)) { Ok("python3t".to_string()) - } else if abi.kind.free_threaded() { + } else if abi.kind.is_free_threaded() { ensure!(abi.version >= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", abi.version.major, abi.version.minor); Ok(format!( "python{}.{}t", @@ -2360,18 +2381,9 @@ fn get_host_interpreter(stable_abi_version: Option) -> Result Result> { let interpreter_config = if let Some(cross_config) = cross_compiling_from_cargo_env()? { - let mut config = load_cross_compile_config(cross_config)?; - let mut abi_builder = PythonAbiBuilder::from_build_env( - config.implementation, - config.version, - get_abi3_version(), - )?; - if config.target_abi.kind.free_threaded() && abi_builder.kind.is_none() { - abi_builder = abi_builder.free_threaded()?; - } - config.target_abi = abi_builder.finalize(); - - Some(config) + Some(apply_build_env_to_config(load_cross_compile_config( + cross_config, + )?)?) } else { None }; @@ -2408,14 +2420,9 @@ pub fn make_interpreter_config() -> Result { warn!("Compiling without a working Python interpreter."); } } - } else { - ensure!( - abi3_version.is_some() || abi3t_version.is_some(), - "An abi3-py3* or abi3t-py3* feature must be specified when compiling without a Python interpreter." - ); - }; + } - let interpreter_config = default_abi3_config(&host, abi3_version.unwrap())?; + let interpreter_config = default_stable_abi_config(&host, abi3_version, abi3t_version)?; Ok(interpreter_config) } @@ -2467,15 +2474,10 @@ mod tests { .stable_abi(StableAbi::Abi3) .unwrap() .pointer_width(32) - .unwrap() .executable(Some("executable".into())) - .unwrap() .lib_dir(Some("lib_name".into())) - .unwrap() .lib_name(Some("lib_name".into())) - .unwrap() .extra_build_script_lines(vec!["cargo:test1".to_string(), "cargo:test2".to_string()]) - .unwrap() .finalize() .unwrap(); let mut buf: Vec = Vec::new(); @@ -2512,15 +2514,10 @@ mod tests { .stable_abi(StableAbi::Abi3) .unwrap() .pointer_width(32) - .unwrap() .executable(Some("executable".into())) - .unwrap() .lib_name(Some("lib_name".into())) - .unwrap() .lib_dir(Some("lib_dir\\n".into())) - .unwrap() .extra_build_script_lines(vec!["cargo:test1".to_string(), "cargo:test2".to_string()]) - .unwrap() .finalize() .unwrap(); let mut buf: Vec = Vec::new(); @@ -2607,7 +2604,12 @@ mod tests { flags.0.insert(BuildFlag::Py_GIL_DISABLED); assert!(InterpreterConfigBuilder::new(implementation, version) .build_flags(flags) - .is_err()); + .unwrap() + .finalize() + .unwrap() + .target_abi + .kind + .is_free_threaded()); } #[test] @@ -2726,11 +2728,8 @@ mod tests { .build_flags(BuildFlags::from_sysconfigdata(&sysconfigdata)) .unwrap() .lib_dir(Some("/usr/lib".into())) - .unwrap() .lib_name(Some("python3.8".into())) - .unwrap() .pointer_width(64) - .unwrap() .finalize() .unwrap() ); @@ -2755,11 +2754,8 @@ mod tests { .build_flags(BuildFlags::from_sysconfigdata(&sysconfigdata)) .unwrap() .lib_dir(Some("/usr/lib".into())) - .unwrap() .lib_name(Some("python3.8".into())) - .unwrap() .pointer_width(64) - .unwrap() .finalize() .unwrap() ); @@ -2781,13 +2777,9 @@ mod tests { .build_flags(BuildFlags::from_sysconfigdata(&sysconfigdata)) .unwrap() .lib_dir(Some("/usr/lib".into())) - .unwrap() .lib_name(Some("python3.8".into())) - .unwrap() .pointer_width(64) - .unwrap() .shared(false) - .unwrap() .finalize() .unwrap() ); @@ -2804,7 +2796,6 @@ mod tests { .stable_abi(StableAbi::Abi3) .unwrap() .lib_name(Some("python3".into())) - .unwrap() .finalize() .unwrap(); assert_eq!(default_abi3_config(&host, min_version).unwrap(), config); @@ -2844,9 +2835,7 @@ mod tests { let version = PythonVersion::PY38; let config = InterpreterConfigBuilder::new(implementation, version) .lib_name(Some("python38".into())) - .unwrap() .lib_dir(Some("C:\\some\\path".into())) - .unwrap() .finalize() .unwrap(); assert_eq!(default_cross_compile(&cross_config).unwrap(), config); @@ -2872,9 +2861,7 @@ mod tests { let version = PythonVersion::PY38; let config = InterpreterConfigBuilder::new(implementation, version) .lib_name(Some("python38".into())) - .unwrap() .lib_dir(Some("/usr/lib/mingw".into())) - .unwrap() .finalize() .unwrap(); assert_eq!(default_cross_compile(&cross_config).unwrap(), config); @@ -2900,9 +2887,7 @@ mod tests { let version = PythonVersion::PY39; let config = InterpreterConfigBuilder::new(implementation, version) .lib_name(Some("python3.9".into())) - .unwrap() .lib_dir(Some("/usr/arm64/lib".into())) - .unwrap() .finalize() .unwrap(); assert_eq!(default_cross_compile(&cross_config).unwrap(), config); @@ -2927,7 +2912,6 @@ mod tests { let version = PythonVersion::PY311; let config = InterpreterConfigBuilder::new(implementation, version) .lib_name(Some("pypy3.11-c".into())) - .unwrap() .finalize() .unwrap(); assert_eq!(default_cross_compile(&cross_config).unwrap(), config); @@ -3466,7 +3450,6 @@ mod tests { let version = PythonVersion::PY311; let interpreter_config = InterpreterConfigBuilder::new(implementation, version) .lib_name(Some("python3".into())) - .unwrap() .finalize() .unwrap(); assert_eq!( @@ -3506,7 +3489,6 @@ mod tests { .stable_abi(StableAbi::Abi3) .unwrap() .lib_name(Some("python3".into())) - .unwrap() .finalize() .unwrap(); @@ -3568,7 +3550,6 @@ mod tests { .free_threaded() .unwrap() .lib_name(Some("python3".into())) - .unwrap() .finalize() .unwrap(); assert_eq!( @@ -3591,7 +3572,7 @@ mod tests { InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); let mut flags = BuildFlags::new(); flags.0.insert(BuildFlag::Py_GIL_DISABLED); - assert!(builder.build_flags(flags).is_err()); + assert!(builder.build_flags(flags).is_ok()); let builder = InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); @@ -3606,7 +3587,7 @@ mod tests { let builder = InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); let config = builder.free_threaded().unwrap().finalize().unwrap(); - assert!(config.target_abi.kind.free_threaded()); + assert!(config.target_abi.kind.is_free_threaded()); assert!(config.build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED)); } @@ -3618,7 +3599,6 @@ mod tests { let version = PythonVersion::PY38; let interpreter_config = InterpreterConfigBuilder::new(implementation, version) .lib_name(Some("python3".into())) - .unwrap() .build_flags(build_flags) .unwrap() .finalize() diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index db652952e5c..2c36f621f70 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -20,7 +20,7 @@ use std::{env, process::Command, str::FromStr, sync::OnceLock}; pub use impl_::{ cross_compiling_from_to, find_all_sysconfigdata, parse_sysconfigdata, BuildFlag, BuildFlags, - CrossCompileConfig, InterpreterConfig, InterpreterConfigBuilder, PythonAbi, + CrossCompileConfig, InterpreterConfig, InterpreterConfigBuilder, PythonAbi, PythonAbiBuilder, PythonImplementation, PythonVersion, Triple, }; @@ -487,24 +487,13 @@ mod tests { let mut buf = Vec::new(); let implementation = PythonImplementation::CPython; let version = PythonVersion::PY313; - let target_abi = PythonAbiBuilder::new(implementation, version).finalize(); - let interpreter_config = InterpreterConfig { - implementation, - version, - target_abi, - abi3: false, - shared: true, - lib_name: None, - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: Some( + let interpreter_config = InterpreterConfigBuilder::new(implementation, version) + .python_framework_prefix(Some( "/Applications/Xcode.app/Contents/Developer/Library/Frameworks".to_string(), - ), - }; + )) + .unwrap() + .finalize() + .unwrap(); // Does nothing on non-mac _add_python_framework_link_args( &interpreter_config, @@ -531,22 +520,9 @@ mod tests { fn test_maximum_version_exceeded_formatting() { let implementation = PythonImplementation::CPython; let version = PythonVersion::PY313; - let target_abi = PythonAbiBuilder::new(implementation, version).finalize(); - let interpreter_config = InterpreterConfig { - implementation, - version, - target_abi, - abi3: false, - shared: true, - lib_name: None, - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - }; + let interpreter_config = InterpreterConfigBuilder::new(implementation, version) + .finalize() + .unwrap(); let mut error = pyo3_build_script_impl::MaximumVersionExceeded::new( &interpreter_config, PythonVersion { diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index f0e4f131fff..f3ea191ca1e 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -70,9 +70,9 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { let mut error = MaximumVersionExceeded::new(interpreter_config, versions.max); let major = interp_version.major; let minor = interp_version.minor; - if interpreter_config.target_abi.kind.free_threaded() { + if interpreter_config.target_abi.kind.is_free_threaded() { error.add_help(&format!( - "the free-threaded build of CPython {major}{minor} does not support the limited API so this check cannot be suppressed.", + "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()); } @@ -84,7 +84,7 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { } } - if interpreter_config.target_abi.kind.free_threaded() { + if interpreter_config.target_abi.kind.is_free_threaded() { let min_free_threaded_version = PythonVersion { major: 3, minor: 14, @@ -134,13 +134,7 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { StableAbi::Abi3t => { bail!("Abi3t builds are not yet supported") } - StableAbi::Abi3 => { - if interpreter_config.target_abi.kind.free_threaded() { - warn!( - "The free-threaded build of CPython does not support abi3 so the build artifacts will be version-specific." - ) - } - } + StableAbi::Abi3 => {} }, PythonImplementation::PyPy => warn!( "PyPy does not yet support {abi} so the build artifacts will be version-specific. \ From 766ecbf4933f5bd3902889cc7a2ab5ed4f985972 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 6 May 2026 16:22:33 -0600 Subject: [PATCH 131/195] fix fallback to version-specific builds for free-threaded abi3 builds --- pyo3-build-config/src/impl_.rs | 39 +++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 572df63505d..4ad658fbd86 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -886,16 +886,29 @@ impl InterpreterConfigBuilder { pub fn build_flags(self, build_flags: BuildFlags) -> Result { ensure!(self.build_flags.is_none(), "Build flags already set!"); - if let Some(target_abi) = self.target_abi { + let target_abi = if let Some(target_abi) = self.target_abi { if build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { - ensure!( - target_abi.kind != PythonAbiKind::Stable(StableAbi::Abi3), - "Targeting an abi3 build so cannot set build_flags containing Py_GIL_DISABLED." - ) + if target_abi.kind == PythonAbiKind::Stable(StableAbi::Abi3) { + warn!( + "Targeting an abi3 build but build_flags contains Py_GIL_DISABLED, falling back to a version-specific build" + ); + Some( + PythonAbiBuilder::new(target_abi.implementation, target_abi.version) + .free_threaded()? + .finalize(), + ) + } else { + Some(target_abi) + } + } else { + Some(target_abi) } - } + } else { + None + }; Ok(InterpreterConfigBuilder { build_flags: Some(build_flags), + target_abi, ..self }) } @@ -1101,7 +1114,10 @@ impl FromStr for PythonAbi { } } -/// The "kind" of stable ABI. Either abi3 or abi3t currently. +/// The "kind" of ABI. +/// +/// Either a variety of stable ABI or a GIL-enabled or free-threaded +/// version-specific ABI. #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(test, derive(Debug))] pub enum PythonAbiKind { @@ -1150,7 +1166,7 @@ impl PythonAbiKind { } } -/// Whether the ABI is for the GIL-enabled or free-threaded build. +/// The the variety of stable ABI #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(test, derive(Debug))] pub enum StableAbi { @@ -1311,6 +1327,9 @@ fn have_python_interpreter() -> bool { env_var("PYO3_NO_PYTHON").is_none() } +/// Checks if `abi3` or any of the `abi3-py3*` features is enabled for the PyO3 crate. +/// +/// Must be called from a PyO3 crate build script. fn is_abi3() -> bool { cargo_env_var("CARGO_FEATURE_ABI3").is_some() || env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY").is_some_and(|os_str| os_str == "1") @@ -1647,8 +1666,8 @@ impl FromStr for BuildFlag { /// is the equivalent of `#ifdef {varname}` in C. /// /// see Misc/SpecialBuilds.txt in the python source for what these mean. -#[cfg_attr(test, derive(PartialEq, Eq))] -#[derive(Debug, Clone, Default)] +#[cfg_attr(test, derive(Debug, PartialEq, Eq))] +#[derive(Clone, Default)] pub struct BuildFlags(pub HashSet); impl BuildFlags { From deb83a7f9c63e1b0c1308aa3090a9f91a666e635 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 6 May 2026 16:36:22 -0600 Subject: [PATCH 132/195] More minor fixes --- pyo3-build-config/src/impl_.rs | 41 ++++++++++++++++------------------ pyo3-build-config/src/lib.rs | 1 - 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 4ad658fbd86..0a10d17a84c 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -150,7 +150,7 @@ pub struct InterpreterConfig { /// `implementation` set to `PythonImplementation::CPython` and `version` set /// to `PythonVersion {major: 3, minor: 9}` is equivalent to setting `target_abi` /// to `PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion - /// {major: 3, minor: 9}).stable_abi(StableAbi::Abi3)?.finalize()?`. + /// {major: 3, minor: 9}).stable_abi(StableAbi::Abi3)?.finalize()`. /// /// Serialized to `abi3`. #[deprecated(note = "Set a target_abi that is abi3 instead")] @@ -619,8 +619,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) } else if flags_contains_free_threaded { // This fires even if is_abi3() is True for backward compatibility reasons PythonAbiBuilder::new(implementation, version) - .free_threaded() - .unwrap() + .free_threaded()? .finalize() } else if (abi3 == Some(true)) || is_abi3() { if abi3 == Some(true) { @@ -889,6 +888,7 @@ impl InterpreterConfigBuilder { let target_abi = if let Some(target_abi) = self.target_abi { if build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { if target_abi.kind == PythonAbiKind::Stable(StableAbi::Abi3) { + // Not an error for backward compatibility reasons warn!( "Targeting an abi3 build but build_flags contains Py_GIL_DISABLED, falling back to a version-specific build" ); @@ -897,6 +897,8 @@ impl InterpreterConfigBuilder { .free_threaded()? .finalize(), ) + } else if target_abi.kind == PythonAbiKind::VersionSpecific(GilUsed::GilEnabled) { + bail!("Py_GIL_DISABLED is in build_flags bug target_abi '{target_abi}' is not free-threaded") } else { Some(target_abi) } @@ -925,23 +927,19 @@ impl InterpreterConfigBuilder { pub fn finalize(self) -> Result { let mut build_flags = self.build_flags.unwrap_or_default(); - let target_abi = if build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { - self.target_abi.unwrap_or_else(|| { + let target_abi = match self.target_abi { + Some(t) => t, + None if build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) => { PythonAbiBuilder::new(self.implementation, self.version) - .free_threaded() - // cannot panic because this is a freshly created PythonAbiBuilder - .unwrap() + .free_threaded()? .finalize() - }) - } else { - self.target_abi.unwrap_or_else(|| { - PythonAbiBuilder::new(self.implementation, self.version).finalize() - }) + } + None => PythonAbiBuilder::new(self.implementation, self.version).finalize(), }; if target_abi.kind.is_free_threaded() { build_flags.0.insert(BuildFlag::Py_GIL_DISABLED); } else { - ensure!(!build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED), "Build flags contains Py_GIL_DISABLED but target ABI '{target_abi} is not free-threaded.") + ensure!(!build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED), "Build flags contains Py_GIL_DISABLED but target ABI '{target_abi}' is not free-threaded.") } #[allow(deprecated)] Ok(InterpreterConfig { @@ -2418,11 +2416,6 @@ pub fn make_interpreter_config() -> Result { let abi3_version = get_abi3_version(); let abi3t_version = get_abi3t_version(); - ensure!( - !(abi3_version.is_some() && abi3t_version.is_some()), - "Cannot simultaneously enable abi3 and abi3t features" - ); - // See if we can safely skip the Python interpreter configuration detection. // Unix stable ABI extension modules can usually be built without any interpreter. let need_interpreter = @@ -2618,7 +2611,7 @@ mod tests { ); // target_abi=gil_enabled with build_flags=Py_GIL_DISABLED is inconsistent and rejected. assert!(InterpreterConfig::from_reader("version=3.13\ntarget_abi=CPython-version_specific(gil_enabled)-3.13\nbuild_flags=Py_GIL_DISABLED".as_bytes()).is_err()); - // build_flags=Py_GIL_DISABLED on a builder without target_abi is also rejected. + // build_flags=Py_GIL_DISABLED on a builder without target_abi is ok let mut flags = BuildFlags::default(); flags.0.insert(BuildFlag::Py_GIL_DISABLED); assert!(InterpreterConfigBuilder::new(implementation, version) @@ -3597,11 +3590,15 @@ mod tests { InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); let mut flags = BuildFlags::new(); flags.0.insert(BuildFlag::Py_GIL_DISABLED); - assert!(builder + let config = builder .stable_abi(StableAbi::Abi3) .unwrap() .build_flags(flags) - .is_err()); + .unwrap() + .finalize() + .unwrap(); + // build flags win due to backward compatbility (abi3 feature is a no-op on ft builds) + assert!(config.target_abi.kind == PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)); let builder = InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 2c36f621f70..11297ef9efa 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -491,7 +491,6 @@ mod tests { .python_framework_prefix(Some( "/Applications/Xcode.app/Contents/Developer/Library/Frameworks".to_string(), )) - .unwrap() .finalize() .unwrap(); // Does nothing on non-mac From 9174ca744db84d45a62e998fe8f2145575165dab Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 6 May 2026 16:59:24 -0600 Subject: [PATCH 133/195] expand test coverage --- pyo3-build-config/src/impl_.rs | 38 ++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 0a10d17a84c..470ec54e3bb 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -1018,10 +1018,6 @@ impl PythonAbiBuilder { self.kind.unwrap(), kind ); - ensure!( - kind == StableAbi::Abi3 || kind == StableAbi::Abi3t, - "Cannot set a stable abi build with a version-specific ABI kind '{kind}'" - ); let mut build_version = self.version; if self.version.minor > STABLE_ABI_MAX_MINOR { warn!("Automatically falling back to {kind}-py3{STABLE_ABI_MAX_MINOR} because current Python is higher than the maximum supported"); @@ -2622,6 +2618,26 @@ mod tests { .target_abi .kind .is_free_threaded()); + + let mut flags = BuildFlags::default(); + flags.0.insert(BuildFlag::Py_GIL_DISABLED); + assert!( + InterpreterConfigBuilder::new(implementation, PythonVersion::PY312) + .build_flags(flags) + .unwrap() + .finalize() + .is_err() + ); + + let mut flags = BuildFlags::default(); + flags.0.insert(BuildFlag::Py_GIL_DISABLED); + assert!( + InterpreterConfigBuilder::new(implementation, PythonVersion::PY312) + .stable_abi(StableAbi::Abi3) + .unwrap() + .build_flags(flags) + .is_err() + ); } #[test] @@ -3196,6 +3212,20 @@ mod tests { .minor, STABLE_ABI_MAX_MINOR ); + + assert!("invalid".parse::().is_err()); + assert!("CPython-invalid".parse::().is_err()); + assert!("CPython-version_specific(free_threaded)-invalid" + .parse::() + .is_err()); + + assert!( + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY314) + .stable_abi(StableAbi::Abi3) + .unwrap() + .free_threaded() + .is_err() + ); } #[test] From 01a6b0a122a1c7576bc2e502f25e2ff249dcbc79 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 6 May 2026 17:18:01 -0600 Subject: [PATCH 134/195] more fixes --- noxfile.py | 1 + pyo3-build-config/src/impl_.rs | 107 +++++++++++++++++++++------------ 2 files changed, 71 insertions(+), 37 deletions(-) diff --git a/noxfile.py b/noxfile.py index b7130b30d4c..b7c0e0ac624 100644 --- a/noxfile.py +++ b/noxfile.py @@ -101,6 +101,7 @@ def test(session: nox.Session) -> None: @nox.session(name="test-rust", venv_backend="none") def test_rust(session: nox.Session): _run_cargo_test(session, package="pyo3-build-config") + _run_cargo_test(session, package="pyo3-build-config", features="resolve-config") _run_cargo_test(session, package="pyo3-macros-backend") _run_cargo_test(session, package="pyo3-macros") diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 470ec54e3bb..ba96ea01007 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -885,32 +885,8 @@ impl InterpreterConfigBuilder { pub fn build_flags(self, build_flags: BuildFlags) -> Result { ensure!(self.build_flags.is_none(), "Build flags already set!"); - let target_abi = if let Some(target_abi) = self.target_abi { - if build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { - if target_abi.kind == PythonAbiKind::Stable(StableAbi::Abi3) { - // Not an error for backward compatibility reasons - warn!( - "Targeting an abi3 build but build_flags contains Py_GIL_DISABLED, falling back to a version-specific build" - ); - Some( - PythonAbiBuilder::new(target_abi.implementation, target_abi.version) - .free_threaded()? - .finalize(), - ) - } else if target_abi.kind == PythonAbiKind::VersionSpecific(GilUsed::GilEnabled) { - bail!("Py_GIL_DISABLED is in build_flags bug target_abi '{target_abi}' is not free-threaded") - } else { - Some(target_abi) - } - } else { - Some(target_abi) - } - } else { - None - }; Ok(InterpreterConfigBuilder { build_flags: Some(build_flags), - target_abi, ..self }) } @@ -927,19 +903,43 @@ impl InterpreterConfigBuilder { pub fn finalize(self) -> Result { let mut build_flags = self.build_flags.unwrap_or_default(); - let target_abi = match self.target_abi { - Some(t) => t, - None if build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) => { - PythonAbiBuilder::new(self.implementation, self.version) - .free_threaded()? - .finalize() - } - None => PythonAbiBuilder::new(self.implementation, self.version).finalize(), + let py_gil_disabled = build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED); + let target_abi = match (self.target_abi, py_gil_disabled) { + // No target ABI set, no Py_GIL_DISABLED: default to GIL-enabled version-specific. + (None, false) => PythonAbiBuilder::new(self.implementation, self.version).finalize(), + // No target ABI set, Py_GIL_DISABLED in build flags: infer free-threaded. + (None, true) => PythonAbiBuilder::new(self.implementation, self.version) + .free_threaded()? + .finalize(), + // Target ABI set, no Py_GIL_DISABLED: use as-is. + (Some(target_abi), false) => target_abi, + // Target ABI set + Py_GIL_DISABLED: reconcile. + (Some(target_abi), true) => match target_abi.kind { + // abi3 + Py_GIL_DISABLED: the abi3 feature is a no-op on free-threaded + // interpreters, so for backward compatibility fall back to a free-threaded + // version-specific build. + PythonAbiKind::Stable(StableAbi::Abi3) => { + let new_abi = + PythonAbiBuilder::new(target_abi.implementation, target_abi.version) + .free_threaded()? + .finalize(); + warn!( + "Targeting an abi3 build but build_flags contains Py_GIL_DISABLED, \ + falling back to a version-specific free-threaded build" + ); + new_abi + } + // GIL-enabled version-specific + Py_GIL_DISABLED is contradictory. + PythonAbiKind::VersionSpecific(GilUsed::GilEnabled) => bail!( + "build_flags contains Py_GIL_DISABLED but target_abi \ + '{target_abi}' is not free-threaded" + ), + // Already free-threaded (Stable(Abi3t) or VersionSpecific(FreeThreaded)). + _ => target_abi, + }, }; if target_abi.kind.is_free_threaded() { build_flags.0.insert(BuildFlag::Py_GIL_DISABLED); - } else { - ensure!(!build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED), "Build flags contains Py_GIL_DISABLED but target ABI '{target_abi}' is not free-threaded.") } #[allow(deprecated)] Ok(InterpreterConfig { @@ -2636,6 +2636,8 @@ mod tests { .stable_abi(StableAbi::Abi3) .unwrap() .build_flags(flags) + .unwrap() + .finalize() .is_err() ); } @@ -3346,7 +3348,7 @@ mod tests { version: Some(interpreter_config.version), implementation: Some(interpreter_config.implementation), target: triple!("x86_64-unknown-linux-gnu"), - abiflags: if interpreter_config.target_abi.kind.free_threaded() { + abiflags: if interpreter_config.target_abi.kind.is_free_threaded() { Some("t".into()) } else { None @@ -3367,8 +3369,8 @@ mod tests { PythonImplementation::CPython ); assert_eq!( - parsed_config.free_threaded(), - interpreter_config.free_threaded() + parsed_config.is_free_threaded(), + interpreter_config.is_free_threaded() ); assert_eq!(parsed_config.pointer_width, Some(64)); } @@ -3630,6 +3632,37 @@ mod tests { // build flags win due to backward compatbility (abi3 feature is a no-op on ft builds) assert!(config.target_abi.kind == PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)); + // The reconciliation is order-independent: build_flags first, then stable_abi(Abi3) + // produces the same result as the previous ordering. + let builder = + InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); + let mut flags = BuildFlags::new(); + flags.0.insert(BuildFlag::Py_GIL_DISABLED); + let config = builder + .build_flags(flags) + .unwrap() + .stable_abi(StableAbi::Abi3) + .unwrap() + .finalize() + .unwrap(); + assert!(config.target_abi.kind == PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)); + + // Explicit GIL-enabled target with Py_GIL_DISABLED in build flags is contradictory and + // is rejected at finalize regardless of the order in which the setters were called. + let builder = + InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); + let target_abi = + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY314).finalize(); + let mut flags = BuildFlags::new(); + flags.0.insert(BuildFlag::Py_GIL_DISABLED); + assert!(builder + .target_abi(target_abi) + .unwrap() + .build_flags(flags) + .unwrap() + .finalize() + .is_err()); + let builder = InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); let config = builder.free_threaded().unwrap().finalize().unwrap(); From b8b5bff198a76cc2f7157cbd9968a1187fb696f5 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 6 May 2026 17:21:04 -0600 Subject: [PATCH 135/195] update changelog entries --- newsfragments/5924.added.md | 7 +++++++ newsfragments/5924.changed.md | 5 +++-- 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 newsfragments/5924.added.md diff --git a/newsfragments/5924.added.md b/newsfragments/5924.added.md new file mode 100644 index 00000000000..d64a4507475 --- /dev/null +++ b/newsfragments/5924.added.md @@ -0,0 +1,7 @@ +* Added a PythonAbi struct, along with PythonAbiKind, StableAbi, and GilUsed +enums to pyo3-build-config. These types allow specifying the Python ABI to use +for a build. You can construct a PythonAbi struct using a PythonAbiBuilder +struct. + +* Added an InterpreterConfigBuilder struct for building InterpreterConfig +structs with a more ergonomic syntax. diff --git a/newsfragments/5924.changed.md b/newsfragments/5924.changed.md index 07d2d59c8f6..2e614599e42 100644 --- a/newsfragments/5924.changed.md +++ b/newsfragments/5924.changed.md @@ -1,2 +1,3 @@ -The boolean abi3 field of pyo3_build_config::impl_::InterpreterConfig is now a -variant of a new CPythonABI enum. +The boolean abi3 field of pyo3_build_config::impl_::InterpreterConfig is now +deprecated and has been replaced by a target_abi field, which is a PythonAbi +struct. From 285eb197e0e92f237f0b57cab4344bf91442edf7 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 6 May 2026 17:22:09 -0600 Subject: [PATCH 136/195] Add missing exports to public pyo3-build-config API --- pyo3-build-config/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 11297ef9efa..23cbe1d7c62 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -20,8 +20,8 @@ use std::{env, process::Command, str::FromStr, sync::OnceLock}; pub use impl_::{ cross_compiling_from_to, find_all_sysconfigdata, parse_sysconfigdata, BuildFlag, BuildFlags, - CrossCompileConfig, InterpreterConfig, InterpreterConfigBuilder, PythonAbi, PythonAbiBuilder, - PythonImplementation, PythonVersion, Triple, + CrossCompileConfig, GilUsed, InterpreterConfig, InterpreterConfigBuilder, PythonAbi, + PythonAbiBuilder, PythonAbiKind, PythonImplementation, PythonVersion, StableAbi, Triple, }; use target_lexicon::OperatingSystem; From 95be8d092211e695020258596e5544d6da749507 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 6 May 2026 17:26:18 -0600 Subject: [PATCH 137/195] fix linux test --- pyo3-build-config/src/impl_.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index ba96ea01007..81c64241f4b 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -3369,8 +3369,8 @@ mod tests { PythonImplementation::CPython ); assert_eq!( - parsed_config.is_free_threaded(), - interpreter_config.is_free_threaded() + parsed_config.target_abi.kind.is_free_threaded(), + interpreter_config.target_abi.kind.is_free_threaded() ); assert_eq!(parsed_config.pointer_width, Some(64)); } From 9a3359204aeebafd1eda0f18a8fe77fecd7608b4 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 6 May 2026 20:08:03 -0600 Subject: [PATCH 138/195] minor touch-ups --- newsfragments/5924.changed.md | 9 +++-- pyo3-build-config/src/impl_.rs | 73 +++++++++++++++++----------------- 2 files changed, 43 insertions(+), 39 deletions(-) diff --git a/newsfragments/5924.changed.md b/newsfragments/5924.changed.md index 2e614599e42..195b8408e77 100644 --- a/newsfragments/5924.changed.md +++ b/newsfragments/5924.changed.md @@ -1,3 +1,6 @@ -The boolean abi3 field of pyo3_build_config::impl_::InterpreterConfig is now -deprecated and has been replaced by a target_abi field, which is a PythonAbi -struct. +* The boolean `abi3` field of `pyo3_build_config::impl_::InterpreterConfig` is now + deprecated and has been replaced by a target_abi field, which is a `PythonAbi` + struct. +* The InterpreterConfig::from_interpreter function now accepts a second + `abi3_version` argument. The old behavior is the same as passing + `abi3_version = None` . diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 81c64241f4b..ec0ce0032e5 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -109,6 +109,8 @@ fn apply_build_env_to_config(mut config: InterpreterConfig) -> Result Result { out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); if kind == StableAbi::Abi3t { - out.push("cargo:rustc-cfg=Py_GIL_DISABLED".to_owned()); - out.push("cargo:rustc-cfg=Py_TARGET_ABI3T".to_owned()); + // out.push("cargo:rustc-cfg=Py_GIL_DISABLED".to_owned()); + // out.push("cargo:rustc-cfg=Py_TARGET_ABI3T".to_owned()); + panic!("Abi3t builds are not yet supported"); } } PythonAbiKind::VersionSpecific(kind) => match kind { @@ -419,7 +426,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .lib_name(Some(lib_name)) .lib_dir(lib_dir) .executable(map.get("executable").cloned()) - .pointer_width(calcsize_pointer * 8) + .pointer_width(Some(calcsize_pointer * 8)) .build_flags(BuildFlags::from_interpreter(interpreter)?)? .python_framework_prefix(python_framework_prefix) .finalize() @@ -476,8 +483,8 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) cygwin, sysconfigdata.get_value("LDVERSION"), )?); - let pointer_width = - parse_key!(sysconfigdata, "SIZEOF_VOID_P").map(|bytes_width: u32| bytes_width * 8)?; + let pointer_width = parse_key!(sysconfigdata, "SIZEOF_VOID_P") + .map(|bytes_width: u32| Some(bytes_width * 8))?; let build_flags = BuildFlags::from_sysconfigdata(sysconfigdata); InterpreterConfigBuilder::new(implementation, version) @@ -645,13 +652,8 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .build_flags(build_flags)? .suppress_build_script_link_lines(suppress_build_script_link_lines) .extra_build_script_lines(extra_build_script_lines) - .python_framework_prefix(python_framework_prefix); - - let builder = if let Some(pointer_width) = pointer_width { - builder.pointer_width(pointer_width) - } else { - builder - }; + .python_framework_prefix(python_framework_prefix) + .pointer_width(pointer_width); builder.finalize() } @@ -762,6 +764,10 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) envs, ) } + + pub fn is_free_threaded(&self) -> bool { + self.target_abi.kind.is_free_threaded() + } } #[cfg_attr(test, derive(Debug))] @@ -818,9 +824,7 @@ impl InterpreterConfigBuilder { let version = self.version; self.target_abi( PythonAbiBuilder::new(implementation, version) - .stable_abi(kind) - // this can't panic because stable_abi() is called on a builder with no chosen ABI - .unwrap() + .stable_abi(kind)? .finalize(), ) } @@ -830,9 +834,7 @@ impl InterpreterConfigBuilder { let version: PythonVersion = self.version; self.target_abi( PythonAbiBuilder::new(implementation, version) - .free_threaded() - // this can't panic because abi3() is called on a builder with no chosen ABI - .unwrap() + .free_threaded()? .finalize(), ) } @@ -841,9 +843,9 @@ impl InterpreterConfigBuilder { InterpreterConfigBuilder { lib_name, ..self } } - pub fn pointer_width(self, pointer_width: u32) -> InterpreterConfigBuilder { + pub fn pointer_width(self, pointer_width: Option) -> InterpreterConfigBuilder { InterpreterConfigBuilder { - pointer_width: Some(pointer_width), + pointer_width, ..self } } @@ -989,7 +991,6 @@ impl PythonAbiBuilder { kind: None, }; if is_abi3() { - // freshly created builder without an ABI kind so this can't fail builder.stable_abi(StableAbi::Abi3) } else { Ok(builder) @@ -1126,7 +1127,7 @@ impl Display for PythonAbiKind { match self { PythonAbiKind::Stable(stable_abi) => write!(f, "{stable_abi}"), PythonAbiKind::VersionSpecific(gil_used) => { - write!(f, "version_specific({gil_used})") + write!(f, "{gil_used}") } } } @@ -1139,12 +1140,8 @@ impl FromStr for PythonAbiKind { match value { "abi3" => Ok(PythonAbiKind::Stable(StableAbi::Abi3)), "abi3t" => Ok(PythonAbiKind::Stable(StableAbi::Abi3t)), - "version_specific(free_threaded)" => { - Ok(PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)) - } - "version_specific(gil_enabled)" => { - Ok(PythonAbiKind::VersionSpecific(GilUsed::GilEnabled)) - } + "free_threaded" => Ok(PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)), + "gil_enabled" => Ok(PythonAbiKind::VersionSpecific(GilUsed::GilEnabled)), _ => Err(format!("Unrecognized ABI name: {value}").into()), } } @@ -2481,7 +2478,7 @@ mod tests { let config = InterpreterConfigBuilder::new(implementation, version) .stable_abi(StableAbi::Abi3) .unwrap() - .pointer_width(32) + .pointer_width(Some(32)) .executable(Some("executable".into())) .lib_dir(Some("lib_name".into())) .lib_name(Some("lib_name".into())) @@ -2521,7 +2518,7 @@ mod tests { let config = InterpreterConfigBuilder::new(implementation, version) .stable_abi(StableAbi::Abi3) .unwrap() - .pointer_width(32) + .pointer_width(Some(32)) .executable(Some("executable".into())) .lib_name(Some("lib_name".into())) .lib_dir(Some("lib_dir\\n".into())) @@ -2596,7 +2593,7 @@ mod tests { // Canonical: target_abi=free_threaded. assert_eq!( InterpreterConfig::from_reader( - "version=3.13\ntarget_abi=CPython-version_specific(free_threaded)-3.13".as_bytes() + "version=3.13\ntarget_abi=CPython-free_threaded-3.13".as_bytes() ) .unwrap(), InterpreterConfigBuilder::new(implementation, version) @@ -2606,7 +2603,11 @@ mod tests { .unwrap() ); // target_abi=gil_enabled with build_flags=Py_GIL_DISABLED is inconsistent and rejected. - assert!(InterpreterConfig::from_reader("version=3.13\ntarget_abi=CPython-version_specific(gil_enabled)-3.13\nbuild_flags=Py_GIL_DISABLED".as_bytes()).is_err()); + assert!(InterpreterConfig::from_reader( + "version=3.13\ntarget_abi=CPython-gil_enabled-3.13\nbuild_flags=Py_GIL_DISABLED" + .as_bytes() + ) + .is_err()); // build_flags=Py_GIL_DISABLED on a builder without target_abi is ok let mut flags = BuildFlags::default(); flags.0.insert(BuildFlag::Py_GIL_DISABLED); @@ -2759,7 +2760,7 @@ mod tests { .unwrap() .lib_dir(Some("/usr/lib".into())) .lib_name(Some("python3.8".into())) - .pointer_width(64) + .pointer_width(Some(64)) .finalize() .unwrap() ); @@ -2785,7 +2786,7 @@ mod tests { .unwrap() .lib_dir(Some("/usr/lib".into())) .lib_name(Some("python3.8".into())) - .pointer_width(64) + .pointer_width(Some(64)) .finalize() .unwrap() ); @@ -2808,7 +2809,7 @@ mod tests { .unwrap() .lib_dir(Some("/usr/lib".into())) .lib_name(Some("python3.8".into())) - .pointer_width(64) + .pointer_width(Some(64)) .shared(false) .finalize() .unwrap() @@ -3217,7 +3218,7 @@ mod tests { assert!("invalid".parse::().is_err()); assert!("CPython-invalid".parse::().is_err()); - assert!("CPython-version_specific(free_threaded)-invalid" + assert!("CPython-free_threaded-invalid" .parse::() .is_err()); From 0c7b53accde2215827905482328a485d48fdbe90 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 6 May 2026 20:08:03 -0600 Subject: [PATCH 139/195] minor touch-ups --- pyo3-build-config/src/impl_.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index ec0ce0032e5..a7652739b03 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -2641,6 +2641,12 @@ mod tests { .finalize() .is_err() ); + + assert!( + InterpreterConfigBuilder::new(implementation, PythonVersion::PY38) + .free_threaded() + .is_err() + ); } #[test] From 505d9e1376b52adbdeeb73dda1aedc790a2519f0 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 6 May 2026 20:12:11 -0600 Subject: [PATCH 140/195] skip redundant test --- noxfile.py | 1 - 1 file changed, 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index b7c0e0ac624..e2c34066962 100644 --- a/noxfile.py +++ b/noxfile.py @@ -100,7 +100,6 @@ def test(session: nox.Session) -> None: @nox.session(name="test-rust", venv_backend="none") def test_rust(session: nox.Session): - _run_cargo_test(session, package="pyo3-build-config") _run_cargo_test(session, package="pyo3-build-config", features="resolve-config") _run_cargo_test(session, package="pyo3-macros-backend") _run_cargo_test(session, package="pyo3-macros") From 20c8f57aeba9c0ce3f67d0c687e76534153c6faa Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 6 May 2026 20:13:35 -0600 Subject: [PATCH 141/195] whitespace --- newsfragments/5924.added.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/newsfragments/5924.added.md b/newsfragments/5924.added.md index d64a4507475..6d8e93af861 100644 --- a/newsfragments/5924.added.md +++ b/newsfragments/5924.added.md @@ -4,4 +4,4 @@ for a build. You can construct a PythonAbi struct using a PythonAbiBuilder struct. * Added an InterpreterConfigBuilder struct for building InterpreterConfig -structs with a more ergonomic syntax. +structs with a more ergonomic syntax. From afd0eeca54958a31921d133417e3a543d050d152 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 6 May 2026 20:43:50 -0600 Subject: [PATCH 142/195] more tweaks --- pyo3-build-config/src/impl_.rs | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index a7652739b03..3707bb8995d 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -107,10 +107,12 @@ fn apply_build_env_to_config(mut config: InterpreterConfig) -> Result { out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); if kind == StableAbi::Abi3t { - // out.push("cargo:rustc-cfg=Py_GIL_DISABLED".to_owned()); - // out.push("cargo:rustc-cfg=Py_TARGET_ABI3T".to_owned()); panic!("Abi3t builds are not yet supported"); } } @@ -830,8 +830,8 @@ impl InterpreterConfigBuilder { } pub fn free_threaded(self) -> Result { - let implementation: PythonImplementation = self.implementation; - let version: PythonVersion = self.version; + let implementation = self.implementation; + let version = self.version; self.target_abi( PythonAbiBuilder::new(implementation, version) .free_threaded()? @@ -1033,9 +1033,11 @@ impl PythonAbiBuilder { } pub fn free_threaded(self) -> Result { - if let Some(kind) = self.kind { - bail!("Target ABI already set to {kind}, cannot set to free-threaded",) - } + ensure!( + self.kind.is_none(), + "Target ABI already set to {}, cannot set to free-threaded", + self.kind.unwrap() + ); if self.version < PythonVersion::PY313 { let version = self.version; bail!( @@ -2065,7 +2067,7 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result Date: Wed, 6 May 2026 20:45:11 -0600 Subject: [PATCH 143/195] grammar in comment --- pyo3-build-config/src/impl_.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 3707bb8995d..a30b59f8f9e 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -120,9 +120,9 @@ fn apply_build_env_to_config(mut config: InterpreterConfig) -> Result Date: Thu, 7 May 2026 13:49:25 -0600 Subject: [PATCH 144/195] fix cfg gating --- pyo3-ffi/src/critical_section.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyo3-ffi/src/critical_section.rs b/pyo3-ffi/src/critical_section.rs index 9249df7aad4..558a7cb8c12 100644 --- a/pyo3-ffi/src/critical_section.rs +++ b/pyo3-ffi/src/critical_section.rs @@ -1,9 +1,9 @@ -#[cfg(any(all(Py_GIL_DISABLED, Py_3_13, not(Py_LIMITED_API)), Py_3_15,))] +#[cfg(all(Py_GIL_DISABLED, Py_3_13, not(Py_LIMITED_API)))] use crate::PyMutex; #[cfg(any(all(Py_GIL_DISABLED, Py_3_13), Py_3_15))] use crate::PyObject; -#[cfg(all(Py_LIMITED_API, Py_3_13, not(Py_3_15)))] +#[cfg(all(Py_LIMITED_API, Py_3_13))] opaque_struct!(pub PyMutex); #[cfg(any(all(Py_GIL_DISABLED, Py_3_13, not(Py_LIMITED_API)), Py_3_15,))] From 5334eaa1fc79e15f38225f7d297918f9a481bc3c Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 8 May 2026 13:28:07 -0600 Subject: [PATCH 145/195] activate abi3t builds --- pyo3-build-config/src/impl_.rs | 97 +++++++++++++++++++++------------- 1 file changed, 61 insertions(+), 36 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index d8f9fe9937a..59c96e0e26e 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -235,7 +235,7 @@ impl InterpreterConfig { #[doc(hidden)] pub fn from_interpreter( interpreter: impl AsRef, - abi3_version: Option, + stable_abi_version: Option, ) -> Result { const SCRIPT: &str = r#" # Allow the script to run on Python 2, so that nicer error can be printed later. @@ -343,7 +343,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) _ => panic!("Unknown Py_GIL_DISABLED value"), }; - let target_version = if let Some(min_version) = abi3_version { + let target_version = if let Some(min_version) = stable_abi_version { ensure!( min_version <= version, "cannot set a minimum Python version {} higher than the interpreter version {} \ @@ -357,17 +357,15 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) version }; - let mut abi_builder = if gil_disabled { - PythonAbiBuilder::new(implementation, target_version) - } else { - PythonAbiBuilder::from_build_env(implementation, target_version, abi3_version)? - }; - - if gil_disabled { - abi_builder = abi_builder.free_threaded()?; - } + let target_abi = PythonAbiBuilder::from_build_env( + implementation, + target_version, + stable_abi_version, + gil_disabled, + )? + .finalize(); - let target_abi = abi_builder.finalize(); + dbg!(format!("{}", &target_abi)); let cygwin = map["cygwin"].as_str() == "True"; @@ -388,6 +386,9 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) )? }; + dbg!(&lib_name); + dbg!(format!("{target_abi}")); + let lib_dir = if cfg!(windows) { map.get("base_prefix") .map(|base_prefix| format!("{base_prefix}\\libs")) @@ -459,12 +460,9 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) None => false, }; let cygwin = soabi.ends_with("cygwin"); - let target_abi = if gil_disabled { - PythonAbiBuilder::new(implementation, version).free_threaded()? - } else { - PythonAbiBuilder::from_build_env(implementation, version, Some(version))? - } - .finalize(); + let target_abi = + PythonAbiBuilder::from_build_env(implementation, version, Some(version), gil_disabled)? + .finalize(); let lib_name = Some(default_lib_name_unix( target_abi, cygwin, @@ -504,16 +502,13 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .context("failed to parse contents of PYO3_CONFIG_FILE")?; // If the abi3 feature is enabled, the minimum Python version is constrained by the abi3 // feature. - let abi3_version = get_abi3_version(); - let mut abi_builder = PythonAbiBuilder::from_build_env( + let stable_abi_version = get_abi3_version().or(get_abi3t_version()); + let abi_builder = PythonAbiBuilder::from_build_env( config.implementation, config.version, - abi3_version, + stable_abi_version, + config.target_abi.kind.is_free_threaded() && stable_abi_version.is_none(), )?; - // only allow free-threaded builds if the build environment didn't force an abi3 build - if config.target_abi.kind.is_free_threaded() && abi_builder.kind.is_none() { - abi_builder = abi_builder.free_threaded()?; - } config.target_abi = abi_builder.finalize(); Ok(config) @@ -974,15 +969,20 @@ impl PythonAbiBuilder { pub fn from_build_env( implementation: PythonImplementation, version: PythonVersion, - abi3_version: Option, + stable_abi_version: Option, + gil_disabled: bool, ) -> Result { let builder = PythonAbiBuilder { implementation, - version: abi3_version.unwrap_or(version), + version: stable_abi_version.unwrap_or(version), kind: None, }; if is_abi3() { builder.stable_abi(StableAbi::Abi3) + } else if is_abi3t() { + builder.stable_abi(StableAbi::Abi3t) + } else if gil_disabled { + builder.free_threaded() } else { Ok(builder) } @@ -2053,13 +2053,9 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result< WINDOWS_STABLE_ABI_LIB_NAME.to_owned() }; if abi.kind == PythonAbiKind::Stable(StableAbi::Abi3t) { - lib_name.push('t'); + lib_name.insert_str( + lib_name.find("python3").expect("invalid windows DLL name") + 7, + "t", + ); } Ok(lib_name) } else if mingw { @@ -2363,10 +2362,12 @@ fn get_host_interpreter(stable_abi_version: Option) -> Result Result> { let interpreter_config = if let Some(cross_config) = cross_compiling_from_cargo_env()? { let mut config = load_cross_compile_config(cross_config)?; + let stable_abi_version = get_abi3_version().or(get_abi3t_version()); let mut abi_builder = PythonAbiBuilder::from_build_env( config.implementation, config.version, - get_abi3_version(), + stable_abi_version, + config.target_abi.kind.is_free_threaded() && stable_abi_version.is_none(), )?; if config.target_abi.kind.is_free_threaded() && abi_builder.kind.is_none() { abi_builder = abi_builder.free_threaded()?; @@ -2963,6 +2964,30 @@ mod tests { .unwrap(), "python313t_d", ); + assert_eq!( + super::default_lib_name_windows( + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY315) + .stable_abi(StableAbi::Abi3t) + .unwrap() + .finalize(), + false, + false, + ) + .unwrap(), + "python3t", + ); + assert_eq!( + super::default_lib_name_windows( + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY315) + .stable_abi(StableAbi::Abi3t) + .unwrap() + .finalize(), + false, + true, + ) + .unwrap(), + "python3t_d", + ); } #[test] From 12fa458957a9a3a6250084493ef3f21232ed4cf0 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 11 May 2026 16:12:57 -0600 Subject: [PATCH 146/195] fix PyMutex gating --- pyo3-ffi/src/critical_section.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyo3-ffi/src/critical_section.rs b/pyo3-ffi/src/critical_section.rs index 558a7cb8c12..eda9553df06 100644 --- a/pyo3-ffi/src/critical_section.rs +++ b/pyo3-ffi/src/critical_section.rs @@ -1,9 +1,9 @@ -#[cfg(all(Py_GIL_DISABLED, Py_3_13, not(Py_LIMITED_API)))] +#[cfg(all(any(all(Py_GIL_DISABLED, Py_3_13), Py_3_15), not(Py_LIMITED_API)))] use crate::PyMutex; #[cfg(any(all(Py_GIL_DISABLED, Py_3_13), Py_3_15))] use crate::PyObject; -#[cfg(all(Py_LIMITED_API, Py_3_13))] +#[cfg(all(Py_LIMITED_API, Py_3_15))] opaque_struct!(pub PyMutex); #[cfg(any(all(Py_GIL_DISABLED, Py_3_13, not(Py_LIMITED_API)), Py_3_15,))] From 9337202b50c3025622b85c15f0ef0a4190d2c7ee Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 11 May 2026 16:46:58 -0600 Subject: [PATCH 147/195] simplify windows stable ABI lib name text manipulation --- pyo3-build-config/src/impl_.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index e3383dec6d7..192a359c6a9 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -2224,10 +2224,7 @@ fn default_lib_name_windows(abi: PythonAbi, mingw: bool, debug: bool) -> Result< WINDOWS_STABLE_ABI_LIB_NAME.to_owned() }; if abi.kind == PythonAbiKind::Stable(StableAbi::Abi3t) { - lib_name.insert_str( - lib_name.find("python3").expect("invalid windows DLL name") + "python3".len(), - "t", - ); + lib_name = lib_name.replace("python3", "python3t"); } Ok(lib_name) } else if mingw { From 52124f8751bb6577bdb65291a40468c5ac7cffd0 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 11 May 2026 17:16:23 -0600 Subject: [PATCH 148/195] fix ruff --- noxfile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/noxfile.py b/noxfile.py index c2ecf8b781d..08743fdfd58 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1296,8 +1296,8 @@ def _check_raw_dylib_macro(session: nox.Session): expected_dlls.add(f"python3{minor}t") expected_dlls.add(f"python3{minor}t_d") if minor >= 15: - expected_dlls.add(f"python3t") - expected_dlls.add(f"python3t_d") + expected_dlls.add("python3t") + expected_dlls.add("python3t_d") # PyPy DLL names (libpypy3.X-c.dll) pypy_min, pypy_max = _parse_supported_interpreter_version("pypy") From d9133fecd8745060046b7b6aac6a9651368cfed2 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 11 May 2026 17:38:42 -0600 Subject: [PATCH 149/195] fix cfg gating for critical section API --- pyo3-ffi/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index cd23a6ac3ca..8367c91bc1a 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -436,7 +436,7 @@ pub use self::compile::*; pub use self::complexobject::*; #[cfg(not(Py_LIMITED_API))] pub use self::context::*; -#[cfg(Py_3_13)] +#[cfg(any(all(Py_3_13, not(Py_LIMITED_API)), Py_3_15))] pub use self::critical_section::*; #[cfg(not(Py_LIMITED_API))] pub use self::datetime::*; From 1aea9b0b4e9d6b29cb118c7b64cbe142099b2ca3 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 11 May 2026 20:34:03 -0600 Subject: [PATCH 150/195] adjust PyMutex cfg gating --- pyo3-ffi/src/critical_section.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyo3-ffi/src/critical_section.rs b/pyo3-ffi/src/critical_section.rs index 9249df7aad4..ca585287d43 100644 --- a/pyo3-ffi/src/critical_section.rs +++ b/pyo3-ffi/src/critical_section.rs @@ -1,9 +1,12 @@ -#[cfg(any(all(Py_GIL_DISABLED, Py_3_13, not(Py_LIMITED_API)), Py_3_15,))] +#[cfg(any( + all(Py_GIL_DISABLED, Py_3_13, not(Py_LIMITED_API)), + all(Py_3_15, not(Py_LIMITED_API)) +))] use crate::PyMutex; #[cfg(any(all(Py_GIL_DISABLED, Py_3_13), Py_3_15))] use crate::PyObject; -#[cfg(all(Py_LIMITED_API, Py_3_13, not(Py_3_15)))] +#[cfg(all(Py_LIMITED_API, Py_3_13))] opaque_struct!(pub PyMutex); #[cfg(any(all(Py_GIL_DISABLED, Py_3_13, not(Py_LIMITED_API)), Py_3_15,))] From 4bb2202a7aa30ed90d879bd5981f9df3e4412f47 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 11 May 2026 21:34:51 -0600 Subject: [PATCH 151/195] attempt to fix abi3 feature for free-threaded targets from a config file --- 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 a3ad1f7d825..15a017fbb91 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -964,7 +964,7 @@ impl PythonAbiBuilder { version, kind: None, }; - if is_abi3() { + if is_abi3() && !gil_disabled { builder.stable_abi(StableAbi::Abi3) } else if is_abi3t() { builder.stable_abi(StableAbi::Abi3t) From 3e1ec3469e18d35951114b7a5bee4f1c8a224848 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 24 Apr 2026 14:13:34 -0600 Subject: [PATCH 152/195] introduce a builder for InterpreterConfig Co-authored-by: David Hewitt --- build.rs | 2 +- pyo3-build-config/src/impl_.rs | 974 +++++++++++++++------------- pyo3-build-config/src/lib.rs | 83 +-- pyo3-ffi-check/definitions/build.rs | 2 +- pyo3-ffi-check/macro/src/lib.rs | 21 +- pyo3-ffi/build.rs | 43 +- 6 files changed, 586 insertions(+), 539 deletions(-) diff --git a/build.rs b/build.rs index 2f33c61dc35..7100e62e281 100644 --- a/build.rs +++ b/build.rs @@ -6,7 +6,7 @@ use pyo3_build_config::{ }; fn ensure_auto_initialize_ok(interpreter_config: &InterpreterConfig) -> Result<()> { - if cargo_env_var("CARGO_FEATURE_AUTO_INITIALIZE").is_some() && !interpreter_config.shared { + if cargo_env_var("CARGO_FEATURE_AUTO_INITIALIZE").is_some() && !interpreter_config.shared() { bail!( "The `auto-initialize` feature is enabled, but your python installation only supports \ embedding the Python interpreter statically. If you are attempting to run tests, or a \ diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index ea6653d145c..b60d9ae1ba6 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -90,21 +90,37 @@ pub struct InterpreterConfig { /// The Python implementation flavor. /// /// Serialized to `implementation`. + #[deprecated( + since = "0.29.0", + note = "please use `.implementation()` getter or `InterpreterConfigBuilder` instead" + )] pub implementation: PythonImplementation, /// Python `X.Y` version. e.g. `3.9`. /// /// Serialized to `version`. + #[deprecated( + since = "0.29.0", + note = "please use `.version()` getter or `InterpreterConfigBuilder` instead" + )] pub version: PythonVersion, /// Whether link library is shared. /// /// Serialized to `shared`. + #[deprecated( + since = "0.29.0", + note = "please use `.shared()` getter or `InterpreterConfigBuilder` instead" + )] pub shared: bool, /// Whether linking against the stable/limited Python 3 API. /// /// Serialized to `abi3`. + #[deprecated( + since = "0.29.0", + note = "please use `.abi3()` getter or `InterpreterConfigBuilder` instead" + )] pub abi3: bool, /// The name of the link library defining Python. @@ -114,6 +130,10 @@ pub struct InterpreterConfig { /// prefix. /// /// Serialized to `lib_name`. + #[deprecated( + since = "0.29.0", + note = "please use `.lib_name()` getter or `InterpreterConfigBuilder` instead" + )] pub lib_name: Option, /// The directory containing the Python library to link against. @@ -122,6 +142,10 @@ pub struct InterpreterConfig { /// to add an additional library search path for the linker. /// /// Serialized to `lib_dir`. + #[deprecated( + since = "0.29.0", + note = "please use `.lib_dir()` getter or `InterpreterConfigBuilder` instead" + )] pub lib_dir: Option, /// Path of host `python` executable. @@ -131,16 +155,28 @@ pub struct InterpreterConfig { /// executable invoked. /// /// Serialized to `executable`. + #[deprecated( + since = "0.29.0", + note = "please use `.executable()` getter or `InterpreterConfigBuilder` instead" + )] pub executable: Option, /// Width in bits of pointers on the target machine. /// /// Serialized to `pointer_width`. + #[deprecated( + since = "0.29.0", + note = "please use `.pointer_width()` getter or `InterpreterConfigBuilder` instead" + )] pub pointer_width: Option, /// Additional relevant Python build flags / configuration settings. /// /// Serialized to `build_flags`. + #[deprecated( + since = "0.29.0", + note = "please use `.build_flags()` getter or `InterpreterConfigBuilder` instead" + )] pub build_flags: BuildFlags, /// Whether to suppress emitting of `cargo:rustc-link-*` lines from the build script. @@ -153,6 +189,10 @@ pub struct InterpreterConfig { /// /// If suppression is enabled, `extra_build_script_lines` should contain equivalent /// functionality or else a build failure is likely. + #[deprecated( + since = "0.29.0", + note = "please use `.suppress_build_script_link_lines()` getter or `InterpreterConfigBuilder` instead" + )] pub suppress_build_script_link_lines: bool, /// Additional lines to `println!()` from Cargo build scripts. @@ -165,12 +205,113 @@ pub struct InterpreterConfig { /// is build/configured. /// /// Serialized to multiple `extra_build_script_line` values. + #[deprecated( + since = "0.29.0", + note = "please use `.extra_build_script_lines()` getter or `InterpreterConfigBuilder` instead" + )] pub extra_build_script_lines: Vec, /// macOS Python3.framework requires special rpath handling + #[deprecated( + since = "0.29.0", + note = "please use `.python_framework_prefix()` getter or `InterpreterConfigBuilder` instead" + )] pub python_framework_prefix: Option, } +// Should no longer be deprecated once the internal fields are private +#[expect(deprecated, reason = "this impl block touches the internal fields")] impl InterpreterConfig { + /// The Python implementation flavor. + /// + /// Serialized to `implementation`. + pub fn implementation(&self) -> PythonImplementation { + self.implementation + } + + /// Python `X.Y` version. e.g. `3.9`. + /// + /// Serialized to `version`. + pub fn version(&self) -> PythonVersion { + self.version + } + + /// Whether link library is shared. + /// + /// Serialized to `shared`. + pub fn shared(&self) -> bool { + self.shared + } + + /// Whether linking against the stable/limited Python 3 API. + /// + /// Serialized to `abi3`. + pub fn abi3(&self) -> bool { + self.abi3 + } + + /// The name of the link library defining Python. + /// + /// This effectively controls the `cargo:rustc-link-lib=` value to + /// control how libpython is linked. Values should not contain the `lib` + /// prefix. + /// + /// Serialized to `lib_name`. + pub fn lib_name(&self) -> Option<&str> { + self.lib_name.as_deref() + } + + /// The directory containing the Python library to link against. + /// + /// The effectively controls the `cargo:rustc-link-search=native=` value + /// to add an additional library search path for the linker. + /// + /// Serialized to `lib_dir`. + pub fn lib_dir(&self) -> Option<&str> { + self.lib_dir.as_deref() + } + + /// Path of host `python` executable. + /// + /// This is a valid executable capable of running on the host/building machine. + /// For configurations derived by invoking a Python interpreter, it was the + /// executable invoked. + /// + /// Serialized to `executable`. + pub fn executable(&self) -> Option<&str> { + self.executable.as_deref() + } + + /// Width in bits of pointers on the target machine. + /// + /// Serialized to `pointer_width`. + pub fn pointer_width(&self) -> Option { + self.pointer_width + } + + /// Additional relevant Python build flags / configuration settings. + /// + /// Serialized to `build_flags`. + pub fn build_flags(&self) -> &BuildFlags { + &self.build_flags + } + + /// Whether to suppress emitting of `cargo:rustc-link-*` lines from the build script. + pub fn suppress_build_script_link_lines(&self) -> bool { + self.suppress_build_script_link_lines + } + + /// Additional lines to `println!()` from Cargo build scripts. + /// + /// Serialized to multiple `extra_build_script_line` values. + pub fn extra_build_script_lines(&self) -> &[String] { + &self.extra_build_script_lines + } + + /// macOS Python3.framework prefix used for special rpath handling. + pub fn python_framework_prefix(&self) -> Option<&str> { + self.python_framework_prefix.as_deref() + } + #[doc(hidden)] pub fn build_script_outputs(&self) -> Vec { // This should have been checked during pyo3-build-config build time. @@ -178,7 +319,7 @@ impl InterpreterConfig { let mut out = vec![]; - for i in MINIMUM_SUPPORTED_VERSION.minor..=self.version.minor { + for i in MINIMUM_SUPPORTED_VERSION.minor..=self.version().minor { out.push(format!("cargo:rustc-cfg=Py_3_{i}")); } @@ -356,20 +497,17 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .parse() .context("failed to parse calcsize_pointer")?; - Ok(InterpreterConfig { - version, - implementation, - shared, - abi3, - lib_name: Some(lib_name), - lib_dir, - executable: map.get("executable").cloned(), - pointer_width: Some(calcsize_pointer * 8), - build_flags: BuildFlags::from_interpreter(interpreter)?, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix, - }) + let builder = InterpreterConfigBuilder::new(implementation, version) + .abi3(abi3) + .shared(shared) + .lib_name(lib_name) + .lib_dir_opt(lib_dir) + .executable(map["executable"].clone()) + .pointer_width(calcsize_pointer * 8) + .build_flags(BuildFlags::from_interpreter(interpreter)?) + .python_framework_prefix_opt(python_framework_prefix); + + Ok(builder.finalize()) } /// Generate from parsed sysconfigdata file @@ -429,20 +567,16 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .ok(); let build_flags = BuildFlags::from_sysconfigdata(sysconfigdata); - Ok(InterpreterConfig { - implementation, - version, - shared: shared || framework, - abi3, - lib_dir, - lib_name, - executable: None, - pointer_width, - build_flags, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix, - }) + let builder = InterpreterConfigBuilder::new(implementation, version) + .abi3(abi3) + .shared(shared || framework) + .pointer_width_opt(pointer_width) + .lib_name_opt(lib_name) + .lib_dir_opt(lib_dir) + .python_framework_prefix_opt(python_framework_prefix) + .build_flags(build_flags); + + Ok(builder.finalize()) } /// Import an externally-provided config file. @@ -559,20 +693,19 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let abi3 = abi3.unwrap_or(false); let build_flags = build_flags.unwrap_or_default(); - Ok(InterpreterConfig { - implementation, - version, - shared: shared.unwrap_or(true), - abi3, - lib_name, - lib_dir, - executable, - pointer_width, - build_flags, - suppress_build_script_link_lines: suppress_build_script_link_lines.unwrap_or(false), - extra_build_script_lines, - python_framework_prefix, - }) + let builder = InterpreterConfigBuilder::new(implementation, version) + .abi3(abi3) + .shared(shared.unwrap_or(true)) + .lib_name_opt(lib_name) + .lib_dir_opt(lib_dir) + .executable_opt(executable) + .pointer_width_opt(pointer_width) + .build_flags(build_flags) + .suppress_build_script_link_lines(suppress_build_script_link_lines.unwrap_or(false)) + .extra_build_script_lines(extra_build_script_lines) + .python_framework_prefix_opt(python_framework_prefix); + + Ok(builder.finalize()) } /// Helper function to apply a default lib_name if none is set in `PYO3_CONFIG_FILE`. @@ -722,6 +855,157 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) } } +#[cfg_attr(test, derive(Debug))] +pub struct InterpreterConfigBuilder { + implementation: PythonImplementation, + version: PythonVersion, + shared: Option, + abi3: Option, + lib_name: Option, + lib_dir: Option, + executable: Option, + pointer_width: Option, + build_flags: Option, + suppress_build_script_link_lines: Option, + extra_build_script_lines: Vec, + python_framework_prefix: Option, +} + +impl InterpreterConfigBuilder { + pub fn new( + implementation: PythonImplementation, + version: PythonVersion, + ) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { + implementation, + version, + shared: None, + abi3: None, + lib_name: None, + lib_dir: None, + executable: None, + pointer_width: None, + build_flags: None, + suppress_build_script_link_lines: None, + extra_build_script_lines: vec![], + python_framework_prefix: None, + } + } + + pub fn abi3(mut self, abi3: bool) -> InterpreterConfigBuilder { + self.abi3 = Some(abi3); + self + } + + pub fn lib_name(mut self, lib_name: String) -> InterpreterConfigBuilder { + self.lib_name = Some(lib_name); + self + } + + pub fn pointer_width(mut self, pointer_width: u32) -> InterpreterConfigBuilder { + self.pointer_width = Some(pointer_width); + self + } + + pub fn executable(mut self, executable: String) -> InterpreterConfigBuilder { + self.executable = Some(executable); + self + } + + pub fn suppress_build_script_link_lines( + mut self, + suppress_build_script_link_lines: bool, + ) -> InterpreterConfigBuilder { + self.suppress_build_script_link_lines = Some(suppress_build_script_link_lines); + self + } + + pub fn extra_build_script_lines( + mut self, + extra_build_script_lines: Vec, + ) -> InterpreterConfigBuilder { + self.extra_build_script_lines = extra_build_script_lines; + self + } + + pub fn lib_dir(mut self, lib_dir: String) -> InterpreterConfigBuilder { + self.lib_dir = Some(lib_dir); + self + } + + pub fn shared(mut self, shared: bool) -> InterpreterConfigBuilder { + self.shared = Some(shared); + self + } + + pub fn build_flags(mut self, build_flags: BuildFlags) -> InterpreterConfigBuilder { + self.build_flags = Some(build_flags); + self + } + + pub fn python_framework_prefix( + self, + python_framework_prefix: String, + ) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { + python_framework_prefix: Some(python_framework_prefix), + ..self + } + } + + pub fn finalize(self) -> InterpreterConfig { + #[expect( + deprecated, + reason = "constructing an InterpreterConfig directly, need to write to fields" + )] + InterpreterConfig { + implementation: self.implementation, + version: self.version, + shared: self.shared.unwrap_or(true), + abi3: self.abi3.unwrap_or(false), + lib_name: self.lib_name, + lib_dir: self.lib_dir, + executable: self.executable, + pointer_width: self.pointer_width, + build_flags: self.build_flags.unwrap_or_default(), + suppress_build_script_link_lines: self + .suppress_build_script_link_lines + .unwrap_or(false), + extra_build_script_lines: self.extra_build_script_lines, + python_framework_prefix: self.python_framework_prefix, + } + } + + // private variants of some methods where it's convenient to potentially pass None + fn pointer_width_opt(mut self, pointer_width: Option) -> InterpreterConfigBuilder { + self.pointer_width = pointer_width; + self + } + + fn executable_opt(mut self, executable: Option) -> InterpreterConfigBuilder { + self.executable = executable; + self + } + + fn lib_name_opt(mut self, lib_name: Option) -> InterpreterConfigBuilder { + self.lib_name = lib_name; + self + } + + fn lib_dir_opt(mut self, lib_dir: Option) -> InterpreterConfigBuilder { + self.lib_dir = lib_dir; + self + } + + fn python_framework_prefix_opt( + mut self, + python_framework_prefix: Option, + ) -> InterpreterConfigBuilder { + self.python_framework_prefix = python_framework_prefix; + self + } +} + #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct PythonVersion { pub major: u8, @@ -729,22 +1013,35 @@ pub struct PythonVersion { } impl PythonVersion { - pub const PY315: Self = PythonVersion { - major: 3, - minor: 15, - }; + #[deprecated( + since = "0.29.0", + note = "please construct `PythonVersion` directly rather than use these constants" + )] pub const PY313: Self = PythonVersion { major: 3, minor: 13, }; + #[deprecated( + since = "0.29.0", + note = "please construct `PythonVersion` directly rather than use these constants" + )] pub const PY312: Self = PythonVersion { major: 3, minor: 12, }; + #[cfg(test)] + const PY311: Self = PythonVersion { + major: 3, + minor: 11, + }; const PY310: Self = PythonVersion { major: 3, minor: 10, }; + #[cfg(test)] + const PY39: Self = PythonVersion { major: 3, minor: 9 }; + #[cfg(test)] + const PY38: Self = PythonVersion { major: 3, minor: 8 }; } impl Display for PythonVersion { @@ -1526,6 +1823,7 @@ fn cross_compile_from_sysconfigdata( if let Some(path) = find_sysconfigdata(cross_compile_config)? { let data = parse_sysconfigdata(path)?; let mut config = InterpreterConfig::from_sysconfigdata(&data)?; + #[expect(deprecated, reason = "modifying config inline")] if let Some(cross_lib_dir) = cross_compile_config.lib_dir_string() { config.lib_dir = Some(cross_lib_dir) } @@ -1569,20 +1867,13 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", version.major, version.minor); + #[expect(deprecated, reason = "using constant internally")] + { + ensure!(version >= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", version.major, version.minor); + } if debug { Ok(format!("python{}{}t_d", version.major, version.minor)) } else { @@ -1742,7 +2028,10 @@ fn default_lib_name_unix( if cygwin && abi3 { Ok("python3".to_string()) } else if gil_disabled { - ensure!(version >= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", version.major, version.minor); + #[expect(deprecated, reason = "using constant internally")] + { + ensure!(version >= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", version.major, version.minor); + } Ok(format!("python{}.{}t", version.major, version.minor)) } else { Ok(format!("python{}.{}", version.major, version.minor)) @@ -1968,6 +2257,8 @@ fn unescape(escaped: &str) -> Vec { } #[cfg(test)] +// can remove this expect when fields are private +#[expect(deprecated, reason = "accessing config fields directly for testing")] mod tests { use target_lexicon::triple; @@ -1975,49 +2266,34 @@ mod tests { #[test] fn test_config_file_roundtrip() { - let config = InterpreterConfig { - abi3: true, - build_flags: BuildFlags::default(), - pointer_width: Some(32), - executable: Some("executable".into()), - implementation: PythonImplementation::CPython, - lib_name: Some("lib_name".into()), - lib_dir: Some("lib_dir".into()), - shared: true, - version: MINIMUM_SUPPORTED_VERSION, - suppress_build_script_link_lines: true, - extra_build_script_lines: vec!["cargo:test1".to_string(), "cargo:test2".to_string()], - python_framework_prefix: None, - }; + let implementation = PythonImplementation::CPython; + let version = MINIMUM_SUPPORTED_VERSION; + let config = InterpreterConfigBuilder::new(implementation, version) + .abi3(true) + .pointer_width(32) + .executable("executable".into()) + .lib_dir("lib_name".into()) + .lib_name("lib_name".into()) + .extra_build_script_lines(vec!["cargo:test1".to_string(), "cargo:test2".to_string()]) + .finalize(); let mut buf: Vec = Vec::new(); config.to_writer(&mut buf).unwrap(); assert_eq!(config, InterpreterConfig::from_reader(&*buf).unwrap()); // And some different options, for variety - - let config = InterpreterConfig { - abi3: false, - build_flags: { - let mut flags = HashSet::new(); - flags.insert(BuildFlag::Py_DEBUG); - flags.insert(BuildFlag::Other(String::from("Py_SOME_FLAG"))); - BuildFlags(flags) - }, - pointer_width: None, - executable: None, - implementation: PythonImplementation::PyPy, - lib_dir: None, - lib_name: None, - shared: true, - version: PythonVersion { - major: 3, - minor: 10, - }, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, + let version = PythonVersion::PY310; + let implementation = PythonImplementation::PyPy; + let build_flags = { + let mut flags = HashSet::new(); + flags.insert(BuildFlag::Py_DEBUG); + flags.insert(BuildFlag::Other(String::from("Py_SOME_FLAG"))); + BuildFlags(flags) }; + let config = InterpreterConfigBuilder::new(implementation, version) + .build_flags(build_flags) + .finalize(); + let mut buf: Vec = Vec::new(); config.to_writer(&mut buf).unwrap(); @@ -2026,20 +2302,16 @@ mod tests { #[test] fn test_config_file_roundtrip_with_escaping() { - let config = InterpreterConfig { - abi3: true, - build_flags: BuildFlags::default(), - pointer_width: Some(32), - executable: Some("executable".into()), - implementation: PythonImplementation::CPython, - lib_name: Some("lib_name".into()), - lib_dir: Some("lib_dir\\n".into()), - shared: true, - version: MINIMUM_SUPPORTED_VERSION, - suppress_build_script_link_lines: true, - extra_build_script_lines: vec!["cargo:test1".to_string(), "cargo:test2".to_string()], - python_framework_prefix: None, - }; + let implementation = PythonImplementation::CPython; + let version = MINIMUM_SUPPORTED_VERSION; + let config = InterpreterConfigBuilder::new(implementation, version) + .abi3(true) + .pointer_width(32) + .executable("executable".into()) + .lib_name("lib_name".into()) + .lib_dir("lib_dir\\n".into()) + .extra_build_script_lines(vec!["cargo:test1".to_string(), "cargo:test2".to_string()]) + .finalize(); let mut buf: Vec = Vec::new(); config.to_writer(&mut buf).unwrap(); @@ -2051,45 +2323,23 @@ mod tests { #[test] fn test_config_file_defaults() { // Only version is required + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY38; assert_eq!( InterpreterConfig::from_reader("version=3.8".as_bytes()).unwrap(), - InterpreterConfig { - version: PythonVersion { major: 3, minor: 8 }, - implementation: PythonImplementation::CPython, - shared: true, - abi3: false, - lib_name: None, - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } + InterpreterConfigBuilder::new(implementation, version,).finalize() ) } #[test] fn test_config_file_unknown_keys() { // ext_suffix is unknown to pyo3-build-config, but it shouldn't error + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY38; assert_eq!( InterpreterConfig::from_reader("version=3.8\next_suffix=.python38.so".as_bytes()) .unwrap(), - InterpreterConfig { - version: PythonVersion { major: 3, minor: 8 }, - implementation: PythonImplementation::CPython, - shared: true, - abi3: false, - lib_name: None, - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } + InterpreterConfigBuilder::new(implementation, version,).finalize() ) } @@ -2177,22 +2427,16 @@ mod tests { sysconfigdata.insert("LIBDIR", "/usr/lib"); sysconfigdata.insert("LDVERSION", "3.8"); sysconfigdata.insert("SIZEOF_VOID_P", "8"); + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY38; assert_eq!( InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), - InterpreterConfig { - abi3: false, - build_flags: BuildFlags::from_sysconfigdata(&sysconfigdata), - pointer_width: Some(64), - executable: None, - implementation: PythonImplementation::CPython, - lib_dir: Some("/usr/lib".into()), - lib_name: Some("python3.8".into()), - shared: true, - version: PythonVersion { major: 3, minor: 8 }, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } + InterpreterConfigBuilder::new(implementation, version,) + .build_flags(BuildFlags::from_sysconfigdata(&sysconfigdata)) + .lib_dir("/usr/lib".into()) + .lib_name("python3.8".into()) + .pointer_width(64) + .finalize() ); } @@ -2207,22 +2451,16 @@ mod tests { sysconfigdata.insert("LIBDIR", "/usr/lib"); sysconfigdata.insert("LDVERSION", "3.8"); sysconfigdata.insert("SIZEOF_VOID_P", "8"); + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY38; assert_eq!( InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), - InterpreterConfig { - abi3: false, - build_flags: BuildFlags::from_sysconfigdata(&sysconfigdata), - pointer_width: Some(64), - executable: None, - implementation: PythonImplementation::CPython, - lib_dir: Some("/usr/lib".into()), - lib_name: Some("python3.8".into()), - shared: true, - version: PythonVersion { major: 3, minor: 8 }, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } + InterpreterConfigBuilder::new(implementation, version,) + .build_flags(BuildFlags::from_sysconfigdata(&sysconfigdata)) + .lib_dir("/usr/lib".into()) + .lib_name("python3.8".into()) + .pointer_width(64) + .finalize() ); sysconfigdata = Sysconfigdata::new(); @@ -2234,22 +2472,17 @@ mod tests { sysconfigdata.insert("LIBDIR", "/usr/lib"); sysconfigdata.insert("LDVERSION", "3.8"); sysconfigdata.insert("SIZEOF_VOID_P", "8"); + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY38; assert_eq!( InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), - InterpreterConfig { - abi3: false, - build_flags: BuildFlags::from_sysconfigdata(&sysconfigdata), - pointer_width: Some(64), - executable: None, - implementation: PythonImplementation::CPython, - lib_dir: Some("/usr/lib".into()), - lib_name: Some("python3.8".into()), - shared: false, - version: PythonVersion { major: 3, minor: 8 }, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } + InterpreterConfigBuilder::new(implementation, version,) + .build_flags(BuildFlags::from_sysconfigdata(&sysconfigdata)) + .lib_dir("/usr/lib".into()) + .lib_name("python3.8".into()) + .pointer_width(64) + .shared(false) + .finalize() ); } @@ -2258,47 +2491,25 @@ mod tests { let host = triple!("x86_64-pc-windows-msvc"); let min_version = "3.8".parse().unwrap(); - assert_eq!( - default_abi3_config(&host, min_version).unwrap(), - InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 8 }, - shared: true, - abi3: true, - lib_name: Some("python3".into()), - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } - ); + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY38; + let config = InterpreterConfigBuilder::new(implementation, version) + .abi3(true) + .lib_name("python3".into()) + .finalize(); + assert_eq!(default_abi3_config(&host, min_version).unwrap(), config); } #[test] fn unix_hardcoded_abi3_compile() { let host = triple!("x86_64-unknown-linux-gnu"); let min_version = "3.9".parse().unwrap(); - - assert_eq!( - default_abi3_config(&host, min_version).unwrap(), - InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 9 }, - shared: true, - abi3: true, - lib_name: None, - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } - ); + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY39; + let config = InterpreterConfigBuilder::new(implementation, version) + .abi3(true) + .finalize(); + assert_eq!(default_abi3_config(&host, min_version).unwrap(), config); } #[test] @@ -2317,23 +2528,13 @@ mod tests { .unwrap() .unwrap(); - assert_eq!( - default_cross_compile(&cross_config).unwrap(), - InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 8 }, - shared: true, - abi3: false, - lib_name: Some("python38".into()), - lib_dir: Some("C:\\some\\path".into()), - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } - ); + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY38; + let config = InterpreterConfigBuilder::new(implementation, version) + .lib_name("python38".into()) + .lib_dir("C:\\some\\path".into()) + .finalize(); + assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } #[test] @@ -2352,23 +2553,13 @@ mod tests { .unwrap() .unwrap(); - assert_eq!( - default_cross_compile(&cross_config).unwrap(), - InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 8 }, - shared: true, - abi3: false, - lib_name: Some("python38".into()), - lib_dir: Some("/usr/lib/mingw".into()), - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } - ); + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY38; + let config = InterpreterConfigBuilder::new(implementation, version) + .lib_name("python38".into()) + .lib_dir("/usr/lib/mingw".into()) + .finalize(); + assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } #[test] @@ -2387,23 +2578,13 @@ mod tests { .unwrap() .unwrap(); - assert_eq!( - default_cross_compile(&cross_config).unwrap(), - InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 9 }, - shared: true, - abi3: false, - lib_name: Some("python3.9".into()), - lib_dir: Some("/usr/arm64/lib".into()), - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } - ); + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY39; + let config = InterpreterConfigBuilder::new(implementation, version) + .lib_name("python3.9".into()) + .lib_dir("/usr/arm64/lib".into()) + .finalize(); + assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } #[test] @@ -2421,26 +2602,12 @@ mod tests { .unwrap() .unwrap(); - assert_eq!( - default_cross_compile(&cross_config).unwrap(), - InterpreterConfig { - implementation: PythonImplementation::PyPy, - version: PythonVersion { - major: 3, - minor: 11 - }, - shared: true, - abi3: false, - lib_name: Some("pypy3.11-c".into()), - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } - ); + let implementation = PythonImplementation::PyPy; + let version = PythonVersion::PY311; + let config = InterpreterConfigBuilder::new(implementation, version) + .lib_name("pypy3.11-c".into()) + .finalize(); + assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } #[test] @@ -2816,47 +2983,31 @@ mod tests { #[test] fn interpreter_version_reduced_to_abi3() { - let mut config = InterpreterConfig { - abi3: true, - build_flags: BuildFlags::default(), - pointer_width: None, - executable: None, - implementation: PythonImplementation::CPython, - lib_dir: None, - lib_name: None, - shared: true, + let builder = InterpreterConfigBuilder::new( + PythonImplementation::CPython, // Make this greater than the target abi3 version to reduce to below - version: PythonVersion { major: 3, minor: 9 }, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - }; + PythonVersion::PY39, + ) + .abi3(true); + + let mut config = builder.finalize(); config - .fixup_for_abi3_version(Some(PythonVersion { major: 3, minor: 8 })) + .fixup_for_abi3_version(Some(PythonVersion::PY38)) .unwrap(); - assert_eq!(config.version, PythonVersion { major: 3, minor: 8 }); + assert_eq!(config.version(), PythonVersion::PY38); } #[test] fn abi3_version_cannot_be_higher_than_interpreter() { - let mut config = InterpreterConfig { - abi3: true, - build_flags: BuildFlags::new(), - pointer_width: None, - executable: None, - implementation: PythonImplementation::CPython, - lib_dir: None, - lib_name: None, - shared: true, - version: PythonVersion { major: 3, minor: 8 }, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - }; + let builder = + InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) + .abi3(true); + + let mut config = builder.finalize(); assert!(config - .fixup_for_abi3_version(Some(PythonVersion { major: 3, minor: 9 })) + .fixup_for_abi3_version(Some(PythonVersion::PY39)) .unwrap_err() .to_string() .contains( @@ -2901,33 +3052,22 @@ mod tests { _ => return, }; let sysconfigdata = super::parse_sysconfigdata(sysconfigdata_path).unwrap(); - let mut parsed_config = InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(); - - // Workaround case where empty `PYTHONFRAMEWORKPREFIX` is returned as empty string instead of None, - // which causes the assert_eq! below to fail. - // - // TODO: probably should deprecate using this variable at all, seemingly only used in `add_python_framework_link_args` - // which is probably a strictly worse version of `add_libpython_rpath_link_args`. - if parsed_config.python_framework_prefix.as_deref() == Some("") { - parsed_config.python_framework_prefix = None; - } + let parsed_config = InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(); + let implementation = PythonImplementation::CPython; assert_eq!( parsed_config, - InterpreterConfig { - abi3: false, - build_flags: BuildFlags(interpreter_config.build_flags.0.clone()), - pointer_width: Some(64), - executable: None, - implementation: PythonImplementation::CPython, - lib_dir: interpreter_config.lib_dir.to_owned(), - lib_name: interpreter_config.lib_name.to_owned(), - shared: true, - version: interpreter_config.version, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } + InterpreterConfigBuilder::new( + implementation, + interpreter_config.version, + PythonAbiBuilder::new(implementation, interpreter_config.version).finalize() + ) + .build_flags(interpreter_config.build_flags.0.clone()) + .pointer_width(64) + .lib_dir(interpreter_config.lib_dir.to_owned()) + .lib_name(interpreter_config.lib_name.to_owned()) + .finalize() + .unwrap() ) } @@ -3046,23 +3186,9 @@ mod tests { #[test] fn test_build_script_outputs_base() { - let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { - major: 3, - minor: 11, - }, - shared: true, - abi3: false, - lib_name: Some("python3".into()), - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - }; + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY311; + let interpreter_config = InterpreterConfigBuilder::new(implementation, version).finalize(); assert_eq!( interpreter_config.build_script_outputs(), [ @@ -3073,10 +3199,8 @@ mod tests { ] ); - let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::PyPy, - ..interpreter_config - }; + let interpreter_config = + InterpreterConfigBuilder::new(PythonImplementation::PyPy, version).finalize(); assert_eq!( interpreter_config.build_script_outputs(), [ @@ -3091,20 +3215,11 @@ mod tests { #[test] fn test_build_script_outputs_abi3() { - let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 9 }, - shared: true, - abi3: true, - lib_name: Some("python3".into()), - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - }; + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY39; + let interpreter_config = InterpreterConfigBuilder::new(implementation, version) + .abi3(true) + .finalize(); assert_eq!( interpreter_config.build_script_outputs(), @@ -3115,10 +3230,9 @@ mod tests { ] ); - let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::PyPy, - ..interpreter_config - }; + let interpreter_config = InterpreterConfigBuilder::new(PythonImplementation::PyPy, version) + .abi3(true) + .finalize(); assert_eq!( interpreter_config.build_script_outputs(), [ @@ -3134,24 +3248,11 @@ mod tests { fn test_build_script_outputs_gil_disabled() { let mut build_flags = BuildFlags::default(); build_flags.0.insert(BuildFlag::Py_GIL_DISABLED); - let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { - major: 3, - minor: 13, - }, - shared: true, - abi3: false, - lib_name: Some("python3".into()), - lib_dir: None, - executable: None, - pointer_width: None, - build_flags, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - }; - + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY313; + let interpreter_config = InterpreterConfigBuilder::new(implementation, version) + .build_flags(build_flags) + .finalize(); assert_eq!( interpreter_config.build_script_outputs(), [ @@ -3170,21 +3271,11 @@ mod tests { fn test_build_script_outputs_debug() { let mut build_flags = BuildFlags::default(); build_flags.0.insert(BuildFlag::Py_DEBUG); - let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 8 }, - shared: true, - abi3: false, - lib_name: Some("python3".into()), - lib_dir: None, - executable: None, - pointer_width: None, - build_flags, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - }; - + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY38; + let interpreter_config = InterpreterConfigBuilder::new(implementation, version) + .build_flags(build_flags) + .finalize(); assert_eq!( interpreter_config.build_script_outputs(), [ @@ -3225,35 +3316,24 @@ mod tests { #[test] fn test_apply_default_lib_name_to_config_file() { - let mut config = InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 9 }, - shared: true, - abi3: false, - lib_name: None, - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - }; + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY39; + let mut config = InterpreterConfigBuilder::new(implementation, version).finalize(); let unix = Triple::from_str("x86_64-unknown-linux-gnu").unwrap(); let win_x64 = Triple::from_str("x86_64-pc-windows-msvc").unwrap(); let win_arm64 = Triple::from_str("aarch64-pc-windows-msvc").unwrap(); config.apply_default_lib_name_to_config_file(&unix); - assert_eq!(config.lib_name, Some("python3.9".into())); + assert_eq!(config.lib_name(), Some("python3.9".into())); config.lib_name = None; config.apply_default_lib_name_to_config_file(&win_x64); - assert_eq!(config.lib_name, Some("python39".into())); + assert_eq!(config.lib_name(), Some("python39".into())); config.lib_name = None; config.apply_default_lib_name_to_config_file(&win_arm64); - assert_eq!(config.lib_name, Some("python39".into())); + assert_eq!(config.lib_name(), Some("python39".into())); // PyPy config.implementation = PythonImplementation::PyPy; diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index fb56f848b94..0dd4a850a51 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -14,7 +14,8 @@ use std::{env, process::Command, str::FromStr, sync::LazyLock}; pub use impl_::{ cross_compiling_from_to, find_all_sysconfigdata, parse_sysconfigdata, BuildFlag, BuildFlags, - CrossCompileConfig, InterpreterConfig, PythonImplementation, PythonVersion, Triple, + CrossCompileConfig, InterpreterConfig, InterpreterConfigBuilder, PythonImplementation, + PythonVersion, Triple, }; use target_lexicon::OperatingSystem; @@ -107,7 +108,7 @@ fn _add_libpython_rpath_link_args( mut writer: impl std::io::Write, ) { if is_linking_libpython { - if let Some(lib_dir) = interpreter_config.lib_dir.as_ref() { + if let Some(lib_dir) = interpreter_config.lib_dir() { writeln!(writer, "cargo:rustc-link-arg=-Wl,-rpath,{lib_dir}").unwrap(); } } @@ -138,7 +139,7 @@ fn _add_python_framework_link_args( mut writer: impl std::io::Write, ) { if matches!(triple.operating_system, OperatingSystem::Darwin(_)) && link_libpython { - if let Some(framework_prefix) = interpreter_config.python_framework_prefix.as_ref() { + if let Some(framework_prefix) = interpreter_config.python_framework_prefix() { writeln!(writer, "cargo:rustc-link-arg=-Wl,-rpath,{framework_prefix}").unwrap(); } } @@ -303,13 +304,13 @@ pub mod pyo3_build_script_impl { interpreter_config: &InterpreterConfig, supported_version: PythonVersion, ) -> Self { - let implementation = match interpreter_config.implementation { + let implementation = match interpreter_config.implementation() { PythonImplementation::CPython => "Python", PythonImplementation::PyPy => "PyPy", PythonImplementation::GraalPy => "GraalPy", PythonImplementation::RustPython => "RustPython", }; - let version = &interpreter_config.version; + let version = interpreter_config.version(); let message = format!( "the configured {implementation} version ({version}) is newer than PyO3's maximum supported version ({supported_version})\n\ = help: this package is being built with PyO3 version {current_version}\n\ @@ -346,6 +347,7 @@ fn rustc_minor_version() -> Option { } #[cfg(test)] +#[expect(deprecated, reason = "accessing config directly")] mod tests { use crate::impl_::escape; @@ -397,26 +399,14 @@ mod tests { #[test] fn python_framework_link_args() { let mut buf = Vec::new(); + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY313; + let interpreter_config = InterpreterConfigBuilder::new(implementation, version) + .python_framework_prefix( + "/Applications/Xcode.app/Contents/Developer/Library/Frameworks".into(), + ) + .finalize(); - let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { - major: 3, - minor: 13, - }, - shared: true, - abi3: false, - lib_name: None, - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: Some( - "/Applications/Xcode.app/Contents/Developer/Library/Frameworks".to_string(), - ), - }; // Does nothing on non-mac _add_python_framework_link_args( &interpreter_config, @@ -440,29 +430,12 @@ mod tests { #[test] fn test_maximum_version_exceeded_formatting() { - let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { - major: 3, - minor: 13, - }, - shared: true, - abi3: false, - lib_name: None, - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - }; + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY313; + let interpreter_config = InterpreterConfigBuilder::new(implementation, version).finalize(); let mut error = pyo3_build_script_impl::MaximumVersionExceeded::new( &interpreter_config, - PythonVersion { - major: 3, - minor: 12, - }, + PythonVersion::PY312, ); error.add_help("this is a help message"); let error = error.finish(); @@ -481,23 +454,9 @@ mod tests { // There should be no other tests or config in the environment assert!(InterpreterConfig::from_cargo_dep_env().is_none()); - let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { - major: 3, - minor: 13, - }, - shared: true, - abi3: false, - lib_name: None, - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - }; + let interpreter_config = + InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) + .finalize(); let mut buf = Vec::new(); interpreter_config.to_writer(&mut buf).unwrap(); let config_string = escape(&buf); diff --git a/pyo3-ffi-check/definitions/build.rs b/pyo3-ffi-check/definitions/build.rs index 80e5a37fa56..992da8a360c 100644 --- a/pyo3-ffi-check/definitions/build.rs +++ b/pyo3-ffi-check/definitions/build.rs @@ -64,7 +64,7 @@ fn main() { .parse_callbacks(Box::new(ParseCallbacks)); if matches!( - config.implementation, + config.implementation(), pyo3_build_config::PythonImplementation::PyPy ) { builder = builder.parse_callbacks(Box::new(PyPyReplaceCallbacks)); diff --git a/pyo3-ffi-check/macro/src/lib.rs b/pyo3-ffi-check/macro/src/lib.rs index 7151e938c2a..1aa9b9f2f37 100644 --- a/pyo3-ffi-check/macro/src/lib.rs +++ b/pyo3-ffi-check/macro/src/lib.rs @@ -9,6 +9,16 @@ use proc_macro2::{Ident, Span, TokenStream, TokenTree}; use pyo3_build_config::PythonVersion; use quote::quote; +const PY_3_15: PythonVersion = PythonVersion { + major: 3, + minor: 15, +}; + +const PY_3_12: PythonVersion = PythonVersion { + major: 3, + minor: 12, +}; + /// Macro which expands to multiple macro calls, one per pyo3-ffi struct. #[proc_macro] pub fn for_all_structs(input: proc_macro::TokenStream) -> proc_macro::TokenStream { @@ -34,8 +44,7 @@ pub fn for_all_structs(input: proc_macro::TokenStream) -> proc_macro::TokenStrea .strip_suffix(".html") .unwrap(); - if pyo3_build_config::get().version < PythonVersion::PY315 && struct_name == "PyBytesWriter" - { + if pyo3_build_config::get().version() < PY_3_15 && struct_name == "PyBytesWriter" { // PyBytesWriter was added in Python 3.15 continue; } @@ -158,8 +167,7 @@ pub fn for_all_fields(input: proc_macro::TokenStream) -> proc_macro::TokenStream if struct_name == "PyMemberDef" { // bindgen picked `type_` as the field name to avoid the `type` keyword, but PyO3 uses `type_code` all_fields.remove("type_"); - } else if struct_name == "PyObject" && pyo3_build_config::get().version >= PythonVersion::PY312 - { + } else if struct_name == "PyObject" && pyo3_build_config::get().version() >= PY_3_12 { // bindgen picked `__bindgen_anon_1` as the field name for the anonymous union containing ob_refcnt, // PyO3 uses ob_refcnt directly all_fields.remove("__bindgen_anon_1"); @@ -176,7 +184,7 @@ pub fn for_all_fields(input: proc_macro::TokenStream) -> proc_macro::TokenStream let field_ident = Ident::new(&field_name, Span::call_site()); - let bindgen_field_ident = if (pyo3_build_config::get().version >= PythonVersion::PY312) + let bindgen_field_ident = if (pyo3_build_config::get().version() >= PY_3_12) && struct_name == "PyObject" && field_name == "ob_refcnt" { @@ -551,7 +559,8 @@ pub fn for_all_functions(_input: proc_macro::TokenStream) -> proc_macro::TokenSt continue; } - if pyo3_build_config::get().implementation == pyo3_build_config::PythonImplementation::PyPy + if pyo3_build_config::get().implementation() + == pyo3_build_config::PythonImplementation::PyPy { // If the function doesn't exist in PyPy, for now we don't care: // - For PyO3 inline functions it's probably fine to include anyway diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index e59b7ba5eeb..d60dc34ff8b 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -45,27 +45,27 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { return Ok(()); } - match interpreter_config.implementation { + match interpreter_config.implementation() { PythonImplementation::CPython => { let versions = SUPPORTED_VERSIONS_CPYTHON; ensure!( - interpreter_config.version >= versions.min, + interpreter_config.version() >= versions.min, "the configured Python interpreter version ({}) is lower than PyO3's minimum supported version ({})", - interpreter_config.version, + interpreter_config.version(), versions.min, ); let v_plus_1 = PythonVersion { major: versions.max.major, minor: versions.max.minor + 1, }; - if interpreter_config.version == v_plus_1 { + if interpreter_config.version() == v_plus_1 { warn!( "Using experimental support for the Python {}.{} ABI. \ Build artifacts may not be compatible with the final release of CPython, \ so do not distribute them.", v_plus_1.major, v_plus_1.minor, ); - } else if interpreter_config.version > v_plus_1 { + } 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( @@ -87,23 +87,23 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { minor: 14, }; ensure!( - interpreter_config.version >= min_free_threaded_version, + interpreter_config.version() >= min_free_threaded_version, "PyO3 does not support the free-threaded build of CPython versions below {}, the selected Python version is {}", min_free_threaded_version, - interpreter_config.version, + interpreter_config.version(), ); } } PythonImplementation::PyPy => { let versions = SUPPORTED_VERSIONS_PYPY; ensure!( - interpreter_config.version >= versions.min, + interpreter_config.version() >= versions.min, "the configured PyPy interpreter version ({}) is lower than PyO3's minimum supported version ({})", - interpreter_config.version, + interpreter_config.version(), versions.min, ); // PyO3 does not support abi3, so we cannot offer forward compatibility - if interpreter_config.version > versions.max { + if interpreter_config.version() > versions.max { let error = MaximumVersionExceeded::new(interpreter_config, versions.max); return Err(error.finish().into()); } @@ -111,13 +111,13 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { PythonImplementation::GraalPy => { let versions = SUPPORTED_VERSIONS_GRAALPY; ensure!( - interpreter_config.version >= versions.min, + interpreter_config.version() >= versions.min, "the configured GraalPy interpreter version ({}) is lower than PyO3's minimum supported version ({})", - interpreter_config.version, + interpreter_config.version(), versions.min, ); // GraalPy does not support abi3, so we cannot offer forward compatibility - if interpreter_config.version > versions.max { + if interpreter_config.version() > versions.max { let error = MaximumVersionExceeded::new(interpreter_config, versions.max); return Err(error.finish().into()); } @@ -125,8 +125,8 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { PythonImplementation::RustPython => {} } - if interpreter_config.abi3 { - match interpreter_config.implementation { + if interpreter_config.abi3() { + match interpreter_config.implementation() { PythonImplementation::CPython => { if interpreter_config.is_free_threaded() { warn!( @@ -149,7 +149,7 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { } fn ensure_target_pointer_width(interpreter_config: &InterpreterConfig) -> Result<()> { - if let Some(pointer_width) = interpreter_config.pointer_width { + if let Some(pointer_width) = interpreter_config.pointer_width() { // Try to check whether the target architecture matches the python library let rust_target = match cargo_env_var("CARGO_CFG_TARGET_POINTER_WIDTH") .unwrap() @@ -175,8 +175,7 @@ fn emit_link_config(build_config: &BuildConfig) -> Result<()> { let target_os = cargo_env_var("CARGO_CFG_TARGET_OS").unwrap(); let lib_name = interpreter_config - .lib_name - .as_ref() + .lib_name() .ok_or("attempted to link to Python shared library but config does not contain lib_name")?; if target_os == "windows" { @@ -191,14 +190,14 @@ fn emit_link_config(build_config: &BuildConfig) -> Result<()> { } else { println!( "cargo:rustc-link-lib={link_model}{lib_name}", - link_model = if interpreter_config.shared { + link_model = if interpreter_config.shared() { "" } else { "static=" }, ); - if let Some(lib_dir) = &interpreter_config.lib_dir { + if let Some(lib_dir) = interpreter_config.lib_dir() { println!("cargo:rustc-link-search=native={lib_dir}"); } else if matches!(build_config.source, BuildConfigSource::CrossCompile) { warn!( @@ -236,7 +235,7 @@ fn configure_pyo3() -> Result<()> { interpreter_config.to_cargo_dep_env()?; if is_linking_libpython_for_target(&target) - && !interpreter_config.suppress_build_script_link_lines + && !interpreter_config.suppress_build_script_link_lines() { emit_link_config(&build_config)?; } @@ -246,7 +245,7 @@ fn configure_pyo3() -> Result<()> { } // Extra lines come last, to support last write wins. - for line in &interpreter_config.extra_build_script_lines { + for line in interpreter_config.extra_build_script_lines() { println!("{line}"); } From 781648bebe34d3adfc5abc90d7f3471af39ebc2d Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 12 May 2026 08:33:23 +0100 Subject: [PATCH 153/195] newsfragments --- newsfragments/6034.added.md | 1 + newsfragments/6034.changed.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 newsfragments/6034.added.md create mode 100644 newsfragments/6034.changed.md diff --git a/newsfragments/6034.added.md b/newsfragments/6034.added.md new file mode 100644 index 00000000000..a64fa6cfa6e --- /dev/null +++ b/newsfragments/6034.added.md @@ -0,0 +1 @@ +Add `pyo3_build_config::InterpreterConfigBuilder`. diff --git a/newsfragments/6034.changed.md b/newsfragments/6034.changed.md new file mode 100644 index 00000000000..055a1f3bd75 --- /dev/null +++ b/newsfragments/6034.changed.md @@ -0,0 +1 @@ +Deprecate direct access to all `pyo3_build_config::InterpreterConfig` fields; getter methods have been added as replacements. From f091c9a1b7b778f62db6992223310dd7cff9e62f Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 12 May 2026 08:53:46 +0100 Subject: [PATCH 154/195] fixup ubuntu test --- pyo3-build-config/src/impl_.rs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index b60d9ae1ba6..50fe06d917e 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -3057,17 +3057,12 @@ mod tests { assert_eq!( parsed_config, - InterpreterConfigBuilder::new( - implementation, - interpreter_config.version, - PythonAbiBuilder::new(implementation, interpreter_config.version).finalize() - ) - .build_flags(interpreter_config.build_flags.0.clone()) - .pointer_width(64) - .lib_dir(interpreter_config.lib_dir.to_owned()) - .lib_name(interpreter_config.lib_name.to_owned()) - .finalize() - .unwrap() + InterpreterConfigBuilder::new(implementation, interpreter_config.version,) + .build_flags(interpreter_config.build_flags().clone()) + .pointer_width(64) + .lib_dir_opt(interpreter_config.lib_dir().map(str::to_owned)) + .lib_name_opt(interpreter_config.lib_name().map(str::to_owned)) + .finalize() ) } From ef4fb922202d55923ce81245e9ce0e04266dcf0f Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 12 May 2026 11:34:00 +0100 Subject: [PATCH 155/195] fixup --- pyo3-build-config/src/impl_.rs | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 50fe06d917e..4c0ee838036 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -3053,16 +3053,27 @@ mod tests { }; let sysconfigdata = super::parse_sysconfigdata(sysconfigdata_path).unwrap(); let parsed_config = InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(); - let implementation = PythonImplementation::CPython; + + // Workaround case where empty `PYTHONFRAMEWORKPREFIX` is returned as empty string instead of None, + // which causes the assert_eq! below to fail. + // + // TODO: probably should deprecate using this variable at all, seemingly only used in `add_python_framework_link_args` + // which is probably a strictly worse version of `add_libpython_rpath_link_args`. + if parsed_config.python_framework_prefix.as_deref() == Some("") { + parsed_config.python_framework_prefix = None; + } assert_eq!( parsed_config, - InterpreterConfigBuilder::new(implementation, interpreter_config.version,) - .build_flags(interpreter_config.build_flags().clone()) - .pointer_width(64) - .lib_dir_opt(interpreter_config.lib_dir().map(str::to_owned)) - .lib_name_opt(interpreter_config.lib_name().map(str::to_owned)) - .finalize() + InterpreterConfigBuilder::new( + interpreter_config.implementation, + interpreter_config.version, + ) + .build_flags(interpreter_config.build_flags().clone()) + .pointer_width(64) + .lib_dir_opt(interpreter_config.lib_dir().map(str::to_owned)) + .lib_name_opt(interpreter_config.lib_name().map(str::to_owned)) + .finalize() ) } @@ -3320,15 +3331,15 @@ mod tests { let win_arm64 = Triple::from_str("aarch64-pc-windows-msvc").unwrap(); config.apply_default_lib_name_to_config_file(&unix); - assert_eq!(config.lib_name(), Some("python3.9".into())); + assert_eq!(config.lib_name, Some("python3.9".into())); config.lib_name = None; config.apply_default_lib_name_to_config_file(&win_x64); - assert_eq!(config.lib_name(), Some("python39".into())); + assert_eq!(config.lib_name, Some("python39".into())); config.lib_name = None; config.apply_default_lib_name_to_config_file(&win_arm64); - assert_eq!(config.lib_name(), Some("python39".into())); + assert_eq!(config.lib_name, Some("python39".into())); // PyPy config.implementation = PythonImplementation::PyPy; From 7ee1ed07aef82d3fdcf8205087f35b43c8cec5fc Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 12 May 2026 14:44:19 +0100 Subject: [PATCH 156/195] fix missing `mut` --- 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 4c0ee838036..ba10ee4a96e 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -3052,7 +3052,7 @@ mod tests { _ => return, }; let sysconfigdata = super::parse_sysconfigdata(sysconfigdata_path).unwrap(); - let parsed_config = InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(); + let mut parsed_config = InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(); // Workaround case where empty `PYTHONFRAMEWORKPREFIX` is returned as empty string instead of None, // which causes the assert_eq! below to fail. From f509e72690f7fe4ea7ab1c8c16e3728406ca29ed Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 12 May 2026 08:20:25 -0600 Subject: [PATCH 157/195] fix merge error --- pyo3-ffi/src/slots.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyo3-ffi/src/slots.rs b/pyo3-ffi/src/slots.rs index 8e8f52fd1de..86b3912eb8f 100644 --- a/pyo3-ffi/src/slots.rs +++ b/pyo3-ffi/src/slots.rs @@ -78,7 +78,7 @@ pub const unsafe fn PySlot_FUNC(NAME: c_int, VALUE: *mut c_void) -> PySlot { sl_flags: 0, anon1: _anon_union_32b { sl_reserved: 0 }, anon2: _anon_union_64b { - sl_func: unsafe { std::mem::transmute::<*mut c_void, _Py_funcptr_t>(VALUE) }, + sl_func: unsafe { core::mem::transmute::<*mut c_void, _Py_funcptr_t>(VALUE) }, }, } } From f8aa9a53aaaaf489ec3908355b31e99f77f719b6 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 12 May 2026 08:20:52 -0600 Subject: [PATCH 158/195] fix incorrect merge conflict resolution --- pyo3-build-config/src/impl_.rs | 79 +++++++++++++++++++++++++--------- 1 file changed, 59 insertions(+), 20 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 15a017fbb91..b0c09548e7a 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -78,10 +78,30 @@ pub fn target_triple_from_env() -> Triple { .expect("Unrecognized TARGET environment variable value") } +fn sanitize_stable_abi_version( + stable_abi_version: Option, + version: PythonVersion, +) -> Result { + if let Some(min_version) = stable_abi_version { + ensure!( + min_version <= version, + "cannot set a minimum Python version {} higher than the interpreter version {} \ + (the minimum Python version is implied by the abi3-py3{} feature)", + min_version, + version, + min_version.minor + ); + Ok(min_version) + } else { + Ok(version) + } +} + fn apply_build_env_to_config(mut config: InterpreterConfig) -> Result { let mut abi_builder = PythonAbiBuilder::from_build_env( config.implementation, config.version, + get_abi3_version().or(get_abi3t_version()), config.target_abi.kind.is_free_threaded(), )?; // only allow free-threaded builds if the build environment didn't force an abi3 build @@ -249,7 +269,10 @@ impl InterpreterConfig { out } - fn from_interpreter(interpreter: impl AsRef) -> Result { + fn from_interpreter( + interpreter: impl AsRef, + abi3_version: Option, + ) -> Result { const SCRIPT: &str = r#" # Allow the script to run on Python 2, so that nicer error can be printed later. from __future__ import print_function @@ -357,7 +380,8 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) }; let target_abi = - PythonAbiBuilder::from_sysconfig(implementation, version, gil_disabled)?.finalize(); + PythonAbiBuilder::from_build_env(implementation, version, abi3_version, gil_disabled)? + .finalize(); let cygwin = map["cygwin"].as_str() == "True"; @@ -450,7 +474,8 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) }; let cygwin = soabi.ends_with("cygwin"); let target_abi = - PythonAbiBuilder::from_sysconfig(implementation, version, gil_disabled)?.finalize(); + PythonAbiBuilder::from_build_env(implementation, version, None, gil_disabled)? + .finalize(); let lib_name = Some(default_lib_name_unix( target_abi, cygwin, @@ -957,11 +982,12 @@ impl PythonAbiBuilder { pub fn from_build_env( implementation: PythonImplementation, version: PythonVersion, + stable_abi_version: Option, gil_disabled: bool, ) -> Result { let builder = PythonAbiBuilder { implementation, - version, + version: sanitize_stable_abi_version(stable_abi_version, version)?, kind: None, }; if is_abi3() && !gil_disabled { @@ -975,19 +1001,6 @@ impl PythonAbiBuilder { } } - pub fn from_sysconfig( - implementation: PythonImplementation, - version: PythonVersion, - gil_disabled: bool, - ) -> Result { - if gil_disabled { - // TODO: fall back to abi3t builds on 3.15t and newer - PythonAbiBuilder::new(implementation, version).free_threaded() - } else { - PythonAbiBuilder::from_build_env(implementation, version, gil_disabled) - } - } - pub fn stable_abi(self, kind: StableAbi) -> Result { ensure!( self.kind.is_none(), @@ -2034,7 +2047,8 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result { /// Locates and extracts the build host Python interpreter configuration. /// /// Lowers the configured Python version to `abi3_version` if required. -fn get_host_interpreter(_stable_abi_version: Option) -> Result { +fn get_host_interpreter(stable_abi_version: Option) -> Result { let interpreter_path = find_interpreter()?; - let interpreter_config = InterpreterConfig::from_interpreter(interpreter_path)?; + let interpreter_config = + InterpreterConfig::from_interpreter(interpreter_path, stable_abi_version)?; Ok(interpreter_config) } @@ -3298,6 +3313,30 @@ mod tests { assert_eq!(config.version, host_version); } + #[test] + fn abi3_version_cannot_be_higher_than_interpreter() { + if !have_python_interpreter() { + return; + } + + let interpreter = get_host_interpreter(Some(PythonVersion { + major: 3, + minor: 45, + })); + assert!(interpreter.unwrap_err().to_string().contains( + "cannot set a minimum Python version 3.45 higher than the interpreter version" + )); + + let host_version = get_host_interpreter(None).unwrap().version; + if host_version >= PythonVersion::PY313 { + let interpreter = get_host_interpreter(Some(PythonVersion::PY313)); + assert_eq!( + interpreter.unwrap().target_abi.version, + PythonVersion::PY313 + ); + } + } + #[test] #[cfg(all(target_os = "linux", target_arch = "x86_64",))] fn parse_sysconfigdata() { From 037563af6f21758019ed5a010981c527194f21e3 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 12 May 2026 10:56:24 -0600 Subject: [PATCH 159/195] refactor is_abi3(t) in get_abit3(t)_version.is_some() --- pyo3-build-config/src/impl_.rs | 53 ++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index b0c09548e7a..9b2bac569fb 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -34,6 +34,11 @@ pub(crate) const MAXIMUM_SUPPORTED_VERSION_PYPY: PythonVersion = PythonVersion { minor: 11, }; +pub(crate) const MINIMUM_SUPPORTED_VERSION_ABI3T: PythonVersion = PythonVersion { + major: 3, + minor: 15, +}; + /// GraalPy may implement the same CPython version over multiple releases. const MINIMUM_SUPPORTED_VERSION_GRAALPY: PythonVersion = PythonVersion { major: 25, @@ -614,9 +619,9 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) "Invalid config that sets both target_abi and abi3." ); target_abi - } else if is_abi3t() { + } else if get_abi3t_version().is_some() { ensure!( - !is_abi3(), + !get_abi3_version().is_some(), "Cannot simultaneously enable features that enable abi3 and abi3t builds" ); PythonAbiBuilder::new(implementation, version) @@ -624,11 +629,11 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .unwrap() .finalize() } else if flags_contains_free_threaded { - // This fires even if is_abi3() is True for backward compatibility reasons + // This fires even if get_abi3_version().is_some() is True for backward compatibility reasons PythonAbiBuilder::new(implementation, version) .free_threaded()? .finalize() - } else if (abi3 == Some(true)) || is_abi3() { + } else if (abi3 == Some(true)) || get_abi3_version().is_some() { if abi3 == Some(true) { warn!("abi3 configuration file option is deprecated, set target_abi instead"); } @@ -990,9 +995,9 @@ impl PythonAbiBuilder { version: sanitize_stable_abi_version(stable_abi_version, version)?, kind: None, }; - if is_abi3() && !gil_disabled { + if get_abi3_version().is_some() && !gil_disabled { builder.stable_abi(StableAbi::Abi3) - } else if is_abi3t() { + } else if get_abi3t_version().is_some() { builder.stable_abi(StableAbi::Abi3t) } else if gil_disabled { builder.free_threaded() @@ -1303,26 +1308,17 @@ fn have_python_interpreter() -> bool { env_var("PYO3_NO_PYTHON").is_none() } -/// Checks if `abi3` or any of the `abi3-py3*` features is enabled for the PyO3 crate. -/// -/// Must be called from a PyO3 crate build script. -fn is_abi3() -> bool { - cargo_env_var("CARGO_FEATURE_ABI3").is_some() - || 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. +/// Must be called from a PyO3 crate build script. Returns None if an `abi3-py*` +/// feature is activated that is unsupported on the target Python version or if +/// no `abi3` or `abi3-py3*` feature is active. pub fn get_abi3_version() -> Option { + if !(cargo_env_var("CARGO_FEATURE_ABI3").is_some() + || env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY").is_some_and(|os_str| os_str == "1")) + { + return None; + } 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 }) @@ -1330,9 +1326,16 @@ pub fn get_abi3_version() -> Option { /// Gets the minimum supported Python version from PyO3 `abi3t-py*` features. /// -/// Must be called from a PyO3 crate build script. +/// Must be called from a PyO3 crate build script. Returns None if an `abi3t-py*` +/// feature is activated that is unsupported on the target Python version or if +/// no `abi3t` or `abi3t-py3*` feature is active. pub fn get_abi3t_version() -> Option { - let minor_version = (MINIMUM_SUPPORTED_VERSION.minor..=STABLE_ABI_MAX_MINOR) + if !(cargo_env_var("CARGO_FEATURE_ABI3T").is_some() + || env_var("PYO3_USE_ABI3T_FORWARD_COMPATIBILITY").is_some_and(|os_str| os_str == "1")) + { + return None; + } + let minor_version = (MINIMUM_SUPPORTED_VERSION_ABI3T.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 }) } From 23bf337f8564b738bfb55bdbaaca55a5fc82cb3d Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 12 May 2026 11:44:46 -0600 Subject: [PATCH 160/195] apply suggestions from icxolu --- pyo3-build-config/src/impl_.rs | 106 ++++++++++++++------------------- 1 file changed, 45 insertions(+), 61 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 9b2bac569fb..9a420755674 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -102,25 +102,6 @@ fn sanitize_stable_abi_version( } } -fn apply_build_env_to_config(mut config: InterpreterConfig) -> Result { - let mut abi_builder = PythonAbiBuilder::from_build_env( - config.implementation, - config.version, - get_abi3_version().or(get_abi3t_version()), - config.target_abi.kind.is_free_threaded(), - )?; - // only allow free-threaded builds if the build environment didn't force an abi3 build - if abi_builder.kind.is_none() { - if let PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded) = config.target_abi.kind { - abi_builder = abi_builder.free_threaded()?; - } else if let PythonAbiKind::Stable(stable_abi) = config.target_abi.kind { - abi_builder = abi_builder.stable_abi(stable_abi)?; - } - } - config.target_abi = abi_builder.finalize(); - Ok(config) -} - /// Configuration needed by PyO3 to build for the correct Python implementation. /// /// The version and implementation fields correspond to the interpreter @@ -385,8 +366,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) }; let target_abi = - PythonAbiBuilder::from_build_env(implementation, version, abi3_version, gil_disabled)? - .finalize(); + PythonAbi::from_build_env(implementation, version, abi3_version, gil_disabled)?; let cygwin = map["cygwin"].as_str() == "True"; @@ -478,9 +458,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) None => false, }; let cygwin = soabi.ends_with("cygwin"); - let target_abi = - PythonAbiBuilder::from_build_env(implementation, version, None, gil_disabled)? - .finalize(); + let target_abi = PythonAbi::from_build_env(implementation, version, None, gil_disabled)?; let lib_name = Some(default_lib_name_unix( target_abi, cygwin, @@ -515,10 +493,9 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) "PYO3_CONFIG_FILE must be an absolute path" ); - apply_build_env_to_config( - InterpreterConfig::from_path(path) - .context("failed to parse contents of PYO3_CONFIG_FILE")?, - ) + InterpreterConfig::from_path(path) + .context("failed to parse contents of PYO3_CONFIG_FILE")? + .apply_build_env() }) } @@ -621,7 +598,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) target_abi } else if get_abi3t_version().is_some() { ensure!( - !get_abi3_version().is_some(), + get_abi3_version().is_none(), "Cannot simultaneously enable features that enable abi3 and abi3t builds" ); PythonAbiBuilder::new(implementation, version) @@ -772,6 +749,16 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) pub fn is_free_threaded(&self) -> bool { self.target_abi.kind.is_free_threaded() } + + fn apply_build_env(mut self) -> Result { + self.target_abi = PythonAbi::from_build_env( + self.implementation, + self.version, + get_abi3_version().or(get_abi3t_version()), + self.target_abi.kind.is_free_threaded(), + )?; + Ok(self) + } } #[cfg_attr(test, derive(Debug))] @@ -984,28 +971,6 @@ impl PythonAbiBuilder { } } - pub fn from_build_env( - implementation: PythonImplementation, - version: PythonVersion, - stable_abi_version: Option, - gil_disabled: bool, - ) -> Result { - let builder = PythonAbiBuilder { - implementation, - version: sanitize_stable_abi_version(stable_abi_version, version)?, - kind: None, - }; - if get_abi3_version().is_some() && !gil_disabled { - builder.stable_abi(StableAbi::Abi3) - } else if get_abi3t_version().is_some() { - builder.stable_abi(StableAbi::Abi3t) - } else if gil_disabled { - builder.free_threaded() - } else { - Ok(builder) - } - } - pub fn stable_abi(self, kind: StableAbi) -> Result { ensure!( self.kind.is_none(), @@ -1105,6 +1070,31 @@ impl FromStr for PythonAbi { } } +impl PythonAbi { + pub fn from_build_env( + implementation: PythonImplementation, + version: PythonVersion, + stable_abi_version: Option, + gil_disabled: bool, + ) -> Result { + let builder = PythonAbiBuilder { + implementation, + version: sanitize_stable_abi_version(stable_abi_version, version)?, + kind: None, + }; + let builder = if get_abi3_version().is_some() && !gil_disabled { + builder.stable_abi(StableAbi::Abi3) + } else if get_abi3t_version().is_some() { + builder.stable_abi(StableAbi::Abi3t) + } else if gil_disabled { + builder.free_threaded() + } else { + Ok(builder) + }?; + Ok(builder.finalize()) + } +} + /// The "kind" of ABI. /// /// Either a variety of stable ABI or a GIL-enabled or free-threaded @@ -2050,13 +2040,9 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result) -> Result Result> { let interpreter_config = if let Some(cross_config) = cross_compiling_from_cargo_env()? { - Some(apply_build_env_to_config(load_cross_compile_config( - cross_config, - )?)?) + Some(load_cross_compile_config(cross_config)?.apply_build_env()?) } else { None }; From 14c2d9f37a0e7ac8c01c82372068273c88932e78 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 12 May 2026 12:14:55 -0600 Subject: [PATCH 161/195] Make PythonAbi fields private, replace with accessor methods --- pyo3-build-config/src/impl_.rs | 176 +++++++++++++++++++++------------ pyo3-build-config/src/lib.rs | 4 +- pyo3-ffi/build.rs | 28 +++--- 3 files changed, 127 insertions(+), 81 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 9a420755674..aea5f9e961d 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -215,22 +215,22 @@ impl InterpreterConfig { #[doc(hidden)] pub fn build_script_outputs(&self) -> Vec { // This should have been checked during pyo3-build-config build time. - assert!(self.target_abi.version >= MINIMUM_SUPPORTED_VERSION); + assert!(self.target_abi.version() >= MINIMUM_SUPPORTED_VERSION); let mut out = vec![]; - for i in MINIMUM_SUPPORTED_VERSION.minor..=self.target_abi.version.minor { + for i in MINIMUM_SUPPORTED_VERSION.minor..=self.target_abi.version().minor { out.push(format!("cargo:rustc-cfg=Py_3_{i}")); } - match self.target_abi.implementation { + match self.target_abi.implementation() { PythonImplementation::CPython => {} PythonImplementation::PyPy => out.push("cargo:rustc-cfg=PyPy".to_owned()), PythonImplementation::GraalPy => out.push("cargo:rustc-cfg=GraalPy".to_owned()), PythonImplementation::RustPython => out.push("cargo:rustc-cfg=RustPython".to_owned()), } - match self.target_abi.kind { + match self.target_abi.kind() { PythonAbiKind::Stable(kind) => { out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); if kind == StableAbi::Abi3t { @@ -747,7 +747,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) } pub fn is_free_threaded(&self) -> bool { - self.target_abi.kind.is_free_threaded() + self.target_abi.kind().is_free_threaded() } fn apply_build_env(mut self) -> Result { @@ -755,7 +755,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) self.implementation, self.version, get_abi3_version().or(get_abi3t_version()), - self.target_abi.kind.is_free_threaded(), + self.target_abi.kind().is_free_threaded(), )?; Ok(self) } @@ -907,13 +907,13 @@ impl InterpreterConfigBuilder { // Target ABI set, no Py_GIL_DISABLED: use as-is. (Some(target_abi), false) => target_abi, // Target ABI set + Py_GIL_DISABLED: reconcile. - (Some(target_abi), true) => match target_abi.kind { + (Some(target_abi), true) => match target_abi.kind() { // abi3 + Py_GIL_DISABLED: the abi3 feature is a no-op on free-threaded // interpreters, so for backward compatibility fall back to a free-threaded // version-specific build. PythonAbiKind::Stable(StableAbi::Abi3) => { let new_abi = - PythonAbiBuilder::new(target_abi.implementation, target_abi.version) + PythonAbiBuilder::new(target_abi.implementation(), target_abi.version()) .free_threaded()? .finalize(); warn!( @@ -931,7 +931,7 @@ impl InterpreterConfigBuilder { _ => target_abi, }, }; - if target_abi.kind.is_free_threaded() { + if target_abi.kind().is_free_threaded() { build_flags.0.insert(BuildFlag::Py_GIL_DISABLED); } #[allow(deprecated)] @@ -940,7 +940,7 @@ impl InterpreterConfigBuilder { version: self.version, shared: self.shared.unwrap_or(true), target_abi, - abi3: matches!(target_abi.kind, PythonAbiKind::Stable(StableAbi::Abi3)), + abi3: matches!(target_abi.kind(), PythonAbiKind::Stable(StableAbi::Abi3)), lib_name: self.lib_name, lib_dir: self.lib_dir, executable: self.executable, @@ -1026,20 +1026,9 @@ impl PythonAbiBuilder { #[derive(Copy, Clone, PartialEq, Eq)] #[cfg_attr(test, derive(Debug))] pub struct PythonAbi { - /// The Python implementation flavor. - /// - /// Serialized to `implementation`. - pub implementation: PythonImplementation, - - /// The ABI flavor - /// - /// Serialized to `kind` - pub kind: PythonAbiKind, - - /// Python `X.Y` version. e.g. `3.9`. - /// - /// Serialized to `version`. - pub version: PythonVersion, + implementation: PythonImplementation, + kind: PythonAbiKind, + version: PythonVersion, } impl Display for PythonAbi { @@ -1093,6 +1082,27 @@ impl PythonAbi { }?; Ok(builder.finalize()) } + + /// The Python implementation flavor. + /// + /// Serialized to `implementation`. + pub fn implementation(&self) -> PythonImplementation { + self.implementation + } + + /// The ABI flavor + /// + /// Serialized to `kind` + pub fn kind(&self) -> PythonAbiKind { + self.kind + } + + /// Python `X.Y` version. e.g. `3.9`. + /// + /// Serialized to `version`. + pub fn version(&self) -> PythonVersion { + self.version + } } /// The "kind" of ABI. @@ -3296,7 +3306,7 @@ mod tests { .unwrap() .finalize() .unwrap(); - assert_eq!(config.target_abi.version, target_version); + assert_eq!(config.target_abi.version(), target_version); assert_eq!(config.version, host_version); } @@ -3318,7 +3328,7 @@ mod tests { if host_version >= PythonVersion::PY313 { let interpreter = get_host_interpreter(Some(PythonVersion::PY313)); assert_eq!( - interpreter.unwrap().target_abi.version, + interpreter.unwrap().target_abi.version(), PythonVersion::PY313 ); } @@ -3348,7 +3358,7 @@ mod tests { version: Some(interpreter_config.version), implementation: Some(interpreter_config.implementation), target: triple!("x86_64-unknown-linux-gnu"), - abiflags: if interpreter_config.target_abi.kind.is_free_threaded() { + abiflags: if interpreter_config.target_abi.kind().is_free_threaded() { Some("t".into()) } else { None @@ -3374,12 +3384,12 @@ mod tests { assert_eq!(parsed_config.implementation, PythonImplementation::CPython); assert_eq!( - parsed_config.target_abi.implementation, + parsed_config.target_abi.implementation(), PythonImplementation::CPython ); assert_eq!( - parsed_config.target_abi.kind.is_free_threaded(), - interpreter_config.target_abi.kind.is_free_threaded() + parsed_config.target_abi.kind().is_free_threaded(), + interpreter_config.target_abi.kind().is_free_threaded() ); assert_eq!(parsed_config.pointer_width, Some(64)); } @@ -3639,7 +3649,7 @@ mod tests { .finalize() .unwrap(); // build flags win due to backward compatbility (abi3 feature is a no-op on ft builds) - assert!(config.target_abi.kind == PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)); + assert!(config.target_abi.kind() == PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)); // The reconciliation is order-independent: build_flags first, then stable_abi(Abi3) // produces the same result as the previous ordering. @@ -3654,7 +3664,7 @@ mod tests { .unwrap() .finalize() .unwrap(); - assert!(config.target_abi.kind == PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)); + assert!(config.target_abi.kind() == PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)); // Explicit GIL-enabled target with Py_GIL_DISABLED in build flags is contradictory and // is rejected at finalize regardless of the order in which the setters were called. @@ -3675,7 +3685,7 @@ mod tests { let builder = InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); let config = builder.free_threaded().unwrap().finalize().unwrap(); - assert!(config.target_abi.kind.is_free_threaded()); + assert!(config.target_abi.kind().is_free_threaded()); assert!(config.build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED)); } @@ -3731,78 +3741,114 @@ mod tests { #[test] fn test_apply_default_lib_name_to_config_file() { - let implementation = PythonImplementation::CPython; - let version = PythonVersion::PY39; - let mut config = InterpreterConfigBuilder::new(implementation, version) + fn py39_config() -> InterpreterConfig { + InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .finalize() + .unwrap() + } + + fn pypy_config() -> InterpreterConfig { + InterpreterConfigBuilder::new( + PythonImplementation::PyPy, + PythonVersion { + major: 3, + minor: 11, + }, + ) .finalize() - .unwrap(); + .unwrap() + } + + fn free_threaded_config() -> InterpreterConfig { + InterpreterConfigBuilder::new( + PythonImplementation::CPython, + PythonVersion { + major: 3, + minor: 13, + }, + ) + .free_threaded() + .unwrap() + .finalize() + .unwrap() + } + + fn stable_abi_config(kind: StableAbi) -> InterpreterConfig { + InterpreterConfigBuilder::new( + PythonImplementation::CPython, + PythonVersion { + major: 3, + minor: 13, + }, + ) + .stable_abi(kind) + .unwrap() + .finalize() + .unwrap() + } let unix = Triple::from_str("x86_64-unknown-linux-gnu").unwrap(); let win_x64 = Triple::from_str("x86_64-pc-windows-msvc").unwrap(); let win_arm64 = Triple::from_str("aarch64-pc-windows-msvc").unwrap(); + let mut config = py39_config(); config.apply_default_lib_name_to_config_file(&unix); assert_eq!(config.lib_name, Some("python3.9".into())); - config.lib_name = None; + let mut config = py39_config(); config.apply_default_lib_name_to_config_file(&win_x64); assert_eq!(config.lib_name, Some("python39".into())); - config.lib_name = None; + let mut config = py39_config(); config.apply_default_lib_name_to_config_file(&win_arm64); assert_eq!(config.lib_name, Some("python39".into())); // PyPy - config.target_abi.implementation = PythonImplementation::PyPy; - config.target_abi.version = PythonVersion { - major: 3, - minor: 11, - }; - config.lib_name = None; + let mut config = pypy_config(); config.apply_default_lib_name_to_config_file(&unix); assert_eq!(config.lib_name, Some("pypy3.11-c".into())); - config.lib_name = None; + let mut config = pypy_config(); config.apply_default_lib_name_to_config_file(&win_x64); assert_eq!(config.lib_name, Some("libpypy3.11-c".into())); - config.target_abi.implementation = PythonImplementation::CPython; - // Free-threaded - config.target_abi.kind = PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded); - config.target_abi.version = PythonVersion { - major: 3, - minor: 13, - }; - config.lib_name = None; + let mut config = free_threaded_config(); config.apply_default_lib_name_to_config_file(&unix); assert_eq!(config.lib_name, Some("python3.13t".into())); - config.lib_name = None; + let mut config = free_threaded_config(); config.apply_default_lib_name_to_config_file(&win_x64); assert_eq!(config.lib_name, Some("python313t".into())); - config.lib_name = None; + let mut config = free_threaded_config(); config.apply_default_lib_name_to_config_file(&win_arm64); assert_eq!(config.lib_name, Some("python313t".into())); - config.build_flags.0.remove(&BuildFlag::Py_GIL_DISABLED); - // abi3 - config.target_abi = PythonAbi { - kind: PythonAbiKind::Stable(StableAbi::Abi3), - ..config.target_abi - }; - config.lib_name = None; + let mut config = stable_abi_config(StableAbi::Abi3); config.apply_default_lib_name_to_config_file(&unix); assert_eq!(config.lib_name, Some("python3.13".into())); - config.lib_name = None; + let mut config = stable_abi_config(StableAbi::Abi3); config.apply_default_lib_name_to_config_file(&win_x64); assert_eq!(config.lib_name, Some("python3".into())); - config.lib_name = None; + let mut config = stable_abi_config(StableAbi::Abi3); config.apply_default_lib_name_to_config_file(&win_arm64); assert_eq!(config.lib_name, Some("python3".into())); + + // abi3t + let mut config = stable_abi_config(StableAbi::Abi3t); + config.apply_default_lib_name_to_config_file(&unix); + assert_eq!(config.lib_name, Some("python3.13t".into())); + + let mut config = stable_abi_config(StableAbi::Abi3t); + config.apply_default_lib_name_to_config_file(&win_x64); + assert_eq!(config.lib_name, Some("python3t".into())); + + let mut config = stable_abi_config(StableAbi::Abi3t); + config.apply_default_lib_name_to_config_file(&win_arm64); + assert_eq!(config.lib_name, Some("python3t".into())); } } diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index c1fd7cc75ab..e2587062a86 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -311,13 +311,13 @@ pub mod pyo3_build_script_impl { interpreter_config: &InterpreterConfig, supported_version: PythonVersion, ) -> Self { - let implementation = match interpreter_config.target_abi.implementation { + let implementation = match interpreter_config.target_abi.implementation() { PythonImplementation::CPython => "Python", PythonImplementation::PyPy => "PyPy", PythonImplementation::GraalPy => "GraalPy", PythonImplementation::RustPython => "RustPython", }; - let version = &interpreter_config.target_abi.version; + let version = &interpreter_config.target_abi.version(); let message = format!( "the configured {implementation} version ({version}) is newer than PyO3's maximum supported version ({supported_version})\n\ = help: this package is being built with PyO3 version {current_version}\n\ diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index e7b87df7232..a8dba79dadd 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -45,10 +45,10 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { return Ok(()); } - match interpreter_config.target_abi.implementation { + match interpreter_config.target_abi.implementation() { PythonImplementation::CPython => { let versions = SUPPORTED_VERSIONS_CPYTHON; - let interp_version = interpreter_config.target_abi.version; + let interp_version = interpreter_config.target_abi.version(); ensure!( interp_version >= versions.min, "the configured Python interpreter version ({}) is lower than PyO3's minimum supported version ({})", @@ -84,29 +84,29 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { } } - if interpreter_config.target_abi.kind.is_free_threaded() { + if interpreter_config.target_abi.kind().is_free_threaded() { let min_free_threaded_version = PythonVersion { major: 3, minor: 14, }; ensure!( - interpreter_config.target_abi.version >= min_free_threaded_version, + interpreter_config.target_abi.version() >= min_free_threaded_version, "PyO3 does not support the free-threaded build of CPython versions below {}, the selected Python version is {}", min_free_threaded_version, - interpreter_config.target_abi.version, + interpreter_config.target_abi.version(), ); } } PythonImplementation::PyPy => { let versions = SUPPORTED_VERSIONS_PYPY; ensure!( - interpreter_config.target_abi.version >= versions.min, + interpreter_config.target_abi.version() >= versions.min, "the configured PyPy interpreter version ({}) is lower than PyO3's minimum supported version ({})", - interpreter_config.target_abi.version, + interpreter_config.target_abi.version(), versions.min, ); // PyO3 does not support abi3, so we cannot offer forward compatibility - if interpreter_config.target_abi.version > versions.max { + if interpreter_config.target_abi.version() > versions.max { let error = MaximumVersionExceeded::new(interpreter_config, versions.max); return Err(error.finish().into()); } @@ -114,13 +114,13 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { PythonImplementation::GraalPy => { let versions = SUPPORTED_VERSIONS_GRAALPY; ensure!( - interpreter_config.target_abi.version >= versions.min, + interpreter_config.target_abi.version() >= versions.min, "the configured GraalPy interpreter version ({}) is lower than PyO3's minimum supported version ({})", - interpreter_config.target_abi.version, + interpreter_config.target_abi.version(), versions.min, ); // GraalPy does not support abi3, so we cannot offer forward compatibility - if interpreter_config.target_abi.version > versions.max { + if interpreter_config.target_abi.version() > versions.max { let error = MaximumVersionExceeded::new(interpreter_config, versions.max); return Err(error.finish().into()); } @@ -128,12 +128,12 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { PythonImplementation::RustPython => {} } - if let PythonAbiKind::Stable(abi) = interpreter_config.target_abi.kind { - match interpreter_config.target_abi.implementation { + if let PythonAbiKind::Stable(abi) = interpreter_config.target_abi.kind() { + match interpreter_config.target_abi.implementation() { PythonImplementation::CPython => match abi { StableAbi::Abi3t => { ensure!( - interpreter_config.target_abi.version >= PythonVersion::PY315, + interpreter_config.target_abi.version() >= PythonVersion::PY315, "Abi3t builds are not supported on CPython targets before Python 3.15" ) } From 9af08e139de10f87e007fd917377600598469073 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 12 May 2026 13:30:37 -0600 Subject: [PATCH 162/195] fix test-version-limits --- pyo3-build-config/src/impl_.rs | 46 +++++++++++++++++++--------------- pyo3-ffi/build.rs | 25 ++++++++++++++---- 2 files changed, 46 insertions(+), 25 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index aea5f9e961d..83933f8974e 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -596,9 +596,9 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) "Invalid config that sets both target_abi and abi3." ); target_abi - } else if get_abi3t_version().is_some() { + } else if is_abi3t() { ensure!( - get_abi3_version().is_none(), + !is_abi3(), "Cannot simultaneously enable features that enable abi3 and abi3t builds" ); PythonAbiBuilder::new(implementation, version) @@ -606,7 +606,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .unwrap() .finalize() } else if flags_contains_free_threaded { - // This fires even if get_abi3_version().is_some() is True for backward compatibility reasons + // This fires even if is_abi3() is True for backward compatibility reasons PythonAbiBuilder::new(implementation, version) .free_threaded()? .finalize() @@ -1071,9 +1071,9 @@ impl PythonAbi { version: sanitize_stable_abi_version(stable_abi_version, version)?, kind: None, }; - let builder = if get_abi3_version().is_some() && !gil_disabled { + let builder = if is_abi3() && !gil_disabled { builder.stable_abi(StableAbi::Abi3) - } else if get_abi3t_version().is_some() { + } else if is_abi3t() { builder.stable_abi(StableAbi::Abi3t) } else if gil_disabled { builder.free_threaded() @@ -1308,17 +1308,28 @@ fn have_python_interpreter() -> bool { env_var("PYO3_NO_PYTHON").is_none() } +/// Checks if `abi3` or any of the `abi3-py3*` features is enabled for the PyO3 crate. +/// +/// Must be called from a PyO3 crate build script. +fn is_abi3() -> bool { + cargo_env_var("CARGO_FEATURE_ABI3").is_some() + || 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. Returns None if an `abi3-py*` -/// feature is activated that is unsupported on the target Python version or if -/// no `abi3` or `abi3-py3*` feature is active. +/// feature is activated that is unsupported or if no `abi3-py3*` feature is +/// active. pub fn get_abi3_version() -> Option { - if !(cargo_env_var("CARGO_FEATURE_ABI3").is_some() - || env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY").is_some_and(|os_str| os_str == "1")) - { - return None; - } 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 }) @@ -1327,14 +1338,9 @@ pub fn get_abi3_version() -> Option { /// Gets the minimum supported Python version from PyO3 `abi3t-py*` features. /// /// Must be called from a PyO3 crate build script. Returns None if an `abi3t-py*` -/// feature is activated that is unsupported on the target Python version or if -/// no `abi3t` or `abi3t-py3*` feature is active. +/// feature is activated that is unsupported or if no `abi3t-py3*` feature is +/// active. pub fn get_abi3t_version() -> Option { - if !(cargo_env_var("CARGO_FEATURE_ABI3T").is_some() - || env_var("PYO3_USE_ABI3T_FORWARD_COMPATIBILITY").is_some_and(|os_str| os_str == "1")) - { - return None; - } let minor_version = (MINIMUM_SUPPORTED_VERSION_ABI3T.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 }) @@ -2384,7 +2390,7 @@ pub fn make_interpreter_config() -> Result { let abi3_version = get_abi3_version(); let abi3t_version = get_abi3t_version(); ensure!( - !(abi3_version.is_some() && abi3t_version.is_some()), + !(is_abi3t() && is_abi3()), "Cannot simultaneously enable abi3 and abi3t features" ); diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index a8dba79dadd..74ab3af918b 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -70,11 +70,26 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { let mut error = MaximumVersionExceeded::new(interpreter_config, versions.max); let major = interp_version.major; let minor = interp_version.minor; - if interpreter_config.target_abi.kind.is_free_threaded() { - 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()); + let py_3_15 = PythonVersion { + major: 3, + minor: 15, + }; + if interpreter_config.target_abi.kind().is_free_threaded() { + if (PythonVersion { major, minor }) > py_3_15 { + if env_var("PYO3_USE_ABI3T_FORWARD_COMPATIBILITY") + .is_none_or(|os_str| os_str != "1") + { + error.add_help(&format!( + "set PYO3_USE_ABI3T_FORWARD_COMPATIBILITY=1 to suppress this check and build anyway using the free-threaded stable ABI" + )); + return Err(error.finish().into()); + } + } else { + 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()); + } } if env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY").is_none_or(|os_str| os_str != "1") From 13448e3554f6138965a5db1faf0fb89289c4b650 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 12 May 2026 21:43:26 +0100 Subject: [PATCH 163/195] Apply suggestions from code review Co-authored-by: Thomas Tanon --- pyo3-build-config/src/impl_.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index ba10ee4a96e..e525ea2910c 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -1868,7 +1868,6 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result Date: Tue, 12 May 2026 21:52:30 +0100 Subject: [PATCH 164/195] review feedback --- pyo3-build-config/src/impl_.rs | 246 ++++++++++++++++----------------- pyo3-build-config/src/lib.rs | 12 +- 2 files changed, 129 insertions(+), 129 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index e525ea2910c..e0f2fe0ebc1 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -501,13 +501,13 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .abi3(abi3) .shared(shared) .lib_name(lib_name) - .lib_dir_opt(lib_dir) + .lib_dir(lib_dir) .executable(map["executable"].clone()) .pointer_width(calcsize_pointer * 8) .build_flags(BuildFlags::from_interpreter(interpreter)?) - .python_framework_prefix_opt(python_framework_prefix); + .python_framework_prefix(python_framework_prefix); - Ok(builder.finalize()) + builder.finalize() } /// Generate from parsed sysconfigdata file @@ -570,13 +570,13 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let builder = InterpreterConfigBuilder::new(implementation, version) .abi3(abi3) .shared(shared || framework) - .pointer_width_opt(pointer_width) - .lib_name_opt(lib_name) - .lib_dir_opt(lib_dir) - .python_framework_prefix_opt(python_framework_prefix) + .pointer_width(pointer_width) + .lib_name(lib_name) + .lib_dir(lib_dir) + .python_framework_prefix(python_framework_prefix) .build_flags(build_flags); - Ok(builder.finalize()) + builder.finalize() } /// Import an externally-provided config file. @@ -696,16 +696,16 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let builder = InterpreterConfigBuilder::new(implementation, version) .abi3(abi3) .shared(shared.unwrap_or(true)) - .lib_name_opt(lib_name) - .lib_dir_opt(lib_dir) - .executable_opt(executable) - .pointer_width_opt(pointer_width) + .lib_name(lib_name) + .lib_dir(lib_dir) + .executable(executable) + .pointer_width(pointer_width) .build_flags(build_flags) .suppress_build_script_link_lines(suppress_build_script_link_lines.unwrap_or(false)) .extra_build_script_lines(extra_build_script_lines) - .python_framework_prefix_opt(python_framework_prefix); + .python_framework_prefix(python_framework_prefix); - Ok(builder.finalize()) + builder.finalize() } /// Helper function to apply a default lib_name if none is set in `PYO3_CONFIG_FILE`. @@ -859,14 +859,14 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) pub struct InterpreterConfigBuilder { implementation: PythonImplementation, version: PythonVersion, - shared: Option, - abi3: Option, + shared: bool, + abi3: bool, lib_name: Option, lib_dir: Option, executable: Option, pointer_width: Option, - build_flags: Option, - suppress_build_script_link_lines: Option, + build_flags: BuildFlags, + suppress_build_script_link_lines: bool, extra_build_script_lines: Vec, python_framework_prefix: Option, } @@ -879,36 +879,39 @@ impl InterpreterConfigBuilder { InterpreterConfigBuilder { implementation, version, - shared: None, - abi3: None, + shared: true, + abi3: false, lib_name: None, lib_dir: None, executable: None, pointer_width: None, - build_flags: None, - suppress_build_script_link_lines: None, + build_flags: BuildFlags::default(), + suppress_build_script_link_lines: false, extra_build_script_lines: vec![], python_framework_prefix: None, } } pub fn abi3(mut self, abi3: bool) -> InterpreterConfigBuilder { - self.abi3 = Some(abi3); + self.abi3 = abi3; self } - pub fn lib_name(mut self, lib_name: String) -> InterpreterConfigBuilder { - self.lib_name = Some(lib_name); + pub fn lib_name(mut self, lib_name: impl Into>) -> InterpreterConfigBuilder { + self.lib_name = lib_name.into(); self } - pub fn pointer_width(mut self, pointer_width: u32) -> InterpreterConfigBuilder { - self.pointer_width = Some(pointer_width); + pub fn pointer_width( + mut self, + pointer_width: impl Into>, + ) -> InterpreterConfigBuilder { + self.pointer_width = pointer_width.into(); self } - pub fn executable(mut self, executable: String) -> InterpreterConfigBuilder { - self.executable = Some(executable); + pub fn executable(mut self, executable: impl Into>) -> InterpreterConfigBuilder { + self.executable = executable.into(); self } @@ -916,7 +919,7 @@ impl InterpreterConfigBuilder { mut self, suppress_build_script_link_lines: bool, ) -> InterpreterConfigBuilder { - self.suppress_build_script_link_lines = Some(suppress_build_script_link_lines); + self.suppress_build_script_link_lines = suppress_build_script_link_lines; self } @@ -928,81 +931,48 @@ impl InterpreterConfigBuilder { self } - pub fn lib_dir(mut self, lib_dir: String) -> InterpreterConfigBuilder { - self.lib_dir = Some(lib_dir); + pub fn lib_dir(mut self, lib_dir: impl Into>) -> InterpreterConfigBuilder { + self.lib_dir = lib_dir.into(); self } pub fn shared(mut self, shared: bool) -> InterpreterConfigBuilder { - self.shared = Some(shared); + self.shared = shared; self } pub fn build_flags(mut self, build_flags: BuildFlags) -> InterpreterConfigBuilder { - self.build_flags = Some(build_flags); + self.build_flags = build_flags; self } pub fn python_framework_prefix( - self, - python_framework_prefix: String, + mut self, + python_framework_prefix: impl Into>, ) -> InterpreterConfigBuilder { - InterpreterConfigBuilder { - python_framework_prefix: Some(python_framework_prefix), - ..self - } + self.python_framework_prefix = python_framework_prefix.into(); + self } - pub fn finalize(self) -> InterpreterConfig { + pub fn finalize(self) -> Result { #[expect( deprecated, reason = "constructing an InterpreterConfig directly, need to write to fields" )] - InterpreterConfig { + Ok(InterpreterConfig { implementation: self.implementation, version: self.version, - shared: self.shared.unwrap_or(true), - abi3: self.abi3.unwrap_or(false), + shared: self.shared, + abi3: self.abi3, lib_name: self.lib_name, lib_dir: self.lib_dir, executable: self.executable, pointer_width: self.pointer_width, - build_flags: self.build_flags.unwrap_or_default(), - suppress_build_script_link_lines: self - .suppress_build_script_link_lines - .unwrap_or(false), + build_flags: self.build_flags, + suppress_build_script_link_lines: self.suppress_build_script_link_lines, extra_build_script_lines: self.extra_build_script_lines, python_framework_prefix: self.python_framework_prefix, - } - } - - // private variants of some methods where it's convenient to potentially pass None - fn pointer_width_opt(mut self, pointer_width: Option) -> InterpreterConfigBuilder { - self.pointer_width = pointer_width; - self - } - - fn executable_opt(mut self, executable: Option) -> InterpreterConfigBuilder { - self.executable = executable; - self - } - - fn lib_name_opt(mut self, lib_name: Option) -> InterpreterConfigBuilder { - self.lib_name = lib_name; - self - } - - fn lib_dir_opt(mut self, lib_dir: Option) -> InterpreterConfigBuilder { - self.lib_dir = lib_dir; - self - } - - fn python_framework_prefix_opt( - mut self, - python_framework_prefix: Option, - ) -> InterpreterConfigBuilder { - self.python_framework_prefix = python_framework_prefix; - self + }) } } @@ -1870,9 +1840,9 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result = Vec::new(); config.to_writer(&mut buf).unwrap(); @@ -2290,7 +2261,8 @@ mod tests { }; let config = InterpreterConfigBuilder::new(implementation, version) .build_flags(build_flags) - .finalize(); + .finalize() + .unwrap(); let mut buf: Vec = Vec::new(); config.to_writer(&mut buf).unwrap(); @@ -2305,11 +2277,12 @@ mod tests { let config = InterpreterConfigBuilder::new(implementation, version) .abi3(true) .pointer_width(32) - .executable("executable".into()) - .lib_name("lib_name".into()) - .lib_dir("lib_dir\\n".into()) + .executable("executable".to_string()) + .lib_name("lib_name".to_string()) + .lib_dir("lib_dir\\n".to_string()) .extra_build_script_lines(vec!["cargo:test1".to_string(), "cargo:test2".to_string()]) - .finalize(); + .finalize() + .unwrap(); let mut buf: Vec = Vec::new(); config.to_writer(&mut buf).unwrap(); @@ -2325,7 +2298,9 @@ mod tests { let version = PythonVersion::PY38; assert_eq!( InterpreterConfig::from_reader("version=3.8".as_bytes()).unwrap(), - InterpreterConfigBuilder::new(implementation, version,).finalize() + InterpreterConfigBuilder::new(implementation, version,) + .finalize() + .unwrap() ) } @@ -2337,7 +2312,9 @@ mod tests { assert_eq!( InterpreterConfig::from_reader("version=3.8\next_suffix=.python38.so".as_bytes()) .unwrap(), - InterpreterConfigBuilder::new(implementation, version,).finalize() + InterpreterConfigBuilder::new(implementation, version,) + .finalize() + .unwrap() ) } @@ -2431,10 +2408,11 @@ mod tests { InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), InterpreterConfigBuilder::new(implementation, version,) .build_flags(BuildFlags::from_sysconfigdata(&sysconfigdata)) - .lib_dir("/usr/lib".into()) - .lib_name("python3.8".into()) + .lib_dir("/usr/lib".to_string()) + .lib_name("python3.8".to_string()) .pointer_width(64) .finalize() + .unwrap() ); } @@ -2455,10 +2433,11 @@ mod tests { InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), InterpreterConfigBuilder::new(implementation, version,) .build_flags(BuildFlags::from_sysconfigdata(&sysconfigdata)) - .lib_dir("/usr/lib".into()) - .lib_name("python3.8".into()) + .lib_dir("/usr/lib".to_string()) + .lib_name("python3.8".to_string()) .pointer_width(64) .finalize() + .unwrap() ); sysconfigdata = Sysconfigdata::new(); @@ -2476,11 +2455,12 @@ mod tests { InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), InterpreterConfigBuilder::new(implementation, version,) .build_flags(BuildFlags::from_sysconfigdata(&sysconfigdata)) - .lib_dir("/usr/lib".into()) - .lib_name("python3.8".into()) + .lib_dir("/usr/lib".to_string()) + .lib_name("python3.8".to_string()) .pointer_width(64) .shared(false) .finalize() + .unwrap() ); } @@ -2493,8 +2473,9 @@ mod tests { let version = PythonVersion::PY38; let config = InterpreterConfigBuilder::new(implementation, version) .abi3(true) - .lib_name("python3".into()) - .finalize(); + .lib_name("python3".to_string()) + .finalize() + .unwrap(); assert_eq!(default_abi3_config(&host, min_version).unwrap(), config); } @@ -2506,7 +2487,8 @@ mod tests { let version = PythonVersion::PY39; let config = InterpreterConfigBuilder::new(implementation, version) .abi3(true) - .finalize(); + .finalize() + .unwrap(); assert_eq!(default_abi3_config(&host, min_version).unwrap(), config); } @@ -2529,9 +2511,10 @@ mod tests { let implementation = PythonImplementation::CPython; let version = PythonVersion::PY38; let config = InterpreterConfigBuilder::new(implementation, version) - .lib_name("python38".into()) - .lib_dir("C:\\some\\path".into()) - .finalize(); + .lib_name("python38".to_string()) + .lib_dir("C:\\some\\path".to_string()) + .finalize() + .unwrap(); assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } @@ -2554,9 +2537,10 @@ mod tests { let implementation = PythonImplementation::CPython; let version = PythonVersion::PY38; let config = InterpreterConfigBuilder::new(implementation, version) - .lib_name("python38".into()) - .lib_dir("/usr/lib/mingw".into()) - .finalize(); + .lib_name("python38".to_string()) + .lib_dir("/usr/lib/mingw".to_string()) + .finalize() + .unwrap(); assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } @@ -2579,9 +2563,10 @@ mod tests { let implementation = PythonImplementation::CPython; let version = PythonVersion::PY39; let config = InterpreterConfigBuilder::new(implementation, version) - .lib_name("python3.9".into()) - .lib_dir("/usr/arm64/lib".into()) - .finalize(); + .lib_name("python3.9".to_string()) + .lib_dir("/usr/arm64/lib".to_string()) + .finalize() + .unwrap(); assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } @@ -2603,8 +2588,9 @@ mod tests { let implementation = PythonImplementation::PyPy; let version = PythonVersion::PY311; let config = InterpreterConfigBuilder::new(implementation, version) - .lib_name("pypy3.11-c".into()) - .finalize(); + .lib_name("pypy3.11-c".to_string()) + .finalize() + .unwrap(); assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } @@ -2988,7 +2974,7 @@ mod tests { ) .abi3(true); - let mut config = builder.finalize(); + let mut config = builder.finalize().unwrap(); config .fixup_for_abi3_version(Some(PythonVersion::PY38)) @@ -3002,7 +2988,7 @@ mod tests { InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) .abi3(true); - let mut config = builder.finalize(); + let mut config = builder.finalize().unwrap(); assert!(config .fixup_for_abi3_version(Some(PythonVersion::PY39)) @@ -3069,9 +3055,10 @@ mod tests { ) .build_flags(interpreter_config.build_flags().clone()) .pointer_width(64) - .lib_dir_opt(interpreter_config.lib_dir().map(str::to_owned)) - .lib_name_opt(interpreter_config.lib_name().map(str::to_owned)) + .lib_dir(interpreter_config.lib_dir().map(str::to_owned)) + .lib_name(interpreter_config.lib_name().map(str::to_owned)) .finalize() + .unwrap() ) } @@ -3192,7 +3179,9 @@ mod tests { fn test_build_script_outputs_base() { let implementation = PythonImplementation::CPython; let version = PythonVersion::PY311; - let interpreter_config = InterpreterConfigBuilder::new(implementation, version).finalize(); + let interpreter_config = InterpreterConfigBuilder::new(implementation, version) + .finalize() + .unwrap(); assert_eq!( interpreter_config.build_script_outputs(), [ @@ -3203,8 +3192,9 @@ mod tests { ] ); - let interpreter_config = - InterpreterConfigBuilder::new(PythonImplementation::PyPy, version).finalize(); + let interpreter_config = InterpreterConfigBuilder::new(PythonImplementation::PyPy, version) + .finalize() + .unwrap(); assert_eq!( interpreter_config.build_script_outputs(), [ @@ -3223,7 +3213,8 @@ mod tests { let version = PythonVersion::PY39; let interpreter_config = InterpreterConfigBuilder::new(implementation, version) .abi3(true) - .finalize(); + .finalize() + .unwrap(); assert_eq!( interpreter_config.build_script_outputs(), @@ -3236,7 +3227,8 @@ mod tests { let interpreter_config = InterpreterConfigBuilder::new(PythonImplementation::PyPy, version) .abi3(true) - .finalize(); + .finalize() + .unwrap(); assert_eq!( interpreter_config.build_script_outputs(), [ @@ -3256,7 +3248,8 @@ mod tests { let version = PythonVersion::PY313; let interpreter_config = InterpreterConfigBuilder::new(implementation, version) .build_flags(build_flags) - .finalize(); + .finalize() + .unwrap(); assert_eq!( interpreter_config.build_script_outputs(), [ @@ -3279,7 +3272,8 @@ mod tests { let version = PythonVersion::PY38; let interpreter_config = InterpreterConfigBuilder::new(implementation, version) .build_flags(build_flags) - .finalize(); + .finalize() + .unwrap(); assert_eq!( interpreter_config.build_script_outputs(), [ @@ -3322,7 +3316,9 @@ mod tests { fn test_apply_default_lib_name_to_config_file() { let implementation = PythonImplementation::CPython; let version = PythonVersion::PY39; - let mut config = InterpreterConfigBuilder::new(implementation, version).finalize(); + let mut config = InterpreterConfigBuilder::new(implementation, version) + .finalize() + .unwrap(); let unix = Triple::from_str("x86_64-unknown-linux-gnu").unwrap(); let win_x64 = Triple::from_str("x86_64-pc-windows-msvc").unwrap(); diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 0dd4a850a51..febb9637cf2 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -403,9 +403,10 @@ mod tests { let version = PythonVersion::PY313; let interpreter_config = InterpreterConfigBuilder::new(implementation, version) .python_framework_prefix( - "/Applications/Xcode.app/Contents/Developer/Library/Frameworks".into(), + "/Applications/Xcode.app/Contents/Developer/Library/Frameworks".to_string(), ) - .finalize(); + .finalize() + .unwrap(); // Does nothing on non-mac _add_python_framework_link_args( @@ -432,7 +433,9 @@ mod tests { fn test_maximum_version_exceeded_formatting() { let implementation = PythonImplementation::CPython; let version = PythonVersion::PY313; - let interpreter_config = InterpreterConfigBuilder::new(implementation, version).finalize(); + let interpreter_config = InterpreterConfigBuilder::new(implementation, version) + .finalize() + .unwrap(); let mut error = pyo3_build_script_impl::MaximumVersionExceeded::new( &interpreter_config, PythonVersion::PY312, @@ -456,7 +459,8 @@ mod tests { let interpreter_config = InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) - .finalize(); + .finalize() + .unwrap(); let mut buf = Vec::new(); interpreter_config.to_writer(&mut buf).unwrap(); let config_string = escape(&buf); From 4bcd9fa42ba4f63e05a2e2f3e88257b2e71b6b99 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 12 May 2026 15:25:34 -0600 Subject: [PATCH 165/195] fix merge conflicts --- pyo3-build-config/src/impl_.rs | 710 ++++++++++++++------------------- pyo3-build-config/src/lib.rs | 12 +- pyo3-ffi/build.rs | 10 +- 3 files changed, 312 insertions(+), 420 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index ecb96202d10..2f0f9dc27fc 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -271,11 +271,11 @@ impl InterpreterConfig { self.shared } - /// Whether linking against the stable/limited Python 3 API. + /// The target ABI to build for /// - /// Serialized to `abi3`. - pub fn abi3(&self) -> bool { - self.abi3 + /// Serialized to `target_abi` + pub fn target_abi(&self) -> PythonAbi { + self.target_abi } /// The name of the link library defining Python. @@ -533,14 +533,14 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .context("failed to parse calcsize_pointer")?; InterpreterConfigBuilder::new(implementation, version) - .target_abi(target_abi)? + .target_abi(target_abi) .shared(shared) - .lib_name(Some(lib_name)) - .lib_dir(lib_dir) - .executable(map.get("executable").cloned()) - .pointer_width(Some(calcsize_pointer * 8)) - .build_flags(BuildFlags::from_interpreter(interpreter)?)? - .python_framework_prefix(python_framework_prefix) + .lib_name(lib_name) + .lib_dir_opt(lib_dir) + .executable_opt(map.get("executable").cloned()) + .pointer_width(calcsize_pointer * 8) + .build_flags(BuildFlags::from_interpreter(interpreter)?) + .python_framework_prefix_opt(python_framework_prefix) .finalize() } @@ -588,23 +588,20 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) }; let cygwin = soabi.ends_with("cygwin"); let target_abi = PythonAbi::from_build_env(implementation, version, None, gil_disabled)?; - let lib_name = Some(default_lib_name_unix( - target_abi, - cygwin, - sysconfigdata.get_value("LDVERSION"), - )?); - let pointer_width = parse_key!(sysconfigdata, "SIZEOF_VOID_P") - .map(|bytes_width: u32| Some(bytes_width * 8))?; + let lib_name = + default_lib_name_unix(target_abi, cygwin, sysconfigdata.get_value("LDVERSION"))?; + let pointer_width = + parse_key!(sysconfigdata, "SIZEOF_VOID_P").map(|bytes_width: u32| bytes_width * 8)?; let build_flags = BuildFlags::from_sysconfigdata(sysconfigdata); InterpreterConfigBuilder::new(implementation, version) - .target_abi(target_abi)? + .target_abi(target_abi) .shared(shared || framework) - .lib_dir(lib_dir) + .lib_dir_opt(lib_dir) .lib_name(lib_name) .pointer_width(pointer_width) - .build_flags(build_flags)? - .python_framework_prefix(python_framework_prefix) + .build_flags(build_flags) + .python_framework_prefix_opt(python_framework_prefix) .finalize() } @@ -665,11 +662,11 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) } let mut implementation = None; - let mut version: Option = None; + let mut version = None; let mut shared = None; let mut target_abi = None; // deprecated in the struct but we still allow it to support old config files - let mut abi3: Option = None; + let mut abi3 = None; let mut lib_name = None; let mut lib_dir = None; let mut executable = None; @@ -732,39 +729,35 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) ); PythonAbiBuilder::new(implementation, version) .stable_abi(StableAbi::Abi3t) - .unwrap() - .finalize() + .finalize()? } else if flags_contains_free_threaded { // This fires even if is_abi3() is True for backward compatibility reasons PythonAbiBuilder::new(implementation, version) - .free_threaded()? - .finalize() + .free_threaded() + .finalize()? } else if (abi3 == Some(true)) || get_abi3_version().is_some() { if abi3 == Some(true) { warn!("abi3 configuration file option is deprecated, set target_abi instead"); } PythonAbiBuilder::new(implementation, version) .stable_abi(StableAbi::Abi3) - .unwrap() - .finalize() + .finalize()? } else { - PythonAbiBuilder::new(implementation, version).finalize() + PythonAbiBuilder::new(implementation, version).finalize()? }; let build_flags = build_flags.unwrap_or_default(); let builder = InterpreterConfigBuilder::new(implementation, version) .target_abi(target_abi) - // cannot fail because this is a newly-created InterpreterConfigBuilder with no target ABI set - .unwrap() .shared(shared.unwrap_or(true)) - .lib_name(lib_name) - .lib_dir(lib_dir) - .executable(executable) - .build_flags(build_flags)? - .suppress_build_script_link_lines(suppress_build_script_link_lines) + .lib_name_opt(lib_name) + .lib_dir_opt(lib_dir) + .executable_opt(executable) + .build_flags(build_flags) + .suppress_build_script_link_lines_opt(suppress_build_script_link_lines) .extra_build_script_lines(extra_build_script_lines) - .python_framework_prefix(python_framework_prefix) - .pointer_width(pointer_width); + .python_framework_prefix_opt(python_framework_prefix) + .pointer_width_opt(pointer_width); builder.finalize() } @@ -890,200 +883,6 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) } } -#[cfg_attr(test, derive(Debug))] -pub struct InterpreterConfigBuilder { - implementation: PythonImplementation, - version: PythonVersion, - shared: Option, - target_abi: Option, - lib_name: Option, - lib_dir: Option, - executable: Option, - pointer_width: Option, - build_flags: Option, - suppress_build_script_link_lines: Option, - extra_build_script_lines: Vec, - python_framework_prefix: Option, -} - -impl InterpreterConfigBuilder { - pub fn new( - implementation: PythonImplementation, - version: PythonVersion, - ) -> InterpreterConfigBuilder { - InterpreterConfigBuilder { - implementation, - version, - shared: None, - target_abi: None, - lib_name: None, - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: None, - suppress_build_script_link_lines: None, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } - } - - pub fn target_abi(self, target_abi: PythonAbi) -> Result { - ensure!( - self.target_abi.is_none(), - "Target ABI already set to {}", - target_abi - ); - Ok(InterpreterConfigBuilder { - target_abi: Some(target_abi), - ..self - }) - } - - pub fn stable_abi(self, kind: StableAbi) -> Result { - let implementation = self.implementation; - let version = self.version; - self.target_abi( - PythonAbiBuilder::new(implementation, version) - .stable_abi(kind)? - .finalize(), - ) - } - - pub fn free_threaded(self) -> Result { - let implementation = self.implementation; - let version = self.version; - self.target_abi( - PythonAbiBuilder::new(implementation, version) - .free_threaded()? - .finalize(), - ) - } - - pub fn lib_name(self, lib_name: Option) -> InterpreterConfigBuilder { - InterpreterConfigBuilder { lib_name, ..self } - } - - pub fn pointer_width(self, pointer_width: Option) -> InterpreterConfigBuilder { - InterpreterConfigBuilder { - pointer_width, - ..self - } - } - - pub fn executable(self, executable: Option) -> InterpreterConfigBuilder { - InterpreterConfigBuilder { executable, ..self } - } - - pub fn suppress_build_script_link_lines( - self, - suppress_build_script_link_lines: Option, - ) -> InterpreterConfigBuilder { - InterpreterConfigBuilder { - suppress_build_script_link_lines, - ..self - } - } - - pub fn extra_build_script_lines( - self, - extra_build_script_lines: Vec, - ) -> InterpreterConfigBuilder { - InterpreterConfigBuilder { - extra_build_script_lines, - ..self - } - } - - pub fn lib_dir(self, lib_dir: Option) -> InterpreterConfigBuilder { - InterpreterConfigBuilder { lib_dir, ..self } - } - - pub fn shared(self, shared: bool) -> InterpreterConfigBuilder { - InterpreterConfigBuilder { - shared: Some(shared), - ..self - } - } - - pub fn build_flags(self, build_flags: BuildFlags) -> Result { - ensure!(self.build_flags.is_none(), "Build flags already set!"); - Ok(InterpreterConfigBuilder { - build_flags: Some(build_flags), - ..self - }) - } - - pub fn python_framework_prefix( - self, - python_framework_prefix: Option, - ) -> InterpreterConfigBuilder { - InterpreterConfigBuilder { - python_framework_prefix, - ..self - } - } - - pub fn finalize(self) -> Result { - let mut build_flags = self.build_flags.unwrap_or_default(); - let py_gil_disabled = build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED); - let target_abi = match (self.target_abi, py_gil_disabled) { - // No target ABI set, no Py_GIL_DISABLED: default to GIL-enabled version-specific. - (None, false) => PythonAbiBuilder::new(self.implementation, self.version).finalize(), - // No target ABI set, Py_GIL_DISABLED in build flags: infer free-threaded. - (None, true) => PythonAbiBuilder::new(self.implementation, self.version) - .free_threaded()? - .finalize(), - // Target ABI set, no Py_GIL_DISABLED: use as-is. - (Some(target_abi), false) => target_abi, - // Target ABI set + Py_GIL_DISABLED: reconcile. - (Some(target_abi), true) => match target_abi.kind() { - // abi3 + Py_GIL_DISABLED: the abi3 feature is a no-op on free-threaded - // interpreters, so for backward compatibility fall back to a free-threaded - // version-specific build. - PythonAbiKind::Stable(StableAbi::Abi3) => { - let new_abi = - PythonAbiBuilder::new(target_abi.implementation(), target_abi.version()) - .free_threaded()? - .finalize(); - warn!( - "Targeting an abi3 build but build_flags contains Py_GIL_DISABLED, \ - falling back to a version-specific free-threaded build" - ); - new_abi - } - // GIL-enabled version-specific + Py_GIL_DISABLED is contradictory. - PythonAbiKind::VersionSpecific(GilUsed::GilEnabled) => bail!( - "build_flags contains Py_GIL_DISABLED but target_abi \ - '{target_abi}' is not free-threaded" - ), - // Already free-threaded (Stable(Abi3t) or VersionSpecific(FreeThreaded)). - _ => target_abi, - }, - }; - if target_abi.kind().is_free_threaded() { - build_flags.0.insert(BuildFlag::Py_GIL_DISABLED); - } - #[allow(deprecated)] - Ok(InterpreterConfig { - implementation: self.implementation, - version: self.version, - shared: self.shared.unwrap_or(true), - target_abi, - abi3: matches!(target_abi.kind(), PythonAbiKind::Stable(StableAbi::Abi3)), - lib_name: self.lib_name, - lib_dir: self.lib_dir, - executable: self.executable, - pointer_width: self.pointer_width, - build_flags, - suppress_build_script_link_lines: self - .suppress_build_script_link_lines - .unwrap_or(false), - extra_build_script_lines: self.extra_build_script_lines, - python_framework_prefix: self.python_framework_prefix, - }) - } -} - #[cfg_attr(test, derive(Debug))] pub struct PythonAbiBuilder { implementation: PythonImplementation, @@ -1100,54 +899,48 @@ impl PythonAbiBuilder { } } - pub fn stable_abi(self, kind: StableAbi) -> Result { - ensure!( - self.kind.is_none(), - "ABI kind already set to {}, cannot set to {}", - self.kind.unwrap(), - kind - ); + pub fn stable_abi(self, kind: StableAbi) -> PythonAbiBuilder { let mut build_version = self.version; if self.version.minor > STABLE_ABI_MAX_MINOR { warn!("Automatically falling back to {kind}-py3{STABLE_ABI_MAX_MINOR} because current Python is higher than the maximum supported"); build_version.minor = STABLE_ABI_MAX_MINOR; } - Ok(PythonAbiBuilder { + PythonAbiBuilder { kind: Some(PythonAbiKind::Stable(kind)), version: build_version, ..self - }) + } } - pub fn free_threaded(self) -> Result { - ensure!( - self.kind.is_none(), - "Target ABI already set to {}, cannot set to free-threaded", - self.kind.unwrap() - ); - if self.version < PythonVersion::PY313 { - let version = self.version; - bail!( - "Cannot target free-threaded builds for Python versions before 3.13, tried to build for {version}" - ) - } - Ok(PythonAbiBuilder { + pub fn free_threaded(self) -> PythonAbiBuilder { + PythonAbiBuilder { kind: Some(PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)), ..self - }) + } } - pub fn finalize(self) -> PythonAbi { + pub fn finalize(self) -> Result { // default to GIL-enabled version-specific ABI let kind = self .kind .unwrap_or(PythonAbiKind::VersionSpecific(GilUsed::GilEnabled)); - PythonAbi { + if matches!(kind, PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)) + && self.version + < (PythonVersion { + major: 3, + minor: 13, + }) + { + bail!( + "Cannot target free-threaded builds for Python versions before 3.13, tried to build for {}", self.version + ) + } + Ok(PythonAbi { implementation: self.implementation, kind, version: self.version, - } + }) } } @@ -1207,9 +1000,9 @@ impl PythonAbi { } else if gil_disabled { builder.free_threaded() } else { - Ok(builder) - }?; - Ok(builder.finalize()) + builder + }; + builder.finalize() } /// The Python implementation flavor. @@ -1325,7 +1118,7 @@ pub struct InterpreterConfigBuilder { implementation: PythonImplementation, version: PythonVersion, shared: Option, - abi3: Option, + target_abi: Option, lib_name: Option, lib_dir: Option, executable: Option, @@ -1345,7 +1138,7 @@ impl InterpreterConfigBuilder { implementation, version, shared: None, - abi3: None, + target_abi: None, lib_name: None, lib_dir: None, executable: None, @@ -1357,9 +1150,31 @@ impl InterpreterConfigBuilder { } } - pub fn abi3(mut self, abi3: bool) -> InterpreterConfigBuilder { - self.abi3 = Some(abi3); - self + pub fn target_abi(self, target_abi: PythonAbi) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { + target_abi: Some(target_abi), + ..self + } + } + + pub fn stable_abi(self, kind: StableAbi) -> Result { + let implementation = self.implementation; + let version = self.version; + Ok(self.target_abi( + PythonAbiBuilder::new(implementation, version) + .stable_abi(kind) + .finalize()?, + )) + } + + pub fn free_threaded(self) -> Result { + let implementation = self.implementation; + let version = self.version; + Ok(self.target_abi( + PythonAbiBuilder::new(implementation, version) + .free_threaded() + .finalize()?, + )) } pub fn lib_name(mut self, lib_name: String) -> InterpreterConfigBuilder { @@ -1418,27 +1233,67 @@ impl InterpreterConfigBuilder { } } - pub fn finalize(self) -> InterpreterConfig { + pub fn finalize(self) -> Result { + let mut build_flags = self.build_flags.unwrap_or_default(); + let py_gil_disabled = build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED); + let target_abi = match (self.target_abi, py_gil_disabled) { + // No target ABI set, no Py_GIL_DISABLED: default to GIL-enabled version-specific. + (None, false) => PythonAbiBuilder::new(self.implementation, self.version).finalize()?, + // No target ABI set, Py_GIL_DISABLED in build flags: infer free-threaded. + (None, true) => PythonAbiBuilder::new(self.implementation, self.version) + .free_threaded() + .finalize()?, + // Target ABI set, no Py_GIL_DISABLED: use as-is. + (Some(target_abi), false) => target_abi, + // Target ABI set + Py_GIL_DISABLED: reconcile. + (Some(target_abi), true) => match target_abi.kind() { + // abi3 + Py_GIL_DISABLED: the abi3 feature is a no-op on free-threaded + // interpreters, so for backward compatibility fall back to a free-threaded + // version-specific build. + PythonAbiKind::Stable(StableAbi::Abi3) => { + let new_abi = + PythonAbiBuilder::new(target_abi.implementation(), target_abi.version()) + .free_threaded() + .finalize()?; + warn!( + "Targeting an abi3 build but build_flags contains Py_GIL_DISABLED, \ + falling back to a version-specific free-threaded build" + ); + new_abi + } + // GIL-enabled version-specific + Py_GIL_DISABLED is contradictory. + PythonAbiKind::VersionSpecific(GilUsed::GilEnabled) => bail!( + "build_flags contains Py_GIL_DISABLED but target_abi \ + '{target_abi}' is not free-threaded" + ), + // Already free-threaded (Stable(Abi3t) or VersionSpecific(FreeThreaded)). + _ => target_abi, + }, + }; + if target_abi.kind().is_free_threaded() { + build_flags.0.insert(BuildFlag::Py_GIL_DISABLED); + } #[expect( deprecated, reason = "constructing an InterpreterConfig directly, need to write to fields" )] - InterpreterConfig { + Ok(InterpreterConfig { implementation: self.implementation, version: self.version, shared: self.shared.unwrap_or(true), - abi3: self.abi3.unwrap_or(false), + target_abi, + abi3: matches!(target_abi.kind(), PythonAbiKind::Stable(StableAbi::Abi3)), lib_name: self.lib_name, lib_dir: self.lib_dir, executable: self.executable, pointer_width: self.pointer_width, - build_flags: self.build_flags.unwrap_or_default(), + build_flags, suppress_build_script_link_lines: self .suppress_build_script_link_lines .unwrap_or(false), extra_build_script_lines: self.extra_build_script_lines, python_framework_prefix: self.python_framework_prefix, - } + }) } // private variants of some methods where it's convenient to potentially pass None @@ -1469,6 +1324,14 @@ impl InterpreterConfigBuilder { self.python_framework_prefix = python_framework_prefix; self } + + fn suppress_build_script_link_lines_opt( + mut self, + suppress_build_script_link_lines: Option, + ) -> InterpreterConfigBuilder { + self.suppress_build_script_link_lines = suppress_build_script_link_lines; + self + } } #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] @@ -2349,9 +2212,9 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result { // FIXME: PyPy & GraalPy do not support the Stable ABI. let target_abi = PythonAbiBuilder::new(PythonImplementation::CPython, version) - .stable_abi(StableAbi::Abi3)? - .finalize(); + .stable_abi(StableAbi::Abi3) + .finalize()?; let builder = InterpreterConfigBuilder::new(PythonImplementation::CPython, version) - .target_abi(target_abi)?; + .target_abi(target_abi); if host.operating_system == OperatingSystem::Windows { - builder.lib_name(Some(default_lib_name_windows(target_abi, false, false)?)) + builder.lib_name(default_lib_name_windows(target_abi, false, false)?) } else { builder } @@ -2477,10 +2340,10 @@ fn default_lib_name_windows(abi: PythonAbi, mingw: bool, debug: bool) -> Result< ); // https://packages.msys2.org/base/mingw-w64-python Ok(format!("python{}.{}", abi.version.major, abi.version.minor)) - } else if gil_disabled { + } else if abi.kind().is_free_threaded() { #[expect(deprecated, reason = "using constant internally")] { - ensure!(version >= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", abi.version.major, abi.version.minor); + ensure!(abi.version() >= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", abi.version.major, abi.version.minor); } if debug { Ok(format!( @@ -2755,10 +2618,10 @@ mod tests { let config = InterpreterConfigBuilder::new(implementation, version) .stable_abi(StableAbi::Abi3) .unwrap() - .pointer_width(Some(32)) - .executable(Some("executable".into())) - .lib_dir(Some("lib_name".into())) - .lib_name(Some("lib_name".into())) + .pointer_width(32) + .executable("executable".into()) + .lib_dir("lib_name".into()) + .lib_name("lib_name".into()) .extra_build_script_lines(vec!["cargo:test1".to_string(), "cargo:test2".to_string()]) .finalize() .unwrap(); @@ -2778,7 +2641,8 @@ mod tests { }; let config = InterpreterConfigBuilder::new(implementation, version) .build_flags(build_flags) - .finalize(); + .finalize() + .unwrap(); let mut buf: Vec = Vec::new(); config.to_writer(&mut buf).unwrap(); @@ -2793,10 +2657,10 @@ mod tests { let config = InterpreterConfigBuilder::new(implementation, version) .stable_abi(StableAbi::Abi3) .unwrap() - .pointer_width(Some(32)) - .executable(Some("executable".into())) - .lib_name(Some("lib_name".into())) - .lib_dir(Some("lib_dir\\n".into())) + .pointer_width(32) + .executable("executable".into()) + .lib_name("lib_name".into()) + .lib_dir("lib_dir\\n".into()) .extra_build_script_lines(vec!["cargo:test1".to_string(), "cargo:test2".to_string()]) .finalize() .unwrap(); @@ -2815,7 +2679,9 @@ mod tests { let version = PythonVersion::PY38; assert_eq!( InterpreterConfig::from_reader("version=3.8".as_bytes()).unwrap(), - InterpreterConfigBuilder::new(implementation, version,).finalize() + InterpreterConfigBuilder::new(implementation, version,) + .finalize() + .unwrap() ) } @@ -2827,7 +2693,9 @@ mod tests { assert_eq!( InterpreterConfig::from_reader("version=3.8\next_suffix=.python38.so".as_bytes()) .unwrap(), - InterpreterConfigBuilder::new(implementation, version,).finalize() + InterpreterConfigBuilder::new(implementation, version,) + .finalize() + .unwrap() ) } @@ -2884,7 +2752,6 @@ mod tests { flags.0.insert(BuildFlag::Py_GIL_DISABLED); assert!(InterpreterConfigBuilder::new(implementation, version) .build_flags(flags) - .unwrap() .finalize() .unwrap() .target_abi @@ -2896,7 +2763,6 @@ mod tests { assert!( InterpreterConfigBuilder::new(implementation, PythonVersion::PY312) .build_flags(flags) - .unwrap() .finalize() .is_err() ); @@ -2908,7 +2774,6 @@ mod tests { .stable_abi(StableAbi::Abi3) .unwrap() .build_flags(flags) - .unwrap() .finalize() .is_err() ); @@ -3038,6 +2903,7 @@ mod tests { .lib_name("python3.8".into()) .pointer_width(64) .finalize() + .unwrap() ); } @@ -3062,6 +2928,7 @@ mod tests { .lib_name("python3.8".into()) .pointer_width(64) .finalize() + .unwrap() ); sysconfigdata = Sysconfigdata::new(); @@ -3084,6 +2951,7 @@ mod tests { .pointer_width(64) .shared(false) .finalize() + .unwrap() ); } @@ -3097,7 +2965,7 @@ mod tests { let config = InterpreterConfigBuilder::new(implementation, version) .stable_abi(StableAbi::Abi3) .unwrap() - .lib_name(Some("python3".into())) + .lib_name("python3".into()) .finalize() .unwrap(); assert_eq!(default_abi3_config(&host, min_version).unwrap(), config); @@ -3138,7 +3006,8 @@ mod tests { let config = InterpreterConfigBuilder::new(implementation, version) .lib_name("python38".into()) .lib_dir("C:\\some\\path".into()) - .finalize(); + .finalize() + .unwrap(); assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } @@ -3163,7 +3032,8 @@ mod tests { let config = InterpreterConfigBuilder::new(implementation, version) .lib_name("python38".into()) .lib_dir("/usr/lib/mingw".into()) - .finalize(); + .finalize() + .unwrap(); assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } @@ -3188,7 +3058,8 @@ mod tests { let config = InterpreterConfigBuilder::new(implementation, version) .lib_name("python3.9".into()) .lib_dir("/usr/arm64/lib".into()) - .finalize(); + .finalize() + .unwrap(); assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } @@ -3211,7 +3082,8 @@ mod tests { let version = PythonVersion::PY311; let config = InterpreterConfigBuilder::new(implementation, version) .lib_name("pypy3.11-c".into()) - .finalize(); + .finalize() + .unwrap(); assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } @@ -3220,7 +3092,8 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .finalize(), + .finalize() + .unwrap(), false, false, ) @@ -3231,14 +3104,15 @@ mod tests { assert!( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) .free_threaded() + .finalize() .is_err() ); assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) .stable_abi(StableAbi::Abi3) - .unwrap() - .finalize(), + .finalize() + .unwrap(), false, false, ) @@ -3248,7 +3122,8 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .finalize(), + .finalize() + .unwrap(), true, false, ) @@ -3259,8 +3134,8 @@ mod tests { super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) .stable_abi(StableAbi::Abi3) - .unwrap() - .finalize(), + .finalize() + .unwrap(), true, false, ) @@ -3271,8 +3146,8 @@ mod tests { super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY39) .stable_abi(StableAbi::Abi3) - .unwrap() - .finalize(), + .finalize() + .unwrap(), false, false, ) @@ -3283,8 +3158,8 @@ mod tests { super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY311) .stable_abi(StableAbi::Abi3) - .unwrap() - .finalize(), + .finalize() + .unwrap(), false, false, ) @@ -3295,8 +3170,8 @@ mod tests { super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY310) .stable_abi(StableAbi::Abi3) - .unwrap() - .finalize(), + .finalize() + .unwrap(), false, true, ) @@ -3309,8 +3184,8 @@ mod tests { super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) .stable_abi(StableAbi::Abi3) - .unwrap() - .finalize(), + .finalize() + .unwrap(), false, true, ) @@ -3321,8 +3196,8 @@ mod tests { super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY310) .stable_abi(StableAbi::Abi3) - .unwrap() - .finalize(), + .finalize() + .unwrap(), false, true, ) @@ -3333,8 +3208,8 @@ mod tests { assert!(super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) .free_threaded() - .unwrap() - .finalize(), + .finalize() + .unwrap(), true, false, ) @@ -3343,8 +3218,8 @@ mod tests { super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) .free_threaded() - .unwrap() - .finalize(), + .finalize() + .unwrap(), false, false, ) @@ -3355,8 +3230,8 @@ mod tests { super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) .free_threaded() - .unwrap() - .finalize(), + .finalize() + .unwrap(), false, true, ) @@ -3365,10 +3240,16 @@ mod tests { ); assert_eq!( super::default_lib_name_windows( - PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY315) - .stable_abi(StableAbi::Abi3t) - .unwrap() - .finalize(), + PythonAbiBuilder::new( + PythonImplementation::CPython, + PythonVersion { + major: 3, + minor: 15 + } + ) + .stable_abi(StableAbi::Abi3t) + .finalize() + .unwrap(), false, false, ) @@ -3377,10 +3258,16 @@ mod tests { ); assert_eq!( super::default_lib_name_windows( - PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY315) - .stable_abi(StableAbi::Abi3t) - .unwrap() - .finalize(), + PythonAbiBuilder::new( + PythonImplementation::CPython, + PythonVersion { + major: 3, + minor: 15 + } + ) + .stable_abi(StableAbi::Abi3t) + .finalize() + .unwrap(), false, true, ) @@ -3395,7 +3282,8 @@ mod tests { assert_eq!( super::default_lib_name_unix( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) - .finalize(), + .finalize() + .unwrap(), false, None, ) @@ -3405,7 +3293,8 @@ mod tests { assert_eq!( super::default_lib_name_unix( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .finalize(), + .finalize() + .unwrap(), false, None, ) @@ -3416,7 +3305,8 @@ mod tests { assert_eq!( super::default_lib_name_unix( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .finalize(), + .finalize() + .unwrap(), false, Some("3.8d"), ) @@ -3427,7 +3317,9 @@ mod tests { // PyPy 3.11 includes ldversion assert_eq!( super::default_lib_name_unix( - PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY311).finalize(), + PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY311) + .finalize() + .unwrap(), false, None, ) @@ -3437,7 +3329,9 @@ mod tests { assert_eq!( super::default_lib_name_unix( - PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY39).finalize(), + PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY39) + .finalize() + .unwrap(), false, Some("3.11d"), ) @@ -3450,8 +3344,8 @@ mod tests { super::default_lib_name_unix( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) .free_threaded() - .unwrap() - .finalize(), + .finalize() + .unwrap(), false, None, ) @@ -3463,8 +3357,8 @@ mod tests { super::default_lib_name_unix( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) .stable_abi(StableAbi::Abi3) - .unwrap() - .finalize(), + .finalize() + .unwrap(), true, None, ) @@ -3475,18 +3369,9 @@ mod tests { #[test] fn abi_builder_error_paths() { - let builder = PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY314) - .free_threaded() - .unwrap() - .stable_abi(StableAbi::Abi3); - assert!(builder.is_err()); - assert!(builder - .unwrap_err() - .to_string() - .contains("ABI kind already set to")); - let builder = PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .free_threaded(); + .free_threaded() + .finalize(); assert!(builder.is_err()); assert!(builder.unwrap_err().to_string().contains("Cannot target")); @@ -3500,8 +3385,8 @@ mod tests { }, ) .stable_abi(StableAbi::Abi3) - .unwrap() .finalize() + .unwrap() .version .minor, STABLE_ABI_MAX_MINOR @@ -3512,14 +3397,6 @@ mod tests { assert!("CPython-free_threaded-invalid" .parse::() .is_err()); - - assert!( - PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY314) - .stable_abi(StableAbi::Abi3) - .unwrap() - .free_threaded() - .is_err() - ); } #[test] @@ -3582,10 +3459,9 @@ mod tests { .target_abi( PythonAbiBuilder::new(implementation, target_version) .stable_abi(StableAbi::Abi3) - .unwrap() - .finalize(), + .finalize() + .unwrap(), ) - .unwrap() .finalize() .unwrap(); assert_eq!(config.target_abi.version(), target_version); @@ -3614,20 +3490,6 @@ mod tests { PythonVersion::PY313 ); } - - let builder = - InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) - .stable_abi(StableAbi::Abi3); - - let mut config = builder.finalize(); - - assert!(config - .fixup_for_abi3_version(Some(PythonVersion::PY39)) - .unwrap_err() - .to_string() - .contains( - "cannot set a minimum Python version 3.9 higher than the interpreter version 3.8" - )); } #[test] @@ -3810,7 +3672,9 @@ mod tests { fn test_build_script_outputs_base() { let implementation = PythonImplementation::CPython; let version = PythonVersion::PY311; - let interpreter_config = InterpreterConfigBuilder::new(implementation, version).finalize(); + let interpreter_config = InterpreterConfigBuilder::new(implementation, version) + .finalize() + .unwrap(); assert_eq!( interpreter_config.build_script_outputs(), [ @@ -3821,8 +3685,9 @@ mod tests { ] ); - let interpreter_config = - InterpreterConfigBuilder::new(PythonImplementation::PyPy, version).finalize(); + let interpreter_config = InterpreterConfigBuilder::new(PythonImplementation::PyPy, version) + .finalize() + .unwrap(); assert_eq!( interpreter_config.build_script_outputs(), [ @@ -3842,7 +3707,7 @@ mod tests { let interpreter_config = InterpreterConfigBuilder::new(implementation, version) .stable_abi(StableAbi::Abi3) .unwrap() - .lib_name(Some("python3".into())) + .lib_name("python3".into()) .finalize() .unwrap(); @@ -3857,7 +3722,9 @@ mod tests { let interpreter_config = InterpreterConfigBuilder::new(PythonImplementation::PyPy, version) .stable_abi(StableAbi::Abi3) - .finalize(); + .unwrap() + .finalize() + .unwrap(); assert_eq!( interpreter_config.build_script_outputs(), [ @@ -3868,14 +3735,17 @@ mod tests { ] ); - let interpreter_config = InterpreterConfig { - target_abi: PythonAbi { - implementation: PythonImplementation::CPython, - version: PythonVersion::PY315, - ..interpreter_config.target_abi + let interpreter_config = InterpreterConfigBuilder::new( + PythonImplementation::CPython, + PythonVersion { + major: 3, + minor: 15, }, - ..interpreter_config - }; + ) + .stable_abi(StableAbi::Abi3) + .unwrap() + .finalize() + .unwrap(); assert_eq!( interpreter_config.build_script_outputs(), [ @@ -3899,7 +3769,7 @@ mod tests { let interpreter_config = InterpreterConfigBuilder::new(implementation, version) .free_threaded() .unwrap() - .lib_name(Some("python3".into())) + .lib_name("python3".into()) .finalize() .unwrap(); assert_eq!( @@ -3918,21 +3788,19 @@ mod tests { #[test] fn test_interpreter_config_builder_gil_disabled_flag() { - let builder = - InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); - let mut flags = BuildFlags::new(); - flags.0.insert(BuildFlag::Py_GIL_DISABLED); - assert!(builder.build_flags(flags).is_ok()); - - let builder = - InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); + let builder = InterpreterConfigBuilder::new( + PythonImplementation::CPython, + PythonVersion { + major: 3, + minor: 14, + }, + ); let mut flags = BuildFlags::new(); flags.0.insert(BuildFlag::Py_GIL_DISABLED); let config = builder .stable_abi(StableAbi::Abi3) .unwrap() .build_flags(flags) - .unwrap() .finalize() .unwrap(); // build flags win due to backward compatbility (abi3 feature is a no-op on ft builds) @@ -3940,13 +3808,17 @@ mod tests { // The reconciliation is order-independent: build_flags first, then stable_abi(Abi3) // produces the same result as the previous ordering. - let builder = - InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); + let builder = InterpreterConfigBuilder::new( + PythonImplementation::CPython, + PythonVersion { + major: 3, + minor: 14, + }, + ); let mut flags = BuildFlags::new(); flags.0.insert(BuildFlag::Py_GIL_DISABLED); let config = builder .build_flags(flags) - .unwrap() .stable_abi(StableAbi::Abi3) .unwrap() .finalize() @@ -3955,22 +3827,37 @@ mod tests { // Explicit GIL-enabled target with Py_GIL_DISABLED in build flags is contradictory and // is rejected at finalize regardless of the order in which the setters were called. - let builder = - InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); - let target_abi = - PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY314).finalize(); + let builder = InterpreterConfigBuilder::new( + PythonImplementation::CPython, + PythonVersion { + major: 3, + minor: 14, + }, + ); + let target_abi = PythonAbiBuilder::new( + PythonImplementation::CPython, + PythonVersion { + major: 3, + minor: 14, + }, + ) + .finalize() + .unwrap(); let mut flags = BuildFlags::new(); flags.0.insert(BuildFlag::Py_GIL_DISABLED); assert!(builder .target_abi(target_abi) - .unwrap() .build_flags(flags) - .unwrap() .finalize() .is_err()); - let builder = - InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); + let builder = InterpreterConfigBuilder::new( + PythonImplementation::CPython, + PythonVersion { + major: 3, + minor: 14, + }, + ); let config = builder.free_threaded().unwrap().finalize().unwrap(); assert!(config.target_abi.kind().is_free_threaded()); assert!(config.build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED)); @@ -3984,7 +3871,8 @@ mod tests { let version = PythonVersion::PY38; let interpreter_config = InterpreterConfigBuilder::new(implementation, version) .build_flags(build_flags) - .finalize(); + .finalize() + .unwrap(); assert_eq!( interpreter_config.build_script_outputs(), [ diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index ccc14e1c681..475ed989e66 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -14,8 +14,8 @@ use std::{env, process::Command, str::FromStr, sync::LazyLock}; pub use impl_::{ cross_compiling_from_to, find_all_sysconfigdata, parse_sysconfigdata, BuildFlag, BuildFlags, - CrossCompileConfig, GilUsed, InterpreterConfigBuilder, PythonAbi, PythonAbiBuilder, - PythonAbiKind, PythonImplementation, PythonVersion, StableAbi, Triple, + CrossCompileConfig, GilUsed, InterpreterConfig, InterpreterConfigBuilder, PythonAbi, + PythonAbiBuilder, PythonAbiKind, PythonImplementation, PythonVersion, StableAbi, Triple, }; use target_lexicon::OperatingSystem; @@ -311,13 +311,13 @@ pub mod pyo3_build_script_impl { interpreter_config: &InterpreterConfig, supported_version: PythonVersion, ) -> Self { - let implementation = match interpreter_config.target_abi.implementation() { + let implementation = match interpreter_config.target_abi().implementation() { PythonImplementation::CPython => "Python", PythonImplementation::PyPy => "PyPy", PythonImplementation::GraalPy => "GraalPy", PythonImplementation::RustPython => "RustPython", }; - let version = &interpreter_config.target_abi.version(); + let version = &interpreter_config.target_abi().version(); let message = format!( "the configured {implementation} version ({version}) is newer than PyO3's maximum supported version ({supported_version})\n\ = help: this package is being built with PyO3 version {current_version}\n\ @@ -409,9 +409,9 @@ mod tests { let implementation = PythonImplementation::CPython; let version = PythonVersion::PY313; let interpreter_config = InterpreterConfigBuilder::new(implementation, version) - .python_framework_prefix(Some( + .python_framework_prefix( "/Applications/Xcode.app/Contents/Developer/Library/Frameworks".to_string(), - )) + ) .finalize() .unwrap(); diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 4601d0a9748..3963978389f 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -74,7 +74,7 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { major: 3, minor: 15, }; - if interpreter_config.target_abi.kind().is_free_threaded() { + if interpreter_config.target_abi().kind().is_free_threaded() { if (PythonVersion { major, minor }) > py_3_15 { if env_var("PYO3_USE_ABI3T_FORWARD_COMPATIBILITY") .is_none_or(|os_str| os_str != "1") @@ -108,7 +108,7 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { interpreter_config.target_abi().version() >= min_free_threaded_version, "PyO3 does not support the free-threaded build of CPython versions below {}, the selected Python version is {}", min_free_threaded_version, - interpreter_config.target_abi.version(), + interpreter_config.target_abi().version(), ); } } @@ -148,7 +148,11 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { PythonImplementation::CPython => match abi { StableAbi::Abi3t => { ensure!( - interpreter_config.target_abi().version() >= PythonVersion::PY315, + interpreter_config.target_abi().version() + >= PythonVersion { + major: 3, + minor: 15 + }, "Abi3t builds are not supported on CPython targets before Python 3.15" ) } From 9a3272738e33ca2ff324acf074e2357108c39dcc Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 12 May 2026 15:48:51 -0600 Subject: [PATCH 166/195] fix clippy --- pyo3-build-config/src/impl_.rs | 2 +- pyo3-ffi/build.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 719753f2f57..3f11c888b24 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -1290,7 +1290,7 @@ impl InterpreterConfigBuilder { lib_dir: self.lib_dir, executable: self.executable, pointer_width: self.pointer_width, - build_flags: build_flags, + build_flags, suppress_build_script_link_lines: self.suppress_build_script_link_lines, extra_build_script_lines: self.extra_build_script_lines, python_framework_prefix: self.python_framework_prefix, diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 3963978389f..01e9b72de6e 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -79,15 +79,15 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { if env_var("PYO3_USE_ABI3T_FORWARD_COMPATIBILITY") .is_none_or(|os_str| os_str != "1") { - error.add_help(&format!( + error.add_help( "set PYO3_USE_ABI3T_FORWARD_COMPATIBILITY=1 to suppress this check and build anyway using the free-threaded stable ABI" - )); + ); return Err(error.finish().into()); } } else { - error.add_help(&format!( + error.add_help(format!( "the free-threaded build of CPython {major}.{minor} does not support the limited API so this check cannot be suppressed.", - )); + ).as_str()); return Err(error.finish().into()); } } From c536ebe53abb02279a8db8dd8b4a276d812bd7ce Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 12 May 2026 16:04:55 -0600 Subject: [PATCH 167/195] re-enable functionality that depends on critical sections --- src/conversions/bytes.rs | 2 - src/conversions/std/num.rs | 13 ----- src/pybacked.rs | 88 ++++++++++------------------- src/sync.rs | 3 - src/sync/critical_section.rs | 44 +++++++-------- src/types/bytearray.rs | 12 ---- src/types/dict.rs | 105 ++++++----------------------------- src/types/list.rs | 7 --- 8 files changed, 65 insertions(+), 209 deletions(-) diff --git a/src/conversions/bytes.rs b/src/conversions/bytes.rs index eec2d26c3e2..ab8a9734d5a 100644 --- a/src/conversions/bytes.rs +++ b/src/conversions/bytes.rs @@ -115,7 +115,6 @@ impl<'py> IntoPyObject<'py> for &Bytes { mod tests { use super::*; use crate::types::{PyAnyMethods, PyBytes}; - #[cfg(not(Py_TARGET_ABI3T))] use crate::types::{PyByteArray, PyByteArrayMethods}; use crate::Python; @@ -132,7 +131,6 @@ 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/conversions/std/num.rs b/src/conversions/std/num.rs index 12722e06e2f..0cf82f1dc9d 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -6,7 +6,6 @@ use crate::inspect::PyStaticExpr; use crate::py_result_ext::PyResultExt; #[cfg(feature = "experimental-inspect")] use crate::type_object::PyTypeInfo; -#[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,15 +256,6 @@ impl<'py> FromPyObject<'_, 'py> for u8 { obj: Borrowed<'_, 'py, PyAny>, _: crate::conversion::private::Token, ) -> Option> { - #[cfg(Py_TARGET_ABI3T)] - { - if let Ok(bytes) = obj.cast::() { - Some(BytesSequenceExtractor::Bytes(bytes)) - } else { - None - } - } - #[cfg(not(Py_TARGET_ABI3T))] { if let Ok(bytes) = obj.cast::() { Some(BytesSequenceExtractor::Bytes(bytes)) @@ -280,7 +270,6 @@ impl<'py> FromPyObject<'_, 'py> for u8 { pub(crate) enum BytesSequenceExtractor<'a, 'py> { Bytes(Borrowed<'a, 'py, PyBytes>), - #[cfg(not(Py_TARGET_ABI3T))] ByteArray(Borrowed<'a, 'py, PyByteArray>), } @@ -299,7 +288,6 @@ impl BytesSequenceExtractor<'_, '_> { match self { BytesSequenceExtractor::Bytes(b) => copy_slice(b.as_bytes()), - #[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 +304,6 @@ impl FromPyObjectSequence for BytesSequenceExtractor<'_, '_> { fn to_vec(&self) -> Vec { match self { BytesSequenceExtractor::Bytes(b) => b.as_bytes().to_vec(), - #[cfg(not(Py_TARGET_ABI3T))] BytesSequenceExtractor::ByteArray(b) => b.to_vec(), } } diff --git a/src/pybacked.rs b/src/pybacked.rs index ae7b99a1d91..bb78ebfce03 100644 --- a/src/pybacked.rs +++ b/src/pybacked.rs @@ -4,15 +4,13 @@ use crate::inspect::PyStaticExpr; #[cfg(feature = "experimental-inspect")] use crate::type_hint_union; -#[cfg(any(feature = "experimental-inspect", not(Py_TARGET_ABI3T)))] -use crate::types::bytearray::PyByteArray; -#[cfg(not(Py_TARGET_ABI3T))] -use crate::types::bytearray::PyByteArrayMethods; use crate::{ - types::{bytes::PyBytesMethods, string::PyStringMethods, PyBytes, PyString, PyTuple}, + types::{ + bytearray::PyByteArrayMethods, bytes::PyBytesMethods, string::PyStringMethods, PyByteArray, + PyBytes, PyString, PyTuple, + }, Borrowed, Bound, CastError, FromPyObject, IntoPyObject, Py, PyAny, PyErr, PyTypeInfo, Python, }; -#[cfg(not(Py_TARGET_ABI3T))] use std::sync::Arc; use std::{borrow::Borrow, convert::Infallible, ops::Deref, ptr::NonNull}; @@ -192,7 +190,6 @@ pub struct PyBackedBytes { #[cfg_attr(feature = "py-clone", derive(Clone))] enum PyBackedBytesStorage { Python(Py), - #[cfg(not(Py_TARGET_ABI3T))] Rust(Arc<[u8]>), } @@ -206,7 +203,6 @@ impl PyBackedBytes { PyBackedBytesStorage::Python(bytes) => { PyBackedBytesStorage::Python(bytes.clone_ref(py)) } - #[cfg(not(Py_TARGET_ABI3T))] PyBackedBytesStorage::Rust(bytes) => PyBackedBytesStorage::Rust(bytes.clone()), }, data: self.data, @@ -270,7 +266,6 @@ impl From> for PyBackedBytes { } } -#[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()); @@ -289,39 +284,23 @@ 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(Py_TARGET_ABI3T))] - { - 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(Py_TARGET_ABI3T)] - { - 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(), - )) - } + 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(), + )) } } } @@ -337,7 +316,6 @@ 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(Py_TARGET_ABI3T))] PyBackedBytesStorage::Rust(bytes) => Ok(PyBytes::new(py, &bytes)), } } @@ -354,7 +332,6 @@ 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(Py_TARGET_ABI3T))] PyBackedBytesStorage::Rust(bytes) => Ok(PyBytes::new(py, bytes)), } } @@ -520,7 +497,6 @@ mod test { } #[test] - #[cfg(not(Py_TARGET_ABI3T))] fn py_backed_bytes_from_bytearray() { Python::attach(|py| { let b = PyByteArray::new(py, b"abcde"); @@ -542,7 +518,6 @@ mod test { } #[test] - #[cfg(not(Py_TARGET_ABI3T))] fn rust_backed_bytes_into_pyobject() { Python::attach(|py| { let orig_bytes = PyByteArray::new(py, b"abcde"); @@ -694,7 +669,6 @@ mod test { let b1: PyBackedBytes = PyBytes::new(py, b"abcde").into(); let b2 = b1.clone_ref(py); assert_eq!(b1, b2); - #[cfg_attr(Py_TARGET_ABI3T, allow(irrefutable_let_patterns))] let (PyBackedBytesStorage::Python(s1), PyBackedBytesStorage::Python(s2)) = (&b1.storage, &b2.storage) else { @@ -708,7 +682,6 @@ mod test { } #[cfg(feature = "py-clone")] - #[cfg(not(Py_TARGET_ABI3T))] #[test] fn test_backed_bytes_from_bytearray_clone() { Python::attach(|py| { @@ -721,7 +694,6 @@ mod test { }); } - #[cfg(not(Py_TARGET_ABI3T))] #[test] fn test_backed_bytes_from_bytearray_clone_ref() { Python::attach(|py| { @@ -741,7 +713,6 @@ mod test { } #[test] - #[cfg(not(Py_TARGET_ABI3T))] fn test_backed_bytes_eq() { Python::attach(|py| { let b1: PyBackedBytes = PyBytes::new(py, b"abcde").into(); @@ -773,17 +744,14 @@ mod test { }; assert_eq!(h, h1); - #[cfg(not(Py_TARGET_ABI3T))] - { - let b2: PyBackedBytes = PyByteArray::new(py, b"abcde").into(); - let h2 = { - let mut hasher = DefaultHasher::new(); - b2.hash(&mut hasher); - hasher.finish() - }; + 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, h2); - } + assert_eq!(h, h2); }); } diff --git a/src/sync.rs b/src/sync.rs index 06e8326b15b..a088e5dd4b6 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -9,7 +9,6 @@ //! interpreter. //! //! This module provides synchronization primitives which are able to synchronize under these conditions. -#[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 +26,6 @@ pub(crate) mod once_lock; since = "0.28.0", note = "use pyo3::sync::critical_section::with_critical_section instead" )] -#[cfg(not(Py_TARGET_ABI3T))] pub fn with_critical_section(object: &Bound<'_, PyAny>, f: F) -> R where F: FnOnce() -> R, @@ -40,7 +38,6 @@ where since = "0.28.0", note = "use pyo3::sync::critical_section::with_critical_section2 instead" )] -#[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 06ff92cdd51..b121de50a84 100644 --- a/src/sync/critical_section.rs +++ b/src/sync/critical_section.rs @@ -42,15 +42,20 @@ use crate::types::PyMutex; #[cfg(all(Py_3_14, not(Py_LIMITED_API)))] use crate::Python; -#[cfg(not(Py_TARGET_ABI3T))] use crate::{types::PyAny, Bound}; #[cfg(all(Py_3_14, not(Py_LIMITED_API)))] use std::cell::UnsafeCell; -#[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API)))] +#[cfg(all( + Py_GIL_DISABLED, + any(all(not(Py_LIMITED_API), not(Py_3_15)), all(Py_LIMITED_API, Py_3_15)) +))] struct CSGuard(crate::ffi::PyCriticalSection); -#[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API)))] +#[cfg(all( + Py_GIL_DISABLED, + any(all(not(Py_LIMITED_API), not(Py_3_15)), all(Py_LIMITED_API, Py_3_15)) +))] impl Drop for CSGuard { fn drop(&mut self) { unsafe { @@ -59,10 +64,16 @@ impl Drop for CSGuard { } } -#[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API)))] +#[cfg(all( + Py_GIL_DISABLED, + any(all(not(Py_LIMITED_API), not(Py_3_15)), all(Py_LIMITED_API, Py_3_15)) +))] struct CS2Guard(crate::ffi::PyCriticalSection2); -#[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API)))] +#[cfg(all( + Py_GIL_DISABLED, + any(all(not(Py_LIMITED_API), not(Py_3_15)), all(Py_LIMITED_API, Py_3_15)) +))] impl Drop for CS2Guard { fn drop(&mut self) { unsafe { @@ -126,7 +137,6 @@ 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_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 +164,6 @@ 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_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 @@ -271,40 +280,30 @@ where #[cfg(test)] mod tests { #[cfg(feature = "macros")] - #[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}; #[cfg(all(not(Py_LIMITED_API), Py_3_14))] use crate::types::PyMutex; - #[cfg(all(not(Py_TARGET_ABI3T), feature = "macros"))] + #[cfg(feature = "macros")] use std::sync::atomic::{AtomicBool, Ordering}; - #[cfg(all( - not(Py_TARGET_ABI3T), - any(feature = "macros", all(not(Py_LIMITED_API), Py_3_14)) - ))] + #[cfg(any(feature = "macros", all(not(Py_LIMITED_API), Py_3_14)))] use std::sync::Barrier; - #[cfg(all(not(Py_TARGET_ABI3T), feature = "macros"))] + #[cfg(feature = "macros")] use crate::Py; - #[cfg(all( - not(Py_TARGET_ABI3T), - any(feature = "macros", all(not(Py_LIMITED_API), Py_3_14)) - ))] + #[cfg(any(feature = "macros", all(not(Py_LIMITED_API), Py_3_14)))] use crate::Python; - #[cfg(not(Py_TARGET_ABI3T))] #[cfg(feature = "macros")] #[crate::pyclass(crate = "crate")] struct VecWrapper(Vec); - #[cfg(not(Py_TARGET_ABI3T))] #[cfg(feature = "macros")] #[crate::pyclass(crate = "crate")] struct BoolWrapper(AtomicBool); #[cfg(feature = "macros")] - #[cfg(not(Py_TARGET_ABI3T))] #[test] fn test_critical_section() { let barrier = Barrier::new(2); @@ -370,7 +369,6 @@ mod tests { #[cfg(feature = "macros")] #[test] - #[cfg(not(Py_TARGET_ABI3T))] fn test_critical_section2() { let barrier = Barrier::new(3); @@ -453,7 +451,6 @@ mod tests { #[cfg(feature = "macros")] #[test] - #[cfg(not(Py_TARGET_ABI3T))] fn test_critical_section2_same_object_no_deadlock() { let barrier = Barrier::new(2); @@ -519,7 +516,6 @@ mod tests { #[cfg(feature = "macros")] #[test] - #[cfg(not(Py_TARGET_ABI3T))] 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 ae9e9871e6f..f8e08388163 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -2,7 +2,6 @@ use crate::err::{PyErr, PyResult}; use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::{Borrowed, Bound}; use crate::py_result_ext::PyResultExt; -#[cfg(not(Py_TARGET_ABI3T))] use crate::sync::critical_section::with_critical_section; use crate::{ffi, PyAny, Python}; #[cfg(RustPython)] @@ -151,14 +150,10 @@ pub trait PyByteArrayMethods<'py>: crate::sealed::Sealed { /// /// ```rust /// use pyo3::prelude::*; - /// # #[cfg(not(Py_TARGET_ABI3T))] /// use pyo3::exceptions::PyRuntimeError; - /// # #[cfg(not(Py_TARGET_ABI3T))] /// use pyo3::sync::critical_section::with_critical_section; - /// # #[cfg(not(Py_TARGET_ABI3T))] /// use pyo3::types::PyByteArray; /// - /// # #[cfg(not(Py_TARGET_ABI3T))] /// #[pyfunction] /// fn a_valid_function(bytes: &Bound<'_, PyByteArray>) -> PyResult<()> { /// let section = with_critical_section(bytes, || { @@ -179,9 +174,6 @@ pub trait PyByteArrayMethods<'py>: crate::sealed::Sealed { /// /// Ok(()) /// } - /// # #[cfg(Py_TARGET_ABI3T)] - /// # fn main() -> () {} - /// # #[cfg(not(Py_TARGET_ABI3T))] /// # fn main() -> PyResult<()> { /// # Python::attach(|py| -> PyResult<()> { /// # let fun = wrap_pyfunction!(a_valid_function, py)?; @@ -263,7 +255,6 @@ pub trait PyByteArrayMethods<'py>: crate::sealed::Sealed { /// pyo3::py_run!(py, bytearray, "assert bytearray == b'Hello World.'"); /// # }); /// ``` - #[cfg(not(Py_TARGET_ABI3T))] fn to_vec(&self) -> Vec; /// Resizes the bytearray object to the new length `len`. @@ -296,7 +287,6 @@ impl<'py> PyByteArrayMethods<'py> for Bound<'py, PyByteArray> { unsafe { self.as_borrowed().as_bytes_mut() } } - #[cfg(not(Py_TARGET_ABI3T))] fn to_vec(&self) -> Vec { with_critical_section(self, || { // SAFETY: @@ -387,7 +377,6 @@ mod tests { } #[test] - #[cfg(not(Py_TARGET_ABI3T))] fn test_to_vec() { Python::attach(|py| { let src = b"Hello Python"; @@ -489,7 +478,6 @@ mod tests { any(Py_3_14, not(all(Py_3_13, Py_GIL_DISABLED))) ))] #[test] - #[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 0f7b64cf8d5..afceaaf4ea6 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -202,7 +202,6 @@ 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(Py_TARGET_ABI3T))] fn locked_for_each(&self, closure: F) -> PyResult<()> where F: Fn(Bound<'py, PyAny>, Bound<'py, PyAny>) -> PyResult<()>; @@ -391,7 +390,6 @@ impl<'py> PyDictMethods<'py> for Bound<'py, PyDict> { BoundDictIterator::new(self.clone()) } - #[cfg(not(Py_TARGET_ABI3T))] fn locked_for_each(&self, f: F) -> PyResult<()> where F: Fn(Bound<'py, PyAny>, Bound<'py, PyAny>) -> PyResult<()>, @@ -549,27 +547,12 @@ pub struct BoundDictIterator<'py> { enum DictIterImpl { DictIter { - #[cfg(not(Py_TARGET_ABI3T))] ppos: ffi::Py_ssize_t, di_used: ffi::Py_ssize_t, remaining: ffi::Py_ssize_t, - #[cfg(Py_TARGET_ABI3T)] - iter: *mut ffi::PyObject, }, } -#[cfg(Py_TARGET_ABI3T)] -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] @@ -583,10 +566,7 @@ impl DictIterImpl { Self::DictIter { di_used, remaining, - #[cfg(not(Py_TARGET_ABI3T))] ppos, - #[cfg(Py_TARGET_ABI3T)] - iter, .. } => { let ma_used = dict_len(dict); @@ -615,55 +595,26 @@ impl DictIterImpl { panic!("dictionary keys changed during iteration"); }; - #[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(); - - 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(Py_TARGET_ABI3T)] - { - let py = dict.py(); - 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), - }; - // 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. + 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 value = match dict.get_item(&key) { - Ok(value) => value?, - Err(e) => { - panic!("Iterating over dictionary failed with error '{}'", e) - } - }; - Some((key, value)) + 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(not(Py_TARGET_ABI3T))] #[cfg(Py_GIL_DISABLED)] #[inline] fn with_critical_section(&mut self, dict: &Bound<'_, PyDict>, f: F) -> R @@ -683,7 +634,8 @@ impl<'py> Iterator for BoundDictIterator<'py> { #[inline] fn next(&mut self) -> Option { - #[cfg(not(Py_TARGET_ABI3T))] + // SAFETY: Iteration uses an iterator object, relying on CPython + // guarantees that the PyDict and PyIter APIs are safe. #[cfg(Py_GIL_DISABLED)] { self.inner @@ -691,12 +643,8 @@ impl<'py> Iterator for BoundDictIterator<'py> { inner.next_unchecked(&self.dict) }) } - #[cfg(any(Py_TARGET_ABI3T, not(Py_GIL_DISABLED)))] + #[cfg(not(Py_GIL_DISABLED))] { - // SAFETY: next_unchecked always owns strong references to - // 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) } } } @@ -717,7 +665,6 @@ impl<'py> Iterator for BoundDictIterator<'py> { #[inline] #[cfg(Py_GIL_DISABLED)] - #[cfg(not(Py_TARGET_ABI3T))] fn fold(mut self, init: B, mut f: F) -> B where Self: Sized, @@ -750,7 +697,6 @@ impl<'py> Iterator for BoundDictIterator<'py> { } #[inline] - #[cfg(not(Py_TARGET_ABI3T))] #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] fn all(&mut self, mut f: F) -> bool where @@ -768,7 +714,6 @@ impl<'py> Iterator for BoundDictIterator<'py> { } #[inline] - #[cfg(not(Py_TARGET_ABI3T))] #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] fn any(&mut self, mut f: F) -> bool where @@ -786,7 +731,6 @@ impl<'py> Iterator for BoundDictIterator<'py> { } #[inline] - #[cfg(not(Py_TARGET_ABI3T))] #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] fn find

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

(&mut self, mut predicate: P) -> Option where @@ -853,25 +795,12 @@ impl ExactSizeIterator for BoundDictIterator<'_> { impl<'py> BoundDictIterator<'py> { fn new(dict: Bound<'py, PyDict>) -> Self { let di_used = dict_len(&dict); - #[cfg(Py_TARGET_ABI3T)] - 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, inner: DictIterImpl::DictIter { - #[cfg(not(Py_TARGET_ABI3T))] ppos: 0, di_used, remaining: di_used, - #[cfg(Py_TARGET_ABI3T)] - iter, }, } } diff --git a/src/types/list.rs b/src/types/list.rs index b55ddbce49e..edd24948aff 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -230,7 +230,6 @@ 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(Py_TARGET_ABI3T))] fn locked_for_each(&self, closure: F) -> PyResult<()> where F: Fn(Bound<'py, PyAny>) -> PyResult<()>; @@ -440,7 +439,6 @@ 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(Py_TARGET_ABI3T))] fn locked_for_each(&self, closure: F) -> PyResult<()> where F: Fn(Bound<'py, PyAny>) -> PyResult<()>, @@ -538,7 +536,6 @@ impl<'py> BoundListIterator<'py> { #[inline] #[cfg(not(feature = "nightly"))] - #[cfg(not(Py_TARGET_ABI3T))] fn nth( index: &mut Index, length: &mut Length, @@ -610,7 +607,6 @@ impl<'py> BoundListIterator<'py> { #[inline] #[cfg(not(feature = "nightly"))] - #[cfg(not(Py_TARGET_ABI3T))] fn nth_back( index: &mut Index, length: &mut Length, @@ -639,7 +635,6 @@ impl<'py> BoundListIterator<'py> { } #[allow(dead_code)] - #[cfg(not(Py_TARGET_ABI3T))] fn with_critical_section( &mut self, f: impl FnOnce(&mut Index, &mut Length, &Bound<'py, PyList>) -> R, @@ -677,7 +672,6 @@ impl<'py> Iterator for BoundListIterator<'py> { #[inline] #[cfg(not(feature = "nightly"))] - #[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)) } @@ -873,7 +867,6 @@ impl DoubleEndedIterator for BoundListIterator<'_> { #[inline] #[cfg(not(feature = "nightly"))] - #[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)) } From 53014c0004e25dc43ad6daf50e03d15cc0b7dbf0 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 12 May 2026 16:23:42 -0600 Subject: [PATCH 168/195] revert more unnecessary changes --- src/conversions/bytes.rs | 3 +-- src/conversions/std/num.rs | 17 +++++++---------- src/pybacked.rs | 5 ++--- src/sync.rs | 8 ++++++-- src/types/dict.rs | 9 ++++----- 5 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/conversions/bytes.rs b/src/conversions/bytes.rs index ab8a9734d5a..78979d6be68 100644 --- a/src/conversions/bytes.rs +++ b/src/conversions/bytes.rs @@ -114,8 +114,7 @@ impl<'py> IntoPyObject<'py> for &Bytes { #[cfg(test)] mod tests { use super::*; - use crate::types::{PyAnyMethods, PyBytes}; - use crate::types::{PyByteArray, PyByteArrayMethods}; + use crate::types::{PyAnyMethods, PyByteArray, PyByteArrayMethods, PyBytes}; use crate::Python; #[test] diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index 0cf82f1dc9d..fb122c279ef 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -6,8 +6,7 @@ use crate::inspect::PyStaticExpr; use crate::py_result_ext::PyResultExt; #[cfg(feature = "experimental-inspect")] use crate::type_object::PyTypeInfo; -use crate::types::{PyByteArray, PyByteArrayMethods}; -use crate::types::{PyBytes, PyInt}; +use crate::types::{PyByteArray, PyByteArrayMethods, PyBytes, PyInt}; use crate::{exceptions, ffi, Borrowed, Bound, FromPyObject, PyAny, PyErr, PyResult, Python}; use std::convert::Infallible; use std::ffi::c_long; @@ -256,14 +255,12 @@ 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 - } + 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 } } } diff --git a/src/pybacked.rs b/src/pybacked.rs index bb78ebfce03..d7feaa2bfa2 100644 --- a/src/pybacked.rs +++ b/src/pybacked.rs @@ -11,8 +11,7 @@ use crate::{ }, Borrowed, Bound, CastError, FromPyObject, IntoPyObject, Py, PyAny, PyErr, PyTypeInfo, Python, }; -use std::sync::Arc; -use std::{borrow::Borrow, convert::Infallible, ops::Deref, ptr::NonNull}; +use std::{borrow::Borrow, convert::Infallible, ops::Deref, ptr::NonNull, sync::Arc}; /// An equivalent to `String` where the storage is owned by a Python `bytes` or `str` object. /// @@ -742,7 +741,6 @@ mod test { b1.hash(&mut hasher); hasher.finish() }; - assert_eq!(h, h1); let b2: PyBackedBytes = PyByteArray::new(py, b"abcde").into(); let h2 = { @@ -751,6 +749,7 @@ mod test { hasher.finish() }; + assert_eq!(h, h1); assert_eq!(h, h2); }); } diff --git a/src/sync.rs b/src/sync.rs index a088e5dd4b6..cb71ccd10f5 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -9,8 +9,12 @@ //! interpreter. //! //! This module provides synchronization primitives which are able to synchronize under these conditions. -use crate::types::PyAny; -use crate::{internal::state::SuspendAttach, sealed::Sealed, types::PyString, Bound, Py, Python}; +use crate::{ + internal::state::SuspendAttach, + sealed::Sealed, + types::{PyAny, PyString}, + Bound, Py, Python, +}; use std::{ cell::UnsafeCell, marker::PhantomData, diff --git a/src/types/dict.rs b/src/types/dict.rs index afceaaf4ea6..443ba69f8dc 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -794,13 +794,13 @@ impl ExactSizeIterator for BoundDictIterator<'_> { impl<'py> BoundDictIterator<'py> { fn new(dict: Bound<'py, PyDict>) -> Self { - let di_used = dict_len(&dict); + let remaining = dict_len(&dict); Self { dict, inner: DictIterImpl::DictIter { ppos: 0, - di_used, - remaining: di_used, + di_used: remaining, + remaining, }, } } @@ -954,8 +954,7 @@ where #[cfg(test)] mod tests { use super::*; - use crate::types::PyAnyMethods as _; - use crate::types::PyTuple; + use crate::types::{PyAnyMethods as _, PyTuple}; use std::collections::{BTreeMap, HashMap}; #[test] From 208fe8f55aa20534fd1d1e28f03e0bb5b8665797 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 12 May 2026 18:36:23 -0600 Subject: [PATCH 169/195] reduce diff further --- src/types/dict.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/types/dict.rs b/src/types/dict.rs index 443ba69f8dc..f3eabcddbd8 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -634,8 +634,6 @@ impl<'py> Iterator for BoundDictIterator<'py> { #[inline] fn next(&mut self) -> Option { - // SAFETY: Iteration uses an iterator object, relying on CPython - // guarantees that the PyDict and PyIter APIs are safe. #[cfg(Py_GIL_DISABLED)] { self.inner @@ -795,6 +793,7 @@ impl ExactSizeIterator for BoundDictIterator<'_> { impl<'py> BoundDictIterator<'py> { fn new(dict: Bound<'py, PyDict>) -> Self { let remaining = dict_len(&dict); + Self { dict, inner: DictIterImpl::DictIter { From d9ade286f4666ba7abbf7fda1bb8c264cf0180d1 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 12 May 2026 19:01:37 -0600 Subject: [PATCH 170/195] fix cfg gating for critical section guards --- src/sync/critical_section.rs | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/src/sync/critical_section.rs b/src/sync/critical_section.rs index b121de50a84..e6ae83999b0 100644 --- a/src/sync/critical_section.rs +++ b/src/sync/critical_section.rs @@ -46,16 +46,10 @@ use crate::{types::PyAny, Bound}; #[cfg(all(Py_3_14, not(Py_LIMITED_API)))] use std::cell::UnsafeCell; -#[cfg(all( - Py_GIL_DISABLED, - any(all(not(Py_LIMITED_API), not(Py_3_15)), all(Py_LIMITED_API, Py_3_15)) -))] +#[cfg(all(Py_GIL_DISABLED, any(not(Py_LIMITED_API), not(Py_3_15), Py_3_15)))] struct CSGuard(crate::ffi::PyCriticalSection); -#[cfg(all( - Py_GIL_DISABLED, - any(all(not(Py_LIMITED_API), not(Py_3_15)), all(Py_LIMITED_API, Py_3_15)) -))] +#[cfg(all(Py_GIL_DISABLED, any(not(Py_LIMITED_API), not(Py_3_15), Py_3_15)))] impl Drop for CSGuard { fn drop(&mut self) { unsafe { @@ -64,16 +58,10 @@ impl Drop for CSGuard { } } -#[cfg(all( - Py_GIL_DISABLED, - any(all(not(Py_LIMITED_API), not(Py_3_15)), all(Py_LIMITED_API, Py_3_15)) -))] +#[cfg(all(Py_GIL_DISABLED, any(not(Py_LIMITED_API), not(Py_3_15), Py_3_15)))] struct CS2Guard(crate::ffi::PyCriticalSection2); -#[cfg(all( - Py_GIL_DISABLED, - any(all(not(Py_LIMITED_API), not(Py_3_15)), all(Py_LIMITED_API, Py_3_15)) -))] +#[cfg(all(Py_GIL_DISABLED, any(not(Py_LIMITED_API), not(Py_3_15), Py_3_15)))] impl Drop for CS2Guard { fn drop(&mut self) { unsafe { From 0e4d6d881360c5502512eb6ff6514a6eddc6b949 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 14 May 2026 12:10:10 -0600 Subject: [PATCH 171/195] Merging with David's default_lib_name_for_config_file branch --- pyo3-build-config/src/impl_.rs | 185 +++++++++++---------------------- pyo3-build-config/src/lib.rs | 7 +- 2 files changed, 64 insertions(+), 128 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index c713f9e5ffc..b37e06eef60 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -616,7 +616,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) /// Import an externally-provided config file. /// /// The `abi3` features, if set, may apply an `abi3` constraint to the Python version. - pub(super) fn from_pyo3_config_file_env() -> Option> { + pub(super) fn from_pyo3_config_file_env(target: &Triple) -> Option> { env_var("PYO3_CONFIG_FILE").map(|path| { let path = Path::new(&path); println!("cargo:rerun-if-changed={}", path.display()); @@ -627,9 +627,16 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) "PYO3_CONFIG_FILE must be an absolute path" ); - InterpreterConfig::from_path(path) + let mut config = InterpreterConfig::from_path(path) .context("failed to parse contents of PYO3_CONFIG_FILE")? - .apply_build_env() + .apply_build_env()?; + // For config files which don't apply a lib name, apply a default which we can use + // for linking. + if config.lib_name.is_none() { + config.lib_name = Some(default_lib_name_for_target(config.target_abi, target)); + } + + Ok(config) }) } @@ -769,17 +776,6 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) builder.finalize() } - /// Helper function to apply a default lib_name if none is set in `PYO3_CONFIG_FILE`. - /// - /// This requires knowledge of the final target, so cannot be done when the config file is - /// inlined into `pyo3-build-config` at build time and instead needs to be done when - /// resolving the build config for linking. - pub(crate) fn apply_default_lib_name_to_config_file(&mut self, target: &Triple) { - if self.lib_name.is_none() { - self.lib_name = Some(default_lib_name_for_target(self.target_abi, target)); - } - } - #[doc(hidden)] /// Serialize the `InterpreterConfig` and print it to the environment for Cargo to pass along /// to dependent packages during build time. @@ -1706,19 +1702,6 @@ pub fn cross_compiling_from_to( CrossCompileConfig::try_from_env_vars_host_target(env_vars, host, target) } -/// Detect whether we are cross compiling from Cargo and `PYO3_CROSS_*` environment -/// variables and return an assembled `CrossCompileConfig` if so. -/// -/// This must be called from PyO3's build script, because it relies on environment -/// variables such as `CARGO_CFG_TARGET_OS` which aren't available at any other time. -pub fn cross_compiling_from_cargo_env() -> Result> { - let env_vars = CrossCompileEnvVars::from_env(); - let host = Triple::host(); - let target = target_triple_from_env(); - - CrossCompileConfig::try_from_env_vars_host_target(env_vars, &host, &target) -} - #[allow(non_camel_case_types)] #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum BuildFlag { @@ -2498,12 +2481,13 @@ fn get_host_interpreter(stable_abi_version: Option) -> Result Result> { - let interpreter_config = if let Some(cross_config) = cross_compiling_from_cargo_env()? { - Some(load_cross_compile_config(cross_config)?.apply_build_env()?) - } else { - None - }; +pub fn make_cross_compile_config(target: &Triple) -> Result> { + let interpreter_config = + if let Some(cross_config) = cross_compiling_from_to(&Triple::host(), target)? { + Some(load_cross_compile_config(cross_config)?.apply_build_env()?) + } else { + None + }; Ok(interpreter_config) } @@ -3864,120 +3848,73 @@ mod tests { #[test] fn test_from_pyo3_config_file_env_rebuild() { READ_ENV_VARS.with(|vars| vars.borrow_mut().clear()); - let _ = InterpreterConfig::from_pyo3_config_file_env(); + let _ = InterpreterConfig::from_pyo3_config_file_env(&Triple::host()); // it's possible that other env vars were also read, hence just checking for contains READ_ENV_VARS.with(|vars| assert!(vars.borrow().contains(&"PYO3_CONFIG_FILE".to_string()))); } #[test] - fn test_apply_default_lib_name_to_config_file() { - fn py39_config() -> InterpreterConfig { - InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .finalize() - .unwrap() - } - - fn pypy_config() -> InterpreterConfig { - InterpreterConfigBuilder::new( - PythonImplementation::PyPy, - PythonVersion { - major: 3, - minor: 11, - }, - ) - .finalize() - .unwrap() - } - - fn free_threaded_config() -> InterpreterConfig { - InterpreterConfigBuilder::new( - PythonImplementation::CPython, - PythonVersion { - major: 3, - minor: 13, - }, - ) + fn test_default_lib_name_for_target() { + let cpython = PythonImplementation::CPython; + let pypy = PythonImplementation::PyPy; + let py39 = PythonVersion::PY39; + let py311 = PythonVersion { + major: 3, + minor: 11, + }; + let py313 = PythonVersion { + major: 3, + minor: 13, + }; + let cpy39 = PythonAbiBuilder::new(cpython, py39).finalize().unwrap(); + let pypy311 = PythonAbiBuilder::new(pypy, py311).finalize().unwrap(); + let cpy313t = PythonAbiBuilder::new(cpython, py313) .free_threaded() - .unwrap() .finalize() - .unwrap() - } - - fn stable_abi_config(kind: StableAbi) -> InterpreterConfig { - InterpreterConfigBuilder::new( - PythonImplementation::CPython, - PythonVersion { - major: 3, - minor: 13, - }, - ) - .stable_abi(kind) + .unwrap(); + let cpy313_abi3 = PythonAbiBuilder::new(cpython, py313) + .stable_abi(StableAbi::Abi3) .finalize() - .unwrap() - } + .unwrap(); let unix = Triple::from_str("x86_64-unknown-linux-gnu").unwrap(); let win_x64 = Triple::from_str("x86_64-pc-windows-msvc").unwrap(); let win_arm64 = Triple::from_str("aarch64-pc-windows-msvc").unwrap(); - let mut config = py39_config(); - config.apply_default_lib_name_to_config_file(&unix); - assert_eq!(config.lib_name, Some("python3.9".into())); + let lib_name = default_lib_name_for_target(cpy39, &unix); + assert_eq!(lib_name, "python3.9"); - let mut config = py39_config(); - config.apply_default_lib_name_to_config_file(&win_x64); - assert_eq!(config.lib_name, Some("python39".into())); + let lib_name = default_lib_name_for_target(cpy39, &win_x64); + assert_eq!(lib_name, "python39"); - let mut config = py39_config(); - config.apply_default_lib_name_to_config_file(&win_arm64); - assert_eq!(config.lib_name, Some("python39".into())); + let lib_name = default_lib_name_for_target(cpy39, &win_arm64); + assert_eq!(lib_name, "python39"); // PyPy - let mut config = pypy_config(); - config.apply_default_lib_name_to_config_file(&unix); - assert_eq!(config.lib_name, Some("pypy3.11-c".into())); + let lib_name = default_lib_name_for_target(pypy311, &unix); + assert_eq!(lib_name, "pypy3.11-c"); - let mut config = pypy_config(); - config.apply_default_lib_name_to_config_file(&win_x64); - assert_eq!(config.lib_name, Some("libpypy3.11-c".into())); + let lib_name = default_lib_name_for_target(pypy311, &win_x64); + assert_eq!(lib_name, "libpypy3.11-c"); // Free-threaded - let mut config = free_threaded_config(); - config.apply_default_lib_name_to_config_file(&unix); - assert_eq!(config.lib_name, Some("python3.13t".into())); + let lib_name = default_lib_name_for_target(cpy313t, &unix); + assert_eq!(lib_name, "python3.13t"); - let mut config = free_threaded_config(); - config.apply_default_lib_name_to_config_file(&win_x64); - assert_eq!(config.lib_name, Some("python313t".into())); + let lib_name = default_lib_name_for_target(cpy313t, &win_x64); + assert_eq!(lib_name, "python313t"); - let mut config = free_threaded_config(); - config.apply_default_lib_name_to_config_file(&win_arm64); - assert_eq!(config.lib_name, Some("python313t".into())); + let lib_name = default_lib_name_for_target(cpy313t, &win_arm64); + assert_eq!(lib_name, "python313t"); // abi3 - let mut config = stable_abi_config(StableAbi::Abi3); - config.apply_default_lib_name_to_config_file(&unix); - assert_eq!(config.lib_name, Some("python3.13".into())); - - let mut config = stable_abi_config(StableAbi::Abi3); - config.apply_default_lib_name_to_config_file(&win_x64); - assert_eq!(config.lib_name, Some("python3".into())); - - let mut config = stable_abi_config(StableAbi::Abi3); - config.apply_default_lib_name_to_config_file(&win_arm64); - assert_eq!(config.lib_name, Some("python3".into())); - - // abi3t - let mut config = stable_abi_config(StableAbi::Abi3t); - config.apply_default_lib_name_to_config_file(&unix); - assert_eq!(config.lib_name, Some("python3.13t".into())); - - let mut config = stable_abi_config(StableAbi::Abi3t); - config.apply_default_lib_name_to_config_file(&win_x64); - assert_eq!(config.lib_name, Some("python3t".into())); - - let mut config = stable_abi_config(StableAbi::Abi3t); - config.apply_default_lib_name_to_config_file(&win_arm64); - assert_eq!(config.lib_name, Some("python3t".into())); + let lib_name = default_lib_name_for_target(cpy313_abi3, &unix); + assert_eq!(lib_name, "python3.13"); + + let lib_name = default_lib_name_for_target(cpy313_abi3, &win_x64); + assert_eq!(lib_name, "python3"); + + let lib_name = default_lib_name_for_target(cpy313_abi3, &win_arm64); + assert_eq!(lib_name, "python3"); } } diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 475ed989e66..86521d68849 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -277,15 +277,14 @@ pub mod pyo3_build_script_impl { clippy::const_is_empty, reason = "CONFIG_FILE is generated in build.rs, content can vary" )] - if let Some(mut interpreter_config) = - InterpreterConfig::from_pyo3_config_file_env().transpose()? + if let Some(interpreter_config) = + InterpreterConfig::from_pyo3_config_file_env(target).transpose()? { - interpreter_config.apply_default_lib_name_to_config_file(target); Ok(BuildConfig { interpreter_config, source: BuildConfigSource::ConfigFile, }) - } else if let Some(interpreter_config) = make_cross_compile_config()? { + } else if let Some(interpreter_config) = make_cross_compile_config(target)? { Ok(BuildConfig { interpreter_config, source: BuildConfigSource::CrossCompile, From 6c514fbae268d59c7d5cf65e703170d861e49b35 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 14 May 2026 14:20:22 -0600 Subject: [PATCH 172/195] remove unnecessary get_abi3_version().is_some() --- pyo3-build-config/src/impl_.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 604be778d9a..b57d43daa86 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -756,10 +756,8 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) PythonAbiBuilder::new(implementation, version) .free_threaded() .finalize()? - } else if (abi3 == Some(true)) || get_abi3_version().is_some() { - if abi3 == Some(true) { - warn!("abi3 configuration file option is deprecated, set target_abi instead"); - } + } else if abi3 == Some(true) { + warn!("abi3 configuration file option is deprecated, set target_abi instead"); PythonAbiBuilder::new(implementation, version) .stable_abi(StableAbi::Abi3) .finalize()? From 5518b84e7df2cf08ceabdd78802ef3bad1b0cff0 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 14 May 2026 14:23:47 -0600 Subject: [PATCH 173/195] bring back Option --- pyo3-ffi/src/slots.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyo3-ffi/src/slots.rs b/pyo3-ffi/src/slots.rs index 86b3912eb8f..0a023ec4079 100644 --- a/pyo3-ffi/src/slots.rs +++ b/pyo3-ffi/src/slots.rs @@ -18,7 +18,7 @@ pub union _anon_union_32b { #[cfg(Py_3_15)] pub union _anon_union_64b { pub sl_ptr: *mut c_void, - pub sl_func: _Py_funcptr_t, + pub sl_func: Option<_Py_funcptr_t>, pub sl_size: Py_ssize_t, pub sl_int64: i64, pub sl_uint64: u64, @@ -78,7 +78,7 @@ pub const unsafe fn PySlot_FUNC(NAME: c_int, VALUE: *mut c_void) -> PySlot { sl_flags: 0, anon1: _anon_union_32b { sl_reserved: 0 }, anon2: _anon_union_64b { - sl_func: unsafe { core::mem::transmute::<*mut c_void, _Py_funcptr_t>(VALUE) }, + sl_func: Some(unsafe { core::mem::transmute::<*mut c_void, _Py_funcptr_t>(VALUE) }), }, } } From 01232318da0aba86833a50a06c679ffc338067f0 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 14 May 2026 14:24:55 -0600 Subject: [PATCH 174/195] fix comparison for ABI3T_FORWARD_COMPATIBILITY --- 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 002571f7890..bc7a0746137 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -75,7 +75,7 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { minor: 15, }; if interpreter_config.target_abi().kind().is_free_threaded() { - if (PythonVersion { major, minor }) > py_3_15 { + if (PythonVersion { major, minor }) >= py_3_15 { if env_var("PYO3_USE_ABI3T_FORWARD_COMPATIBILITY") .is_none_or(|os_str| os_str != "1") { From ed9807c827cfb479d57a3b6d63eb4d1de07b2a57 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 14 May 2026 14:28:39 -0600 Subject: [PATCH 175/195] Apply minor suggestions from code review --- noxfile.py | 5 +++-- src/types/any.rs | 8 ++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/noxfile.py b/noxfile.py index 82d34e52406..04a8241a333 100644 --- a/noxfile.py +++ b/noxfile.py @@ -100,7 +100,7 @@ def test(session: nox.Session) -> None: @nox.session(name="test-rust", venv_backend="none") def test_rust(session: nox.Session): - _run_cargo_test(session, package="pyo3-build-config", features="resolve-config") + _run_cargo_test(session, package="pyo3-build-config") _run_cargo_test(session, package="pyo3-macros-backend") _run_cargo_test(session, package="pyo3-macros") @@ -1207,8 +1207,9 @@ def test_version_limits(session: nox.Session): with _config_file() as config_file: env["PYO3_CONFIG_FILE"] = config_file.name + # Oldest-support Python version - 1 should error assert "3.7" not in PY_VERSIONS - config_file.set("CPython", "3.6") + config_file.set("CPython", "3.7") _run_cargo(session, "check", env=env, expect_error=True) # We allow building with our max version + 1, to support diff --git a/src/types/any.rs b/src/types/any.rs index 225eecfeffd..5b8013c289c 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -68,8 +68,12 @@ 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 +// We could use pyobject_subclassable_native_type for all builds here, but for +// now only on opaque PyObject builds to not introduce unintended behavior +// changes on older Python releases. +// +// The difference is that pyobject_subclassable_native_type will use variable +// object size for inheritance rather than PyStaticClassObject #[cfg(not(Py_TARGET_ABI3T))] impl crate::impl_::pyclass::PyClassBaseType for PyAny { type LayoutAsBase = PyClassObjectBase; From 2b8d7a2a56fb80901c20f655c3e59935e15426e2 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 14 May 2026 14:33:41 -0600 Subject: [PATCH 176/195] delete docs for non-pub field --- pyo3-build-config/src/impl_.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index b57d43daa86..1b7bd49f155 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -142,10 +142,6 @@ pub struct InterpreterConfig { )] pub shared: bool, - /// The ABI to use for the compilation target. - /// See the documentation for the PythonAbi enum for more details. - /// - /// Serialized to `target_abi`. target_abi: PythonAbi, /// Serialized to `abi3`. From b772cf3cb7b8ffa4dbc33822e9fa7514a2d73b1b Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 14 May 2026 14:51:01 -0600 Subject: [PATCH 177/195] Don't define a Py_TARGET_ABI3T cfg check --- .../python-from-rust/calling-existing-code.md | 4 ++-- pyo3-build-config/src/impl_.rs | 1 - pyo3-build-config/src/lib.rs | 1 - pyo3-ffi/src/modsupport.rs | 6 ++--- pyo3-ffi/src/moduleobject.rs | 10 ++++----- pyo3-ffi/src/object.rs | 22 ++++++++----------- pyo3-ffi/src/refcount.rs | 2 +- src/impl_/pyclass.rs | 10 ++++----- src/impl_/pymodule.rs | 14 ++++++------ src/pycell/impl_.rs | 8 +++---- src/types/any.rs | 6 ++--- tests/test_append_to_inittab.rs | 2 +- 12 files changed, 40 insertions(+), 46 deletions(-) diff --git a/guide/src/python-from-rust/calling-existing-code.md b/guide/src/python-from-rust/calling-existing-code.md index 576caa49532..48f54d05532 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_TARGET_ABI3T))] +# #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] 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_TARGET_ABI3T)] +# #[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))] # fn main() -> () {} ``` diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 1b7bd49f155..9929dff3115 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -368,7 +368,6 @@ impl InterpreterConfig { out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); if kind == StableAbi::Abi3t { out.push("cargo:rustc-cfg=Py_GIL_DISABLED".to_owned()); - out.push("cargo:rustc-cfg=Py_TARGET_ABI3T".to_owned()); } } PythonAbiKind::VersionSpecific(kind) => match kind { diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 86521d68849..be241fbc8b7 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -195,7 +195,6 @@ pub fn print_feature_cfgs() { 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_TARGET_ABI3T)"); println!("cargo:rustc-check-cfg=cfg(PyPy)"); println!("cargo:rustc-check-cfg=cfg(GraalPy)"); println!("cargo:rustc-check-cfg=cfg(RustPython)"); diff --git a/pyo3-ffi/src/modsupport.rs b/pyo3-ffi/src/modsupport.rs index 837235460b4..9971556cffc 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_TARGET_ABI3T))] +#[cfg(all(Py_3_15, all(Py_LIMITED_API, Py_GIL_DISABLED)))] const _PyABIInfo_DEFAULT_FLAG_FT: u16 = PyABIInfo_FREETHREADING_AGNOSTIC; -#[cfg(all(Py_3_15, Py_GIL_DISABLED, not(Py_TARGET_ABI3T)))] +#[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), not(Py_TARGET_ABI3T)))] +#[cfg(all(Py_3_15, not(Py_GIL_DISABLED)))] const _PyABIInfo_DEFAULT_FLAG_FT: u16 = PyABIInfo_GIL; #[cfg(Py_3_15)] diff --git a/pyo3-ffi/src/moduleobject.rs b/pyo3-ffi/src/moduleobject.rs index f0ab5657342..22379e18766 100644 --- a/pyo3-ffi/src/moduleobject.rs +++ b/pyo3-ffi/src/moduleobject.rs @@ -1,4 +1,4 @@ -#[cfg(not(Py_TARGET_ABI3T))] +#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] use crate::methodobject::PyMethodDef; use crate::object::*; use crate::pyport::Py_ssize_t; @@ -59,7 +59,7 @@ extern_libpython! { pub static mut PyModuleDef_Type: PyTypeObject; } -#[cfg(not(Py_TARGET_ABI3T))] +#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] #[repr(C)] pub struct PyModuleDef_Base { pub ob_base: PyObject, @@ -69,7 +69,7 @@ pub struct PyModuleDef_Base { pub m_copy: *mut PyObject, } -#[cfg(not(Py_TARGET_ABI3T))] +#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] #[allow( clippy::declare_interior_mutable_const, reason = "contains atomic refcount on free-threaded builds" @@ -130,7 +130,7 @@ extern_libpython! { pub fn PyModule_GetToken(module: *mut PyObject, result: *mut *mut c_void) -> c_int; } -#[cfg(not(Py_TARGET_ABI3T))] +#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] #[repr(C)] pub struct PyModuleDef { pub m_base: PyModuleDef_Base, @@ -146,5 +146,5 @@ pub struct PyModuleDef { } // from pytypedefs.h -#[cfg(Py_TARGET_ABI3T)] +#[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))] opaque_struct!(pub PyModuleDef); diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index 4c26627725f..d07afcc2475 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -1,15 +1,11 @@ use crate::pyport::{Py_hash_t, Py_ssize_t}; -#[cfg(not(Py_TARGET_ABI3T))] -#[cfg(Py_GIL_DISABLED)] -use crate::refcount; -#[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API)))] -use crate::PyMutex; #[cfg(Py_3_15)] use crate::PySlot; +#[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API)))] +use crate::{refcount, PyMutex}; use core::ffi::{c_char, c_int, c_uint, c_ulong, c_void}; use core::mem; -#[cfg(not(Py_TARGET_ABI3T))] -#[cfg(Py_GIL_DISABLED)] +#[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API)))] use core::sync::atomic::{AtomicIsize, AtomicU32}; // from pytypedefs.h @@ -96,7 +92,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_TARGET_ABI3T))] +#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] #[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)] @@ -122,10 +118,10 @@ pub struct PyObject { pub ob_type: *mut PyTypeObject, } -#[cfg(not(Py_TARGET_ABI3T))] +#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] const _: () = assert!(core::mem::align_of::() >= _PyObject_MIN_ALIGNMENT); -#[cfg(not(Py_TARGET_ABI3T))] +#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] #[allow( clippy::declare_interior_mutable_const, reason = "contains atomic refcount on free-threaded builds" @@ -157,14 +153,14 @@ pub const PyObject_HEAD_INIT: PyObject = PyObject { }; // from pytypedefs.h -#[cfg(Py_TARGET_ABI3T)] +#[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))] opaque_struct!(pub PyObject); // skipped _Py_UNOWNED_TID // skipped _PyObject_CAST -#[cfg(not(Py_TARGET_ABI3T))] +#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] #[repr(C)] #[derive(Debug)] pub struct PyVarObject { @@ -177,7 +173,7 @@ pub struct PyVarObject { } // from pytypedefs.h -#[cfg(Py_TARGET_ABI3T)] +#[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))] opaque_struct!(pub PyVarObject); // skipped private _PyVarObject_CAST diff --git a/pyo3-ffi/src/refcount.rs b/pyo3-ffi/src/refcount.rs index 6afdf010e2d..4da10969b4a 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_TARGET_ABI3T))] +#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] #[cfg(Py_3_12)] #[inline(always)] unsafe fn _Py_IsImmortal(op: *mut PyObject) -> c_int { diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 24a34dc4f07..0cd75f7a845 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1443,7 +1443,7 @@ pub trait ExtractPyClassWithClone: generic_pyclass::Sealed {} #[cfg(test)] #[cfg(feature = "macros")] mod tests { - #[cfg(not(Py_TARGET_ABI3T))] + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] use crate::pycell::impl_::PyClassObjectContents; use super::*; @@ -1472,21 +1472,21 @@ 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_TARGET_ABI3T))] + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] #[repr(C)] struct ExpectedLayout { ob_base: ffi::PyObject, contents: PyClassObjectContents, } - #[cfg(not(Py_TARGET_ABI3T))] + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] assert_eq!( member.offset, (offset_of!(ExpectedLayout, contents) + offset_of!(FrozenClass, value)) as ffi::Py_ssize_t ); - #[cfg(not(Py_TARGET_ABI3T))] + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] assert_eq!(member.flags, ffi::Py_READONLY); - #[cfg(Py_TARGET_ABI3T)] + #[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))] // ABI3T builds set other flags besides READONLY assert_eq!(member.flags & ffi::Py_READONLY, ffi::Py_READONLY); } diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index ebb71835c94..7489f3f9ecb 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -47,7 +47,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_TARGET_ABI3T))] + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] 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_TARGET_ABI3T))] + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] #[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_TARGET_ABI3T))] + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] 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_TARGET_ABI3T))] + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] ffi_def, #[cfg(Py_3_15)] name, @@ -122,11 +122,11 @@ impl ModuleDef { } pub fn init_multi_phase(&'static self) -> *mut ffi::PyObject { - #[cfg(not(Py_TARGET_ABI3T))] + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] unsafe { ffi::PyModuleDef_Init(self.ffi_def.get()) } - #[cfg(Py_TARGET_ABI3T)] + #[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))] panic!("Legacy module initialization cannot work under abi3t. Use the PyModExport slots-based initialization hook instead."); } @@ -586,7 +586,7 @@ mod tests { let module_def: ModuleDef = ModuleDef::new(NAME, DOC, &SLOTS); - #[cfg(not(Py_TARGET_ABI3T))] + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] unsafe { assert_eq!((*module_def.ffi_def.get()).m_slots, SLOTS.0.get().cast()); } diff --git a/src/pycell/impl_.rs b/src/pycell/impl_.rs index 26ee6b5b68d..9d1659c283c 100644 --- a/src/pycell/impl_.rs +++ b/src/pycell/impl_.rs @@ -651,16 +651,16 @@ mod tests { #[test] fn test_inherited_size() { - #[cfg(Py_TARGET_ABI3T)] + #[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))] type ClassObject = PyVariableClassObject; - #[cfg(not(Py_TARGET_ABI3T))] + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] 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_TARGET_ABI3T)] + #[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))] { assert!(base_without_data_size < 0); // negative indicates variable sized assert!(base_with_data_size < base_without_data_size); @@ -670,7 +670,7 @@ mod tests { child_with_data_size ); } - #[cfg(not(Py_TARGET_ABI3T))] + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] { 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 5b8013c289c..9ad8e2fcd5c 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -4,7 +4,7 @@ use crate::conversion::{FromPyObject, IntoPyObject}; use crate::err::{error_on_minusone, PyErr, PyResult}; use crate::exceptions::PyTypeError; use crate::ffi_ptr_ext::FfiPtrExt; -#[cfg(not(Py_TARGET_ABI3T))] +#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] use crate::impl_::pycell::{PyClassObjectBase, PyStaticClassObject}; use crate::instance::Bound; use crate::internal::get_slot::TP_DESCR_GET; @@ -74,7 +74,7 @@ pyobject_native_type_sized!(PyAny, ffi::PyObject); // // The difference is that pyobject_subclassable_native_type will use variable // object size for inheritance rather than PyStaticClassObject -#[cfg(not(Py_TARGET_ABI3T))] +#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] impl crate::impl_::pyclass::PyClassBaseType for PyAny { type LayoutAsBase = PyClassObjectBase; type BaseNativeType = PyAny; @@ -83,7 +83,7 @@ impl crate::impl_::pyclass::PyClassBaseType for PyAny { type Layout = PyStaticClassObject; } -#[cfg(Py_TARGET_ABI3T)] +#[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))] pyobject_subclassable_native_type!(PyAny, ffi::PyObject); /// This trait represents the Python APIs which are usable on all Python objects. diff --git a/tests/test_append_to_inittab.rs b/tests/test_append_to_inittab.rs index 80b2f6f37af..20b968fa2dc 100644 --- a/tests/test_append_to_inittab.rs +++ b/tests/test_append_to_inittab.rs @@ -20,7 +20,7 @@ mod module_mod_with_functions { use super::foo; } -#[cfg(not(any(PyPy, GraalPy, Py_TARGET_ABI3T)))] +#[cfg(not(any(PyPy, GraalPy, all(Py_LIMITED_API, Py_GIL_DISABLED))))] #[test] fn test_module_append_to_inittab() { use pyo3::append_to_inittab; From c345a0da16c1785dac65e5a82b1cdad3e69f5587 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 14 May 2026 16:20:50 -0600 Subject: [PATCH 178/195] fix merge errors --- pyo3-build-config/src/impl_.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 9929dff3115..134c3c7affe 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -625,11 +625,6 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let mut config = InterpreterConfig::from_path(path) .context("failed to parse contents of PYO3_CONFIG_FILE")? .apply_build_env()?; - // For config files which don't apply a lib name, apply a default which we can use - // for linking. - if config.lib_name.is_none() { - config.lib_name = Some(default_lib_name_for_target(config.target_abi, target)); - } // For config files which don't apply a lib name, apply a default which we can use // for linking. @@ -2296,7 +2291,7 @@ fn default_lib_name_windows(abi: PythonAbi, mingw: bool, debug: bool) -> Result< } else if abi.kind().is_free_threaded() { #[expect(deprecated, reason = "using constant internally")] { - ensure!(abi.version() >= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", abi.version.major, abi.version.minor); + ensure!(abi.version() >= PythonVersion::PY313, "Cannot compile extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", abi.version.major, abi.version.minor); } if debug { Ok(format!( @@ -2328,7 +2323,7 @@ fn default_lib_name_unix(abi: PythonAbi, cygwin: bool, ld_version: Option<&str>) } else if abi.kind.is_free_threaded() { #[expect(deprecated, reason = "using constant internally")] { - ensure!(abi.version >= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", abi.version.major, abi.version.minor); + ensure!(abi.version >= PythonVersion::PY313, "Cannot compile extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", abi.version.major, abi.version.minor); } Ok(format!( "python{}.{}t", From caf1af81b40fad69dc149d8fbfb2fb950dce5e14 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 14 May 2026 16:23:17 -0600 Subject: [PATCH 179/195] implement Icxolu's suggestion to get rid of is_abi3 and is_abi3t --- pyo3-build-config/src/impl_.rs | 73 +++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 27 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 134c3c7affe..1bc0084ee8d 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -733,9 +733,9 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) "Invalid config that sets both target_abi and abi3." ); target_abi - } else if is_abi3t() { + } else if get_abi3t_version().is_some() { ensure!( - !is_abi3(), + get_abi3_version().is_none(), "Cannot simultaneously enable features that enable abi3 and abi3t builds" ); PythonAbiBuilder::new(implementation, version) @@ -873,7 +873,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) self.target_abi = PythonAbi::from_build_env( self.implementation, self.version, - get_abi3_version().or(get_abi3t_version()), + exact_stable_abi_version(get_abi3_version().or(get_abi3t_version())), self.target_abi.kind().is_free_threaded(), )?; Ok(self) @@ -990,9 +990,9 @@ impl PythonAbi { version: sanitize_stable_abi_version(stable_abi_version, version)?, kind: None, }; - let builder = if is_abi3() && !gil_disabled { + let builder = if get_abi3_version().is_some() && !gil_disabled { builder.stable_abi(StableAbi::Abi3) - } else if is_abi3t() { + } else if get_abi3t_version().is_some() { builder.stable_abi(StableAbi::Abi3t) } else if gil_disabled { builder.free_threaded() @@ -1415,20 +1415,14 @@ fn have_python_interpreter() -> bool { env_var("PYO3_NO_PYTHON").is_none() } -/// Checks if `abi3` or any of the `abi3-py3*` features is enabled for the PyO3 crate. +/// The target stable ABI version. /// -/// Must be called from a PyO3 crate build script. -fn is_abi3() -> bool { - cargo_env_var("CARGO_FEATURE_ABI3").is_some() - || 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") +/// For abi3(t)-py* builds a specific version is chosen, otherwise the builds is +/// for the stable ABI exposed by the host python interpreter version. +#[derive(Debug, Copy, Clone)] +pub enum StableAbiVersion { + Current, + Target(PythonVersion), } /// Gets the minimum supported Python version from PyO3 `abi3-py*` features. @@ -1436,10 +1430,17 @@ fn is_abi3t() -> bool { /// Must be called from a PyO3 crate build script. Returns None if an `abi3-py*` /// feature is activated that is unsupported or if no `abi3-py3*` feature is /// active. -pub fn get_abi3_version() -> Option { +pub fn get_abi3_version() -> Option { 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 }) + minor_version.map_or( + if cargo_env_var("CARGO_FEATURE_ABI3").is_some() { + Some(StableAbiVersion::Current) + } else { + None + }, + |minor| Some(StableAbiVersion::Target(PythonVersion { major: 3, minor })), + ) } /// Gets the minimum supported Python version from PyO3 `abi3t-py*` features. @@ -1447,10 +1448,17 @@ pub fn get_abi3_version() -> Option { /// Must be called from a PyO3 crate build script. Returns None if an `abi3t-py*` /// feature is activated that is unsupported or if no `abi3t-py3*` feature is /// active. -pub fn get_abi3t_version() -> Option { +pub fn get_abi3t_version() -> Option { let minor_version = (MINIMUM_SUPPORTED_VERSION_ABI3T.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 }) + minor_version.map_or( + if cargo_env_var("CARGO_FEATURE_ABI3T").is_some() { + Some(StableAbiVersion::Current) + } else { + None + }, + |minor| Some(StableAbiVersion::Target(PythonVersion { major: 3, minor })), + ) } /// Checks if the `extension-module` feature is enabled for the PyO3 crate. @@ -2127,6 +2135,13 @@ fn cross_compile_from_sysconfigdata( } } +fn exact_stable_abi_version(version: Option) -> Option { + version.and_then(|v| match v { + StableAbiVersion::Current => None, + StableAbiVersion::Target(inner) => Some(inner), + }) +} + /// Generates "default" cross compilation information for the target. /// /// This should work for most CPython extension modules when targeting @@ -2136,8 +2151,8 @@ fn cross_compile_from_sysconfigdata( fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result { let version = cross_compile_config .version - .or_else(get_abi3_version) - .or_else(get_abi3t_version) + .or_else(|| exact_stable_abi_version(get_abi3_version())) + .or_else(|| exact_stable_abi_version(get_abi3t_version())) .ok_or_else(|| format!( "PYO3_CROSS_PYTHON_VERSION or either an abi3-py3* or abi3t-py3* feature must be specified \ @@ -2492,7 +2507,7 @@ pub fn make_interpreter_config() -> Result { let abi3_version = get_abi3_version(); let abi3t_version = get_abi3t_version(); ensure!( - !(is_abi3t() && is_abi3()), + !(abi3_version.is_some() && abi3t_version.is_some()), "Cannot simultaneously enable abi3 and abi3t features" ); @@ -2502,7 +2517,7 @@ pub fn make_interpreter_config() -> Result { (abi3_version.is_none() && abi3t_version.is_none()) || require_libdir_for_target(&host); if have_python_interpreter() { - match get_host_interpreter(abi3_version.or(abi3t_version)) { + match get_host_interpreter(exact_stable_abi_version(abi3_version.or(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), @@ -2514,7 +2529,11 @@ pub fn make_interpreter_config() -> Result { } } - let interpreter_config = default_stable_abi_config(&host, abi3_version, abi3t_version)?; + let interpreter_config = default_stable_abi_config( + &host, + exact_stable_abi_version(abi3_version), + exact_stable_abi_version(abi3t_version), + )?; Ok(interpreter_config) } From 2531c96f324ba064b47e5e22113fc0bfa41fb292 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 14 May 2026 16:23:25 -0600 Subject: [PATCH 180/195] fix clippy-all --- noxfile.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/noxfile.py b/noxfile.py index 04a8241a333..9ff3f05082b 100644 --- a/noxfile.py +++ b/noxfile.py @@ -237,10 +237,10 @@ def clippy(session: nox.Session) -> bool: session.error("one or more jobs failed") -def _clippy(session: nox.Session, *, env: Dict[str, str] = None) -> bool: +def _clippy(session: nox.Session, *, env: Dict[str, str] = None, version: Optional[Tuple[int, int]]) -> bool: success = True env = env or os.environ - for feature_set in _get_feature_sets(): + for feature_set in _get_feature_sets(version): try: _run_cargo( session, @@ -300,9 +300,9 @@ def codspeed(session: nox.Session) -> bool: def clippy_all(session: nox.Session) -> None: success = True - def _clippy_with_config(env: Dict[str, str]) -> None: + def _clippy_with_config(env: Dict[str, str], version: Optional[Tuple[int, int]]) -> None: nonlocal success - success &= _clippy(session, env=env) + success &= _clippy(session, env=env, version=version) _for_all_version_configs(session, _clippy_with_config) success &= _clippy_additional_workspaces(session) @@ -315,7 +315,7 @@ def _clippy_with_config(env: Dict[str, str]) -> None: def check_all(session: nox.Session) -> None: success = True - def _check(env: Dict[str, str]) -> None: + def _check(env: Dict[str, str], version: Tuple[int, int]) -> None: nonlocal success for feature_set in _get_feature_sets(): try: @@ -1653,8 +1653,11 @@ def _get_rust_default_target() -> str: @lru_cache() -def _get_feature_sets() -> Tuple[Optional[str], ...]: +def _get_feature_sets(version: Optional[Tuple[int, int]] = None) -> Tuple[Optional[str], ...]: """Returns feature sets to use for Rust jobs""" + if version == None: + version = sys.version_info[:2] + cargo_target = os.getenv("CARGO_BUILD_TARGET", "") features = "full" @@ -1667,13 +1670,13 @@ def _get_feature_sets() -> Tuple[Optional[str], ...]: features += ",nightly" if FREE_THREADED_BUILD: - if sys.version_info >= (3, 15): + if version >= (3, 15): return (None, "abi3t", features, f"abi3t,{features}") else: return (None, features) # do fewer abi3t builds? - if sys.version_info >= (3, 15): + if version >= (3, 15): return ( None, "abi3", @@ -1795,7 +1798,8 @@ def _for_all_version_configs( def _job_with_config(implementation, version): session.log(f"{implementation} {version}") config_file.set(implementation, version) - job(env) + major_minor = tuple(map(int, version.strip("t").split("."))) + job(env, major_minor) for version in PY_VERSIONS: _job_with_config("CPython", version) From 1908b69bd61f2369e30ba50ba5f73e20d6884fa4 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 14 May 2026 16:27:34 -0600 Subject: [PATCH 181/195] delete unnecessary get_abit3_version() check --- pyo3-build-config/src/impl_.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 1bc0084ee8d..4a45c4e26c2 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -733,14 +733,6 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) "Invalid config that sets both target_abi and abi3." ); target_abi - } else if get_abi3t_version().is_some() { - ensure!( - get_abi3_version().is_none(), - "Cannot simultaneously enable features that enable abi3 and abi3t builds" - ); - PythonAbiBuilder::new(implementation, version) - .stable_abi(StableAbi::Abi3t) - .finalize()? } else if flags_contains_free_threaded { // This fires even if is_abi3() is True for backward compatibility reasons PythonAbiBuilder::new(implementation, version) From d232ac780ea79f58515e93d9fa2d36d0977a9f53 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 14 May 2026 17:07:16 -0600 Subject: [PATCH 182/195] Allow simultaneously enabling abi3 and abi3t features --- noxfile.py | 10 ++- pyo3-build-config/src/impl_.rs | 120 ++++++++++++++++++++++++--------- 2 files changed, 98 insertions(+), 32 deletions(-) diff --git a/noxfile.py b/noxfile.py index 9ff3f05082b..a68d8620daa 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1259,9 +1259,17 @@ def test_version_limits(session: nox.Session): 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 + # 3.17t CPython should build if abi3t is explicitly requested _run_cargo(session, "check", "--features=pyo3/abi3t", env=env) + # abi3t builds should succeed on Python versions that support abi3t + config_file.set("CPython", "3.14") + _run_cargo(session, "check", "--features=pyo3/abi3t", env=env) + + # ... also if both abi3 and abi3t features are enabled + config_file.set("CPython", "3.14") + _run_cargo(session, "check", "--features=pyo3/abi3t,pyo3/abi3", 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 4a45c4e26c2..63c6c50a17c 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -389,7 +389,7 @@ impl InterpreterConfig { fn from_interpreter( interpreter: impl AsRef, - abi3_version: Option, + stable_abi_version: Option, ) -> Result { const SCRIPT: &str = r#" # Allow the script to run on Python 2, so that nicer error can be printed later. @@ -498,7 +498,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) }; let target_abi = - PythonAbi::from_build_env(implementation, version, abi3_version, gil_disabled)?; + PythonAbi::from_build_env(implementation, version, stable_abi_version, gil_disabled)?; let cygwin = map["cygwin"].as_str() == "True"; @@ -984,7 +984,7 @@ impl PythonAbi { }; let builder = if get_abi3_version().is_some() && !gil_disabled { builder.stable_abi(StableAbi::Abi3) - } else if get_abi3t_version().is_some() { + } else if get_abi3t_version().is_some() && version >= MINIMUM_SUPPORTED_VERSION_ABI3T { builder.stable_abi(StableAbi::Abi3t) } else if gil_disabled { builder.free_threaded() @@ -2173,35 +2173,39 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result, abi3t_version: Option, ) -> Result { - if abi3_version.is_some() && abi3t_version.is_some() { - bail!("Cannot simultaneously set abi3 and abi3t features") - } else if let Some(version) = abi3_version { - default_abi3_config(host, version) - } else if abi3t_version.is_some() { - bail!("Abi3t support has not yet been implemented") - } else { + if abi3_version.is_none() && abi3t_version.is_none() { bail!("Neither abi3 or abi3t features are enabled") } -} + let (stable_abi, version) = if let Some(version) = abi3_version { + (StableAbi::Abi3, version) + } else if let Some(version) = abi3t_version { + (StableAbi::Abi3t, version) + } else { + unreachable!(); + }; + + if stable_abi == StableAbi::Abi3t && version < MINIMUM_SUPPORTED_VERSION_ABI3T { + bail!("Cannot target an abi3t version below {MINIMUM_SUPPORTED_VERSION_ABI3T}") + } -/// Generates "default" interpreter configuration when compiling "abi3" extensions -/// without a working Python interpreter. -/// -/// `version` specifies the minimum supported Stable ABI CPython version. -/// -/// This should work for most CPython extension modules when compiling on -/// Windows, macOS and Linux. -/// -/// Must be called from a PyO3 crate build script. -fn default_abi3_config(host: &Triple, version: PythonVersion) -> Result { // FIXME: PyPy & GraalPy do not support the Stable ABI. let target_abi = PythonAbiBuilder::new(PythonImplementation::CPython, version) - .stable_abi(StableAbi::Abi3) + .stable_abi(stable_abi) .finalize()?; let builder = InterpreterConfigBuilder::new(PythonImplementation::CPython, version) .target_abi(target_abi); @@ -2498,10 +2502,6 @@ 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 simultaneously enable abi3 and abi3t features" - ); // See if we can safely skip the Python interpreter configuration detection. // Unix stable ABI extension modules can usually be built without any interpreter. @@ -2914,8 +2914,6 @@ mod tests { #[test] fn windows_hardcoded_abi3_compile() { let host = triple!("x86_64-pc-windows-msvc"); - let min_version = "3.8".parse().unwrap(); - let implementation = PythonImplementation::CPython; let version = PythonVersion::PY38; let config = InterpreterConfigBuilder::new(implementation, version) @@ -2923,20 +2921,80 @@ mod tests { .lib_name("python3".to_string()) .finalize() .unwrap(); - assert_eq!(default_abi3_config(&host, min_version).unwrap(), config); + assert_eq!( + default_stable_abi_config(&host, Some(version), None).unwrap(), + config + ); + } + + #[test] + fn windows_hardcoded_abi3t_compile() { + let host = triple!("x86_64-pc-windows-msvc"); + let implementation = PythonImplementation::CPython; + let version = PythonVersion { + major: 3, + minor: 15, + }; + let config = InterpreterConfigBuilder::new(implementation, version) + .stable_abi(StableAbi::Abi3t) + .lib_name("python3t".to_string()) + .finalize() + .unwrap(); + assert_eq!( + default_stable_abi_config(&host, None, Some(version)).unwrap(), + config + ); } #[test] fn unix_hardcoded_abi3_compile() { let host = triple!("x86_64-unknown-linux-gnu"); - let min_version = "3.9".parse().unwrap(); let implementation = PythonImplementation::CPython; let version = PythonVersion::PY39; let config = InterpreterConfigBuilder::new(implementation, version) .stable_abi(StableAbi::Abi3) .finalize() .unwrap(); - assert_eq!(default_abi3_config(&host, min_version).unwrap(), config); + assert_eq!( + default_stable_abi_config(&host, Some(version), None).unwrap(), + config + ); + } + + #[test] + fn unix_hardcoded_abi3t_compile() { + let host = triple!("x86_64-unknown-linux-gnu"); + let implementation = PythonImplementation::CPython; + let version = PythonVersion { + major: 3, + minor: 15, + }; + let config = InterpreterConfigBuilder::new(implementation, version) + .stable_abi(StableAbi::Abi3t) + .finalize() + .unwrap(); + assert_eq!( + default_stable_abi_config(&host, None, Some(version)).unwrap(), + config + ); + } + + #[test] + fn default_stable_abi_config_corner_cases() { + let host = triple!("x86_64-unknown-linux-gnu"); + let py315 = Some("3.15".parse().unwrap()); + let py39 = Some("3.9".parse().unwrap()); + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY39; + let config = InterpreterConfigBuilder::new(implementation, version) + .stable_abi(StableAbi::Abi3) + .finalize() + .unwrap(); + assert_eq!( + default_stable_abi_config(&host, py39, py315).unwrap(), + config + ); + assert!(default_stable_abi_config(&host, None, py39).is_err()); } #[test] From 941aca4de4cad39520696d4aca5ad82377bbdae1 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 14 May 2026 17:12:42 -0600 Subject: [PATCH 183/195] add release notes --- newsfragments/5807.added.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 newsfragments/5807.added.md diff --git a/newsfragments/5807.added.md b/newsfragments/5807.added.md new file mode 100644 index 00000000000..bb4599f1e45 --- /dev/null +++ b/newsfragments/5807.added.md @@ -0,0 +1,12 @@ +* Support for the new PEP 803 abi3t ABI. +* New `abi3t` and `abi3t-py315` features to enable abi3t builds. These features + work analogously to the `abi3` and `abi3-py3XX` features. +* A `PythonAbi` struct which wraps an `PythonImplementation`, + `PythonVersion`, and a new `PythonAbiKind` enum. The `PythonAbiKind` enum can + itself be one of two enums: `StableAbi` or `VersionSpecific`. The former can + be `Abi3` or `Abi3t` and the latter can be `FreeThreaded` or + `GilEnabled`. Together these types enable fine-grained descriptions of the + target ABI for an extension build. +* A `PythonAbiBuilder` struct for constructing new `PythonAbi` instances. +* A new `InterpreterConfig::target_abi` function for accessing details about the + target ABI for a build. From 4d3864205b8e2c305208a0c2a38debf926e306fd Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 14 May 2026 17:13:43 -0600 Subject: [PATCH 184/195] fix ruff --- noxfile.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/noxfile.py b/noxfile.py index a68d8620daa..89db9afb684 100644 --- a/noxfile.py +++ b/noxfile.py @@ -237,7 +237,12 @@ def clippy(session: nox.Session) -> bool: session.error("one or more jobs failed") -def _clippy(session: nox.Session, *, env: Dict[str, str] = None, version: Optional[Tuple[int, int]]) -> bool: +def _clippy( + session: nox.Session, + *, + env: Dict[str, str] = None, + version: Optional[Tuple[int, int]], +) -> bool: success = True env = env or os.environ for feature_set in _get_feature_sets(version): @@ -300,7 +305,9 @@ def codspeed(session: nox.Session) -> bool: def clippy_all(session: nox.Session) -> None: success = True - def _clippy_with_config(env: Dict[str, str], version: Optional[Tuple[int, int]]) -> None: + def _clippy_with_config( + env: Dict[str, str], version: Optional[Tuple[int, int]] + ) -> None: nonlocal success success &= _clippy(session, env=env, version=version) @@ -1661,7 +1668,9 @@ def _get_rust_default_target() -> str: @lru_cache() -def _get_feature_sets(version: Optional[Tuple[int, int]] = None) -> Tuple[Optional[str], ...]: +def _get_feature_sets( + version: Optional[Tuple[int, int]] = None, +) -> Tuple[Optional[str], ...]: """Returns feature sets to use for Rust jobs""" if version == None: version = sys.version_info[:2] From dd200da53156b66bfc96620e474f58e74937b1aa Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 14 May 2026 17:15:01 -0600 Subject: [PATCH 185/195] really fix ruff --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 89db9afb684..4a2e0c1715c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1672,7 +1672,7 @@ def _get_feature_sets( version: Optional[Tuple[int, int]] = None, ) -> Tuple[Optional[str], ...]: """Returns feature sets to use for Rust jobs""" - if version == None: + if version is None: version = sys.version_info[:2] cargo_target = os.getenv("CARGO_BUILD_TARGET", "") From fd0f14fd0a9506a634116f50eea75c8ea0f6781f Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 15 May 2026 08:23:18 -0600 Subject: [PATCH 186/195] don't run abi3-py38 tests on pypy --- noxfile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/noxfile.py b/noxfile.py index 4a2e0c1715c..7c0833a923f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -134,6 +134,7 @@ def test_rust(session: nox.Session): and "abi3t" not in feature_set and "full" in feature_set and sys.version_info >= (3, 9) + and sys.implementation_name.name != "pypy" ): # run abi3-py38 tests to check abi3 forward compatibility _run_cargo_test( From 13e04c725ba42cbb6b0ee1846a3fe91233d6839c Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 15 May 2026 13:16:01 -0600 Subject: [PATCH 187/195] add docs --- guide/src/building-and-distribution.md | 84 +++++++++++++------ .../multiple-python-versions.md | 20 ++++- guide/src/features.md | 60 +++++++++++-- 3 files changed, 126 insertions(+), 38 deletions(-) diff --git a/guide/src/building-and-distribution.md b/guide/src/building-and-distribution.md index 82b5ea836c6..32f9abc5d92 100644 --- a/guide/src/building-and-distribution.md +++ b/guide/src/building-and-distribution.md @@ -28,28 +28,57 @@ An example output of doing this is shown below: ```console $ PYO3_PRINT_CONFIG=1 cargo build - Compiling pyo3 v0.14.1 (/home/david/dev/pyo3) -error: failed to run custom build command for `pyo3 v0.14.1 (/home/david/dev/pyo3)` + Compiling pyo3-ffi v0.28.3 (/Users/goldbaum/Documents/pyo3/pyo3-ffi) +error: failed to run custom build command for `pyo3-ffi v0.28.3 (/Users/goldbaum/Documents/pyo3/pyo3-ffi)` Caused by: - process didn't exit successfully: `/home/david/dev/pyo3/target/debug/build/pyo3-7a8cf4fe22e959b7/build-script-build` (exit status: 101) + process didn't exit successfully: `/Users/goldbaum/Documents/pyo3/target/debug/build/pyo3-ffi-120b26b17564bf98/build-script-build` (exit status: 101) --- stdout + cargo:rustc-check-cfg=cfg(Py_LIMITED_API) + cargo:rustc-check-cfg=cfg(Py_GIL_DISABLED) + cargo:rustc-check-cfg=cfg(PyPy) + cargo:rustc-check-cfg=cfg(GraalPy) + cargo:rustc-check-cfg=cfg(RustPython) + cargo:rustc-check-cfg=cfg(py_sys_config, values("Py_DEBUG", "Py_REF_DEBUG", "Py_TRACE_REFS", "COUNT_ALLOCS")) + cargo:rustc-check-cfg=cfg(pyo3_disable_reference_pool) + cargo:rustc-check-cfg=cfg(pyo3_leak_on_drop_without_reference_pool) + cargo:rustc-check-cfg=cfg(Py_3_8) + cargo:rustc-check-cfg=cfg(Py_3_9) + cargo:rustc-check-cfg=cfg(Py_3_10) + cargo:rustc-check-cfg=cfg(Py_3_11) + cargo:rustc-check-cfg=cfg(Py_3_12) + cargo:rustc-check-cfg=cfg(Py_3_13) + cargo:rustc-check-cfg=cfg(Py_3_14) + cargo:rustc-check-cfg=cfg(Py_3_15) + cargo:rustc-check-cfg=cfg(Py_3_16) + cargo:rustc-check-cfg=cfg(pyo3_dll, values("python3", "python3_d", "python3t", "python3t_d", "python38", "python38_d", "python39", "python39_d", "python310", "python310_d", "python311", "python311_d", "python312", "python312_d", "python313", "python313_d", "python313t", "python313t_d", "python314", "python314_d", "python314t", "python314t_d", "python315", "python315_d", "python315t", "python315t_d", "python316", "python316_d", "python316t", "python316t_d", "libpypy3.11-c")) + cargo:rerun-if-env-changed=PYO3_CONFIG_FILE cargo:rerun-if-env-changed=PYO3_CROSS cargo:rerun-if-env-changed=PYO3_CROSS_LIB_DIR cargo:rerun-if-env-changed=PYO3_CROSS_PYTHON_VERSION + cargo:rerun-if-env-changed=PYO3_CROSS_PYTHON_IMPLEMENTATION + cargo:rerun-if-env-changed=PYO3_NO_PYTHON + cargo:rerun-if-env-changed=PYO3_ENVIRONMENT_SIGNATURE + cargo:rerun-if-env-changed=PYO3_PYTHON + cargo:rerun-if-env-changed=VIRTUAL_ENV + cargo:rerun-if-env-changed=CONDA_PREFIX + cargo:rerun-if-env-changed=PATH cargo:rerun-if-env-changed=PYO3_PRINT_CONFIG -- PYO3_PRINT_CONFIG=1 is set, printing configuration and halting compile -- implementation=CPython - version=3.8 + version=3.14 shared=true - abi3=false - lib_name=python3.8 - lib_dir=/usr/lib - executable=/usr/bin/python + target_abi=CPython-free_threaded-3.14 + lib_name=python3.14t + lib_dir=/Users/goldbaum/.pyenv/versions/3.14.4t/lib + executable=/Users/goldbaum/.pyenv/versions/3.14.4t/bin/python pointer_width=64 - build_flags= + build_flags=Py_GIL_DISABLED + python_framework_prefix= suppress_build_script_link_lines=false + + note: unset the PYO3_PRINT_CONFIG environment variable and retry to compile with the above config ``` The `PYO3_ENVIRONMENT_SIGNATURE` environment variable can be used to trigger rebuilds when its value changes, it has no other effect. @@ -74,7 +103,7 @@ The PyO3 ecosystem has two packaging tools, [`maturin`] and [`setuptools-rust`], PyO3 has some functionality for configuring projects when building Python extension modules: - The `PYO3_BUILD_EXTENSION_MODULE` environment variable, which must be set when building Python extension modules. `maturin` and `setuptools-rust` set this automatically. -- The `abi3` Cargo feature and its version-specific `abi3-pyXY` companions, which are used to opt-in to the limited Python API in order to support multiple Python versions in a single wheel. +- The `abi3` and `abi3t` Cargo feature and their version-specific `abi3-pyXY` and `abi3t-pyXY` companions, which are used to opt-in to the limited Python API and a flavor of stable ABI in order to support multiple Python versions in a single wheel. The free-threaded build of CPython cannot load `abi3` wheels but both builds can load `abi3t` wheels. This section describes the packaging tools before describing how to build manually without them. It then proceeds with an explanation of the `PYO3_BUILD_EXTENSION_MODULE` environment variable. @@ -212,43 +241,48 @@ This should only be set when building a library for distribution. > > Projects are encouraged to migrate off the feature, as it caused [major development pain](faq.md#i-cant-run-cargo-test-or-i-cant-build-in-a-cargo-workspace-im-having-linker-issues-like-symbol-not-found-or-undefined-reference-to-_pyexc_systemerror) due to the lack of linking. -### `Py_LIMITED_API`/`abi3` +### `Py_LIMITED_API`/`abi3`/`abi3t` By default, Python extension modules can only be used with the same Python version they were compiled against. For example, an extension module built for Python 3.5 can't be imported in Python 3.8. -[PEP 384](https://www.python.org/dev/peps/pep-0384/) introduced the idea of the limited Python API, which would have a stable ABI enabling extension modules built with it to be used against multiple Python versions. -This is also known as `abi3`. +[PEP 384](https://www.python.org/dev/peps/pep-0384/) introduced the idea of the limited Python API, which would have a stable ABI enabling extension modules built with it to be used against multiple Python versions. The ABI defined by PEP 384 is called `abi3`, since it's stable for all Python 3.X releases. In 2026, the steering council approved [PEP 803](https://www.python.org/dev/peps/pep-0803/)) which defines a new stable ABI, `abi3t`, that can be used on the free-threaded build of CPython. Extensions built using the `abit3` ABI are importable on both the GIL-enabled and free-threaded builds of any CPython version newer than the target version. So, `abi3t` extensions built for the Python 3.15 limited API will be importable on Python 3.16, 3.17, and all future Python 3.X versions. The advantage of building extension modules using the limited Python API is that package vendors only need to build and distribute a single copy (for each OS / architecture), and users can install it on all Python versions from the [minimum version](#minimum-python-version-for-abi3) and up. The downside of this is that PyO3 can't use optimizations which rely on being compiled against a known exact Python version. It's up to you to decide whether this matters for your extension module. -It's also possible to design your extension module such that you can distribute `abi3` wheels but allow users compiling from source to benefit from additional optimizations - see the [support for multiple python versions](./building-and-distribution/multiple-python-versions.md) section of this guide, in particular the `#[cfg(Py_LIMITED_API)]` flag. +It's also possible to design your extension module such that you can distribute `abi3` or `abi3t` wheels but allow users compiling from source to benefit from additional optimizations - see the [support for multiple python versions](./building-and-distribution/multiple-python-versions.md) section of this guide, in particular the `#[cfg(Py_LIMITED_API)]` flag. -There are three steps involved in making use of `abi3` when building Python packages as wheels: +There are three steps involved in targeting `abi3` or `abi3t` when building Python packages as wheels: -1. Enable the `abi3` feature in `pyo3`. +1. Enable the `abi3` and/or `abi3t` feature in `pyo3`. This ensures `pyo3` only calls Python C-API functions which are part of the stable API, and on Windows also ensures that the project links against the correct shared object (no special behavior is required on other platforms): ```toml [dependencies] - pyo3 = { {{#PYO3_CRATE_VERSION}}, features = ["abi3"] } + pyo3 = { {{#PYO3_CRATE_VERSION}}, features = ["abi3", "abi3t"] } ``` -2. Ensure that the built shared objects are correctly marked as `abi3`. + Enabling both the `"abi3"` and `"abi3t"` features will produce an abi3 extension if the host interpreter is a GIL-enabled interpreter and an abi3t extension with a host free-threaded interpreter. If you always want to produce `abi3t` wheels, you can enable just the `"abi3t"` feature, which will produce extensions targeting `abi3t` if the host interpreter is Python 3.15 or newer, but will produce version-specific extensions otherwise. If you only enable the `"abi3"` feature, you will produce `abi3` extensions if the host interpreter is a GIL-enabled build of CPython and version-specific extensions otherwise. + + We suggest enabling both features and using multiple python interpreters to build several wheels. You can build an `abi3` wheel for your minimum supported Python version, a `cp314t` version-specific wheel using a free-threaded Python 3.14 interpreter, and an `abi3t` wheel using a Python 3.15 interpreter. This will produce wheels targeting all versions of CPython that PyO3 supports. + +2. Ensure that the built shared objects are correctly marked as `abi3` and `abi3t`. This is accomplished by telling your build system that you're using the limited API. - [`maturin`] >= 0.9.0 and [`setuptools-rust`] >= 0.11.4 support `abi3` wheels. + [`maturin`] >= 0.9.0 and [`setuptools-rust`] >= 0.11.4 support `abi3` wheels and [`maturin`] >= 1.14 supports `abi3t` wheels. - See the [corresponding](https://github.com/PyO3/maturin/pull/353) [PRs](https://github.com/PyO3/setuptools-rust/pull/82) for more. + See the [corresponding](https://github.com/PyO3/maturin/pull/353) [PRs](https://github.com/PyO3/setuptools-rust/pull/82) [for](https://github.com/PyO3/maturin/pull/3113) more. -3. Ensure that the `.whl` is correctly marked as `abi3`. +3. Ensure that the `.whl` is correctly marked as `abi3` or `abi3t`. For projects using `setuptools`, this is accomplished by passing `--py-limited-api=cp3x` (where `x` is the minimum Python version supported by the wheel, e.g. `--py-limited-api=cp35` for Python 3.5) to `setup.py bdist_wheel`. -#### Minimum Python version for `abi3` +#### Minimum Python version for stable ABI builds Because a single `abi3` wheel can be used with many different Python versions, PyO3 has feature flags `abi3-py38`, `abi3-py39`, `abi3-py310` etc. to set the minimum required Python version for your `abi3` wheel. For example, if you set the `abi3-py38` feature, your wheel can be used on all Python 3 versions from Python 3.8 and up. `maturin` and `setuptools-rust` will give the wheel a name like `my-extension-1.0-cp38-abi3-manylinux2020_x86_64.whl`. +Similarly, there is an `abi3t-py315` feature and future PyO3 versions will offer `abi3t-py316` and so on. + As your extension module may be run with multiple different Python versions you may occasionally find you need to check the Python version at runtime to customize behavior. See [the relevant section of this guide](./building-and-distribution/multiple-python-versions.md#checking-the-python-version-at-runtime) on supporting multiple Python versions at runtime. @@ -258,14 +292,14 @@ E.g., if you set `abi3-py39` and try to compile the crate with a host of Python > [!NOTE] > If you set more that one of these `abi3` version feature flags the lowest version always wins. For example, with both `abi3-py38` and `abi3-py39` set, PyO3 would build a wheel which supports Python 3.8 and up. -#### Building `abi3` extension modules without a Python interpreter +#### Building stable ABI extension modules without a Python interpreter As an advanced feature, you can build a PyO3 wheel without calling Python interpreter with the environment variable `PYO3_NO_PYTHON` set. -Also, if the build host Python interpreter is not found or is too old or otherwise unusable, PyO3 will still attempt to compile `abi3` extension modules after displaying a warning message. +Also, if the build host Python interpreter is not found or is too old or otherwise unusable, PyO3 will still attempt to compile stable ABI extension modules after displaying a warning message. #### Missing features -Due to limitations in the Python API, there are a few `pyo3` features that do not work when compiling for `abi3`. +Due to limitations in the Python API, there are a few `pyo3` features that do not work when compiling for either `abi3` or `abi3t`. These are: - `#[pyo3(text_signature = "...")]` does not work on classes until Python 3.10 or greater. diff --git a/guide/src/building-and-distribution/multiple-python-versions.md b/guide/src/building-and-distribution/multiple-python-versions.md index 97d22780625..6bb451995d4 100644 --- a/guide/src/building-and-distribution/multiple-python-versions.md +++ b/guide/src/building-and-distribution/multiple-python-versions.md @@ -25,7 +25,7 @@ fn function_only_supported_on_python_3_8_and_up() {} fn function_only_supported_before_python_3_8() {} #[cfg(not(Py_LIMITED_API))] -fn function_incompatible_with_abi3_feature() {} +fn function_incompatible_with_stable_abi_feature() {} ``` The following sections first show how to add these `#[cfg]` flags to your build process, and then cover some common patterns flags in a little more detail. @@ -72,12 +72,18 @@ There are similar options `Py_3_9`, `Py_3_10`, `Py_3_11` and so on for each mino This `#[cfg]` marks code that will only be present on Python versions before (but not including) Python 3.8. +```text +#[cfg(Py_GIL_DISABLED)] +``` + +This `#[cfg]` marks code that will only be present for free-threaded builds of CPython. You might use this in situations where the GIL provides thread safety but the free-threaded build needs extra synchronization logic. + ```text #[cfg(not(Py_LIMITED_API))] ``` -This `#[cfg]` marks code that is only available when building for the unlimited Python API (i.e. PyO3's `abi3` feature is not enabled). -This might be useful if you want to ship your extension module as an `abi3` wheel and also allow users to compile it from source to make use of optimizations only possible with the unlimited API. +This `#[cfg]` marks code that is only available when building for the unlimited Python API (i.e. PyO3's `abi3` or `abi3t` features are not enabled). +This might be useful if you want to ship your extension module as an `abi3` or `abi3t` wheel and also allow users to compile it from source to make use of optimizations only possible with the unlimited API. ```text #[cfg(any(Py_3_9, not(Py_LIMITED_API)))] @@ -86,6 +92,12 @@ This might be useful if you want to ship your extension module as an `abi3` whee This `#[cfg]` marks code which is available when running Python 3.9 or newer, or when using the unlimited API with an older Python version. Patterns like this are commonly seen on Python APIs which were added to the limited Python API in a specific minor version. +```text +#[cfg(all(Py_GIL_DISABLED, Py_LIMITED_API))] +``` + +This marks code which is available only for the free-threaded `abi3t` stable ABI. You might use this if you need some sort of custom locking implementation in code that makes strong assumptions about the GIL providing thread safety. + ```text #[cfg(PyPy)] ``` @@ -94,7 +106,7 @@ This `#[cfg]` marks code which is running on PyPy. ## Checking the Python version at runtime -When building with PyO3's `abi3` feature, your extension module will be compiled against a specific [minimum version](../building-and-distribution.md#minimum-python-version-for-abi3) of Python, but may be running on newer Python versions. +When building with PyO3's `abi3` or `abi3t` features, your extension module will be compiled against a specific [minimum version](../building-and-distribution.md#minimum-python-version-for-abi3) of Python, but may be running on newer Python versions. For example with PyO3's `abi3-py38` feature, your extension module will be compiled as if it were for Python 3.8. If you were using `pyo3-build-config`, `#[cfg(Py_3_8)]` would be present. diff --git a/guide/src/features.md b/guide/src/features.md index 2b9ee9c4024..bab0156dfcb 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -5,20 +5,26 @@ This chapter of the guide provides detail on each of them. By default, only the `macros` feature is enabled. -## Features for extension module authors +## Features to select the target ABI -### `extension-module` +Each version of CPython exposes an ABI corresponding to the C API that is particular to that version. Extensions built for this "version-specific" ABI can only be imported by Python interpreters with the same major version, since the Python C API and ABI are subject to change in every major release. Python also supports a [limited API](https://docs.python.org/3/c-api/stable.html#limited-c-api) and [stable ABI](https://docs.python.org/3/c-api/stable.html#stable-abi). Extensions targeting the stable ABI defined by Python 3.X can be imported by any subsequent Python version newer than Python 3.X, but not older versions. -Deprecated, users should remove this feature and upgrade to `maturin >= 1.9.4` or `setuptools-rust >= 1.12`. +There are two "flavors" of stable ABI: `abi3`, supported on Python 3.2 and newer but *not* the free-threaded builds and `abi3t`, supported on Python 3.15 and newer for both the GIL-enabled and free-threaded builds of Python. PyO3 supports building extensions targeting both flavors of stable ABI. -See the [building and distribution](building-and-distribution.md#the-extension-module-feature) section for further detail. +When PyO3 finds a "host" python interpreter and no `abi3` or `abi3t` feature is active, it will generate extensions targetting the version-specific ABI for the host Python version. For example, when ``pip`` or ``uv`` installs a Python package that includes Rust dependencies that depend on PyO3, the host interpreter is the interpreter running ``pip`` or the interpreter from the activated ``uv`` environment. + +To build for the stable ABIs, you must activate an `abi3` and/or an `abi3t` feature. You can simultaneously activate both features. This will produce an `abi3` extension on the GIL-enabled and a version-specific extension on Python 3.14 and an `abi3t` extension on Python 3.15 and newer. ### `abi3` -This feature is used when building Python extension modules to create wheels which are compatible with multiple Python versions. +This feature is used when building Python extension modules to create wheels which are compatible with multiple Python versions but not the free-threaded build. It restricts PyO3's API to a subset of the full Python API which is guaranteed by [PEP 384](https://www.python.org/dev/peps/pep-0384/) to be forwards-compatible with future Python versions. +Since no particular "target" Python version is implied by this feature, it will generate an `abi3` extension targeting the stable ABI defined by the "host" Python interpreter. As such, this feature requires a host Python interpreter is present and cannot be used for cross-compilation. + +If you are using this feature to distribute extensions, you should ensure your minimum supported Python version is installed to do the build. However, see the description of the `abi3-pyXY` feature if you would like to generate extensions targeting an older Python version using a recent Python interpreter. + See the [building and distribution](building-and-distribution.md#py_limited_apiabi3) section for further detail. ### The `abi3-pyXY` features @@ -27,13 +33,35 @@ See the [building and distribution](building-and-distribution.md#py_limited_apia These features are extensions of the `abi3` feature to specify the exact minimum Python version which the multiple-version-wheel will support. +They can be used to target versions of the ABI defined by Python versions released before the "host" python interpreter. They can also be used without any host interpreter at all to cross-compile. + See the [building and distribution](building-and-distribution.md#minimum-python-version-for-abi3) section for further detail. -### `generate-import-lib` +### `abi3t` -This feature is deprecated and has no effect. -PyO3 now uses Rust's `raw-dylib` linking feature to link against the Python DLL on Windows, eliminating the need for import library (`.lib`) files entirely. -Cross-compiling for Windows targets works without any additional setup. +This feature is used when building Python extension modules to create wheels which are compatible with Python 3.15 or newer, targeting both the free-threaded and GIL-enabled build with a single extension module. + +It restricts PyO3's API to a subset of the full Python API which is guaranteed by [PEP 384](https://www.python.org/dev/peps/pep-0384/) and [PEP 803](https://www.python.org/dev/peps/pep-0803/) to be forwards-compatible with future Python versions and be importable by the free-threaded and GIL-enabled interpreter. + +Since no particular "target" Python version is implied by this feature, it will generate an `abi3` extension targeting the stable ABI defined by the "host" Python interpreter. As such, this feature requires a host Python interpreter is present and cannot be used for cross-compilation. + +If you are using this feature to distribute extensions, you should ensure your minimum supported Python version is installed to do the build. However, see the description of the `abi3t-pyXY` feature if you would like to generate extensions targeting an older Python version using a recent Python interpreter. + +Activating an `abi3t` and targeting Python 3.14 and older is a no-op: the build will produce an extension but it will not be an `abi3t` extension, since `abi3t` is unsupported before Python 3.15. + +See the [building and distribution](building-and-distribution.md#py_limited_apiabi3) section for further detail. + +### The `abi3t-pyXY` features + +(`abi3t-py315`) + +These features are extensions of the `abi3t` feature to specify the exact minimum Python version which the multiple-version-wheel will support. + +They can be used to target versions of the ABI defined by Python versions released before the "host" python interpreter. They can also be used without any host interpreter at all to cross-compile. + +Activating an `abi3t` and targeting Python 3.14 and older is a no-op: the build will produce an extension but it will not be an `abi3t` extension, since `abi3t` is unsupported before Python 3.15. + +See the [building and distribution](building-and-distribution.md#minimum-python-version-for-abi3) section for further detail. ## Features for embedding Python in Rust @@ -269,3 +297,17 @@ Adds a dependency on [smallvec](https://docs.rs/smallvec) and enables conversion ### `uuid` Adds a dependency on [uuid](https://docs.rs/uuid) and enables conversions into its [`Uuid`](https://docs.rs/uuid/latest/uuid/struct.Uuid.html) type. + +## Deprecated features for extension module authors + +### `extension-module` + +Deprecated, users should remove this feature and upgrade to `maturin >= 1.9.4` or `setuptools-rust >= 1.12`. + +See the [building and distribution](building-and-distribution.md#the-extension-module-feature) section for further detail. + +### `generate-import-lib` + +This feature is deprecated and has no effect. +PyO3 now uses Rust's `raw-dylib` linking feature to link against the Python DLL on Windows, eliminating the need for import library (`.lib`) files entirely. +Cross-compiling for Windows targets works without any additional setup. From ebc99b968b196336dae9b086eb34bf8a80c30515 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 15 May 2026 13:18:44 -0600 Subject: [PATCH 188/195] disable windows gil-enabled abi3t builds to work around upstream bug --- noxfile.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 7c0833a923f..8eda1ef7c5f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1694,7 +1694,9 @@ def _get_feature_sets( return (None, features) # do fewer abi3t builds? - if version >= (3, 15): + # TODO re-enable Windows abi3t builds once 3.15.0b2 is released. + # See https://github.com/python/cpython/issues/149887 + if version >= (3, 15) and sys.platform != "win32": return ( None, "abi3", From 6214c3d7facbd2fa64de7ecdbc1e2d40d219cd4f Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 15 May 2026 13:31:48 -0600 Subject: [PATCH 189/195] fix guide formatting --- guide/src/building-and-distribution.md | 24 +++++++++---- .../multiple-python-versions.md | 6 ++-- guide/src/features.md | 36 +++++++++++++------ 3 files changed, 46 insertions(+), 20 deletions(-) diff --git a/guide/src/building-and-distribution.md b/guide/src/building-and-distribution.md index 32f9abc5d92..3a5825777e6 100644 --- a/guide/src/building-and-distribution.md +++ b/guide/src/building-and-distribution.md @@ -103,7 +103,8 @@ The PyO3 ecosystem has two packaging tools, [`maturin`] and [`setuptools-rust`], PyO3 has some functionality for configuring projects when building Python extension modules: - The `PYO3_BUILD_EXTENSION_MODULE` environment variable, which must be set when building Python extension modules. `maturin` and `setuptools-rust` set this automatically. -- The `abi3` and `abi3t` Cargo feature and their version-specific `abi3-pyXY` and `abi3t-pyXY` companions, which are used to opt-in to the limited Python API and a flavor of stable ABI in order to support multiple Python versions in a single wheel. The free-threaded build of CPython cannot load `abi3` wheels but both builds can load `abi3t` wheels. +- The `abi3` and `abi3t` Cargo feature and their version-specific `abi3-pyXY` and `abi3t-pyXY` companions, which are used to opt-in to the limited Python API and a flavor of stable ABI in order to support multiple Python versions in a single wheel. + The free-threaded build of CPython cannot load `abi3` wheels but both builds can load `abi3t` wheels. This section describes the packaging tools before describing how to build manually without them. It then proceeds with an explanation of the `PYO3_BUILD_EXTENSION_MODULE` environment variable. @@ -245,14 +246,18 @@ This should only be set when building a library for distribution. By default, Python extension modules can only be used with the same Python version they were compiled against. For example, an extension module built for Python 3.5 can't be imported in Python 3.8. -[PEP 384](https://www.python.org/dev/peps/pep-0384/) introduced the idea of the limited Python API, which would have a stable ABI enabling extension modules built with it to be used against multiple Python versions. The ABI defined by PEP 384 is called `abi3`, since it's stable for all Python 3.X releases. In 2026, the steering council approved [PEP 803](https://www.python.org/dev/peps/pep-0803/)) which defines a new stable ABI, `abi3t`, that can be used on the free-threaded build of CPython. Extensions built using the `abit3` ABI are importable on both the GIL-enabled and free-threaded builds of any CPython version newer than the target version. So, `abi3t` extensions built for the Python 3.15 limited API will be importable on Python 3.16, 3.17, and all future Python 3.X versions. +[PEP 384](https://www.python.org/dev/peps/pep-0384/) introduced the idea of the limited Python API, which would have a stable ABI enabling extension modules built with it to be used against multiple Python versions. +The ABI defined by PEP 384 is called `abi3`, since it's stable for all Python 3.X releases. +In 2026, the steering council approved [PEP 803](https://www.python.org/dev/peps/pep-0803/)) which defines a new stable ABI, `abi3t`, that can be used on the free-threaded build of CPython. +Extensions built using the `abit3` ABI are importable on both the GIL-enabled and free-threaded builds of any CPython version newer than the target version. +So, `abi3t` extensions built for the Python 3.15 limited API will be importable on Python 3.16, 3.17, and all future Python 3.X versions. The advantage of building extension modules using the limited Python API is that package vendors only need to build and distribute a single copy (for each OS / architecture), and users can install it on all Python versions from the [minimum version](#minimum-python-version-for-abi3) and up. The downside of this is that PyO3 can't use optimizations which rely on being compiled against a known exact Python version. It's up to you to decide whether this matters for your extension module. -It's also possible to design your extension module such that you can distribute `abi3` or `abi3t` wheels but allow users compiling from source to benefit from additional optimizations - see the [support for multiple python versions](./building-and-distribution/multiple-python-versions.md) section of this guide, in particular the `#[cfg(Py_LIMITED_API)]` flag. +It's also possible to design your extension module such that you can distribute `abi3` or `abi3t` wheels but allow users compiling from source to benefit from additional optimizations - see the [support for multiple python versions](./building-and-distribution/multiple-python-versions.md) section of this guide, in particular the `#[cfg(Py_LIMITED_API)]` flag. -There are three steps involved in targeting `abi3` or `abi3t` when building Python packages as wheels: +There are three steps involved in targeting `abi3` or `abi3t` when building Python packages as wheels: 1. Enable the `abi3` and/or `abi3t` feature in `pyo3`. This ensures `pyo3` only calls Python C-API functions which are part of the stable API, and on Windows also ensures that the project links against the correct shared object (no special behavior is required on other platforms): @@ -262,9 +267,13 @@ There are three steps involved in targeting `abi3` or `abi3t` when building Pyt pyo3 = { {{#PYO3_CRATE_VERSION}}, features = ["abi3", "abi3t"] } ``` - Enabling both the `"abi3"` and `"abi3t"` features will produce an abi3 extension if the host interpreter is a GIL-enabled interpreter and an abi3t extension with a host free-threaded interpreter. If you always want to produce `abi3t` wheels, you can enable just the `"abi3t"` feature, which will produce extensions targeting `abi3t` if the host interpreter is Python 3.15 or newer, but will produce version-specific extensions otherwise. If you only enable the `"abi3"` feature, you will produce `abi3` extensions if the host interpreter is a GIL-enabled build of CPython and version-specific extensions otherwise. + Enabling both the `"abi3"` and `"abi3t"` features will produce an abi3 extension if the host interpreter is a GIL-enabled interpreter and an abi3t extension with a host free-threaded interpreter. + If you always want to produce `abi3t` wheels, you can enable just the `"abi3t"` feature, which will produce extensions targeting `abi3t` if the host interpreter is Python 3.15 or newer, but will produce version-specific extensions otherwise. + If you only enable the `"abi3"` feature, you will produce `abi3` extensions if the host interpreter is a GIL-enabled build of CPython and version-specific extensions otherwise. - We suggest enabling both features and using multiple python interpreters to build several wheels. You can build an `abi3` wheel for your minimum supported Python version, a `cp314t` version-specific wheel using a free-threaded Python 3.14 interpreter, and an `abi3t` wheel using a Python 3.15 interpreter. This will produce wheels targeting all versions of CPython that PyO3 supports. + We suggest enabling both features and using multiple python interpreters to build several wheels. + You can build an `abi3` wheel for your minimum supported Python version, a `cp314t` version-specific wheel using a free-threaded Python 3.14 interpreter, and an `abi3t` wheel using a Python 3.15 interpreter. + This will produce wheels targeting all versions of CPython that PyO3 supports. 2. Ensure that the built shared objects are correctly marked as `abi3` and `abi3t`. This is accomplished by telling your build system that you're using the limited API. @@ -290,7 +299,8 @@ PyO3 is only able to link your extension module to abi3 version up to and includ E.g., if you set `abi3-py39` and try to compile the crate with a host of Python 3.8, the build will fail. > [!NOTE] -> If you set more that one of these `abi3` version feature flags the lowest version always wins. For example, with both `abi3-py38` and `abi3-py39` set, PyO3 would build a wheel which supports Python 3.8 and up. +> If you set more that one of these `abi3` version feature flags the lowest version always wins. +> For example, with both `abi3-py38` and `abi3-py39` set, PyO3 would build a wheel which supports Python 3.8 and up. #### Building stable ABI extension modules without a Python interpreter diff --git a/guide/src/building-and-distribution/multiple-python-versions.md b/guide/src/building-and-distribution/multiple-python-versions.md index 6bb451995d4..eed326c1d74 100644 --- a/guide/src/building-and-distribution/multiple-python-versions.md +++ b/guide/src/building-and-distribution/multiple-python-versions.md @@ -76,7 +76,8 @@ This `#[cfg]` marks code that will only be present on Python versions before (bu #[cfg(Py_GIL_DISABLED)] ``` -This `#[cfg]` marks code that will only be present for free-threaded builds of CPython. You might use this in situations where the GIL provides thread safety but the free-threaded build needs extra synchronization logic. +This `#[cfg]` marks code that will only be present for free-threaded builds of CPython. +You might use this in situations where the GIL provides thread safety but the free-threaded build needs extra synchronization logic. ```text #[cfg(not(Py_LIMITED_API))] @@ -96,7 +97,8 @@ Patterns like this are commonly seen on Python APIs which were added to the limi #[cfg(all(Py_GIL_DISABLED, Py_LIMITED_API))] ``` -This marks code which is available only for the free-threaded `abi3t` stable ABI. You might use this if you need some sort of custom locking implementation in code that makes strong assumptions about the GIL providing thread safety. +This marks code which is available only for the free-threaded `abi3t` stable ABI. +You might use this if you need some sort of custom locking implementation in code that makes strong assumptions about the GIL providing thread safety. ```text #[cfg(PyPy)] diff --git a/guide/src/features.md b/guide/src/features.md index bab0156dfcb..29dd1085543 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -7,13 +7,20 @@ By default, only the `macros` feature is enabled. ## Features to select the target ABI -Each version of CPython exposes an ABI corresponding to the C API that is particular to that version. Extensions built for this "version-specific" ABI can only be imported by Python interpreters with the same major version, since the Python C API and ABI are subject to change in every major release. Python also supports a [limited API](https://docs.python.org/3/c-api/stable.html#limited-c-api) and [stable ABI](https://docs.python.org/3/c-api/stable.html#stable-abi). Extensions targeting the stable ABI defined by Python 3.X can be imported by any subsequent Python version newer than Python 3.X, but not older versions. +Each version of CPython exposes an ABI corresponding to the C API that is particular to that version. +Extensions built for this "version-specific" ABI can only be imported by Python interpreters with the same major version, since the Python C API and ABI are subject to change in every major release. +Python also supports a [limited API](https://docs.python.org/3/c-api/stable.html#limited-c-api) and [stable ABI](https://docs.python.org/3/c-api/stable.html#stable-abi). +Extensions targeting the stable ABI defined by Python 3.X can be imported by any subsequent Python version newer than Python 3.X, but not older versions. -There are two "flavors" of stable ABI: `abi3`, supported on Python 3.2 and newer but *not* the free-threaded builds and `abi3t`, supported on Python 3.15 and newer for both the GIL-enabled and free-threaded builds of Python. PyO3 supports building extensions targeting both flavors of stable ABI. +There are two "flavors" of stable ABI: `abi3`, supported on Python 3.2 and newer but *not* the free-threaded builds and `abi3t`, supported on Python 3.15 and newer for both the GIL-enabled and free-threaded builds of Python. +PyO3 supports building extensions targeting both flavors of stable ABI. -When PyO3 finds a "host" python interpreter and no `abi3` or `abi3t` feature is active, it will generate extensions targetting the version-specific ABI for the host Python version. For example, when ``pip`` or ``uv`` installs a Python package that includes Rust dependencies that depend on PyO3, the host interpreter is the interpreter running ``pip`` or the interpreter from the activated ``uv`` environment. +When PyO3 finds a "host" python interpreter and no `abi3` or `abi3t` feature is active, it will generate extensions targetting the version-specific ABI for the host Python version. +For example, when ``pip`` or ``uv`` installs a Python package that includes Rust dependencies that depend on PyO3, the host interpreter is the interpreter running ``pip`` or the interpreter from the activated ``uv`` environment. -To build for the stable ABIs, you must activate an `abi3` and/or an `abi3t` feature. You can simultaneously activate both features. This will produce an `abi3` extension on the GIL-enabled and a version-specific extension on Python 3.14 and an `abi3t` extension on Python 3.15 and newer. +To build for the stable ABIs, you must activate an `abi3` and/or an `abi3t` feature. +You can simultaneously activate both features. +This will produce an `abi3` extension on the GIL-enabled and a version-specific extension on Python 3.14 and an `abi3t` extension on Python 3.15 and newer. ### `abi3` @@ -21,9 +28,11 @@ This feature is used when building Python extension modules to create wheels whi It restricts PyO3's API to a subset of the full Python API which is guaranteed by [PEP 384](https://www.python.org/dev/peps/pep-0384/) to be forwards-compatible with future Python versions. -Since no particular "target" Python version is implied by this feature, it will generate an `abi3` extension targeting the stable ABI defined by the "host" Python interpreter. As such, this feature requires a host Python interpreter is present and cannot be used for cross-compilation. +Since no particular "target" Python version is implied by this feature, it will generate an `abi3` extension targeting the stable ABI defined by the "host" Python interpreter. +As such, this feature requires a host Python interpreter is present and cannot be used for cross-compilation. -If you are using this feature to distribute extensions, you should ensure your minimum supported Python version is installed to do the build. However, see the description of the `abi3-pyXY` feature if you would like to generate extensions targeting an older Python version using a recent Python interpreter. +If you are using this feature to distribute extensions, you should ensure your minimum supported Python version is installed to do the build. +However, see the description of the `abi3-pyXY` feature if you would like to generate extensions targeting an older Python version using a recent Python interpreter. See the [building and distribution](building-and-distribution.md#py_limited_apiabi3) section for further detail. @@ -33,7 +42,8 @@ See the [building and distribution](building-and-distribution.md#py_limited_apia These features are extensions of the `abi3` feature to specify the exact minimum Python version which the multiple-version-wheel will support. -They can be used to target versions of the ABI defined by Python versions released before the "host" python interpreter. They can also be used without any host interpreter at all to cross-compile. +They can be used to target versions of the ABI defined by Python versions released before the "host" python interpreter. +They can also be used without any host interpreter at all to cross-compile. See the [building and distribution](building-and-distribution.md#minimum-python-version-for-abi3) section for further detail. @@ -43,9 +53,11 @@ This feature is used when building Python extension modules to create wheels whi It restricts PyO3's API to a subset of the full Python API which is guaranteed by [PEP 384](https://www.python.org/dev/peps/pep-0384/) and [PEP 803](https://www.python.org/dev/peps/pep-0803/) to be forwards-compatible with future Python versions and be importable by the free-threaded and GIL-enabled interpreter. -Since no particular "target" Python version is implied by this feature, it will generate an `abi3` extension targeting the stable ABI defined by the "host" Python interpreter. As such, this feature requires a host Python interpreter is present and cannot be used for cross-compilation. +Since no particular "target" Python version is implied by this feature, it will generate an `abi3` extension targeting the stable ABI defined by the "host" Python interpreter. +As such, this feature requires a host Python interpreter is present and cannot be used for cross-compilation. -If you are using this feature to distribute extensions, you should ensure your minimum supported Python version is installed to do the build. However, see the description of the `abi3t-pyXY` feature if you would like to generate extensions targeting an older Python version using a recent Python interpreter. +If you are using this feature to distribute extensions, you should ensure your minimum supported Python version is installed to do the build. +However, see the description of the `abi3t-pyXY` feature if you would like to generate extensions targeting an older Python version using a recent Python interpreter. Activating an `abi3t` and targeting Python 3.14 and older is a no-op: the build will produce an extension but it will not be an `abi3t` extension, since `abi3t` is unsupported before Python 3.15. @@ -57,7 +69,8 @@ See the [building and distribution](building-and-distribution.md#py_limited_apia These features are extensions of the `abi3t` feature to specify the exact minimum Python version which the multiple-version-wheel will support. -They can be used to target versions of the ABI defined by Python versions released before the "host" python interpreter. They can also be used without any host interpreter at all to cross-compile. +They can be used to target versions of the ABI defined by Python versions released before the "host" python interpreter. +They can also be used without any host interpreter at all to cross-compile. Activating an `abi3t` and targeting Python 3.14 and older is a no-op: the build will produce an extension but it will not be an `abi3t` extension, since `abi3t` is unsupported before Python 3.15. @@ -116,7 +129,8 @@ These macros require a number of dependencies which may not be needed by users w Disabling this feature enables faster builds for those users, as these dependencies will not be built if this feature is disabled. > [!NOTE] -> This feature is enabled by default. To disable it, set `default-features = false` for the `pyo3` entry in your Cargo.toml. +> This feature is enabled by default. +> To disable it, set `default-features = false` for the `pyo3` entry in your Cargo.toml. ### `multiple-pymethods` From 77250302a9481617fd96e9734f3168326e3c9ddd Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 15 May 2026 13:43:13 -0600 Subject: [PATCH 190/195] fix internal links in guide --- Architecture.md | 2 +- guide/src/building-and-distribution.md | 4 ++-- .../building-and-distribution/multiple-python-versions.md | 2 +- guide/src/features.md | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Architecture.md b/Architecture.md index 5afa9de693e..a78c0bff9c5 100644 --- a/Architecture.md +++ b/Architecture.md @@ -46,7 +46,7 @@ In the [`pyo3-ffi`] crate, there is lots of conditional compilation such as `#[c `#[cfg(Py_3_8)]`, and `#[cfg(PyPy)]`. `Py_LIMITED_API` corresponds to `#define Py_LIMITED_API` macro in Python/C API. With `Py_LIMITED_API`, we can build a Python-version-agnostic binary called an -[abi3 wheel](https://pyo3.rs/latest/building-and-distribution.html#py_limited_apiabi3). +[abi3 wheel](https://pyo3.rs/latest/building-and-distribution.html#py_limited_apiabi3abi3t). `Py_3_8` means that the API is available from Python >= 3.8. There are also `Py_3_9`, `Py_3_10`, and so on. `PyPy` means that the API definition is for PyPy. diff --git a/guide/src/building-and-distribution.md b/guide/src/building-and-distribution.md index 3a5825777e6..7964b68220b 100644 --- a/guide/src/building-and-distribution.md +++ b/guide/src/building-and-distribution.md @@ -252,7 +252,7 @@ In 2026, the steering council approved [PEP 803](https://www.python.org/dev/peps Extensions built using the `abit3` ABI are importable on both the GIL-enabled and free-threaded builds of any CPython version newer than the target version. So, `abi3t` extensions built for the Python 3.15 limited API will be importable on Python 3.16, 3.17, and all future Python 3.X versions. -The advantage of building extension modules using the limited Python API is that package vendors only need to build and distribute a single copy (for each OS / architecture), and users can install it on all Python versions from the [minimum version](#minimum-python-version-for-abi3) and up. +The advantage of building extension modules using the limited Python API is that package vendors only need to build and distribute a single copy (for each OS / architecture), and users can install it on all Python versions from the [minimum version](#minimum-python-version-for-abi3-and-abi3t-builds) and up. The downside of this is that PyO3 can't use optimizations which rely on being compiled against a known exact Python version. It's up to you to decide whether this matters for your extension module. It's also possible to design your extension module such that you can distribute `abi3` or `abi3t` wheels but allow users compiling from source to benefit from additional optimizations - see the [support for multiple python versions](./building-and-distribution/multiple-python-versions.md) section of this guide, in particular the `#[cfg(Py_LIMITED_API)]` flag. @@ -284,7 +284,7 @@ There are three steps involved in targeting `abi3` or `abi3t` when building Pyth 3. Ensure that the `.whl` is correctly marked as `abi3` or `abi3t`. For projects using `setuptools`, this is accomplished by passing `--py-limited-api=cp3x` (where `x` is the minimum Python version supported by the wheel, e.g. `--py-limited-api=cp35` for Python 3.5) to `setup.py bdist_wheel`. -#### Minimum Python version for stable ABI builds +#### Minimum Python version for `abi3` and `abi3t` builds Because a single `abi3` wheel can be used with many different Python versions, PyO3 has feature flags `abi3-py38`, `abi3-py39`, `abi3-py310` etc. to set the minimum required Python version for your `abi3` wheel. For example, if you set the `abi3-py38` feature, your wheel can be used on all Python 3 versions from Python 3.8 and up. diff --git a/guide/src/building-and-distribution/multiple-python-versions.md b/guide/src/building-and-distribution/multiple-python-versions.md index eed326c1d74..9f9aa996826 100644 --- a/guide/src/building-and-distribution/multiple-python-versions.md +++ b/guide/src/building-and-distribution/multiple-python-versions.md @@ -108,7 +108,7 @@ This `#[cfg]` marks code which is running on PyPy. ## Checking the Python version at runtime -When building with PyO3's `abi3` or `abi3t` features, your extension module will be compiled against a specific [minimum version](../building-and-distribution.md#minimum-python-version-for-abi3) of Python, but may be running on newer Python versions. +When building with PyO3's `abi3` or `abi3t` features, your extension module will be compiled against a specific [minimum version](../building-and-distribution.md#minimum-python-version-for-abi3-and-abi3t-builds) of Python, but may be running on newer Python versions. For example with PyO3's `abi3-py38` feature, your extension module will be compiled as if it were for Python 3.8. If you were using `pyo3-build-config`, `#[cfg(Py_3_8)]` would be present. diff --git a/guide/src/features.md b/guide/src/features.md index 29dd1085543..bff8d03c945 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -34,7 +34,7 @@ As such, this feature requires a host Python interpreter is present and cannot b If you are using this feature to distribute extensions, you should ensure your minimum supported Python version is installed to do the build. However, see the description of the `abi3-pyXY` feature if you would like to generate extensions targeting an older Python version using a recent Python interpreter. -See the [building and distribution](building-and-distribution.md#py_limited_apiabi3) section for further detail. +See the [building and distribution](building-and-distribution.md#py_limited_apiabi3abi3t) section for further detail. ### The `abi3-pyXY` features @@ -45,7 +45,7 @@ These features are extensions of the `abi3` feature to specify the exact minimum They can be used to target versions of the ABI defined by Python versions released before the "host" python interpreter. They can also be used without any host interpreter at all to cross-compile. -See the [building and distribution](building-and-distribution.md#minimum-python-version-for-abi3) section for further detail. +See the [building and distribution](building-and-distribution.md#minimum-python-version-for-abi3-and-abi3t-builds) section for further detail. ### `abi3t` @@ -61,7 +61,7 @@ However, see the description of the `abi3t-pyXY` feature if you would like to ge Activating an `abi3t` and targeting Python 3.14 and older is a no-op: the build will produce an extension but it will not be an `abi3t` extension, since `abi3t` is unsupported before Python 3.15. -See the [building and distribution](building-and-distribution.md#py_limited_apiabi3) section for further detail. +See the [building and distribution](building-and-distribution.md#py_limited_apiabi3abi3t) section for further detail. ### The `abi3t-pyXY` features @@ -74,7 +74,7 @@ They can also be used without any host interpreter at all to cross-compile. Activating an `abi3t` and targeting Python 3.14 and older is a no-op: the build will produce an extension but it will not be an `abi3t` extension, since `abi3t` is unsupported before Python 3.15. -See the [building and distribution](building-and-distribution.md#minimum-python-version-for-abi3) section for further detail. +See the [building and distribution](building-and-distribution.md#minimum-python-version-for-abi3-and-abi3t-builds) section for further detail. ## Features for embedding Python in Rust From 303877a29b5dbfa8d681e76cb5c123f9d4d6648a Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 15 May 2026 13:44:16 -0600 Subject: [PATCH 191/195] fix typo in noxfile --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 8eda1ef7c5f..212568048d6 100644 --- a/noxfile.py +++ b/noxfile.py @@ -134,7 +134,7 @@ def test_rust(session: nox.Session): and "abi3t" not in feature_set and "full" in feature_set and sys.version_info >= (3, 9) - and sys.implementation_name.name != "pypy" + and sys.implementation.name != "pypy" ): # run abi3-py38 tests to check abi3 forward compatibility _run_cargo_test( From 607f8bca7d4500eb9cc8c3be48355aa33d14c930 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Sat, 16 May 2026 15:53:01 -0600 Subject: [PATCH 192/195] fix release notes --- newsfragments/5807.added.md | 18 +++++++++++------- newsfragments/5807.changed.md | 6 ++++++ newsfragments/5924.added.md | 7 ------- newsfragments/5924.changed.md | 6 ------ 4 files changed, 17 insertions(+), 20 deletions(-) create mode 100644 newsfragments/5807.changed.md delete mode 100644 newsfragments/5924.added.md delete mode 100644 newsfragments/5924.changed.md diff --git a/newsfragments/5807.added.md b/newsfragments/5807.added.md index bb4599f1e45..f01b275abdc 100644 --- a/newsfragments/5807.added.md +++ b/newsfragments/5807.added.md @@ -1,12 +1,16 @@ * Support for the new PEP 803 abi3t ABI. * New `abi3t` and `abi3t-py315` features to enable abi3t builds. These features - work analogously to the `abi3` and `abi3-py3XX` features. -* A `PythonAbi` struct which wraps an `PythonImplementation`, - `PythonVersion`, and a new `PythonAbiKind` enum. The `PythonAbiKind` enum can - itself be one of two enums: `StableAbi` or `VersionSpecific`. The former can - be `Abi3` or `Abi3t` and the latter can be `FreeThreaded` or - `GilEnabled`. Together these types enable fine-grained descriptions of the - target ABI for an extension build. + work analogously to the `abi3` and `abi3-py3XX` features but are no-ops on + Python 3.14 and older and prudce abi3t builds on Python 3.15. +* A `PythonAbi` struct which wraps a `PythonImplementation`, `PythonVersion`, + and a new `PythonAbiKind` enum. The `PythonAbiKind` enum can itself be one of + two enums: `PythonAbiKind::StableAbi` or `PythonAbiKind::VersionSpecific`. The + former can be `StableAbi::Abi3` or `StabieAbi::Abi3t` and the latter can be + `VersionSpecific::FreeThreaded` or `VersionSpecific::GilEnabled`. Together + these types enable fine-grained descriptions of the target ABI for an + extension build. * A `PythonAbiBuilder` struct for constructing new `PythonAbi` instances. * A new `InterpreterConfig::target_abi` function for accessing details about the target ABI for a build. +* A new `InterpreterConfigBuilder::target_abi` function for setting the target + ABI for a newly constructed `InterpreterConfig`. diff --git a/newsfragments/5807.changed.md b/newsfragments/5807.changed.md new file mode 100644 index 00000000000..c1d2837d1a4 --- /dev/null +++ b/newsfragments/5807.changed.md @@ -0,0 +1,6 @@ +* The boolean `abi3` field of `pyo3_build_config::impl_::InterpreterConfig` is now + deprecated and has been replaced by a new `target_abi` accessor function, which returns a `PythonAbi` + struct. +* The InterpreterConfig::from_interpreter function now accepts a second + `stable_abi_version` argument. The old behavior is the same as passing + `None`. diff --git a/newsfragments/5924.added.md b/newsfragments/5924.added.md deleted file mode 100644 index 6d8e93af861..00000000000 --- a/newsfragments/5924.added.md +++ /dev/null @@ -1,7 +0,0 @@ -* Added a PythonAbi struct, along with PythonAbiKind, StableAbi, and GilUsed -enums to pyo3-build-config. These types allow specifying the Python ABI to use -for a build. You can construct a PythonAbi struct using a PythonAbiBuilder -struct. - -* Added an InterpreterConfigBuilder struct for building InterpreterConfig -structs with a more ergonomic syntax. diff --git a/newsfragments/5924.changed.md b/newsfragments/5924.changed.md deleted file mode 100644 index 195b8408e77..00000000000 --- a/newsfragments/5924.changed.md +++ /dev/null @@ -1,6 +0,0 @@ -* The boolean `abi3` field of `pyo3_build_config::impl_::InterpreterConfig` is now - deprecated and has been replaced by a target_abi field, which is a `PythonAbi` - struct. -* The InterpreterConfig::from_interpreter function now accepts a second - `abi3_version` argument. The old behavior is the same as passing - `abi3_version = None` . From 335fa8a5fc3373e0d515a6311b148f97308a9329 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Sat, 16 May 2026 15:58:31 -0600 Subject: [PATCH 193/195] revert now-unnecessary CI configuration change --- .github/workflows/ci.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e991f288ec6..6686774ca41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,14 +77,6 @@ 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 be8b1a1caaeec95c93b5c8036e0a5d5a5b95479f Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 18 May 2026 11:28:31 -0600 Subject: [PATCH 194/195] fix abi3t-py315 feature not re-exporting pyo3-ffi feature --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 7742fc4733f..181d18ea139 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -128,7 +128,7 @@ abi3-py315 = ["abi3", "pyo3-ffi/abi3-py315"] abi3t = ["pyo3-ffi/abi3t"] # With abi3t, we can manually set the minimum Python version. -abi3t-py315 = ["abi3t"] +abi3t-py315 = ["abi3t", "pyo3-ffi/abi3t-py315"] # deprecated: no longer needed, raw-dylib is used instead generate-import-lib = ["pyo3-ffi/generate-import-lib"] From 68ffca078d4f9f6c5f9e9235b945636cf3710127 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 18 May 2026 11:32:04 -0600 Subject: [PATCH 195/195] fix typos in comments --- noxfile.py | 4 ++-- pyo3-build-config/src/impl_.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/noxfile.py b/noxfile.py index 18f5a293a46..71c6a906cd3 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1273,11 +1273,11 @@ def test_version_limits(session: nox.Session): # 3.17t CPython should build if abi3t is explicitly requested _run_cargo(session, "check", "--features=pyo3/abi3t", env=env) - # abi3t builds should succeed on Python versions that support abi3t + # abi3t builds should succeed on Python versions that do not support abi3t config_file.set("CPython", "3.14") _run_cargo(session, "check", "--features=pyo3/abi3t", env=env) - # ... also if both abi3 and abi3t features are enabled + # the build should also succeed if both abi3 and abi3t features are enabled config_file.set("CPython", "3.14") _run_cargo(session, "check", "--features=pyo3/abi3t,pyo3/abi3", env=env) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 63c6c50a17c..43754fe9b4d 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -1064,7 +1064,7 @@ impl PythonAbiKind { } } -/// The the variety of stable ABI +/// The variety of stable ABI #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(test, derive(Debug))] pub enum StableAbi {