Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
eb53c2d
Use PyModExport and PyABIInfo APIs in pymodule implementation
ngoldbaum Jan 26, 2026
e41b509
Add PyModExport function
ngoldbaum Jan 26, 2026
91becaf
DNM: temporarily disable append_to_inittab doctest
ngoldbaum Jan 26, 2026
1020688
fix issues seen on older pythons in CI
ngoldbaum Jan 26, 2026
3afa9ae
fix incorrect ModuleDef setup on 3.15
ngoldbaum Jan 26, 2026
f8d6cae
Expose both the PyInit and PyModExport initialization hooks
ngoldbaum Jan 27, 2026
a590874
fix clippy
ngoldbaum Jan 27, 2026
a96219f
add changelog entry
ngoldbaum Jan 27, 2026
e7ac9c0
try use only slots for both init hooks on 3.15
ngoldbaum Jan 29, 2026
d981be7
Always pass m_name and m_doc, following cpython-gh-144340
ngoldbaum Jan 30, 2026
55b6acd
WIP: opaque pyobject support (without Py_GIL_DISABLED)
ngoldbaum Feb 13, 2026
733aa82
delete debug prints
ngoldbaum Feb 13, 2026
c43061e
WIP: fix segfault
ngoldbaum Feb 13, 2026
3812a64
disable append_to_inittab tests
ngoldbaum Feb 13, 2026
25a65a6
fix clippy
ngoldbaum Feb 13, 2026
4a83024
fix ruff
ngoldbaum Feb 13, 2026
9d0e2ed
implement David's suggestion for pyobject_subclassable_native_type
ngoldbaum Feb 13, 2026
a78b5df
replace skipped test with real test
ngoldbaum Feb 13, 2026
42a73e1
fix check-feature-powerset
ngoldbaum Feb 13, 2026
060c3ca
fix clippy-all
ngoldbaum Feb 13, 2026
c1bd2c7
skip test that depend on struct layout on opaque pyobject builds
ngoldbaum Feb 13, 2026
ba8b09a
Expose PyModuleDef as an opaque pointer on opaque PyObject builds
ngoldbaum Feb 16, 2026
f15a7fc
add comments about location of opaque pointers in CPython headers
ngoldbaum Feb 16, 2026
3fa17d0
fix test_inherited_size
ngoldbaum Feb 16, 2026
1970421
Fix doctest on _Py_OPAQUE_PYOBJECT builds
ngoldbaum Feb 17, 2026
57c2045
Merge branch 'main' into opaque-pyobject
ngoldbaum Mar 3, 2026
f80849e
fix build error on non-opaque builds
ngoldbaum Mar 3, 2026
c0805a9
mark BaseWithoutData as subclassable
ngoldbaum Mar 3, 2026
719cef5
relax assert for Windows
ngoldbaum Mar 3, 2026
072ef0a
Make PyAny a PyVarObject only on the opaque PyObject build
ngoldbaum Mar 4, 2026
0b743b6
Merge branch 'main' into opaque-pyobject
ngoldbaum Mar 24, 2026
264307f
fix merge conflict resolution error
ngoldbaum Mar 24, 2026
ed48e75
fix buggy assertion
ngoldbaum Mar 24, 2026
b4138c4
Expose critical section in the limited API starting in Python 3.15
ngoldbaum Mar 24, 2026
f3ee6af
expose critical section API in limited API
ngoldbaum Mar 24, 2026
ba47f50
disable warning in pyo3-ffi build script on sufficiently new Pythons
ngoldbaum Mar 25, 2026
e52cb59
fixup features
ngoldbaum Mar 26, 2026
3cf60b1
Add missing error handling for `PyModule_FromSlotsAndSpec`
ngoldbaum Mar 26, 2026
607e4b0
Merge branch 'main' into expose-critical-section
ngoldbaum Mar 26, 2026
449eb8a
passes cargo tests with the abi3t feature enabled
ngoldbaum Mar 26, 2026
5371fd8
passes unit tests on GIL-enabled build with abi3t feature
ngoldbaum Mar 26, 2026
19d78d2
fix merge mistake
ngoldbaum Mar 26, 2026
14bf4d1
rustfmt
ngoldbaum Mar 27, 2026
91a0419
fix FIXME
ngoldbaum Mar 27, 2026
285e79c
Merge branch 'main' into opaque-pyobject
ngoldbaum Apr 3, 2026
9f23720
replace extern "C" with extern_libpython!
ngoldbaum Apr 3, 2026
d38c274
fix test
ngoldbaum Apr 3, 2026
e2d8f8c
Merge branch 'opaque-pyobject' into expose-critical-section
ngoldbaum Apr 6, 2026
15ff21c
fix merge error
ngoldbaum Apr 6, 2026
930e1a9
Allow abi3t builds without critical section bindings
ngoldbaum Apr 6, 2026
a609f8c
add FIXME
ngoldbaum Apr 6, 2026
dd5b3a9
remove default feature
ngoldbaum Apr 6, 2026
7419b86
fix syntax error in noxfile
ngoldbaum Apr 6, 2026
603aaf9
fix issues spotted by clippy
ngoldbaum Apr 6, 2026
c9b9157
bring over refactoring from PyO3 PR
ngoldbaum Apr 7, 2026
7560348
working!
ngoldbaum Apr 7, 2026
becc8af
Fix memory leak of iterator
ngoldbaum Apr 8, 2026
b734e77
fix size hints
ngoldbaum Apr 8, 2026
a6c8710
delete debug statement
ngoldbaum Apr 8, 2026
a827be9
Use Py_TARGET_ABI3T
ngoldbaum Apr 8, 2026
0ceda48
run formatter
ngoldbaum Apr 8, 2026
d1ef669
fix check-feature-powerset
ngoldbaum Apr 8, 2026
26c7dc0
increment SUPPORTED_VERSIONS_CPYTHON.max
ngoldbaum Apr 8, 2026
95ca7d2
fix conditional compilation for critical section API
ngoldbaum Apr 8, 2026
b596885
fix ruff
ngoldbaum Apr 8, 2026
22a5332
fix incorrect conditional compilation guargs
ngoldbaum Apr 8, 2026
ec1269a
fix noxfile and ban abi3t builds on 3.14 and older
ngoldbaum Apr 8, 2026
04eb8bb
ruff format
ngoldbaum Apr 8, 2026
101949c
attempt to fix semver-checks
ngoldbaum Apr 8, 2026
828afdd
fix test-version-limits
ngoldbaum Apr 8, 2026
cd78153
fix issues spotted by claude
ngoldbaum Apr 8, 2026
5e5671e
strip 't' for version parsing
ngoldbaum Apr 8, 2026
266527f
Adjust comments and error messages
ngoldbaum Apr 8, 2026
294a4f0
fix compiler warning
ngoldbaum Apr 8, 2026
b37445d
more noxfile fixes
ngoldbaum Apr 8, 2026
a2584f5
fix ruff
ngoldbaum Apr 8, 2026
1b5ec7a
use a 3.15 interpreter to run the feature-powerset tests
ngoldbaum Apr 8, 2026
553ef54
fix a few more issues caught by claude
ngoldbaum Apr 8, 2026
2f755ae
add comment
ngoldbaum Apr 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -612,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') }}
Expand Down
9 changes: 8 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,14 @@ 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"]

