Skip to content

Commit 3b3ba4e

Browse files
authored
Merge pull request #1152 from PyO3/abi3
Complete abi3 support
2 parents 522ebee + eb0e6f6 commit 3b3ba4e

Some content is hidden

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

63 files changed

+1683
-1358
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ jobs:
9494
- if: matrix.python-version != 'pypy3'
9595
name: Test
9696
run: cargo test --features "num-bigint num-complex" --target ${{ matrix.platform.rust-target }}
97+
# Run tests again, but in abi3 mode
98+
- if: matrix.python-version != 'pypy3'
99+
name: Test (abi3)
100+
run: cargo test --no-default-features --features "abi3,macros" --target ${{ matrix.platform.rust-target }}
97101

98102
- name: Test proc-macro code
99103
run: cargo test --manifest-path=pyo3-derive-backend/Cargo.toml --target ${{ matrix.platform.rust-target }}

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1212
- Drop support for Python 3.5 (as it is now end-of-life). [#1250](https://github.com/PyO3/pyo3/pull/1250)
1313

1414
### Added
15+
- Add support for building for CPython limited API. This required a few minor changes to runtime behaviour of of pyo3 `#[pyclass]` types. See the migration guide for full details. [#1152](https://github.com/PyO3/pyo3/pull/1152)
1516
- Add argument names to `TypeError` messages generated by pymethod wrappers. [#1212](https://github.com/PyO3/pyo3/pull/1212)
1617
- Add `PyEval_SetProfile` and `PyEval_SetTrace` to FFI. [#1255](https://github.com/PyO3/pyo3/pull/1255)
1718
- Add context.h functions (`PyContext_New`, etc) to FFI. [#1259](https://github.com/PyO3/pyo3/pull/1259)
1819

1920
### Changed
21+
- Change return type `PyType::name()` from `Cow<str>` to `PyResult<&str>`. [#1152](https://github.com/PyO3/pyo3/pull/1152)
22+
- `#[pyclass(subclass)]` is now required for subclassing from Rust (was previously just required for subclassing from Python). [#1152](https://github.com/PyO3/pyo3/pull/1152)
2023
- Change `PyIterator` to be consistent with other native types: it is now used as `&PyIterator` instead of `PyIterator<'a>`. [#1176](https://github.com/PyO3/pyo3/pull/1176)
2124
- Change formatting of `PyDowncastError` messages to be closer to Python's builtin error messages. [#1212](https://github.com/PyO3/pyo3/pull/1212)
2225

Cargo.toml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ rustversion = "1.0"
3535
[features]
3636
default = ["macros"]
3737
macros = ["ctor", "indoc", "inventory", "paste", "pyo3cls", "unindent"]
38+
# Use the Python limited API. See https://www.python.org/dev/peps/pep-0384/ for
39+
# more.
40+
abi3 = []
41+
3842
# Optimizes PyObject to Vec conversion and so on.
3943
nightly = []
4044

@@ -43,11 +47,6 @@ nightly = []
4347
# so that the module can also be used with statically linked python interpreters.
4448
extension-module = []
4549

46-
# The stable cpython abi as defined in PEP 384. Currently broken with
47-
# many compilation errors. Pull Requests working towards fixing that
48-
# are welcome.
49-
# abi3 = []
50-
5150
[workspace]
5251
members = [
5352
"pyo3cls",

build.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,11 @@ fn run_python_script(interpreter: &Path, script: &str) -> Result<String> {
557557

558558
fn get_library_link_name(version: &PythonVersion, ld_version: &str) -> String {
559559
if cfg!(target_os = "windows") {
560+
// Mirrors the behavior in CPython's `PC/pyconfig.h`.
561+
if env::var_os("CARGO_FEATURE_ABI3").is_some() {
562+
return "python3".to_string();
563+
}
564+
560565
let minor_or_empty_string = match version.minor {
561566
Some(minor) => format!("{}", minor),
562567
None => String::new(),

examples/rustapi_module/tests/test_datetime.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import datetime as pdt
22
import platform
33
import struct
4+
import re
45
import sys
56

67
import pytest
@@ -310,4 +311,5 @@ def test_tz_class_introspection():
310311
tzi = rdt.TzClass()
311312

312313
assert tzi.__class__ == rdt.TzClass
313-
assert repr(tzi).startswith("<TzClass object at")
314+
# PyPy generates <importlib.bootstrap.TzClass ...> for some reason.
315+
assert re.match(r"^<[\w\.]*TzClass object at", repr(tzi))

guide/src/building_and_distribution.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,27 @@ On Linux/macOS you might have to change `LD_LIBRARY_PATH` to include libpython,
3636

3737
## Distribution
3838

39-
There are two ways to distribute your module as a Python package: The old, [setuptools-rust](https://github.com/PyO3/setuptools-rust), and the new, [maturin](https://github.com/pyo3/maturin). setuptools-rust needs several configuration files (`setup.py`, `MANIFEST.in`, `build-wheels.sh`, etc.). maturin doesn't need any configuration files, however it does not support some functionality of setuptools such as package data ([pyo3/maturin#258](https://github.com/PyO3/maturin/issues/258)) and requires a rigid project structure, while setuptools-rust allows (and sometimes requires) configuration with python code.
39+
There are two ways to distribute your module as a Python package: The old, [setuptools-rust], and the new, [maturin]. setuptools-rust needs several configuration files (`setup.py`, `MANIFEST.in`, `build-wheels.sh`, etc.). maturin doesn't need any configuration files, however it does not support some functionality of setuptools such as package data ([pyo3/maturin#258](https://github.com/PyO3/maturin/issues/258)) and requires a rigid project structure, while setuptools-rust allows (and sometimes requires) configuration with python code.
40+
41+
## `Py_LIMITED_API`/`abi3`
42+
43+
By default, Python extension modules can only be used with the same Python version they were compiled against -- if you build an extension module with Python 3.5, you can't import it using 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`.
44+
45+
Note that [maturin] >= 0.9.0 or [setuptools-rust] >= 0.12.0 is going to support `abi3` wheels.
46+
See the [corresponding](https://github.com/PyO3/maturin/pull/353) [PRs](https://github.com/PyO3/setuptools-rust/pull/82) for more.
47+
48+
There are three steps involved in making use of `abi3` when building Python packages as wheels:
49+
50+
1. Enable the `abi3` 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):
51+
52+
```toml
53+
[dependencies]
54+
pyo3 = { version = "...", features = ["abi3"]}
55+
```
56+
57+
2. Ensure that the built shared objects are correctly marked as `abi3`. This is accomplished by telling your build system that you're using the limited API.
58+
59+
3. Ensure that the `.whl` is correctly marked as `abi3`. 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`.
4060

4161
## Cross Compiling
4262

@@ -83,3 +103,8 @@ cargo build --target x86_64-pc-windows-gnu
83103
## Bazel
84104

85105
For an example of how to build python extensions using Bazel, see https://github.com/TheButlah/rules_pyo3
106+
107+
108+
[maturin]: https://github.com/PyO3/maturin
109+
[setuptools-rust]: https://github.com/PyO3/setuptools-rust
110+

guide/src/class.md

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ or by `self_.into_super()` as `PyRef<Self::BaseClass>`.
205205
```rust
206206
# use pyo3::prelude::*;
207207

208-
#[pyclass]
208+
#[pyclass(subclass)]
209209
struct BaseClass {
210210
val1: usize,
211211
}
@@ -222,7 +222,7 @@ impl BaseClass {
222222
}
223223
}
224224

225-
#[pyclass(extends=BaseClass)]
225+
#[pyclass(extends=BaseClass, subclass)]
226226
struct SubClass {
227227
val2: usize,
228228
}
@@ -266,12 +266,14 @@ impl SubSubClass {
266266
```
267267

268268
You can also inherit native types such as `PyDict`, if they implement
269-
[`PySizedLayout`](https://docs.rs/pyo3/latest/pyo3/type_object/trait.PySizedLayout.html).
269+
[`PySizedLayout`](https://docs.rs/pyo3/latest/pyo3/type_object/trait.PySizedLayout.html). However, this is not supported when building for the Python limited API (aka the `abi3` feature of PyO3).
270270

271271
However, because of some technical problems, we don't currently provide safe upcasting methods for types
272272
that inherit native types. Even in such cases, you can unsafely get a base class by raw pointer conversion.
273273

274274
```rust
275+
# #[cfg(Py_LIMITED_API)] fn main() {}
276+
# #[cfg(not(Py_LIMITED_API))] fn main() {
275277
# use pyo3::prelude::*;
276278
use pyo3::types::PyDict;
277279
use pyo3::{AsPyPointer, PyNativeType};
@@ -300,6 +302,7 @@ impl DictWithCounter {
300302
# let py = gil.python();
301303
# let cnt = pyo3::PyCell::new(py, DictWithCounter::new()).unwrap();
302304
# pyo3::py_run!(py, cnt, "cnt.set('abc', 10); assert cnt['abc'] == 10")
305+
# }
303306
```
304307

305308
If `SubClass` does not provide a baseclass initialization, the compilation fails.
@@ -769,13 +772,23 @@ impl pyo3::class::methods::HasMethodsInventory for MyClass {
769772
}
770773
pyo3::inventory::collect!(Pyo3MethodsInventoryForMyClass);
771774

772-
impl pyo3::class::proto_methods::HasProtoRegistry for MyClass {
773-
fn registry() -> &'static pyo3::class::proto_methods::PyProtoRegistry {
774-
static REGISTRY: pyo3::class::proto_methods::PyProtoRegistry
775-
= pyo3::class::proto_methods::PyProtoRegistry::new();
776-
&REGISTRY
775+
776+
pub struct Pyo3ProtoInventoryForMyClass {
777+
def: pyo3::class::proto_methods::PyProtoMethodDef,
778+
}
779+
impl pyo3::class::proto_methods::PyProtoInventory for Pyo3ProtoInventoryForMyClass {
780+
fn new(def: pyo3::class::proto_methods::PyProtoMethodDef) -> Self {
781+
Self { def }
782+
}
783+
fn get(&'static self) -> &'static pyo3::class::proto_methods::PyProtoMethodDef {
784+
&self.def
777785
}
778786
}
787+
impl pyo3::class::proto_methods::HasProtoInventory for MyClass {
788+
type ProtoMethods = Pyo3ProtoInventoryForMyClass;
789+
}
790+
pyo3::inventory::collect!(Pyo3ProtoInventoryForMyClass);
791+
779792

780793
impl pyo3::pyclass::PyClassSend for MyClass {
781794
type ThreadChecker = pyo3::pyclass::ThreadCheckerStub<MyClass>;

guide/src/migration.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
33
This guide can help you upgrade code through breaking changes from one PyO3 version to the next.
44
For a detailed list of all changes, see the [CHANGELOG](changelog.md).
55

6+
## from 0.12.* to 0.13
7+
8+
### Runtime changes to support the CPython limited API
9+
10+
In PyO3 `0.13` support was added for compiling against the CPython limited API. This had a number of implications for _all_ PyO3 users, described here.
11+
12+
The largest of these is that all types created from PyO3 are what CPython calls "heap" types. The specific implications of this are:
13+
14+
- If you wish to subclass one of these types _from Rust_ you must mark it `#[pyclass(subclass)]`, as you would if you wished to allow subclassing it from Python code.
15+
- Type objects are now mutable - Python code can set attributes on them.
16+
- `__module__` on types without `#[pyclass(module="mymodule")]` no longer returns `builtins`, it now raises `AttributeError`.
17+
618
## from 0.11.* to 0.12
719

820
### `PyErr` has been reworked

guide/src/trait_bounds.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -408,8 +408,8 @@ impl Model for UserModel {
408408
.call_method("get_results", (), None)
409409
.unwrap();
410410

411-
if py_result.get_type().name() != "list" {
412-
panic!("Expected a list for the get_results() method signature, got {}", py_result.get_type().name());
411+
if py_result.get_type().name().unwrap() != "list" {
412+
panic!("Expected a list for the get_results() method signature, got {}", py_result.get_type().name().unwrap());
413413
}
414414
py_result.extract()
415415
})
@@ -536,8 +536,8 @@ impl Model for UserModel {
536536
.call_method("get_results", (), None)
537537
.unwrap();
538538

539-
if py_result.get_type().name() != "list" {
540-
panic!("Expected a list for the get_results() method signature, got {}", py_result.get_type().name());
539+
if py_result.get_type().name().unwrap() != "list" {
540+
panic!("Expected a list for the get_results() method signature, got {}", py_result.get_type().name().unwrap());
541541
}
542542
py_result.extract()
543543
})

0 commit comments

Comments
 (0)