diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38a8e2bddee..956a1883782 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] @@ -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') }} diff --git a/Cargo.toml b/Cargo.toml index f63bf32c5f9..362d3b552f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] diff --git a/guide/src/python-from-rust/calling-existing-code.md b/guide/src/python-from-rust/calling-existing-code.md index 90a49a988a7..576caa49532 100644 --- a/guide/src/python-from-rust/calling-existing-code.md +++ b/guide/src/python-from-rust/calling-existing-code.md @@ -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`: diff --git a/noxfile.py b/noxfile.py index 03f6bcf10be..88a24bfbe53 100644 --- a/noxfile.py +++ b/noxfile.py @@ -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") @@ -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) ): @@ -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: @@ -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") @@ -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) @@ -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}" ) @@ -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", @@ -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: @@ -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 @@ -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") @@ -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}") diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index 14632951863..a9993107695 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -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"] diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 6a090d93b75..1dd4b1bcf67 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 = 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 { + 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 @@ -109,8 +157,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. /// @@ -193,9 +241,21 @@ 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() { - 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::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_TARGET_ABI3T".to_owned()); + } + CPythonABI::VersionSpecific => {} } for flag in &self.build_flags.0 { @@ -309,7 +369,14 @@ 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()?; + + 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()?; @@ -326,7 +393,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 @@ -338,7 +405,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, @@ -365,7 +432,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(), @@ -420,11 +487,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, @@ -438,7 +505,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) implementation, version, shared: shared || framework, - abi3, + stable_abi, lib_dir, lib_name, executable: None, @@ -472,8 +539,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) }) @@ -515,7 +583,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; @@ -540,7 +608,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), @@ -559,14 +627,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, @@ -589,7 +657,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, )); @@ -644,7 +712,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)?; @@ -706,7 +774,32 @@ 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, "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 abi3t; 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, "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, + abi_name: &str, + ) -> Result<()> { + if let Some(version) = abi_version { ensure!( version <= self.version, "cannot set a minimum Python version {} higher than the interpreter version {} \ @@ -715,11 +808,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 {abi_name}-py3{STABLE_ABI_MAX_MINOR} because current Python is higher than the maximum supported"); + self.version.minor = STABLE_ABI_MAX_MINOR; } Ok(()) @@ -845,15 +937,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: @@ -1564,7 +1673,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, @@ -1627,7 +1736,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, @@ -1699,7 +1816,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, @@ -1713,11 +1830,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!( @@ -1743,7 +1864,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, @@ -1752,7 +1873,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 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); @@ -1883,11 +2004,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) } @@ -1901,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 @@ -1915,26 +2041,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 simultaneously enable abi3 and abi3t 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." ); }; @@ -1985,7 +2118,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()), @@ -2006,7 +2139,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); @@ -2036,7 +2169,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()), @@ -2066,7 +2199,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, @@ -2089,7 +2222,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, @@ -2189,7 +2322,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, @@ -2219,7 +2352,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, @@ -2246,7 +2379,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, @@ -2273,7 +2406,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, @@ -2297,7 +2430,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, @@ -2332,7 +2465,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, @@ -2367,7 +2500,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, @@ -2402,7 +2535,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, @@ -2439,7 +2572,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, @@ -2454,12 +2587,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, @@ -2470,7 +2604,7 @@ mod tests { assert!(super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, CPython, - false, + VersionSpecific, false, false, true, @@ -2480,7 +2614,7 @@ mod tests { super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, CPython, - true, + ABI3, false, false, false, @@ -2492,7 +2626,7 @@ mod tests { super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, CPython, - false, + VersionSpecific, true, false, false, @@ -2504,7 +2638,7 @@ mod tests { super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, CPython, - true, + ABI3, true, false, false, @@ -2516,7 +2650,7 @@ mod tests { super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, PyPy, - true, + ABI3, false, false, false, @@ -2531,7 +2665,7 @@ mod tests { minor: 11 }, PyPy, - false, + ABI3, false, false, false, @@ -2543,7 +2677,7 @@ mod tests { super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, CPython, - false, + ABI3, false, true, false, @@ -2557,7 +2691,7 @@ mod tests { super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, CPython, - true, + ABI3, false, true, false, @@ -2572,7 +2706,7 @@ mod tests { minor: 10 }, CPython, - true, + ABI3, false, true, false, @@ -2587,7 +2721,7 @@ mod tests { minor: 12, }, CPython, - false, + VersionSpecific, false, false, true, @@ -2600,7 +2734,7 @@ mod tests { minor: 12, }, CPython, - false, + VersionSpecific, true, false, true, @@ -2613,7 +2747,7 @@ mod tests { minor: 13 }, CPython, - false, + VersionSpecific, false, false, true, @@ -2628,7 +2762,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, @@ -2643,7 +2777,7 @@ mod tests { minor: 13 }, CPython, - false, + VersionSpecific, false, true, true, @@ -2655,13 +2789,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 @@ -2673,7 +2808,7 @@ mod tests { super::default_lib_name_unix( PythonVersion { major: 3, minor: 9 }, CPython, - false, + VersionSpecific, false, None, false @@ -2686,7 +2821,7 @@ mod tests { super::default_lib_name_unix( PythonVersion { major: 3, minor: 9 }, CPython, - false, + VersionSpecific, false, Some("3.8d"), false @@ -2703,7 +2838,7 @@ mod tests { minor: 11 }, PyPy, - false, + VersionSpecific, false, None, false @@ -2716,7 +2851,7 @@ mod tests { super::default_lib_name_unix( PythonVersion { major: 3, minor: 9 }, PyPy, - false, + VersionSpecific, false, Some("3.11d"), false @@ -2733,7 +2868,7 @@ mod tests { minor: 13 }, CPython, - false, + VersionSpecific, false, None, true @@ -2748,7 +2883,7 @@ mod tests { minor: 12, }, CPython, - false, + VersionSpecific, false, None, true, @@ -2762,7 +2897,7 @@ mod tests { minor: 13 }, CPython, - true, + ABI3, true, None, false @@ -2826,7 +2961,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, @@ -2850,7 +2985,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, @@ -2915,7 +3050,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, @@ -3053,7 +3188,7 @@ mod tests { minor: 11, }, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -3095,7 +3230,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, @@ -3128,6 +3263,29 @@ 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_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] @@ -3141,7 +3299,7 @@ mod tests { minor: 13, }, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -3174,7 +3332,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, @@ -3229,7 +3387,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, @@ -3292,7 +3450,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 4e705d2ca02..a64f9332a2e 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_8)]`, `#[cfg(Py_3_9)]`, `#[cfg(Py_3_10)]`, `#[cfg(Py_3_11)]`, ... | These attributes mark code only for a given Python version and up. For example, `#[cfg(Py_3_8)]` marks code which can run on Python 3.8 **and newer**. There is one attribute for each Python version currently supported by PyO3. | -/// | `#[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 feature 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. | @@ -264,6 +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_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\"))"); @@ -272,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 { @@ -315,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`. @@ -494,7 +495,7 @@ mod tests { minor: 13, }, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: None, lib_dir: None, executable: None, @@ -537,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 3e8116a9493..e9aba899864 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -32,7 +32,14 @@ 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"] + +# 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 127874ffa84..7e52735e4cd 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, @@ -18,7 +18,7 @@ const SUPPORTED_VERSIONS_CPYTHON: SupportedVersions = SupportedVersions { min: PythonVersion { major: 3, minor: 8 }, max: PythonVersion { major: 3, - minor: 14, + minor: 15, }, }; @@ -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() { + 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?" ) } } 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/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/dictobject.rs b/pyo3-ffi/src/dictobject.rs index 4afa2ffb5e8..4df1265d4b8 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(all(Py_3_13, any(not(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/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/modsupport.rs b/pyo3-ffi/src/modsupport.rs index 4697be5bb5b..73edc4d75b4 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_TARGET_ABI3T))] +const _PyABIInfo_DEFAULT_FLAG_FT: u16 = PyABIInfo_FREETHREADING_AGNOSTIC; +#[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)))] +#[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/pyo3-ffi/src/moduleobject.rs b/pyo3-ffi/src/moduleobject.rs index 7ba7701aab9..eac9329f5e4 100644 --- a/pyo3-ffi/src/moduleobject.rs +++ b/pyo3-ffi/src/moduleobject.rs @@ -1,3 +1,4 @@ +#[cfg(not(Py_TARGET_ABI3T))] use crate::methodobject::PyMethodDef; use crate::object::*; use crate::pyport::Py_ssize_t; @@ -49,6 +50,7 @@ extern_libpython! { pub static mut PyModuleDef_Type: PyTypeObject; } +#[cfg(not(Py_TARGET_ABI3T))] #[repr(C)] pub struct PyModuleDef_Base { pub ob_base: PyObject, @@ -58,6 +60,7 @@ pub struct PyModuleDef_Base { pub m_copy: *mut PyObject, } +#[cfg(not(Py_TARGET_ABI3T))] #[allow( clippy::declare_interior_mutable_const, reason = "contains atomic refcount on free-threaded builds" @@ -148,6 +151,7 @@ extern_libpython! { pub fn PyModule_GetToken(module: *mut PyObject, result: *mut *mut c_void) -> c_int; } +#[cfg(not(Py_TARGET_ABI3T))] #[repr(C)] pub struct PyModuleDef { pub m_base: PyModuleDef_Base, @@ -161,3 +165,7 @@ pub struct PyModuleDef { pub m_clear: Option, pub m_free: Option, } + +// from pytypedefs.h +#[cfg(Py_TARGET_ABI3T)] +opaque_struct!(pub PyModuleDef); diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index d2a1160cc55..96b325cbb76 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -1,13 +1,16 @@ use crate::pyport::{Py_hash_t, Py_ssize_t}; +#[cfg(not(Py_TARGET_ABI3T))] #[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; +#[cfg(not(Py_TARGET_ABI3T))] #[cfg(Py_GIL_DISABLED)] use std::sync::atomic::{AtomicIsize, AtomicU32}; +// from pytypedefs.h #[cfg(Py_LIMITED_API)] opaque_struct!(pub PyTypeObject); @@ -91,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_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)] @@ -116,8 +120,10 @@ pub struct PyObject { pub ob_type: *mut PyTypeObject, } +#[cfg(not(Py_TARGET_ABI3T))] const _: () = assert!(std::mem::align_of::() >= _PyObject_MIN_ALIGNMENT); +#[cfg(not(Py_TARGET_ABI3T))] #[allow( clippy::declare_interior_mutable_const, reason = "contains atomic refcount on free-threaded builds" @@ -148,10 +154,15 @@ pub const PyObject_HEAD_INIT: PyObject = PyObject { ob_type: std::ptr::null_mut(), }; +// from pytypedefs.h +#[cfg(Py_TARGET_ABI3T)] +opaque_struct!(pub PyObject); + // skipped _Py_UNOWNED_TID // skipped _PyObject_CAST +#[cfg(not(Py_TARGET_ABI3T))] #[repr(C)] #[derive(Debug)] pub struct PyVarObject { @@ -163,6 +174,10 @@ pub struct PyVarObject { pub _ob_size_graalpy: Py_ssize_t, } +// from pytypedefs.h +#[cfg(Py_TARGET_ABI3T)] +opaque_struct!(pub PyVarObject); + // skipped private _PyVarObject_CAST #[inline] @@ -209,6 +224,16 @@ extern_libpython! { pub fn Py_TYPE(ob: *mut PyObject) -> *mut PyTypeObject; } +#[cfg_attr(windows, link(name = "pythonXY"))] +#[cfg(all(Py_LIMITED_API, Py_3_15))] +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")] + pub fn Py_IS_TYPE(ob: *mut PyObject, tp: *mut PyTypeObject) -> c_int; + // skipped Py_SET_SIZE +} + // skip _Py_TYPE compat shim extern_libpython! { @@ -218,6 +243,7 @@ extern_libpython! { 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))] @@ -230,6 +256,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 diff --git a/pyo3-ffi/src/refcount.rs b/pyo3-ffi/src/refcount.rs index d404660b03a..d637e8fa9cf 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_TARGET_ABI3T))] #[cfg(Py_3_12)] #[inline(always)] unsafe fn _Py_IsImmortal(op: *mut PyObject) -> c_int { diff --git a/src/conversions/bytes.rs b/src/conversions/bytes.rs index 78979d6be68..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] @@ -130,6 +132,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/conversions/std/num.rs b/src/conversions/std/num.rs index fb122c279ef..12722e06e2f 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(Py_TARGET_ABI3T))] +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(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)) + } 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(Py_TARGET_ABI3T))] ByteArray(Borrowed<'a, 'py, PyByteArray>), } @@ -285,6 +299,7 @@ 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 @@ -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(Py_TARGET_ABI3T))] BytesSequenceExtractor::ByteArray(b) => b.to_vec(), } } diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index d195050906e..05e79c30ab4 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1424,6 +1424,7 @@ pub trait ExtractPyClassWithClone {} #[cfg(test)] #[cfg(feature = "macros")] mod tests { + #[cfg(not(Py_TARGET_ABI3T))] use crate::pycell::impl_::PyClassObjectContents; use super::*; @@ -1452,17 +1453,23 @@ 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))] #[repr(C)] struct ExpectedLayout { ob_base: ffi::PyObject, contents: PyClassObjectContents, } + #[cfg(not(Py_TARGET_ABI3T))] assert_eq!( member.offset, (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"), } @@ -1574,17 +1581,17 @@ 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!() + #[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!( def.offset, contents_offset + FIELD_OFFSET as ffi::Py_ssize_t ); - assert_eq!(def.flags, ffi::Py_READONLY); + assert_eq!(def.flags & ffi::Py_READONLY, ffi::Py_READONLY); } #[test] diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 4a99b1b6fb9..8ad6088e686 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_TARGET_ABI3T))] ffi_def: UnsafeCell, #[cfg(Py_3_15)] name: &'static CStr, @@ -72,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. - // Opaque PyObject builds won't be able to use this. + #[cfg(not(Py_TARGET_ABI3T))] #[allow(clippy::declare_interior_mutable_const)] const INIT: ffi::PyModuleDef = ffi::PyModuleDef { m_base: ffi::PyModuleDef_HEAD_INIT, @@ -86,6 +91,7 @@ impl ModuleDef { m_free: None, }; + #[cfg(not(Py_TARGET_ABI3T))] let ffi_def = UnsafeCell::new(ffi::PyModuleDef { m_name: name.as_ptr(), m_doc: doc.as_ptr(), @@ -96,6 +102,7 @@ impl ModuleDef { }); ModuleDef { + #[cfg(not(Py_TARGET_ABI3T))] ffi_def, #[cfg(Py_3_15)] name, @@ -114,7 +121,12 @@ impl ModuleDef { } pub fn init_multi_phase(&'static self) -> *mut ffi::PyObject { - unsafe { ffi::PyModuleDef_Init(self.ffi_def.get()) } + #[cfg(not(Py_TARGET_ABI3T))] + unsafe { + ffi::PyModuleDef_Init(self.ffi_def.get()) + } + #[cfg(Py_TARGET_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. @@ -500,6 +512,7 @@ mod tests { let module_def: ModuleDef = ModuleDef::new(NAME, DOC, &SLOTS); + #[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 d7feaa2bfa2..ae7b99a1d91 100644 --- a/src/pybacked.rs +++ b/src/pybacked.rs @@ -4,14 +4,17 @@ 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::{ - 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(Py_TARGET_ABI3T))] +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 +192,7 @@ pub struct PyBackedBytes { #[cfg_attr(feature = "py-clone", derive(Clone))] enum PyBackedBytesStorage { Python(Py), + #[cfg(not(Py_TARGET_ABI3T))] Rust(Arc<[u8]>), } @@ -202,6 +206,7 @@ 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, @@ -265,6 +270,7 @@ 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()); @@ -283,23 +289,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(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(), + )) + } } } } @@ -315,6 +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(Py_TARGET_ABI3T))] PyBackedBytesStorage::Rust(bytes) => Ok(PyBytes::new(py, &bytes)), } } @@ -331,6 +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(Py_TARGET_ABI3T))] PyBackedBytesStorage::Rust(bytes) => Ok(PyBytes::new(py, bytes)), } } @@ -496,6 +520,7 @@ mod test { } #[test] + #[cfg(not(Py_TARGET_ABI3T))] fn py_backed_bytes_from_bytearray() { Python::attach(|py| { let b = PyByteArray::new(py, b"abcde"); @@ -517,6 +542,7 @@ 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"); @@ -668,6 +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(Py_TARGET_ABI3T, allow(irrefutable_let_patterns))] let (PyBackedBytesStorage::Python(s1), PyBackedBytesStorage::Python(s2)) = (&b1.storage, &b2.storage) else { @@ -681,6 +708,7 @@ mod test { } #[cfg(feature = "py-clone")] + #[cfg(not(Py_TARGET_ABI3T))] #[test] fn test_backed_bytes_from_bytearray_clone() { Python::attach(|py| { @@ -693,6 +721,7 @@ mod test { }); } + #[cfg(not(Py_TARGET_ABI3T))] #[test] fn test_backed_bytes_from_bytearray_clone_ref() { Python::attach(|py| { @@ -712,6 +741,7 @@ 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(); @@ -741,16 +771,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(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() + }; - assert_eq!(h, h1); - assert_eq!(h, h2); + assert_eq!(h, h2); + } }); } diff --git a/src/pycell/impl_.rs b/src/pycell/impl_.rs index 8fbad2a4943..aea9900a1e1 100644 --- a/src/pycell/impl_.rs +++ b/src/pycell/impl_.rs @@ -624,6 +624,9 @@ mod tests { #[pyclass(crate = "crate", extends = ImmutableChildOfImmutableBase, frozen)] struct ImmutableChildOfImmutableChildOfImmutableBase; + #[pyclass(crate = "crate", subclass)] + struct BaseWithoutData; + #[pyclass(crate = "crate", subclass)] struct BaseWithData(#[allow(unused)] u64); @@ -635,13 +638,32 @@ mod tests { #[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); + #[cfg(Py_TARGET_ABI3T)] + type ClassObject = PyVariableClassObject; + #[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_TARGET_ABI3T)] + { + 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_TARGET_ABI3T))] + { + 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>() {} diff --git a/src/sync.rs b/src/sync.rs index cb71ccd10f5..06e8326b15b 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(Py_TARGET_ABI3T))] +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(Py_TARGET_ABI3T))] 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(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 278be0da12a..06ff92cdd51 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(Py_TARGET_ABI3T))] use crate::{types::PyAny, Bound}; #[cfg(all(Py_3_14, not(Py_LIMITED_API)))] use std::cell::UnsafeCell; -#[cfg(Py_GIL_DISABLED)] +#[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API)))] struct CSGuard(crate::ffi::PyCriticalSection); -#[cfg(Py_GIL_DISABLED)] +#[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(Py_GIL_DISABLED)] +#[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API)))] struct CS2Guard(crate::ffi::PyCriticalSection2); -#[cfg(Py_GIL_DISABLED)] +#[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_TARGET_ABI3T))] #[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_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 @@ -268,30 +271,40 @@ 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(feature = "macros")] + #[cfg(all(not(Py_TARGET_ABI3T), feature = "macros"))] use std::sync::atomic::{AtomicBool, Ordering}; - #[cfg(any(feature = "macros", all(not(Py_LIMITED_API), Py_3_14)))] + #[cfg(all( + not(Py_TARGET_ABI3T), + any(feature = "macros", all(not(Py_LIMITED_API), Py_3_14)) + ))] use std::sync::Barrier; - #[cfg(feature = "macros")] + #[cfg(all(not(Py_TARGET_ABI3T), feature = "macros"))] use crate::Py; - #[cfg(any(feature = "macros", all(not(Py_LIMITED_API), Py_3_14)))] + #[cfg(all( + not(Py_TARGET_ABI3T), + 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); @@ -357,6 +370,7 @@ mod tests { #[cfg(feature = "macros")] #[test] + #[cfg(not(Py_TARGET_ABI3T))] fn test_critical_section2() { let barrier = Barrier::new(3); @@ -439,6 +453,7 @@ mod tests { #[cfg(feature = "macros")] #[test] + #[cfg(not(Py_TARGET_ABI3T))] 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(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 8923f429efa..d9dd344a808 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -4,7 +4,8 @@ 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_TARGET_ABI3T))] +use crate::impl_::pycell::{PyClassObjectBase, PyStaticClassObject}; use crate::instance::Bound; use crate::internal::get_slot::TP_DESCR_GET; use crate::py_result_ext::PyResultExt; @@ -51,15 +52,20 @@ 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 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_TARGET_ABI3T))] impl crate::impl_::pyclass::PyClassBaseType for PyAny { - type LayoutAsBase = crate::impl_::pycell::PyClassObjectBase; + type LayoutAsBase = PyClassObjectBase; type BaseNativeType = PyAny; type Initializer = crate::impl_::pyclass_init::PyNativeTypeInitializer; type PyClassMutability = crate::pycell::impl_::ImmutableClass; type Layout = PyStaticClassObject; } +#[cfg(Py_TARGET_ABI3T)] +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 diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index bbcaee28da5..6d403d4d8ed 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(Py_TARGET_ABI3T))] 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(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, || { @@ -155,6 +160,9 @@ 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)?; @@ -236,6 +244,7 @@ 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`. @@ -268,6 +277,7 @@ 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: @@ -358,6 +368,7 @@ mod tests { } #[test] + #[cfg(not(Py_TARGET_ABI3T))] 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(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/code.rs b/src/types/code.rs index 8d38a8fc826..fdb2fe4383d 100644 --- a/src/types/code.rs +++ b/src/types/code.rs @@ -127,28 +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__"); - 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())); - } - } - Ok(()) - })?; + 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 + { + 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()) diff --git a/src/types/dict.rs b/src/types/dict.rs index 0f03a217f79..d828011917b 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(Py_TARGET_ABI3T))] 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(Py_TARGET_ABI3T))] fn locked_for_each(&self, f: F) -> PyResult<()> where F: Fn(Bound<'py, PyAny>, Bound<'py, PyAny>) -> PyResult<()>, @@ -418,12 +420,27 @@ 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] @@ -437,7 +454,10 @@ impl DictIterImpl { Self::DictIter { di_used, remaining, + #[cfg(not(Py_TARGET_ABI3T))] ppos, + #[cfg(Py_TARGET_ABI3T)] + iter, .. } => { let ma_used = dict_len(dict); @@ -466,26 +486,55 @@ impl DictIterImpl { 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(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(); - // 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), + }; + // 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?, + Err(e) => { + panic!("Iterating over dictionary failed with error '{}'", e) + } + }; + Some((key, value)) } } } } + #[cfg(not(Py_TARGET_ABI3T))] #[cfg(Py_GIL_DISABLED)] #[inline] fn with_critical_section(&mut self, dict: &Bound<'_, PyDict>, f: F) -> R @@ -505,6 +554,7 @@ impl<'py> Iterator for BoundDictIterator<'py> { #[inline] fn next(&mut self) -> Option { + #[cfg(not(Py_TARGET_ABI3T))] #[cfg(Py_GIL_DISABLED)] { self.inner @@ -512,8 +562,12 @@ impl<'py> Iterator for BoundDictIterator<'py> { inner.next_unchecked(&self.dict) }) } - #[cfg(not(Py_GIL_DISABLED))] + #[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. Iteration + // uses an iterator object, relying on CPython guarantees + // that the PyDict and PyIter APIs are safe. unsafe { self.inner.next_unchecked(&self.dict) } } } @@ -534,6 +588,7 @@ 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, @@ -566,6 +621,7 @@ 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 @@ -583,6 +639,7 @@ 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 @@ -600,6 +657,7 @@ 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 @@ -617,6 +675,7 @@ 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 @@ -634,6 +693,7 @@ 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 @@ -663,14 +723,26 @@ impl ExactSizeIterator for BoundDictIterator<'_> { impl<'py> BoundDictIterator<'py> { fn new(dict: Bound<'py, PyDict>) -> Self { - let remaining = dict_len(&dict); - + 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, - remaining, + di_used, + remaining: di_used, + #[cfg(Py_TARGET_ABI3T)] + iter, }, } } @@ -824,7 +896,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] diff --git a/src/types/list.rs b/src/types/list.rs index 5d8b29bb310..ce5ef7b68ab 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(Py_TARGET_ABI3T))] 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(Py_TARGET_ABI3T))] 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(Py_TARGET_ABI3T))] fn nth( index: &mut Index, length: &mut Length, @@ -588,6 +591,7 @@ impl<'py> BoundListIterator<'py> { #[inline] #[cfg(not(feature = "nightly"))] + #[cfg(not(Py_TARGET_ABI3T))] fn nth_back( index: &mut Index, length: &mut Length, @@ -616,6 +620,7 @@ 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, @@ -653,6 +658,7 @@ 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)) } @@ -680,7 +686,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 +702,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 +719,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 +736,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 +753,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 +770,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 +787,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, @@ -848,12 +854,13 @@ 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)) } #[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 +876,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, diff --git a/tests/test_append_to_inittab.rs b/tests/test_append_to_inittab.rs index e147967a0c7..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)))] +#[cfg(not(any(PyPy, GraalPy, Py_TARGET_ABI3T)))] #[test] fn test_module_append_to_inittab() { use pyo3::append_to_inittab;