# 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"]
Expand Down
3 changes: 3 additions & 0 deletions guide/src/python-from-rust/calling-existing-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,13 @@ mod foo {
}
}

# #[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_TARGET_ABI3T)]
# 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`:
Expand Down
131 changes: 99 additions & 32 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ 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")
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")


Expand Down Expand Up @@ -127,15 +127,11 @@ 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-py38")]`)
if feature_set and "abi3" in feature_set and FREE_THREADED_BUILD:
# free-threaded builds don't support abi3 yet
continue

_run_cargo_test(session, features=feature_set, extra_flags=flags)

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)
):
Expand All @@ -146,6 +142,20 @@ def test_rust(session: nox.Session):
extra_flags=flags,
)

if (
feature_set
and "abi3t" in feature_set
and "full" in feature_set
and sys.version_info >= (3, 16)
):
# run abi3t-py315 tests to check for abi3t forward
# compatibility
_run_cargo_test(
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:
Expand Down Expand Up @@ -1128,22 +1138,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")
Expand All @@ -1157,6 +1168,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)
Expand All @@ -1178,7 +1206,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}"
)
Expand Down Expand Up @@ -1342,6 +1370,10 @@ def check_feature_powerset(session: nox.Session):
f"abi3-py3{ver.split('.')[1]}" for ver in ABI3_PY_VERSIONS
}

EXPECTED_ABI3T_FEATURES = {
f"abi3t-py3{ver.split('.')[1].strip('t')}" for ver in ABI3T_PY_VERSIONS
}

EXCLUDED_FROM_FULL = {
"nightly",
"extension-module",
Expand All @@ -1355,20 +1387,37 @@ 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 = abi3t_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:
Expand Down Expand Up @@ -1397,6 +1446,7 @@ def check_feature_powerset(session: nox.Session):
features_to_skip = [
*(EXCLUDED_FROM_FULL),
*abi3_version_features,
*abi3t_version_features,
]

# deny warnings
Expand All @@ -1409,17 +1459,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")
Expand Down Expand Up @@ -1508,6 +1559,22 @@ def _get_feature_sets() -> Tuple[Optional[str], ...]:
if is_rust_nightly():
features += ",nightly"

if FREE_THREADED_BUILD:
if sys.version_info >= (3, 15):
return (None, "abi3t", features, f"abi3t,{features}")
else:
return (None, features)

# do fewer abi3t builds?
if sys.version_info >= (3, 15):
return (
None,
"abi3",
"abi3t",
features,
f"abi3,{features}",
f"abi3t,{features}",
)
return (None, "abi3", features, f"abi3,{features}")


Expand Down
7 changes: 6 additions & 1 deletion pyo3-build-config/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@ 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"]

# 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"]
Loading
Loading