diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9c11ee27422..a9f7db7c850 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -132,6 +132,11 @@ jobs: name: Build PyPy (abi3-py39) run: cargo build --lib --tests --no-default-features --features "multiple-pymethods abi3-py39 full $MAYBE_NIGHTLY" + - name: Run pyo3-ffi-check + # pypy 3.9 on windows is not PEP 3123 compliant, nor is graalpy + if: ${{ endsWith(inputs.python-version, '-dev') || (steps.ffi-changes.outputs.changed == 'true' && inputs.rust == 'stable' && !startsWith(inputs.python-version, 'graalpy') && !(inputs.python-version == 'pypy3.9' && contains(inputs.os, 'windows'))) }} + run: nox -s ffi-check + # Run tests (except on PyPy, because no embedding API). - if: ${{ !startsWith(inputs.python-version, 'pypy') && !startsWith(inputs.python-version, 'graalpy') }} name: Test @@ -161,6 +166,7 @@ jobs: - name: Test python examples and tests shell: bash run: nox -s test-py + continue-on-error: ${{ endsWith(inputs.python-version, '-dev') }} env: CARGO_TARGET_DIR: ${{ github.workspace }}/target @@ -177,11 +183,6 @@ jobs: - '.github/workflows/ci.yml' - '.github/workflows/build.yml' - - name: Run pyo3-ffi-check - # pypy 3.9 on windows is not PEP 3123 compliant, nor is graalpy - if: ${{ endsWith(inputs.python-version, '-dev') || (steps.ffi-changes.outputs.changed == 'true' && inputs.rust == 'stable' && !startsWith(inputs.python-version, 'graalpy') && !(inputs.python-version == 'pypy3.9' && contains(inputs.os, 'windows'))) }} - run: nox -s ffi-check - - if: ${{ github.event_name != 'merge_group' }} name: Generate coverage report run: cargo llvm-cov diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c8bdce4d67..645416215f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -264,6 +264,8 @@ jobs: "3.12", "3.13", "3.13t", + "3.14-dev", + "3.14t-dev", "pypy3.9", "pypy3.10", "pypy3.11", @@ -528,8 +530,8 @@ jobs: components: rust-src - name: Install python3 standalone debug build with nox run: | - PBS_RELEASE="20241016" - PBS_PYTHON_VERSION="3.13.0" + PBS_RELEASE="20241219" + PBS_PYTHON_VERSION="3.13.1" PBS_ARCHIVE="cpython-${PBS_PYTHON_VERSION}+${PBS_RELEASE}-x86_64-unknown-linux-gnu-debug-full.tar.zst" wget "https://github.com/indygreg/python-build-standalone/releases/download/${PBS_RELEASE}/${PBS_ARCHIVE}" tar -I zstd -xf "${PBS_ARCHIVE}" diff --git a/Cargo.toml b/Cargo.toml index da573adbd68..b704ae368a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,7 +103,8 @@ abi3-py39 = ["abi3-py310", "pyo3-build-config/abi3-py39", "pyo3-ffi/abi3-py39"] 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", "pyo3-build-config/abi3-py313", "pyo3-ffi/abi3-py313"] +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"] # Automatically generates `python3.dll` import libraries for Windows targets. generate-import-lib = ["pyo3-ffi/generate-import-lib"] diff --git a/newsfragments/4811.fixed.md b/newsfragments/4811.fixed.md new file mode 100644 index 00000000000..cad2a26146b --- /dev/null +++ b/newsfragments/4811.fixed.md @@ -0,0 +1 @@ +Bump supported cpython version to 3.14 for testing diff --git a/noxfile.py b/noxfile.py index 6e180eaf999..ceefab43912 100644 --- a/noxfile.py +++ b/noxfile.py @@ -708,11 +708,11 @@ def test_version_limits(session: nox.Session): config_file.set("CPython", "3.6") _run_cargo(session, "check", env=env, expect_error=True) - assert "3.14" not in PY_VERSIONS - config_file.set("CPython", "3.14") + assert "3.15" not in PY_VERSIONS + config_file.set("CPython", "3.15") _run_cargo(session, "check", env=env, expect_error=True) - # 3.14 CPython should build with forward compatibility + # 3.15 CPython should build with forward compatibility env["PYO3_USE_ABI3_FORWARD_COMPATIBILITY"] = "1" _run_cargo(session, "check", env=env) diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index 86456e33cb5..b2bdc760687 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -38,7 +38,8 @@ abi3-py39 = ["abi3-py310"] abi3-py310 = ["abi3-py311"] abi3-py311 = ["abi3-py312"] abi3-py312 = ["abi3-py313"] -abi3-py313 = ["abi3"] +abi3-py313 = ["abi3-py314"] +abi3-py314 = ["abi3"] [package.metadata.docs.rs] features = ["resolve-config"] diff --git a/pyo3-ffi-check/build.rs b/pyo3-ffi-check/build.rs index 67808888e1e..e7cfbe40df3 100644 --- a/pyo3-ffi-check/build.rs +++ b/pyo3-ffi-check/build.rs @@ -1,6 +1,24 @@ use std::env; use std::path::PathBuf; +#[derive(Debug)] +struct ParseCallbacks; + +impl bindgen::callbacks::ParseCallbacks for ParseCallbacks { + // these are anonymous fields and structs in CPython that we needed to + // invent names for. Bindgen seems to generate stable names, so we remap the + // automatically generated names to the names we invented in the FFI + fn item_name(&self, _original_item_name: &str) -> Option { + if _original_item_name == "_object__bindgen_ty_1__bindgen_ty_1" { + Some("PyObjectObFlagsAndRefcnt".into()) + } else if _original_item_name == "_object__bindgen_ty_1" { + Some("PyObjectObRefcnt".into()) + } else { + None + } + } +} + fn main() { let config = pyo3_build_config::get(); let python_include_dir = config @@ -29,6 +47,7 @@ fn main() { .header("wrapper.h") .clang_args(clang_args) .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + .parse_callbacks(Box::new(ParseCallbacks)) // blocklist some values which apparently have conflicting definitions on unix .blocklist_item("FP_NORMAL") .blocklist_item("FP_SUBNORMAL") diff --git a/pyo3-ffi/Cargo.toml b/pyo3-ffi/Cargo.toml index 8b75c978c2e..e4f4498780d 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -34,7 +34,8 @@ abi3-py39 = ["abi3-py310", "pyo3-build-config/abi3-py39"] 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", "pyo3-build-config/abi3-py313"] +abi3-py313 = ["abi3-py314", "pyo3-build-config/abi3-py313"] +abi3-py314 = ["abi3", "pyo3-build-config/abi3-py314"] # Automatically generates `python3.dll` import libraries for Windows targets. generate-import-lib = ["pyo3-build-config/python3-dll-a"] @@ -50,7 +51,7 @@ workspace = true [package.metadata.cpython] min-version = "3.7" -max-version = "3.13" # inclusive +max-version = "3.14" # inclusive [package.metadata.pypy] min-version = "3.9" diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 096614c7961..da5a81381c3 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -17,7 +17,7 @@ const SUPPORTED_VERSIONS_CPYTHON: SupportedVersions = SupportedVersions { min: PythonVersion { major: 3, minor: 7 }, max: PythonVersion { major: 3, - minor: 13, + minor: 14, }, }; diff --git a/pyo3-ffi/src/abstract_.rs b/pyo3-ffi/src/abstract_.rs index a79ec43f271..84fb98a1b4e 100644 --- a/pyo3-ffi/src/abstract_.rs +++ b/pyo3-ffi/src/abstract_.rs @@ -25,6 +25,7 @@ pub unsafe fn PyObject_DelAttr(o: *mut PyObject, attr_name: *mut PyObject) -> c_ extern "C" { #[cfg(all( not(PyPy), + not(GraalPy), any(Py_3_10, all(not(Py_LIMITED_API), Py_3_9)) // Added to python in 3.9 but to limited API in 3.10 ))] #[cfg_attr(PyPy, link_name = "PyPyObject_CallNoArgs")] diff --git a/pyo3-ffi/src/cpython/dictobject.rs b/pyo3-ffi/src/cpython/dictobject.rs index 93ce560c573..34b66a9699d 100644 --- a/pyo3-ffi/src/cpython/dictobject.rs +++ b/pyo3-ffi/src/cpython/dictobject.rs @@ -17,7 +17,10 @@ pub struct PyDictObject { Py_3_12, deprecated(note = "Deprecated in Python 3.12 and will be removed in the future.") )] + #[cfg(not(Py_3_14))] pub ma_version_tag: u64, + #[cfg(Py_3_14)] + _ma_watcher_tag: u64, pub ma_keys: *mut PyDictKeysObject, #[cfg(not(Py_3_11))] pub ma_values: *mut *mut PyObject, diff --git a/pyo3-ffi/src/cpython/funcobject.rs b/pyo3-ffi/src/cpython/funcobject.rs index 25de30d57f7..cd2052de174 100644 --- a/pyo3-ffi/src/cpython/funcobject.rs +++ b/pyo3-ffi/src/cpython/funcobject.rs @@ -41,6 +41,8 @@ pub struct PyFunctionObject { pub func_weakreflist: *mut PyObject, pub func_module: *mut PyObject, pub func_annotations: *mut PyObject, + #[cfg(Py_3_14)] + pub func_annotate: *mut PyObject, #[cfg(Py_3_12)] pub func_typeparams: *mut PyObject, pub vectorcall: Option, diff --git a/pyo3-ffi/src/cpython/genobject.rs b/pyo3-ffi/src/cpython/genobject.rs index 51c80f1a212..d8973b8676b 100644 --- a/pyo3-ffi/src/cpython/genobject.rs +++ b/pyo3-ffi/src/cpython/genobject.rs @@ -1,11 +1,11 @@ use crate::object::*; use crate::PyFrameObject; -#[cfg(all(Py_3_11, not(any(PyPy, GraalPy))))] +#[cfg(all(Py_3_11, not(any(PyPy, GraalPy, Py_3_14))))] use std::os::raw::c_char; use std::os::raw::c_int; use std::ptr::addr_of_mut; -#[cfg(not(any(PyPy, GraalPy)))] +#[cfg(not(any(PyPy, GraalPy, Py_3_14)))] #[repr(C)] pub struct PyGenObject { pub ob_base: PyObject, @@ -33,6 +33,9 @@ pub struct PyGenObject { pub gi_iframe: [*mut PyObject; 1], } +#[cfg(all(Py_3_14, not(any(PyPy, GraalPy))))] +opaque_struct!(pub PyGenObject); + #[cfg_attr(windows, link(name = "pythonXY"))] extern "C" { pub static mut PyGen_Type: PyTypeObject; diff --git a/pyo3-ffi/src/cpython/initconfig.rs b/pyo3-ffi/src/cpython/initconfig.rs index 321d200e141..2864ebc6218 100644 --- a/pyo3-ffi/src/cpython/initconfig.rs +++ b/pyo3-ffi/src/cpython/initconfig.rs @@ -93,6 +93,8 @@ pub struct PyConfig { pub tracemalloc: c_int, #[cfg(Py_3_12)] pub perf_profiling: c_int, + #[cfg(Py_3_14)] + pub remote_debug: c_int, pub import_time: c_int, #[cfg(Py_3_11)] pub code_debug_ranges: c_int, @@ -141,10 +143,19 @@ pub struct PyConfig { pub safe_path: c_int, #[cfg(Py_3_12)] pub int_max_str_digits: c_int, + // TODO: uncomment for 3.14.0b1 + // #[cfg(Py_3_14)] + // pub thread_inherit_context: c_int, + // #[cfg(Py_3_14)] + // pub context_aware_warnings: c_int, + #[cfg(all(Py_3_14, target_os = "macos"))] + pub use_system_logger: c_int, #[cfg(Py_3_13)] pub cpu_count: c_int, #[cfg(Py_GIL_DISABLED)] pub enable_gil: c_int, + #[cfg(all(Py_3_14, Py_GIL_DISABLED))] + pub tlbc_enabled: c_int, pub pathconfig_warnings: c_int, #[cfg(Py_3_10)] pub program_name: *mut wchar_t, diff --git a/pyo3-ffi/src/cpython/object.rs b/pyo3-ffi/src/cpython/object.rs index 4e6932da789..26ef784dde1 100644 --- a/pyo3-ffi/src/cpython/object.rs +++ b/pyo3-ffi/src/cpython/object.rs @@ -312,8 +312,12 @@ pub struct PyHeapTypeObject { pub ht_module: *mut object::PyObject, #[cfg(all(Py_3_11, not(PyPy)))] _ht_tpname: *mut c_char, + #[cfg(Py_3_14)] + pub ht_token: *mut c_void, #[cfg(all(Py_3_11, not(PyPy)))] _spec_cache: _specialization_cache, + #[cfg(all(Py_GIL_DISABLED, Py_3_14))] + pub unique_id: Py_ssize_t, } impl Default for PyHeapTypeObject { diff --git a/pyo3-ffi/src/cpython/pyerrors.rs b/pyo3-ffi/src/cpython/pyerrors.rs index c6e10e5f07b..d4ff17381b0 100644 --- a/pyo3-ffi/src/cpython/pyerrors.rs +++ b/pyo3-ffi/src/cpython/pyerrors.rs @@ -46,6 +46,9 @@ pub struct PySyntaxErrorObject { pub end_offset: *mut PyObject, pub text: *mut PyObject, pub print_file_and_line: *mut PyObject, + // TODO: uncomment for 3.14.0b1 + // #[cfg(Py_3_14)] + // pub metadata: *mut PyObject, } #[cfg(not(any(PyPy, GraalPy)))] diff --git a/pyo3-ffi/src/cpython/tupleobject.rs b/pyo3-ffi/src/cpython/tupleobject.rs index 9616d4372cc..dc1bf8e40d0 100644 --- a/pyo3-ffi/src/cpython/tupleobject.rs +++ b/pyo3-ffi/src/cpython/tupleobject.rs @@ -1,10 +1,14 @@ use crate::object::*; +#[cfg(Py_3_14)] +use crate::pyport::Py_hash_t; #[cfg(not(PyPy))] use crate::pyport::Py_ssize_t; #[repr(C)] pub struct PyTupleObject { pub ob_base: PyVarObject, + #[cfg(Py_3_14)] + pub ob_hash: Py_hash_t, pub ob_item: [*mut PyObject; 1], } diff --git a/pyo3-ffi/src/cpython/unicodeobject.rs b/pyo3-ffi/src/cpython/unicodeobject.rs index 3527a5aeadb..51317bf0bdd 100644 --- a/pyo3-ffi/src/cpython/unicodeobject.rs +++ b/pyo3-ffi/src/cpython/unicodeobject.rs @@ -2,6 +2,8 @@ use crate::Py_hash_t; use crate::{PyObject, Py_UCS1, Py_UCS2, Py_UCS4, Py_ssize_t}; use libc::wchar_t; +#[cfg(Py_3_14)] +use std::os::raw::c_ushort; use std::os::raw::{c_char, c_int, c_uint, c_void}; // skipped Py_UNICODE_ISSPACE() @@ -119,8 +121,10 @@ where #[cfg(not(GraalPy))] const STATE_INTERNED_INDEX: usize = 0; -#[cfg(not(GraalPy))] +#[cfg(all(not(GraalPy), not(Py_3_14)))] const STATE_INTERNED_WIDTH: u8 = 2; +#[cfg(all(not(GraalPy), Py_3_14))] +const STATE_INTERNED_WIDTH: u8 = 16; #[cfg(not(GraalPy))] const STATE_KIND_INDEX: usize = STATE_INTERNED_WIDTH as usize; @@ -138,6 +142,12 @@ const STATE_ASCII_INDEX: usize = #[cfg(not(GraalPy))] const STATE_ASCII_WIDTH: u8 = 1; +#[cfg(all(not(GraalPy), Py_3_12))] +const STATE_STATICALLY_ALLOCATED_INDEX: usize = + (STATE_INTERNED_WIDTH + STATE_KIND_WIDTH + STATE_COMPACT_WIDTH + STATE_ASCII_WIDTH) as usize; +#[cfg(all(not(GraalPy), Py_3_12))] +const STATE_STATICALLY_ALLOCATED_WIDTH: u8 = 1; + #[cfg(not(any(Py_3_12, GraalPy)))] const STATE_READY_INDEX: usize = (STATE_INTERNED_WIDTH + STATE_KIND_WIDTH + STATE_COMPACT_WIDTH + STATE_ASCII_WIDTH) as usize; @@ -165,6 +175,7 @@ struct PyASCIIObjectState { #[allow(clippy::useless_transmute)] impl PyASCIIObjectState { #[inline] + #[cfg(not(Py_3_14))] unsafe fn interned(&self) -> c_uint { std::mem::transmute( self.bitfield @@ -173,6 +184,7 @@ impl PyASCIIObjectState { } #[inline] + #[cfg(not(Py_3_14))] unsafe fn set_interned(&mut self, val: c_uint) { let val: u32 = std::mem::transmute(val); self.bitfield @@ -180,11 +192,36 @@ impl PyASCIIObjectState { } #[inline] + #[cfg(Py_3_14)] + unsafe fn interned(&self) -> u16 { + std::mem::transmute( + self.bitfield + .get(STATE_INTERNED_INDEX, STATE_INTERNED_WIDTH) as u16, + ) + } + + #[inline] + #[cfg(Py_3_14)] + unsafe fn set_interned(&mut self, val: u16) { + let val: u16 = std::mem::transmute(val); + self.bitfield + .set(STATE_INTERNED_INDEX, STATE_INTERNED_WIDTH, val as u64) + } + + #[inline] + #[cfg(not(Py_3_14))] unsafe fn kind(&self) -> c_uint { std::mem::transmute(self.bitfield.get(STATE_KIND_INDEX, STATE_KIND_WIDTH) as u32) } #[inline] + #[cfg(Py_3_14)] + unsafe fn kind(&self) -> c_ushort { + std::mem::transmute(self.bitfield.get(STATE_KIND_INDEX, STATE_KIND_WIDTH) as c_ushort) + } + + #[inline] + #[cfg(not(Py_3_14))] unsafe fn set_kind(&mut self, val: c_uint) { let val: u32 = std::mem::transmute(val); self.bitfield @@ -192,11 +229,27 @@ impl PyASCIIObjectState { } #[inline] + #[cfg(Py_3_14)] + unsafe fn set_kind(&mut self, val: c_ushort) { + let val: c_ushort = std::mem::transmute(val); + self.bitfield + .set(STATE_KIND_INDEX, STATE_KIND_WIDTH, val as u64) + } + + #[inline] + #[cfg(not(Py_3_14))] unsafe fn compact(&self) -> c_uint { std::mem::transmute(self.bitfield.get(STATE_COMPACT_INDEX, STATE_COMPACT_WIDTH) as u32) } #[inline] + #[cfg(Py_3_14)] + unsafe fn compact(&self) -> c_ushort { + std::mem::transmute(self.bitfield.get(STATE_COMPACT_INDEX, STATE_COMPACT_WIDTH) as c_ushort) + } + + #[inline] + #[cfg(not(Py_3_14))] unsafe fn set_compact(&mut self, val: c_uint) { let val: u32 = std::mem::transmute(val); self.bitfield @@ -204,17 +257,81 @@ impl PyASCIIObjectState { } #[inline] + #[cfg(Py_3_14)] + unsafe fn set_compact(&mut self, val: c_ushort) { + let val: c_ushort = std::mem::transmute(val); + self.bitfield + .set(STATE_COMPACT_INDEX, STATE_COMPACT_WIDTH, val as u64) + } + + #[inline] + #[cfg(not(Py_3_14))] unsafe fn ascii(&self) -> c_uint { std::mem::transmute(self.bitfield.get(STATE_ASCII_INDEX, STATE_ASCII_WIDTH) as u32) } #[inline] + #[cfg(not(Py_3_14))] unsafe fn set_ascii(&mut self, val: c_uint) { let val: u32 = std::mem::transmute(val); self.bitfield .set(STATE_ASCII_INDEX, STATE_ASCII_WIDTH, val as u64) } + #[inline] + #[cfg(Py_3_14)] + unsafe fn ascii(&self) -> c_ushort { + std::mem::transmute(self.bitfield.get(STATE_ASCII_INDEX, STATE_ASCII_WIDTH) as c_ushort) + } + + #[inline] + #[cfg(Py_3_14)] + unsafe fn set_ascii(&mut self, val: c_ushort) { + let val: c_ushort = std::mem::transmute(val); + self.bitfield + .set(STATE_ASCII_INDEX, STATE_ASCII_WIDTH, val as u64) + } + + #[cfg(all(Py_3_12, not(Py_3_14)))] + #[inline] + unsafe fn statically_allocated(&self) -> c_uint { + std::mem::transmute(self.bitfield.get( + STATE_STATICALLY_ALLOCATED_INDEX, + STATE_STATICALLY_ALLOCATED_WIDTH, + ) as u32) + } + + #[cfg(all(Py_3_12, not(Py_3_14)))] + #[inline] + unsafe fn set_statically_allocated(&mut self, val: c_uint) { + let val: u32 = std::mem::transmute(val); + self.bitfield.set( + STATE_STATICALLY_ALLOCATED_INDEX, + STATE_STATICALLY_ALLOCATED_WIDTH, + val as u64, + ) + } + + #[inline] + #[cfg(Py_3_14)] + unsafe fn statically_allocated(&self) -> c_ushort { + std::mem::transmute(self.bitfield.get( + STATE_STATICALLY_ALLOCATED_INDEX, + STATE_STATICALLY_ALLOCATED_WIDTH, + ) as c_ushort) + } + + #[inline] + #[cfg(Py_3_14)] + unsafe fn set_statically_allocated(&mut self, val: c_ushort) { + let val: c_ushort = std::mem::transmute(val); + self.bitfield.set( + STATE_STATICALLY_ALLOCATED_INDEX, + STATE_STATICALLY_ALLOCATED_WIDTH, + val as u64, + ) + } + #[cfg(not(Py_3_12))] #[inline] unsafe fn ready(&self) -> c_uint { @@ -258,12 +375,29 @@ pub struct PyASCIIObject { /// Rust doesn't expose bitfields. So we have accessor functions for /// retrieving values. /// + /// Before 3.12: /// unsigned int interned:2; // SSTATE_* constants. /// unsigned int kind:3; // PyUnicode_*_KIND constants. /// unsigned int compact:1; /// unsigned int ascii:1; /// unsigned int ready:1; /// unsigned int :24; + /// + /// 3.12 and 3.13: + /// unsigned int interned:2; // SSTATE_* constants. + /// unsigned int kind:3; // PyUnicode_*_KIND constants. + /// unsigned int compact:1; + /// unsigned int ascii:1; + /// unsigned int statically_allocated:1; + /// unsigned int :24; + /// + /// 3.14 and later: + /// uint16_t interned; // SSTATE_* constants. + /// unsigned short kind:3; // PyUnicode_*_KIND constants. + /// unsigned short compact:1; + /// unsigned short ascii:1; + /// unsigned int statically_allocated:1; + /// unsigned int :10; pub state: u32, #[cfg(not(Py_3_12))] pub wstr: *mut wchar_t, @@ -278,6 +412,7 @@ impl PyASCIIObject { /// Returns one of: [`SSTATE_NOT_INTERNED`], [`SSTATE_INTERNED_MORTAL`], /// [`SSTATE_INTERNED_IMMORTAL`], or [`SSTATE_INTERNED_IMMORTAL_STATIC`]. #[inline] + #[cfg(not(Py_3_14))] pub unsafe fn interned(&self) -> c_uint { PyASCIIObjectState::from(self.state).interned() } @@ -289,56 +424,131 @@ impl PyASCIIObject { /// [`SSTATE_INTERNED_MORTAL`], [`SSTATE_INTERNED_IMMORTAL`], or /// [`SSTATE_INTERNED_IMMORTAL_STATIC`] is invalid. #[inline] + #[cfg(not(Py_3_14))] pub unsafe fn set_interned(&mut self, val: c_uint) { let mut state = PyASCIIObjectState::from(self.state); state.set_interned(val); self.state = u32::from(state); } + #[cfg_attr(not(Py_3_12), allow(rustdoc::broken_intra_doc_links))] // SSTATE_INTERNED_IMMORTAL_STATIC requires 3.12 + /// Get the `interned` field of the [`PyASCIIObject`] state bitfield. + /// + /// Returns one of: [`SSTATE_NOT_INTERNED`], [`SSTATE_INTERNED_MORTAL`], + /// [`SSTATE_INTERNED_IMMORTAL`], or [`SSTATE_INTERNED_IMMORTAL_STATIC`]. + #[inline] + #[cfg(Py_3_14)] + pub unsafe fn interned(&self) -> u16 { + PyASCIIObjectState::from(self.state).interned() + } + + #[cfg_attr(not(Py_3_12), allow(rustdoc::broken_intra_doc_links))] // SSTATE_INTERNED_IMMORTAL_STATIC requires 3.12 + /// Set the `interned` field of the [`PyASCIIObject`] state bitfield. + /// + /// Calling this function with an argument that is not [`SSTATE_NOT_INTERNED`], + /// [`SSTATE_INTERNED_MORTAL`], [`SSTATE_INTERNED_IMMORTAL`], or + /// [`SSTATE_INTERNED_IMMORTAL_STATIC`] is invalid. + #[inline] + #[cfg(Py_3_14)] + pub unsafe fn set_interned(&mut self, val: u16) { + let mut state = PyASCIIObjectState::from(self.state); + state.set_interned(val); + self.state = u32::from(state); + } + /// Get the `kind` field of the [`PyASCIIObject`] state bitfield. /// /// Returns one of: #[cfg_attr(not(Py_3_12), doc = "[`PyUnicode_WCHAR_KIND`], ")] /// [`PyUnicode_1BYTE_KIND`], [`PyUnicode_2BYTE_KIND`], or [`PyUnicode_4BYTE_KIND`]. #[inline] + #[cfg(not(Py_3_14))] pub unsafe fn kind(&self) -> c_uint { PyASCIIObjectState::from(self.state).kind() } + /// Get the `kind` field of the [`PyASCIIObject`] state bitfield. + /// + /// Returns one of: + #[cfg_attr(not(Py_3_12), doc = "[`PyUnicode_WCHAR_KIND`], ")] + /// [`PyUnicode_1BYTE_KIND`], [`PyUnicode_2BYTE_KIND`], or [`PyUnicode_4BYTE_KIND`]. + #[inline] + #[cfg(Py_3_14)] + pub unsafe fn kind(&self) -> c_ushort { + PyASCIIObjectState::from(self.state).kind() + } + /// Set the `kind` field of the [`PyASCIIObject`] state bitfield. /// /// Calling this function with an argument that is not #[cfg_attr(not(Py_3_12), doc = "[`PyUnicode_WCHAR_KIND`], ")] /// [`PyUnicode_1BYTE_KIND`], [`PyUnicode_2BYTE_KIND`], or [`PyUnicode_4BYTE_KIND`] is invalid. #[inline] + #[cfg(not(Py_3_14))] pub unsafe fn set_kind(&mut self, val: c_uint) { let mut state = PyASCIIObjectState::from(self.state); state.set_kind(val); self.state = u32::from(state); } + /// Set the `kind` field of the [`PyASCIIObject`] state bitfield. + /// + /// Calling this function with an argument that is not + #[cfg_attr(not(Py_3_12), doc = "[`PyUnicode_WCHAR_KIND`], ")] + /// [`PyUnicode_1BYTE_KIND`], [`PyUnicode_2BYTE_KIND`], or [`PyUnicode_4BYTE_KIND`] is invalid. + #[inline] + #[cfg(Py_3_14)] + pub unsafe fn set_kind(&mut self, val: c_ushort) { + let mut state = PyASCIIObjectState::from(self.state); + state.set_kind(val); + self.state = u32::from(state); + } + /// Get the `compact` field of the [`PyASCIIObject`] state bitfield. /// /// Returns either `0` or `1`. #[inline] + #[cfg(not(Py_3_14))] pub unsafe fn compact(&self) -> c_uint { PyASCIIObjectState::from(self.state).compact() } + /// Get the `compact` field of the [`PyASCIIObject`] state bitfield. + /// + /// Returns either `0` or `1`. + #[inline] + #[cfg(Py_3_14)] + pub unsafe fn compact(&self) -> c_ushort { + PyASCIIObjectState::from(self.state).compact() + } + /// Set the `compact` flag of the [`PyASCIIObject`] state bitfield. /// /// Calling this function with an argument that is neither `0` nor `1` is invalid. #[inline] + #[cfg(not(Py_3_14))] pub unsafe fn set_compact(&mut self, val: c_uint) { let mut state = PyASCIIObjectState::from(self.state); state.set_compact(val); self.state = u32::from(state); } + /// Set the `compact` flag of the [`PyASCIIObject`] state bitfield. + /// + /// Calling this function with an argument that is neither `0` nor `1` is invalid. + #[inline] + #[cfg(Py_3_14)] + pub unsafe fn set_compact(&mut self, val: c_ushort) { + let mut state = PyASCIIObjectState::from(self.state); + state.set_compact(val); + self.state = u32::from(state); + } + /// Get the `ascii` field of the [`PyASCIIObject`] state bitfield. /// /// Returns either `0` or `1`. #[inline] + #[cfg(not(Py_3_14))] pub unsafe fn ascii(&self) -> c_uint { PyASCIIObjectState::from(self.state).ascii() } @@ -347,12 +557,33 @@ impl PyASCIIObject { /// /// Calling this function with an argument that is neither `0` nor `1` is invalid. #[inline] + #[cfg(not(Py_3_14))] pub unsafe fn set_ascii(&mut self, val: c_uint) { let mut state = PyASCIIObjectState::from(self.state); state.set_ascii(val); self.state = u32::from(state); } + /// Get the `ascii` field of the [`PyASCIIObject`] state bitfield. + /// + /// Returns either `0` or `1`. + #[inline] + #[cfg(Py_3_14)] + pub unsafe fn ascii(&self) -> c_ushort { + PyASCIIObjectState::from(self.state).ascii() + } + + /// Set the `ascii` flag of the [`PyASCIIObject`] state bitfield. + /// + /// Calling this function with an argument that is neither `0` nor `1` is invalid. + #[inline] + #[cfg(Py_3_14)] + pub unsafe fn set_ascii(&mut self, val: c_ushort) { + let mut state = PyASCIIObjectState::from(self.state); + state.set_ascii(val); + self.state = u32::from(state); + } + /// Get the `ready` field of the [`PyASCIIObject`] state bitfield. /// /// Returns either `0` or `1`. @@ -372,6 +603,46 @@ impl PyASCIIObject { state.set_ready(val); self.state = u32::from(state); } + + /// Get the `statically_allocated` field of the [`PyASCIIObject`] state bitfield. + /// + /// Returns either `0` or `1`. + #[inline] + #[cfg(all(Py_3_12, not(Py_3_14)))] + pub unsafe fn statically_allocated(&self) -> c_uint { + PyASCIIObjectState::from(self.state).statically_allocated() + } + + /// Set the `statically_allocated` flag of the [`PyASCIIObject`] state bitfield. + /// + /// Calling this function with an argument that is neither `0` nor `1` is invalid. + #[inline] + #[cfg(all(Py_3_12, not(Py_3_14)))] + pub unsafe fn set_statically_allocated(&mut self, val: c_uint) { + let mut state = PyASCIIObjectState::from(self.state); + state.set_statically_allocated(val); + self.state = u32::from(state); + } + + /// Get the `statically_allocated` field of the [`PyASCIIObject`] state bitfield. + /// + /// Returns either `0` or `1`. + #[inline] + #[cfg(Py_3_14)] + pub unsafe fn statically_allocated(&self) -> c_ushort { + PyASCIIObjectState::from(self.state).statically_allocated() + } + + /// Set the `statically_allocated` flag of the [`PyASCIIObject`] state bitfield. + /// + /// Calling this function with an argument that is neither `0` nor `1` is invalid. + #[inline] + #[cfg(Py_3_14)] + pub unsafe fn set_statically_allocated(&mut self, val: c_ushort) { + let mut state = PyASCIIObjectState::from(self.state); + state.set_statically_allocated(val); + self.state = u32::from(state); + } } #[repr(C)] @@ -413,7 +684,7 @@ pub const SSTATE_INTERNED_IMMORTAL: c_uint = 2; #[cfg(Py_3_12)] pub const SSTATE_INTERNED_IMMORTAL_STATIC: c_uint = 3; -#[cfg(not(GraalPy))] +#[cfg(all(not(GraalPy), not(Py_3_14)))] #[inline] pub unsafe fn PyUnicode_IS_ASCII(op: *mut PyObject) -> c_uint { debug_assert!(crate::PyUnicode_Check(op) != 0); @@ -423,12 +694,28 @@ pub unsafe fn PyUnicode_IS_ASCII(op: *mut PyObject) -> c_uint { (*(op as *mut PyASCIIObject)).ascii() } -#[cfg(not(GraalPy))] +#[cfg(all(not(GraalPy), not(Py_3_14)))] #[inline] pub unsafe fn PyUnicode_IS_COMPACT(op: *mut PyObject) -> c_uint { (*(op as *mut PyASCIIObject)).compact() } +#[cfg(all(not(GraalPy), Py_3_14))] +#[inline] +pub unsafe fn PyUnicode_IS_ASCII(op: *mut PyObject) -> c_ushort { + debug_assert!(crate::PyUnicode_Check(op) != 0); + #[cfg(not(Py_3_12))] + debug_assert!(PyUnicode_IS_READY(op) != 0); + + (*(op as *mut PyASCIIObject)).ascii() +} + +#[cfg(all(not(GraalPy), Py_3_14))] +#[inline] +pub unsafe fn PyUnicode_IS_COMPACT(op: *mut PyObject) -> c_ushort { + (*(op as *mut PyASCIIObject)).compact() +} + #[cfg(not(GraalPy))] #[inline] pub unsafe fn PyUnicode_IS_COMPACT_ASCII(op: *mut PyObject) -> c_uint { @@ -439,10 +726,20 @@ pub unsafe fn PyUnicode_IS_COMPACT_ASCII(op: *mut PyObject) -> c_uint { #[deprecated(note = "Removed in Python 3.12")] pub const PyUnicode_WCHAR_KIND: c_uint = 0; +#[cfg(not(Py_3_14))] pub const PyUnicode_1BYTE_KIND: c_uint = 1; +#[cfg(not(Py_3_14))] pub const PyUnicode_2BYTE_KIND: c_uint = 2; +#[cfg(not(Py_3_14))] pub const PyUnicode_4BYTE_KIND: c_uint = 4; +#[cfg(Py_3_14)] +pub const PyUnicode_1BYTE_KIND: c_ushort = 1; +#[cfg(Py_3_14)] +pub const PyUnicode_2BYTE_KIND: c_ushort = 2; +#[cfg(Py_3_14)] +pub const PyUnicode_4BYTE_KIND: c_ushort = 4; + #[cfg(not(any(GraalPy, PyPy)))] #[inline] pub unsafe fn PyUnicode_1BYTE_DATA(op: *mut PyObject) -> *mut Py_UCS1 { @@ -461,7 +758,7 @@ pub unsafe fn PyUnicode_4BYTE_DATA(op: *mut PyObject) -> *mut Py_UCS4 { PyUnicode_DATA(op) as *mut Py_UCS4 } -#[cfg(not(GraalPy))] +#[cfg(all(not(GraalPy), not(Py_3_14)))] #[inline] pub unsafe fn PyUnicode_KIND(op: *mut PyObject) -> c_uint { debug_assert!(crate::PyUnicode_Check(op) != 0); @@ -471,6 +768,16 @@ pub unsafe fn PyUnicode_KIND(op: *mut PyObject) -> c_uint { (*(op as *mut PyASCIIObject)).kind() } +#[cfg(all(not(GraalPy), Py_3_14))] +#[inline] +pub unsafe fn PyUnicode_KIND(op: *mut PyObject) -> c_ushort { + debug_assert!(crate::PyUnicode_Check(op) != 0); + #[cfg(not(Py_3_12))] + debug_assert!(PyUnicode_IS_READY(op) != 0); + + (*(op as *mut PyASCIIObject)).kind() +} + #[cfg(not(GraalPy))] #[inline] pub unsafe fn _PyUnicode_COMPACT_DATA(op: *mut PyObject) -> *mut c_void { diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index 9b98d08c3c5..f78c918a8c5 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -448,6 +448,7 @@ pub use self::pystate::*; pub use self::pystrtod::*; pub use self::pythonrun::*; pub use self::rangeobject::*; +pub use self::refcount::*; pub use self::setobject::*; pub use self::sliceobject::*; pub use self::structseq::*; @@ -540,6 +541,7 @@ mod pystrtod; // skipped pythread.h // skipped pytime.h mod rangeobject; +mod refcount; mod setobject; mod sliceobject; mod structseq; diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index 38f79ba111a..5fbf45db617 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -1,5 +1,7 @@ use crate::pyport::{Py_hash_t, Py_ssize_t}; #[cfg(Py_GIL_DISABLED)] +use crate::refcount; +#[cfg(Py_GIL_DISABLED)] use crate::PyMutex; #[cfg(Py_GIL_DISABLED)] use std::marker::PhantomPinned; @@ -7,7 +9,7 @@ use std::mem; use std::os::raw::{c_char, c_int, c_uint, c_ulong, c_void}; use std::ptr; #[cfg(Py_GIL_DISABLED)] -use std::sync::atomic::{AtomicIsize, AtomicU32, AtomicU8, Ordering::Relaxed}; +use std::sync::atomic::{AtomicIsize, AtomicU32, AtomicU8}; #[cfg(Py_LIMITED_API)] opaque_struct!(pub PyTypeObject); @@ -15,73 +17,42 @@ opaque_struct!(pub PyTypeObject); #[cfg(not(Py_LIMITED_API))] pub use crate::cpython::object::PyTypeObject; -#[cfg(Py_3_12)] -const _Py_IMMORTAL_REFCNT: Py_ssize_t = { - if cfg!(target_pointer_width = "64") { - c_uint::MAX as Py_ssize_t - } else { - // for 32-bit systems, use the lower 30 bits (see comment in CPython's object.h) - (c_uint::MAX >> 2) as Py_ssize_t - } -}; - -#[cfg(Py_GIL_DISABLED)] -const _Py_IMMORTAL_REFCNT_LOCAL: u32 = u32::MAX; - -#[allow(clippy::declare_interior_mutable_const)] -pub const PyObject_HEAD_INIT: PyObject = PyObject { - #[cfg(py_sys_config = "Py_TRACE_REFS")] - _ob_next: std::ptr::null_mut(), - #[cfg(py_sys_config = "Py_TRACE_REFS")] - _ob_prev: std::ptr::null_mut(), - #[cfg(Py_GIL_DISABLED)] - ob_tid: 0, - #[cfg(Py_GIL_DISABLED)] - _padding: 0, - #[cfg(Py_GIL_DISABLED)] - ob_mutex: PyMutex { - _bits: AtomicU8::new(0), - _pin: PhantomPinned, - }, - #[cfg(Py_GIL_DISABLED)] - ob_gc_bits: 0, - #[cfg(Py_GIL_DISABLED)] - ob_ref_local: AtomicU32::new(_Py_IMMORTAL_REFCNT_LOCAL), - #[cfg(Py_GIL_DISABLED)] - ob_ref_shared: AtomicIsize::new(0), - #[cfg(all(not(Py_GIL_DISABLED), Py_3_12))] - ob_refcnt: PyObjectObRefcnt { ob_refcnt: 1 }, - #[cfg(not(Py_3_12))] - ob_refcnt: 1, - #[cfg(PyPy)] - ob_pypy_link: 0, - ob_type: std::ptr::null_mut(), -}; - -// skipped PyObject_VAR_HEAD -// skipped Py_INVALID_SIZE - -// skipped private _Py_UNOWNED_TID +// skip PyObject_HEAD -#[cfg(Py_GIL_DISABLED)] -const _Py_REF_SHARED_SHIFT: isize = 2; -// skipped private _Py_REF_SHARED_FLAG_MASK - -// skipped private _Py_REF_SHARED_INIT -// skipped private _Py_REF_MAYBE_WEAKREF -// skipped private _Py_REF_QUEUED -// skipped private _Py_REF_MERGED +#[repr(C)] +#[derive(Copy, Clone)] +#[cfg(all(Py_3_14, not(Py_GIL_DISABLED), target_endian = "big"))] +/// This struct is anonymous in CPython, so the name was given by PyO3 because +/// Rust structs need a name. +pub struct PyObjectObFlagsAndRefcnt { + pub ob_flags: u16, + pub ob_overflow: u16, + pub ob_refcnt: u32, +} -// skipped private _Py_REF_SHARED +#[repr(C)] +#[derive(Copy, Clone)] +#[cfg(all(Py_3_14, not(Py_GIL_DISABLED), target_endian = "little"))] +/// This struct is anonymous in CPython, so the name was given by PyO3 because +/// Rust structs need a name. +pub struct PyObjectObFlagsAndRefcnt { + pub ob_refcnt: u32, + pub ob_overflow: u16, + pub ob_flags: u16, +} #[repr(C)] #[derive(Copy, Clone)] #[cfg(all(Py_3_12, not(Py_GIL_DISABLED)))] /// This union is anonymous in CPython, so the name was given by PyO3 because -/// Rust unions need a name. +/// Rust union need a name. pub union PyObjectObRefcnt { + #[cfg(all(target_pointer_width = "64", Py_3_14))] + pub ob_refcnt_full: crate::PY_INT64_T, + #[cfg(Py_3_14)] + pub refcnt_and_flags: PyObjectObFlagsAndRefcnt, pub ob_refcnt: Py_ssize_t, - #[cfg(target_pointer_width = "64")] + #[cfg(all(target_pointer_width = "64", not(Py_3_14)))] pub ob_refcnt_split: [crate::PY_UINT32_T; 2], } @@ -95,6 +66,9 @@ impl std::fmt::Debug for PyObjectObRefcnt { #[cfg(all(not(Py_3_12), not(Py_GIL_DISABLED)))] pub type PyObjectObRefcnt = Py_ssize_t; +// PyObject_HEAD_INIT comes before the PyObject definition in object.h +// but we put it after PyObject because HEAD_INIT uses PyObject + #[repr(C)] #[derive(Debug)] pub struct PyObject { @@ -104,8 +78,10 @@ pub struct PyObject { pub _ob_prev: *mut PyObject, #[cfg(Py_GIL_DISABLED)] pub ob_tid: libc::uintptr_t, - #[cfg(Py_GIL_DISABLED)] + #[cfg(all(Py_GIL_DISABLED, not(Py_3_14)))] pub _padding: u16, + #[cfg(all(Py_GIL_DISABLED, Py_3_14))] + pub ob_flags: u16, #[cfg(Py_GIL_DISABLED)] pub ob_mutex: PyMutex, // per-object lock #[cfg(Py_GIL_DISABLED)] @@ -121,7 +97,41 @@ pub struct PyObject { pub ob_type: *mut PyTypeObject, } -// skipped private _PyObject_CAST +#[allow(clippy::declare_interior_mutable_const)] +pub const PyObject_HEAD_INIT: PyObject = PyObject { + #[cfg(py_sys_config = "Py_TRACE_REFS")] + _ob_next: std::ptr::null_mut(), + #[cfg(py_sys_config = "Py_TRACE_REFS")] + _ob_prev: std::ptr::null_mut(), + #[cfg(Py_GIL_DISABLED)] + ob_tid: 0, + #[cfg(all(Py_GIL_DISABLED, Py_3_14))] + ob_flags: 0, + #[cfg(all(Py_GIL_DISABLED, not(Py_3_14)))] + _padding: 0, + #[cfg(Py_GIL_DISABLED)] + ob_mutex: PyMutex { + _bits: AtomicU8::new(0), + _pin: PhantomPinned, + }, + #[cfg(Py_GIL_DISABLED)] + ob_gc_bits: 0, + #[cfg(Py_GIL_DISABLED)] + ob_ref_local: AtomicU32::new(refcount::_Py_IMMORTAL_REFCNT_LOCAL), + #[cfg(Py_GIL_DISABLED)] + ob_ref_shared: AtomicIsize::new(0), + #[cfg(all(not(Py_GIL_DISABLED), Py_3_12))] + ob_refcnt: PyObjectObRefcnt { ob_refcnt: 1 }, + #[cfg(not(Py_3_12))] + ob_refcnt: 1, + #[cfg(PyPy)] + ob_pypy_link: 0, + ob_type: std::ptr::null_mut(), +}; + +// skipped _Py_UNOWNED_TID + +// skipped _PyObject_CAST #[repr(C)] #[derive(Debug)] @@ -150,41 +160,23 @@ extern "C" { pub fn Py_Is(x: *mut PyObject, y: *mut PyObject) -> c_int; } -// skipped private _Py_GetThreadLocal_Addr +// skipped _Py_GetThreadLocal_Addr -// skipped private _Py_ThreadId +// skipped _Py_ThreadID -// skipped private _Py_IsOwnedByCurrentThread +// skipped _Py_IsOwnedByCurrentThread -#[inline] -pub unsafe fn Py_REFCNT(ob: *mut PyObject) -> Py_ssize_t { - #[cfg(Py_GIL_DISABLED)] - { - let local = (*ob).ob_ref_local.load(Relaxed); - if local == _Py_IMMORTAL_REFCNT_LOCAL { - return _Py_IMMORTAL_REFCNT; - } - let shared = (*ob).ob_ref_shared.load(Relaxed); - local as Py_ssize_t + Py_ssize_t::from(shared >> _Py_REF_SHARED_SHIFT) - } - - #[cfg(all(not(Py_GIL_DISABLED), Py_3_12))] - { - (*ob).ob_refcnt.ob_refcnt - } - - #[cfg(all(not(Py_GIL_DISABLED), not(Py_3_12), not(GraalPy)))] - { - (*ob).ob_refcnt - } +#[cfg(GraalPy)] +extern "C" { + #[cfg(GraalPy)] + fn _Py_TYPE(arg1: *const PyObject) -> *mut PyTypeObject; - #[cfg(all(not(Py_GIL_DISABLED), not(Py_3_12), GraalPy))] - { - _Py_REFCNT(ob) - } + #[cfg(GraalPy)] + fn _Py_SIZE(arg1: *const PyObject) -> Py_ssize_t; } #[inline] +#[cfg(not(Py_3_14))] pub unsafe fn Py_TYPE(ob: *mut PyObject) -> *mut PyTypeObject { #[cfg(not(GraalPy))] return (*ob).ob_type; @@ -192,6 +184,15 @@ pub unsafe fn Py_TYPE(ob: *mut PyObject) -> *mut PyTypeObject { return _Py_TYPE(ob); } +#[cfg_attr(windows, link(name = "pythonXY"))] +#[cfg(Py_3_14)] +extern "C" { + #[cfg_attr(PyPy, link_name = "PyPy_TYPE")] + pub fn Py_TYPE(ob: *mut PyObject) -> *mut PyTypeObject; +} + +// skip _Py_TYPE compat shim + #[cfg_attr(windows, link(name = "pythonXY"))] extern "C" { #[cfg_attr(PyPy, link_name = "PyPyLong_Type")] @@ -212,29 +213,11 @@ pub unsafe fn Py_SIZE(ob: *mut PyObject) -> Py_ssize_t { _Py_SIZE(ob) } -#[inline(always)] -#[cfg(all(Py_3_12, not(Py_GIL_DISABLED)))] -unsafe fn _Py_IsImmortal(op: *mut PyObject) -> c_int { - #[cfg(target_pointer_width = "64")] - { - (((*op).ob_refcnt.ob_refcnt as crate::PY_INT32_T) < 0) as c_int - } - - #[cfg(target_pointer_width = "32")] - { - ((*op).ob_refcnt.ob_refcnt == _Py_IMMORTAL_REFCNT) as c_int - } -} - #[inline] pub unsafe fn Py_IS_TYPE(ob: *mut PyObject, tp: *mut PyTypeObject) -> c_int { (Py_TYPE(ob) == tp) as c_int } -// skipped _Py_SetRefCnt - -// skipped Py_SET_REFCNT - // skipped Py_SET_TYPE // skipped Py_SET_SIZE @@ -586,222 +569,6 @@ pub const Py_TPFLAGS_DEFAULT: c_ulong = if cfg!(Py_3_10) { pub const Py_TPFLAGS_HAVE_FINALIZE: c_ulong = 1; pub const Py_TPFLAGS_HAVE_VERSION_TAG: c_ulong = 1 << 18; -extern "C" { - #[cfg(all(py_sys_config = "Py_REF_DEBUG", not(Py_LIMITED_API)))] - fn _Py_NegativeRefcount(filename: *const c_char, lineno: c_int, op: *mut PyObject); - #[cfg(all(Py_3_12, py_sys_config = "Py_REF_DEBUG", not(Py_LIMITED_API)))] - fn _Py_INCREF_IncRefTotal(); - #[cfg(all(Py_3_12, py_sys_config = "Py_REF_DEBUG", not(Py_LIMITED_API)))] - fn _Py_DECREF_DecRefTotal(); - - #[cfg_attr(PyPy, link_name = "_PyPy_Dealloc")] - fn _Py_Dealloc(arg1: *mut PyObject); - - #[cfg_attr(PyPy, link_name = "PyPy_IncRef")] - #[cfg_attr(GraalPy, link_name = "_Py_IncRef")] - pub fn Py_IncRef(o: *mut PyObject); - #[cfg_attr(PyPy, link_name = "PyPy_DecRef")] - #[cfg_attr(GraalPy, link_name = "_Py_DecRef")] - pub fn Py_DecRef(o: *mut PyObject); - - #[cfg(all(Py_3_10, not(PyPy)))] - fn _Py_IncRef(o: *mut PyObject); - #[cfg(all(Py_3_10, not(PyPy)))] - fn _Py_DecRef(o: *mut PyObject); - - #[cfg(GraalPy)] - fn _Py_REFCNT(arg1: *const PyObject) -> Py_ssize_t; - - #[cfg(GraalPy)] - fn _Py_TYPE(arg1: *const PyObject) -> *mut PyTypeObject; - - #[cfg(GraalPy)] - fn _Py_SIZE(arg1: *const PyObject) -> Py_ssize_t; -} - -#[inline(always)] -pub unsafe fn Py_INCREF(op: *mut PyObject) { - // On limited API, the free-threaded build, or with refcount debugging, let the interpreter do refcounting - // TODO: reimplement the logic in the header in the free-threaded build, for a little bit of performance. - #[cfg(any( - Py_GIL_DISABLED, - Py_LIMITED_API, - py_sys_config = "Py_REF_DEBUG", - GraalPy - ))] - { - // _Py_IncRef was added to the ABI in 3.10; skips null checks - #[cfg(all(Py_3_10, not(PyPy)))] - { - _Py_IncRef(op); - } - - #[cfg(any(not(Py_3_10), PyPy))] - { - Py_IncRef(op); - } - } - - // version-specific builds are allowed to directly manipulate the reference count - #[cfg(not(any( - Py_GIL_DISABLED, - Py_LIMITED_API, - py_sys_config = "Py_REF_DEBUG", - GraalPy - )))] - { - #[cfg(all(Py_3_12, target_pointer_width = "64"))] - { - let cur_refcnt = (*op).ob_refcnt.ob_refcnt_split[crate::PY_BIG_ENDIAN]; - let new_refcnt = cur_refcnt.wrapping_add(1); - if new_refcnt == 0 { - return; - } - (*op).ob_refcnt.ob_refcnt_split[crate::PY_BIG_ENDIAN] = new_refcnt; - } - - #[cfg(all(Py_3_12, target_pointer_width = "32"))] - { - if _Py_IsImmortal(op) != 0 { - return; - } - (*op).ob_refcnt.ob_refcnt += 1 - } - - #[cfg(not(Py_3_12))] - { - (*op).ob_refcnt += 1 - } - - // Skipped _Py_INCREF_STAT_INC - if anyone wants this, please file an issue - // or submit a PR supporting Py_STATS build option and pystats.h - } -} - -#[inline(always)] -#[cfg_attr( - all(py_sys_config = "Py_REF_DEBUG", Py_3_12, not(Py_LIMITED_API)), - track_caller -)] -pub unsafe fn Py_DECREF(op: *mut PyObject) { - // On limited API, the free-threaded build, or with refcount debugging, let the interpreter do refcounting - // On 3.12+ we implement refcount debugging to get better assertion locations on negative refcounts - // TODO: reimplement the logic in the header in the free-threaded build, for a little bit of performance. - #[cfg(any( - Py_GIL_DISABLED, - Py_LIMITED_API, - all(py_sys_config = "Py_REF_DEBUG", not(Py_3_12)), - GraalPy - ))] - { - // _Py_DecRef was added to the ABI in 3.10; skips null checks - #[cfg(all(Py_3_10, not(PyPy)))] - { - _Py_DecRef(op); - } - - #[cfg(any(not(Py_3_10), PyPy))] - { - Py_DecRef(op); - } - } - - #[cfg(not(any( - Py_GIL_DISABLED, - Py_LIMITED_API, - all(py_sys_config = "Py_REF_DEBUG", not(Py_3_12)), - GraalPy - )))] - { - #[cfg(Py_3_12)] - if _Py_IsImmortal(op) != 0 { - return; - } - - // Skipped _Py_DECREF_STAT_INC - if anyone needs this, please file an issue - // or submit a PR supporting Py_STATS build option and pystats.h - - #[cfg(py_sys_config = "Py_REF_DEBUG")] - _Py_DECREF_DecRefTotal(); - - #[cfg(Py_3_12)] - { - (*op).ob_refcnt.ob_refcnt -= 1; - - #[cfg(py_sys_config = "Py_REF_DEBUG")] - if (*op).ob_refcnt.ob_refcnt < 0 { - let location = std::panic::Location::caller(); - let filename = std::ffi::CString::new(location.file()).unwrap(); - _Py_NegativeRefcount(filename.as_ptr(), location.line() as i32, op); - } - - if (*op).ob_refcnt.ob_refcnt == 0 { - _Py_Dealloc(op); - } - } - - #[cfg(not(Py_3_12))] - { - (*op).ob_refcnt -= 1; - - if (*op).ob_refcnt == 0 { - _Py_Dealloc(op); - } - } - } -} - -#[inline] -pub unsafe fn Py_CLEAR(op: *mut *mut PyObject) { - let tmp = *op; - if !tmp.is_null() { - *op = ptr::null_mut(); - Py_DECREF(tmp); - } -} - -#[inline] -pub unsafe fn Py_XINCREF(op: *mut PyObject) { - if !op.is_null() { - Py_INCREF(op) - } -} - -#[inline] -pub unsafe fn Py_XDECREF(op: *mut PyObject) { - if !op.is_null() { - Py_DECREF(op) - } -} - -extern "C" { - #[cfg(all(Py_3_10, Py_LIMITED_API, not(PyPy)))] - #[cfg_attr(docsrs, doc(cfg(Py_3_10)))] - pub fn Py_NewRef(obj: *mut PyObject) -> *mut PyObject; - #[cfg(all(Py_3_10, Py_LIMITED_API, not(PyPy)))] - #[cfg_attr(docsrs, doc(cfg(Py_3_10)))] - pub fn Py_XNewRef(obj: *mut PyObject) -> *mut PyObject; -} - -// macro _Py_NewRef not public; reimplemented directly inside Py_NewRef here -// macro _Py_XNewRef not public; reimplemented directly inside Py_XNewRef here - -#[cfg(all(Py_3_10, any(not(Py_LIMITED_API), PyPy)))] -#[cfg_attr(docsrs, doc(cfg(Py_3_10)))] -#[inline] -pub unsafe fn Py_NewRef(obj: *mut PyObject) -> *mut PyObject { - Py_INCREF(obj); - obj -} - -#[cfg(all(Py_3_10, any(not(Py_LIMITED_API), PyPy)))] -#[cfg_attr(docsrs, doc(cfg(Py_3_10)))] -#[inline] -pub unsafe fn Py_XNewRef(obj: *mut PyObject) -> *mut PyObject { - Py_XINCREF(obj); - obj -} - #[cfg(Py_3_13)] pub const Py_CONSTANT_NONE: c_uint = 0; #[cfg(Py_3_13)] @@ -942,4 +709,7 @@ extern "C" { arg1: *mut crate::PyTypeObject, arg2: *mut crate::PyModuleDef, ) -> *mut PyObject; + + #[cfg(Py_3_14)] + pub fn PyType_Freeze(tp: *mut crate::PyTypeObject) -> c_int; } diff --git a/pyo3-ffi/src/pystate.rs b/pyo3-ffi/src/pystate.rs index e97cca94ed5..fceb8c6c399 100644 --- a/pyo3-ffi/src/pystate.rs +++ b/pyo3-ffi/src/pystate.rs @@ -80,8 +80,10 @@ pub enum PyGILState_STATE { PyGILState_UNLOCKED, } +#[cfg(not(Py_3_14))] struct HangThread; +#[cfg(not(Py_3_14))] impl Drop for HangThread { fn drop(&mut self) { loop { diff --git a/pyo3-ffi/src/refcount.rs b/pyo3-ffi/src/refcount.rs new file mode 100644 index 00000000000..fcb5f45be6a --- /dev/null +++ b/pyo3-ffi/src/refcount.rs @@ -0,0 +1,369 @@ +use crate::pyport::Py_ssize_t; +use crate::PyObject; +#[cfg(py_sys_config = "Py_REF_DEBUG")] +use std::os::raw::c_char; +#[cfg(Py_3_12)] +use std::os::raw::c_int; +#[cfg(all(Py_3_14, any(not(Py_GIL_DISABLED), target_pointer_width = "32")))] +use std::os::raw::c_long; +#[cfg(any(Py_GIL_DISABLED, all(Py_3_12, not(Py_3_14))))] +use std::os::raw::c_uint; +#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))] +use std::os::raw::c_ulong; +use std::ptr; +#[cfg(Py_GIL_DISABLED)] +use std::sync::atomic::Ordering::Relaxed; + +#[cfg(Py_3_14)] +const _Py_STATICALLY_ALLOCATED_FLAG: c_int = 1 << 7; + +#[cfg(all(Py_3_12, not(Py_3_14)))] +const _Py_IMMORTAL_REFCNT: Py_ssize_t = { + if cfg!(target_pointer_width = "64") { + c_uint::MAX as Py_ssize_t + } else { + // for 32-bit systems, use the lower 30 bits (see comment in CPython's object.h) + (c_uint::MAX >> 2) as Py_ssize_t + } +}; + +// comments in Python.h about the choices for these constants + +#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))] +const _Py_IMMORTAL_INITIAL_REFCNT: Py_ssize_t = { + if cfg!(target_pointer_width = "64") { + ((3 as c_ulong) << (30 as c_ulong)) as Py_ssize_t + } else { + ((5 as c_long) << (28 as c_long)) as Py_ssize_t + } +}; + +#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))] +const _Py_STATIC_IMMORTAL_INITIAL_REFCNT: Py_ssize_t = { + if cfg!(target_pointer_width = "64") { + _Py_IMMORTAL_INITIAL_REFCNT + | ((_Py_STATICALLY_ALLOCATED_FLAG as Py_ssize_t) << (32 as Py_ssize_t)) + } else { + ((7 as c_long) << (28 as c_long)) as Py_ssize_t + } +}; + +#[cfg(all(Py_3_14, target_pointer_width = "32"))] +const _Py_IMMORTAL_MINIMUM_REFCNT: Py_ssize_t = ((1 as c_long) << (30 as c_long)) as Py_ssize_t; + +#[cfg(all(Py_3_14, target_pointer_width = "32"))] +const _Py_STATIC_IMMORTAL_MINIMUM_REFCNT: Py_ssize_t = + ((6 as c_long) << (28 as c_long)) as Py_ssize_t; + +#[cfg(all(Py_3_14, Py_GIL_DISABLED))] +const _Py_IMMORTAL_INITIAL_REFCNT: Py_ssize_t = c_uint::MAX as Py_ssize_t; + +#[cfg(Py_GIL_DISABLED)] +pub(crate) const _Py_IMMORTAL_REFCNT_LOCAL: u32 = u32::MAX; + +#[cfg(Py_GIL_DISABLED)] +const _Py_REF_SHARED_SHIFT: isize = 2; +// skipped private _Py_REF_SHARED_FLAG_MASK + +// skipped private _Py_REF_SHARED_INIT +// skipped private _Py_REF_MAYBE_WEAKREF +// skipped private _Py_REF_QUEUED +// skipped private _Py_REF_MERGED + +// skipped private _Py_REF_SHARED + +extern "C" { + #[cfg(all(Py_3_14, Py_LIMITED_API))] + pub fn Py_REFCNT(ob: *mut PyObject) -> Py_ssize_t; +} + +#[cfg(not(all(Py_3_14, Py_LIMITED_API)))] +#[inline] +pub unsafe fn Py_REFCNT(ob: *mut PyObject) -> Py_ssize_t { + #[cfg(Py_GIL_DISABLED)] + { + let local = (*ob).ob_ref_local.load(Relaxed); + if local == _Py_IMMORTAL_REFCNT_LOCAL { + #[cfg(not(Py_3_14))] + return _Py_IMMORTAL_REFCNT; + #[cfg(Py_3_14)] + return _Py_IMMORTAL_INITIAL_REFCNT; + } + let shared = (*ob).ob_ref_shared.load(Relaxed); + local as Py_ssize_t + Py_ssize_t::from(shared >> _Py_REF_SHARED_SHIFT) + } + + #[cfg(all(Py_LIMITED_API, Py_3_14))] + { + Py_REFCNT(ob) + } + + #[cfg(all(not(Py_GIL_DISABLED), not(all(Py_LIMITED_API, Py_3_14)), Py_3_12))] + { + (*ob).ob_refcnt.ob_refcnt + } + + #[cfg(all(not(Py_GIL_DISABLED), not(Py_3_12), not(GraalPy)))] + { + (*ob).ob_refcnt + } + + #[cfg(all(not(Py_GIL_DISABLED), not(Py_3_12), GraalPy))] + { + _Py_REFCNT(ob) + } +} + +#[cfg(Py_3_12)] +#[inline(always)] +unsafe fn _Py_IsImmortal(op: *mut PyObject) -> c_int { + #[cfg(all(target_pointer_width = "64", not(Py_GIL_DISABLED)))] + { + (((*op).ob_refcnt.ob_refcnt as crate::PY_INT32_T) < 0) as c_int + } + + #[cfg(all(target_pointer_width = "32", not(Py_GIL_DISABLED)))] + { + #[cfg(not(Py_3_14))] + { + ((*op).ob_refcnt.ob_refcnt == _Py_IMMORTAL_REFCNT) as c_int + } + + #[cfg(Py_3_14)] + { + ((*op).ob_refcnt.ob_refcnt >= _Py_IMMORTAL_MINIMUM_REFCNT) as c_int + } + } + + #[cfg(Py_GIL_DISABLED)] + { + ((*op).ob_ref_local.load(Relaxed) == _Py_IMMORTAL_REFCNT_LOCAL) as c_int + } +} + +// skipped _Py_IsStaticImmortal + +// TODO: Py_SET_REFCNT + +extern "C" { + #[cfg(all(py_sys_config = "Py_REF_DEBUG", not(Py_LIMITED_API)))] + fn _Py_NegativeRefcount(filename: *const c_char, lineno: c_int, op: *mut PyObject); + #[cfg(all(Py_3_12, py_sys_config = "Py_REF_DEBUG", not(Py_LIMITED_API)))] + fn _Py_INCREF_IncRefTotal(); + #[cfg(all(Py_3_12, py_sys_config = "Py_REF_DEBUG", not(Py_LIMITED_API)))] + fn _Py_DECREF_DecRefTotal(); + + #[cfg_attr(PyPy, link_name = "_PyPy_Dealloc")] + fn _Py_Dealloc(arg1: *mut PyObject); + + #[cfg_attr(PyPy, link_name = "PyPy_IncRef")] + #[cfg_attr(GraalPy, link_name = "_Py_IncRef")] + pub fn Py_IncRef(o: *mut PyObject); + #[cfg_attr(PyPy, link_name = "PyPy_DecRef")] + #[cfg_attr(GraalPy, link_name = "_Py_DecRef")] + pub fn Py_DecRef(o: *mut PyObject); + + #[cfg(all(Py_3_10, not(PyPy)))] + fn _Py_IncRef(o: *mut PyObject); + #[cfg(all(Py_3_10, not(PyPy)))] + fn _Py_DecRef(o: *mut PyObject); + + #[cfg(GraalPy)] + fn _Py_REFCNT(arg1: *const PyObject) -> Py_ssize_t; +} + +#[inline(always)] +pub unsafe fn Py_INCREF(op: *mut PyObject) { + // On limited API, the free-threaded build, or with refcount debugging, let the interpreter do refcounting + // TODO: reimplement the logic in the header in the free-threaded build, for a little bit of performance. + #[cfg(any( + Py_GIL_DISABLED, + Py_LIMITED_API, + py_sys_config = "Py_REF_DEBUG", + GraalPy + ))] + { + // _Py_IncRef was added to the ABI in 3.10; skips null checks + #[cfg(all(Py_3_10, not(PyPy)))] + { + _Py_IncRef(op); + } + + #[cfg(any(not(Py_3_10), PyPy))] + { + Py_IncRef(op); + } + } + + // version-specific builds are allowed to directly manipulate the reference count + #[cfg(not(any( + Py_GIL_DISABLED, + Py_LIMITED_API, + py_sys_config = "Py_REF_DEBUG", + GraalPy + )))] + { + #[cfg(all(Py_3_14, target_pointer_width = "64"))] + { + let cur_refcnt = (*op).ob_refcnt.ob_refcnt; + if (cur_refcnt as i32) < 0 { + return; + } + (*op).ob_refcnt.ob_refcnt = cur_refcnt.wrapping_add(1); + } + + #[cfg(all(Py_3_12, not(Py_3_14), target_pointer_width = "64"))] + { + let cur_refcnt = (*op).ob_refcnt.ob_refcnt_split[crate::PY_BIG_ENDIAN]; + let new_refcnt = cur_refcnt.wrapping_add(1); + if new_refcnt == 0 { + return; + } + (*op).ob_refcnt.ob_refcnt_split[crate::PY_BIG_ENDIAN] = new_refcnt; + } + + #[cfg(all(Py_3_12, target_pointer_width = "32"))] + { + if _Py_IsImmortal(op) != 0 { + return; + } + (*op).ob_refcnt.ob_refcnt += 1 + } + + #[cfg(not(Py_3_12))] + { + (*op).ob_refcnt += 1 + } + + // Skipped _Py_INCREF_STAT_INC - if anyone wants this, please file an issue + // or submit a PR supporting Py_STATS build option and pystats.h + } +} + +// skipped _Py_DecRefShared +// skipped _Py_DecRefSharedDebug +// skipped _Py_MergeZeroLocalRefcount + +#[inline(always)] +#[cfg_attr( + all(py_sys_config = "Py_REF_DEBUG", Py_3_12, not(Py_LIMITED_API)), + track_caller +)] +pub unsafe fn Py_DECREF(op: *mut PyObject) { + // On limited API, the free-threaded build, or with refcount debugging, let the interpreter do refcounting + // On 3.12+ we implement refcount debugging to get better assertion locations on negative refcounts + // TODO: reimplement the logic in the header in the free-threaded build, for a little bit of performance. + #[cfg(any( + Py_GIL_DISABLED, + Py_LIMITED_API, + all(py_sys_config = "Py_REF_DEBUG", not(Py_3_12)), + GraalPy + ))] + { + // _Py_DecRef was added to the ABI in 3.10; skips null checks + #[cfg(all(Py_3_10, not(PyPy)))] + { + _Py_DecRef(op); + } + + #[cfg(any(not(Py_3_10), PyPy))] + { + Py_DecRef(op); + } + } + + #[cfg(not(any( + Py_GIL_DISABLED, + Py_LIMITED_API, + all(py_sys_config = "Py_REF_DEBUG", not(Py_3_12)), + GraalPy + )))] + { + #[cfg(Py_3_12)] + if _Py_IsImmortal(op) != 0 { + return; + } + + // Skipped _Py_DECREF_STAT_INC - if anyone needs this, please file an issue + // or submit a PR supporting Py_STATS build option and pystats.h + + #[cfg(py_sys_config = "Py_REF_DEBUG")] + _Py_DECREF_DecRefTotal(); + + #[cfg(Py_3_12)] + { + (*op).ob_refcnt.ob_refcnt -= 1; + + #[cfg(py_sys_config = "Py_REF_DEBUG")] + if (*op).ob_refcnt.ob_refcnt < 0 { + let location = std::panic::Location::caller(); + let filename = std::ffi::CString::new(location.file()).unwrap(); + _Py_NegativeRefcount(filename.as_ptr(), location.line() as i32, op); + } + + if (*op).ob_refcnt.ob_refcnt == 0 { + _Py_Dealloc(op); + } + } + + #[cfg(not(Py_3_12))] + { + (*op).ob_refcnt -= 1; + + if (*op).ob_refcnt == 0 { + _Py_Dealloc(op); + } + } + } +} + +#[inline] +pub unsafe fn Py_CLEAR(op: *mut *mut PyObject) { + let tmp = *op; + if !tmp.is_null() { + *op = ptr::null_mut(); + Py_DECREF(tmp); + } +} + +#[inline] +pub unsafe fn Py_XINCREF(op: *mut PyObject) { + if !op.is_null() { + Py_INCREF(op) + } +} + +#[inline] +pub unsafe fn Py_XDECREF(op: *mut PyObject) { + if !op.is_null() { + Py_DECREF(op) + } +} + +extern "C" { + #[cfg(all(Py_3_10, Py_LIMITED_API, not(PyPy)))] + #[cfg_attr(docsrs, doc(cfg(Py_3_10)))] + pub fn Py_NewRef(obj: *mut PyObject) -> *mut PyObject; + #[cfg(all(Py_3_10, Py_LIMITED_API, not(PyPy)))] + #[cfg_attr(docsrs, doc(cfg(Py_3_10)))] + pub fn Py_XNewRef(obj: *mut PyObject) -> *mut PyObject; +} + +// macro _Py_NewRef not public; reimplemented directly inside Py_NewRef here +// macro _Py_XNewRef not public; reimplemented directly inside Py_XNewRef here + +#[cfg(all(Py_3_10, any(not(Py_LIMITED_API), PyPy)))] +#[cfg_attr(docsrs, doc(cfg(Py_3_10)))] +#[inline] +pub unsafe fn Py_NewRef(obj: *mut PyObject) -> *mut PyObject { + Py_INCREF(obj); + obj +} + +#[cfg(all(Py_3_10, any(not(Py_LIMITED_API), PyPy)))] +#[cfg_attr(docsrs, doc(cfg(Py_3_10)))] +#[inline] +pub unsafe fn Py_XNewRef(obj: *mut PyObject) -> *mut PyObject { + Py_XINCREF(obj); + obj +} diff --git a/pytests/pytest.ini b/pytests/pytest.ini new file mode 100644 index 00000000000..3d62037f722 --- /dev/null +++ b/pytests/pytest.ini @@ -0,0 +1,3 @@ +# see https://github.com/PyO3/pyo3/issues/5094 for details +[pytest] +filterwarnings = ignore::DeprecationWarning:pytest_asyncio.* \ No newline at end of file diff --git a/src/ffi/tests.rs b/src/ffi/tests.rs index 065b5f67a4e..1befeb7b90b 100644 --- a/src/ffi/tests.rs +++ b/src/ffi/tests.rs @@ -158,6 +158,11 @@ fn ascii_object_bitfield() { o.set_ready(1); #[cfg(not(Py_3_12))] assert_eq!(o.ready(), 1); + + #[cfg(Py_3_12)] + o.set_statically_allocated(1); + #[cfg(Py_3_12)] + assert_eq!(o.statically_allocated(), 1); } } diff --git a/src/types/dict.rs b/src/types/dict.rs index 0ef5c477ae1..c38fe638d10 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -967,7 +967,7 @@ mod tests { } Err(err) => { assert!(err.is_instance_of::(py)); - assert_eq!(err.value(py).to_string(), "Error from __hash__") + assert!(err.value(py).to_string().contains("Error from __hash__")); } } })