Skip to content

Commit e18aba6

Browse files
authored
Support pyo3 abi3t features on Python3.15 and PyO3 0.29 (#3113)
Adds support for the new abi3t ABI in Python 3.15. See https://pyo3.rs/main/building-and-distribution.html?highlight=abi3t#py_limited_apiabi3abi3t for more info from the pyo3 side of things.
1 parent 8786c59 commit e18aba6

79 files changed

Lines changed: 1271 additions & 484 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ jobs:
4646
- windows-11-arm
4747
python-version:
4848
- "3.9"
49-
- "3.14"
50-
- "3.14t"
49+
- "3.15-dev"
50+
- "3.15t-dev"
5151
- "pypy3.11"
5252
exclude:
5353
# TODO: Tests are getting stuck
@@ -66,6 +66,7 @@ jobs:
6666
runs-on: ${{ matrix.os }}
6767
env:
6868
RUST_BACKTRACE: "1"
69+
PIP_NO_CACHE_DIR: "1"
6970
SCCACHE_GHA_ENABLED: "true"
7071
RUSTC_WRAPPER: "sccache"
7172
steps:
@@ -425,6 +426,7 @@ jobs:
425426
- name: Build wheels
426427
run: |
427428
set -ex
429+
428430
# Use bundled sysconfig
429431
bin/maturin build -i ${{ matrix.platform.python }} --release --out dist --target ${{ matrix.platform.target }} -m test-crates/pyo3-mixed/Cargo.toml ${{ matrix.platform.extra-args }}
430432

guide/src/bindings.md

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,30 @@ maturin automatically detects pyo3 bindings when it's added as a dependency in `
1414

1515
### `Py_LIMITED_API`/abi3
1616

17-
pyo3 bindings has `Py_LIMITED_API`/abi3 support, enable the `abi3` feature of the `pyo3` crate to use it:
17+
The pyo3 bindings supports the Python stable ABI (`Py_LIMITED_API`/abi3/abi3t).
18+
You can use it by enabling `"abi3"` and/or `"abi3t"` features. We suggest
19+
picking a minimum supported Python version for both features:
1820

1921
```toml
20-
pyo3 = { version = "0.28.3", features = ["abi3"] }
22+
pyo3 = { version = "0.29.0", features = ["abi3-py310", "abi3t-py315"] }
2123
```
2224

23-
You may additionally specify a minimum Python version by using the `abi3-pyXX`
24-
format for the pyo3 features, where `XX` is corresponds to a Python version.
25-
For example `abi3-py37` will indicate a minimum Python version of 3.7.
26-
27-
> **Note**: Read more about abi3 support in [pyo3's
28-
> documentation](https://pyo3.rs/latest/building-and-distribution#py_limited_apiabi3).
25+
When selecting a specific interpreter to build against, this will produce an
26+
`abi3-py310` wheel for Python 3.14 and older and an `abi3.abi3t-py315` wheel on
27+
Python 3.15 and newer. If you build with `--find-interpreters`, maturin will
28+
produce an `abi3-py310`, `cp314t-cp314` and an `abi3.abi3t-py315` wheel. These
29+
three wheels cover all non-EOL and non-experimental builds of CPython. Other
30+
python implementations like RustPython may also target the abi3t ABI in the
31+
future.
32+
33+
An `abi3-py310` wheel supports all GIL-enabled Python
34+
versions from Python 3.10 to Python 3.14 and the `abi3.abi3t` wheel supports
35+
Python 3.15 and all newer versions of CPython.
36+
37+
> **Note**: Read more about stable ABI support in [pyo3's
38+
> documentation](https://pyo3.rs/latest/building-and-distribution#py_limited_apiabi3abi3t). You
39+
> can read more about using abi3 and abi3t wheels simultaneously in the
40+
> HOWTO guide on migrating to abi3t: https://docs.python.org/3.16/howto/abi3t-migration.html#why-do-this.
2941
3042
### Cross Compiling
3143

guide/src/distribution.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ or providing any Windows Python library files.
263263

264264
```toml
265265
[dependencies]
266-
pyo3 = { version = "0.28.3", features = ["generate-import-lib"] }
266+
pyo3 = { version = "0.29.0", features = ["generate-import-lib"] }
267267
```
268268

269269
It uses an external [`python3-dll-a`](https://docs.rs/python3-dll-a/latest/python3_dll_a/) crate to

guide/src/tutorial.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ crate-type = ["cdylib"]
3636
rand = "0.9.0"
3737

3838
[dependencies.pyo3]
39-
version = "0.28.3"
39+
version = "0.29.0"
4040
# "abi3-py38" tells pyo3 (and maturin) to build using the stable ABI with minimum Python version 3.8
4141
features = ["abi3-py38"]
4242
```

src/binding_generator/pyo3_binding.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ pub struct Pyo3BindingGenerator<'a> {
3737

3838
enum BindingType<'a> {
3939
Abi3(Option<&'a PythonInterpreter>),
40+
Abi3t(Option<&'a PythonInterpreter>),
4041
VersionSpecific(&'a PythonInterpreter),
4142
}
4243

@@ -49,6 +50,7 @@ impl<'a> Pyo3BindingGenerator<'a> {
4950
let binding_type = match stable_abi {
5051
Some(kind) => match kind {
5152
StableAbiKind::Abi3 => BindingType::Abi3(interpreter),
53+
StableAbiKind::Abi3t => BindingType::Abi3t(interpreter),
5254
},
5355
None => {
5456
let interpreter = interpreter.ok_or_else(|| {
@@ -101,6 +103,7 @@ impl<'a> BindingGenerator for Pyo3BindingGenerator<'a> {
101103

102104
let so_filename = match self.binding_type {
103105
BindingType::Abi3(interpreter) => ext_suffix(target, interpreter, ext_name, "abi3"),
106+
BindingType::Abi3t(interpreter) => ext_suffix(target, interpreter, ext_name, "abi3t"),
104107
BindingType::VersionSpecific(interpreter) => interpreter.get_library_name(ext_name),
105108
};
106109
let artifact_target = ArtifactTarget::ExtensionModule(module.join(so_filename));

src/bridge/detection.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const PYO3_BINDING_CRATES: [PyO3Crate; 2] = [PyO3Crate::PyO3Ffi, PyO3Crate::PyO3
2323
/// is forced; otherwise it's auto-detected from dependencies and target types.
2424
///
2525
/// Conditional pyo3/pyo3-ffi features from pyproject.toml are excluded from
26-
/// abi3 inference here. Use [`upgrade_bridge_abi3`] after interpreter resolution
26+
/// abi3 inference here. Use [`upgrade_bridge_stable_abi`] after interpreter resolution
2727
/// to evaluate them.
2828
pub fn find_bridge(cargo_metadata: &Metadata, bridge: Option<&str>) -> Result<BridgeModel> {
2929
let no_extra_features = HashMap::new();
@@ -147,7 +147,7 @@ pub fn find_bridge(cargo_metadata: &Metadata, bridge: Option<&str>) -> Result<Br
147147
/// This is the second phase of bridge detection: [`find_bridge`] excludes
148148
/// conditional features, then after interpreter resolution this function
149149
/// re-checks whether any conditional abi3 feature applies.
150-
pub fn upgrade_bridge_abi3(
150+
pub fn upgrade_bridge_stable_abi(
151151
bridge: BridgeModel,
152152
cargo_metadata: &Metadata,
153153
pyproject: Option<&PyProjectToml>,
@@ -223,11 +223,11 @@ fn has_stable_abi(
223223
deps: &HashMap<&str, &Node>,
224224
extra_features: &HashMap<&str, Vec<String>>,
225225
) -> Result<Option<StableAbi>> {
226-
let abi3 = has_stable_abi_from_kind(deps, extra_features, StableAbiKind::Abi3)?;
227-
if abi3.is_some() {
228-
return Ok(abi3);
226+
let abi3t = has_stable_abi_from_kind(deps, extra_features, StableAbiKind::Abi3t)?;
227+
if abi3t.is_some() {
228+
return Ok(abi3t);
229229
}
230-
Ok(None)
230+
has_stable_abi_from_kind(deps, extra_features, StableAbiKind::Abi3)
231231
}
232232

233233
/// pyo3 supports building stable abi wheels if the unstable-api feature is not selected
@@ -240,7 +240,6 @@ fn has_stable_abi_from_kind(
240240
let lib = lib.as_str();
241241
if let Some(&pyo3_crate) = deps.get(lib) {
242242
let extra = extra_features.get(lib);
243-
// Find the minimal abi3 python version. If there is none, abi3 hasn't been selected
244243
// Find the minimal stable abi python version. If there is none, stable abi hasn't been selected
245244
// This parses abi3-py{major}{minor} and returns the minimal (major, minor) tuple
246245
let all_features: Vec<&str> = pyo3_crate

src/bridge/mod.rs

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
mod detection;
22

3-
pub use detection::{find_bridge, has_windows_import_lib_support, upgrade_bridge_abi3};
3+
pub use detection::{find_bridge, has_windows_import_lib_support, upgrade_bridge_stable_abi};
44

55
use std::{fmt, str::FromStr};
66

@@ -132,6 +132,23 @@ impl StableAbi {
132132
version: StableAbiVersion::Version(major, minor),
133133
}
134134
}
135+
136+
/// Create a StableAbi instance from a known abi3t version
137+
pub fn from_abi3t_version(major: u8, minor: u8) -> StableAbi {
138+
StableAbi {
139+
kind: StableAbiKind::Abi3t,
140+
version: StableAbiVersion::Version(major, minor),
141+
}
142+
}
143+
}
144+
145+
impl std::fmt::Display for StableAbi {
146+
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
147+
match self.kind {
148+
StableAbiKind::Abi3 => write!(f, "abi3"),
149+
StableAbiKind::Abi3t => write!(f, "abi3t"),
150+
}
151+
}
135152
}
136153

137154
/// Python version to use as the abi3/abi3t target.
@@ -146,7 +163,7 @@ pub enum StableAbiVersion {
146163
}
147164

148165
impl StableAbiVersion {
149-
/// Convert `StableAbiVersion` into an Option, where CurrentPython maps None
166+
/// Convert `StableAbiVersion` into an Option, where CurrentPython maps to None
150167
pub fn min_version(&self) -> Option<(u8, u8)> {
151168
match self {
152169
StableAbiVersion::CurrentPython => None,
@@ -160,12 +177,15 @@ impl StableAbiVersion {
160177
pub enum StableAbiKind {
161178
/// The original stable ABI, supporting Python 3.2 and up
162179
Abi3,
180+
/// The free-threaded stable ABI, supporting Python 3.15 and up
181+
Abi3t,
163182
}
164183

165184
impl fmt::Display for StableAbiKind {
166185
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167186
match self {
168187
StableAbiKind::Abi3 => write!(f, "abi3"),
188+
StableAbiKind::Abi3t => write!(f, "abi3t"),
169189
}
170190
}
171191
}
@@ -175,6 +195,7 @@ impl StableAbiKind {
175195
pub fn wheel_tag(&self) -> &str {
176196
match self {
177197
StableAbiKind::Abi3 => "abi3",
198+
StableAbiKind::Abi3t => "abi3.abi3t",
178199
}
179200
}
180201
}
@@ -340,32 +361,36 @@ impl BridgeModel {
340361
///
341362
/// This is a project-level check — it does not consider whether a particular
342363
/// interpreter meets the abi3 minimum version. For per‑interpreter checks
343-
/// use [`is_abi3_for_interpreter`](Self::is_abi3_for_interpreter).
364+
/// use [`is_stable_abi_for_interpreter`](Self::is_stable_abi_for_interpreter).
344365
pub fn has_stable_abi(&self) -> bool {
345366
self.pyo3()
346367
.and_then(|pyo3| pyo3.stable_abi.as_ref())
347368
.is_some()
348369
}
349370

350-
/// Check whether abi3 should be enabled for a specific interpreter.
371+
/// Check whether an abi3 or abi3t build should be enabled for a specific interpreter.
351372
///
352373
/// Returns `true` only when the bridge model has stable abi support **and**
353374
/// the given interpreter supports the stable ABI **and** meets the abi3
354375
/// minimum version. Version‑specific fallback builds (e.g. Python 3.10 when
355376
/// abi3 targets ≥ 3.11) return `false` so that `Py_LIMITED_API` is not
356377
/// defined and interpreter‑specific linker names are used.
357-
pub fn is_abi3_for_interpreter(&self, interpreter: &PythonInterpreter) -> bool {
358-
if !interpreter.has_stable_api() {
359-
return false;
360-
}
361-
378+
pub fn is_stable_abi_for_interpreter(&self, interpreter: &PythonInterpreter) -> bool {
362379
self.pyo3()
363380
.and_then(|pyo3| pyo3.stable_abi.as_ref())
364-
.is_some_and(|stable_abi| match stable_abi.version.min_version() {
365-
Some((major, minor)) => {
366-
(interpreter.major as u8, interpreter.minor as u8) >= (major, minor)
381+
.is_some_and(|stable_abi| {
382+
if !interpreter.has_stable_api(stable_abi.kind) {
383+
return false;
384+
}
385+
if matches!(stable_abi.kind, StableAbiKind::Abi3) && interpreter.gil_disabled {
386+
return false;
387+
};
388+
match stable_abi.version.min_version() {
389+
Some((major, minor)) => {
390+
(interpreter.major as u8, interpreter.minor as u8) >= (major, minor)
391+
}
392+
None => true, // CurrentPython → compatible when stable ABI is supported
367393
}
368-
None => true, // CurrentPython → compatible when stable ABI is supported
369394
})
370395
}
371396

@@ -392,3 +417,39 @@ impl fmt::Display for BridgeModel {
392417
}
393418
}
394419
}
420+
421+
#[cfg(test)]
422+
mod tests {
423+
use super::*;
424+
425+
#[test]
426+
fn stable_abi_kind_display() {
427+
assert_eq!(StableAbiKind::Abi3.to_string(), "abi3");
428+
assert_eq!(StableAbiKind::Abi3t.to_string(), "abi3t");
429+
}
430+
431+
#[test]
432+
fn stable_abi_kind_wheel_tag() {
433+
assert_eq!(StableAbiKind::Abi3.wheel_tag(), "abi3");
434+
// abi3t wheels are also importable on abi3-capable interpreters, so the
435+
// wheel tag is the compressed form `abi3.abi3t`.
436+
assert_eq!(StableAbiKind::Abi3t.wheel_tag(), "abi3.abi3t");
437+
}
438+
439+
#[test]
440+
fn stable_abi_display() {
441+
assert_eq!(StableAbi::from_abi3_version(3, 7).to_string(), "abi3");
442+
assert_eq!(StableAbi::from_abi3t_version(3, 15).to_string(), "abi3t");
443+
}
444+
445+
#[test]
446+
fn stable_abi_constructors() {
447+
let abi3 = StableAbi::from_abi3_version(3, 9);
448+
assert_eq!(abi3.kind, StableAbiKind::Abi3);
449+
assert_eq!(abi3.version, StableAbiVersion::Version(3, 9));
450+
451+
let abi3t = StableAbi::from_abi3t_version(3, 15);
452+
assert_eq!(abi3t.kind, StableAbiKind::Abi3t);
453+
assert_eq!(abi3t.version, StableAbiVersion::Version(3, 15));
454+
}
455+
}

src/build_context/builder.rs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use crate::auditwheel::{AuditWheelMode, PlatformTag};
2-
use crate::bridge::{find_bridge, has_windows_import_lib_support, upgrade_bridge_abi3};
2+
use crate::bridge::{
3+
StableAbiVersion, find_bridge, has_windows_import_lib_support, upgrade_bridge_stable_abi,
4+
};
35
use crate::build_options::{BuildOptions, TargetTriple};
46
use crate::compile::filter_cargo_targets;
57
use crate::metadata::Metadata24;
@@ -129,7 +131,7 @@ impl BuildContextBuilder {
129131
});
130132

131133
// Detect bridge without conditional pyo3 features — those are
132-
// evaluated after interpreter resolution via upgrade_bridge_abi3.
134+
// evaluated after interpreter resolution via upgrade_bridge_stable_abi.
133135
let bridge = find_bridge(&cargo_metadata, bindings)?;
134136

135137
if !bridge.is_bin() && project_layout.extension_name.contains('-') {
@@ -163,16 +165,26 @@ impl BuildContextBuilder {
163165
// (e.g. abi3-py311 gated on python-version>=3.11) match any
164166
// of the resolved interpreters.
165167
let bridge = if has_conditional_pyo3_features {
166-
upgrade_bridge_abi3(bridge, &cargo_metadata, pyproject, &interpreter)?
168+
upgrade_bridge_stable_abi(bridge, &cargo_metadata, pyproject, &interpreter)?
167169
} else {
168170
bridge
169171
};
170172
debug!("Resolved bridge model: {:?}", bridge);
171173
if let Some(stable_abi) = bridge.pyo3().and_then(|p| p.stable_abi.as_ref()) {
172-
eprintln!(
173-
"🔗 Found {bridge} bindings with {} support",
174-
stable_abi.kind
175-
);
174+
match stable_abi.version {
175+
StableAbiVersion::Version(major, minor) => {
176+
eprintln!(
177+
"🔗 Found {bridge} bindings with {}-py{}.{} support",
178+
stable_abi.kind, major, minor
179+
);
180+
}
181+
StableAbiVersion::CurrentPython => {
182+
eprintln!(
183+
"🔗 Found {bridge} bindings with {} support",
184+
stable_abi.kind
185+
);
186+
}
187+
}
176188
} else {
177189
eprintln!("🔗 Found {bridge} bindings");
178190
}

0 commit comments

Comments
 (0)