From d54b631761ce046e545fdd66e41cec9c5940091b Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 20 Aug 2025 20:38:13 +0100 Subject: [PATCH 1/3] add `PyOnceLock` type --------- Co-authored-by: Nathan Goldbaum --- Cargo.toml | 2 + guide/src/faq.md | 14 +- guide/src/free-threading.md | 57 +++--- guide/src/migration.md | 41 +++- newsfragments/5171.packaging.md | 1 - newsfragments/5223.added.md | 1 + newsfragments/5223.changed.md | 1 + pyo3-macros-backend/src/pyclass.rs | 8 +- src/conversions/bigdecimal.rs | 6 +- src/conversions/chrono.rs | 4 +- src/conversions/num_rational.rs | 4 +- src/conversions/rust_decimal.rs | 4 +- src/conversions/std/ipaddr.rs | 6 +- src/conversions/std/path.rs | 4 +- src/conversions/std/time.rs | 4 +- src/conversions/uuid.rs | 4 +- src/coroutine/waker.rs | 10 +- src/err/err_state.rs | 6 +- src/exceptions.rs | 6 +- src/impl_/exceptions.rs | 8 +- src/impl_/pyclass.rs | 4 +- src/impl_/pyclass/lazy_type_object.rs | 7 +- src/impl_/pymodule.rs | 6 +- src/internal/get_slot.rs | 4 +- src/pyclass/create_type_object.rs | 4 +- src/sync.rs | 29 ++- src/sync/once_lock.rs | 270 ++++++++++++++++++++++++++ src/types/code.rs | 4 +- src/types/datetime.rs | 8 +- src/types/mapping.rs | 4 +- src/types/module.rs | 2 +- src/types/sequence.rs | 4 +- tests/test_class_new.rs | 4 +- 33 files changed, 441 insertions(+), 100 deletions(-) create mode 100644 newsfragments/5223.added.md create mode 100644 newsfragments/5223.changed.md create mode 100644 src/sync/once_lock.rs diff --git a/Cargo.toml b/Cargo.toml index 3758235defa..ca912d91444 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ rust-version.workspace = true [dependencies] libc = "0.2.62" memoffset = "0.9" +once_cell = "1.21" # ffi bindings to the python interpreter, split into a separate crate so they can be used independently pyo3-ffi = { path = "pyo3-ffi", version = "=0.25.1" } @@ -129,6 +130,7 @@ auto-initialize = [] # Enables `Clone`ing references to Python objects `Py` which panics if the GIL is not held. py-clone = [] +# Adds `OnceExt` and `MutexExt` implementations to the `parking_lot` types parking_lot = ["dep:parking_lot", "lock_api"] arc_lock = ["lock_api", "lock_api/arc_lock", "parking_lot?/arc_lock"] diff --git a/guide/src/faq.md b/guide/src/faq.md index 8efe0b61bad..e00bec71f76 100644 --- a/guide/src/faq.md +++ b/guide/src/faq.md @@ -4,18 +4,18 @@ Sorry that you're having trouble using PyO3. If you can't find the answer to you ## I'm experiencing deadlocks using PyO3 with `std::sync::OnceLock`, `std::sync::LazyLock`, `lazy_static`, and `once_cell`! -`OnceLock`, `LazyLock`, and their thirdparty predecessors use blocking to ensure only one thread ever initializes them. Because the Python GIL is an additional lock this can lead to deadlocks in the following way: +`OnceLock`, `LazyLock`, and their thirdparty predecessors use blocking to ensure only one thread ever initializes them. Because the Python interpreter can introduce additional locks (the Python GIL and GC can both require all other threads to pause) this can lead to deadlocks in the following way: -1. A thread (thread A) which has acquired the Python GIL starts initialization of a `OnceLock` value. -2. The initialization code calls some Python API which temporarily releases the GIL e.g. `Python::import`. -3. Another thread (thread B) acquires the Python GIL and attempts to access the same `OnceLock` value. +1. A thread (thread A) which is attached to the Python interpreter starts initialization of a `OnceLock` value. +2. The initialization code calls some Python API which temporarily detaches from the interpreter e.g. `Python::import`. +3. Another thread (thread B) attaches to the Python interpreter and attempts to access the same `OnceLock` value. 4. Thread B is blocked, because it waits for `OnceLock`'s initialization to lock to release. -5. Thread A is blocked, because it waits to re-acquire the GIL which thread B still holds. +5. On non-free-threaded Python, thread A is now also blocked, because it waits to re-attach to the interpreter (by taking the GIL which thread B still holds). 6. Deadlock. -PyO3 provides a struct [`GILOnceCell`] which implements a single-initialization API based on these types that relies on the GIL for locking. If the GIL is released or there is no GIL, then this type allows the initialization function to race but ensures that the data is only ever initialized once. If you need to ensure that the initialization function is called once and only once, you can make use of the [`OnceExt`] and [`OnceLockExt`] extension traits that enable using the standard library types for this purpose but provide new methods for these types that avoid the risk of deadlocking with the Python GIL. This means they can be used in place of other choices when you are experiencing the deadlock described above. See the documentation for [`GILOnceCell`] and [`OnceExt`] for further details and an example how to use them. +PyO3 provides a struct [`PyOnceLock`] which implements a single-initialization API based on these types that avoids deadlocks. You can also make use of the [`OnceExt`] and [`OnceLockExt`] extension traits that enable using the standard library types for this purpose by providing new methods for these types that avoid the risk of deadlocking with the Python interpreter. This means they can be used in place of other choices when you are experiencing the deadlock described above. See the documentation for [`PyOnceLock`] and [`OnceExt`] for further details and an example how to use them. -[`GILOnceCell`]: {{#PYO3_DOCS_URL}}/pyo3/sync/struct.GILOnceCell.html +[`PyOnceLock`]: {{#PYO3_DOCS_URL}}/pyo3/sync/struct.PyOnceLock.html [`OnceExt`]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceExt.html [`OnceLockExt`]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceLockExt.html diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index f00b6afdb94..8ca72714bfa 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -285,31 +285,39 @@ the free-threaded build. ### Thread-safe single initialization -Until version 0.23, PyO3 provided only [`GILOnceCell`] to enable deadlock-free -single initialization of data in contexts that might execute arbitrary Python -code. While we have updated [`GILOnceCell`] to avoid thread safety issues -triggered only under the free-threaded build, the design of [`GILOnceCell`] is -inherently thread-unsafe, in a manner that can be problematic even in the -GIL-enabled build. - -If, for example, the function executed by [`GILOnceCell`] releases the GIL or -calls code that releases the GIL, then it is possible for multiple threads to -race to initialize the cell. While the cell will only ever be initialized -once, it can be problematic in some contexts that [`GILOnceCell`] does not block -like the standard library [`OnceLock`]. - -In cases where the initialization function must run exactly once, you can bring -the [`OnceExt`] or [`OnceLockExt`] traits into scope. The [`OnceExt`] trait adds +To initialize data exactly once, use the [`PyOnceLock`] type, which is a close equivalent +to [`std::sync::OnceLock`][`OnceLock`] that also helps avoid deadlocks by detaching from +the Python interpreter when threads are blocking waiting for another thread to +complete intialization. If already using [`OnceLock`] and it is impractical +to replace with a [`PyOnceLock`], there is the [`OnceLockExt`] extension trait +which adds [`OnceLockExt::get_or_init_py_attached`] to detach from the interpreter +when blocking in the same fashion as [`PyOnceLock`]. Here is an example using +[`PyOnceLock`] to single-initialize a runtime cache holding a `Py`: + +```rust +# use pyo3::prelude::*; +use pyo3::sync::PyOnceLock; +use pyo3::types::PyDict; + +let cache: PyOnceLock> = PyOnceLock::new(); + +Python::attach(|py| { + // guaranteed to be called once and only once + cache.get_or_init(py, || PyDict::new(py).unbind()) +}); +``` + +In cases where a function must run exactly once, you can bring +the [`OnceExt`] trait into scope. The [`OnceExt`] trait adds [`OnceExt::call_once_py_attached`] and [`OnceExt::call_once_force_py_attached`] functions to the api of `std::sync::Once`, enabling use of [`Once`] in contexts -where the GIL is held. Similarly, [`OnceLockExt`] adds -[`OnceLockExt::get_or_init_py_attached`]. These functions are analogous to -[`Once::call_once`], [`Once::call_once_force`], and [`OnceLock::get_or_init`] except -they accept a [`Python<'py>`] token in addition to an `FnOnce`. All of these -functions release the GIL and re-acquire it before executing the function, -avoiding deadlocks with the GIL that are possible without using the PyO3 -extension traits. Here is an example of how to use [`OnceExt`] to -enable single-initialization of a runtime cache holding a `Py`. +where the thread is attached to the Python interpreter. These functions are analogous to +[`Once::call_once`], [`Once::call_once_force`] except they accept a [`Python<'py>`] +token in addition to an `FnOnce`. All of these functions detach from the +interpreter before blocking and re-attach before executing the function, +avoiding deadlocks that are possible without using the PyO3 +extension traits. Here the same example as above built using a [`Once`] instead of a +[`PyOnceLock`]: ```rust # use pyo3::prelude::*; @@ -401,6 +409,8 @@ interpreter. [`Once`]: https://doc.rust-lang.org/stable/std/sync/struct.Once.html [`Once::call_once`]: https://doc.rust-lang.org/stable/std/sync/struct.Once.html#method.call_once [`Once::call_once_force`]: https://doc.rust-lang.org/stable/std/sync/struct.Once.html#method.call_once_force +[`OnceCell`]: https://docs.rs/once_cell/latest/once_cell/sync/struct.OnceCell.html +[`OnceCellExt`]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceCellExt.html [`OnceExt`]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceExt.html [`OnceExt::call_once_py_attached`]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceExt.html#tymethod.call_once_py_attached [`OnceExt::call_once_force_py_attached`]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceExt.html#tymethod.call_once_force_py_attached @@ -411,4 +421,5 @@ interpreter. [`Python::detach`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.detach [`Python::attach`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.attach [`Python<'py>`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html +[`PyOnceLock`]: {{#PYO3_DOCS_URL}}/pyo3/sync/struct.PyOnceLock.html [`threading`]: https://docs.python.org/3/library/threading.html diff --git a/guide/src/migration.md b/guide/src/migration.md index 4a48dbfabef..acb7a5c7c7f 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -21,7 +21,46 @@ For this reason we chose to rename these to more modern terminology introduced i Click to expand The type alias `PyObject` (aka `Py`) is often confused with the identically named FFI definition `pyo3::ffi::PyObject`. For this reason we are deprecating its usage. To migrate simply replace its usage by the target type `Py`. + + +### Replacement of `GILOnceCell` with `PyOnceLock` +
+Click to expand + +Similar to the above renaming of `Python::with_gil` and related APIs, the `GILOnceCell` type was designed for a Python interpreter which was limited by the GIL. Aside from its name, it allowed for the "once" initialization to race because the racing was mediated by the GIL and was extremely unlikely to manifest in practice. + +With the introduction of free-threaded Python the racy initialization behavior is more likely to be problematic and so a new type `PyOnceLock` has been introduced which performs true single-initialization correctly while attached to the Python interpreter. It exposes the same API as `GILOnceCell`, so should be a drop-in replacement with the notable exception that if the racy initialization of `GILOnceCell` was inadvertently relied on (e.g. due to circular references) then the stronger once-ever guarantee of `PyOnceLock` may lead to deadlocking which requires refactoring. + +Before: +```rust +# #![allow(deprecated)] +# use pyo3::prelude::*; +# use pyo3::sync::GILOnceCell; +# use pyo3::types::PyType; +# fn main() -> PyResult<()> { +# Python::attach(|py| { +static DECIMAL_TYPE: GILOnceCell> = GILOnceCell::new(); +DECIMAL_TYPE.import(py, "decimal", "Decimal")?; +# Ok(()) +# }) +# } +``` + +After: + +```rust +# use pyo3::prelude::*; +# use pyo3::sync::PyOnceLock; +# use pyo3::types::PyType; +# fn main() -> PyResult<()> { +# Python::attach(|py| { +static DECIMAL_TYPE: PyOnceLock> = PyOnceLock::new(); +DECIMAL_TYPE.import(py, "decimal", "Decimal")?; +# Ok(()) +# }) +# } +```
### Deprecation of `GILProtected` @@ -64,7 +103,7 @@ Python::attach(|py| { # }) # } ``` - + ### `PyMemoryError` now maps to `io::ErrorKind::OutOfMemory` when converted to `io::Error`
diff --git a/newsfragments/5171.packaging.md b/newsfragments/5171.packaging.md index cb502462c1a..7d54315d9a7 100644 --- a/newsfragments/5171.packaging.md +++ b/newsfragments/5171.packaging.md @@ -1,2 +1 @@ Update MSRV to 1.74. -Drop `once_cell` dependency. diff --git a/newsfragments/5223.added.md b/newsfragments/5223.added.md new file mode 100644 index 00000000000..c8847b310d7 --- /dev/null +++ b/newsfragments/5223.added.md @@ -0,0 +1 @@ +Add `PyOnceLock` type for thread-safe single-initialization. diff --git a/newsfragments/5223.changed.md b/newsfragments/5223.changed.md new file mode 100644 index 00000000000..1c1ff1b2bf1 --- /dev/null +++ b/newsfragments/5223.changed.md @@ -0,0 +1 @@ +Deprecate `GILOnceCell` type in favour of `PyOnceLock`. diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 3ff700074dc..674b41b842a 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -2459,12 +2459,8 @@ impl<'a> PyClassImplsBuilder<'a> { impl #pyo3_path::impl_::pyclass::PyClassWithFreeList for #cls { #[inline] fn get_free_list(py: #pyo3_path::Python<'_>) -> &'static ::std::sync::Mutex<#pyo3_path::impl_::freelist::PyObjectFreeList> { - static FREELIST: #pyo3_path::sync::GILOnceCell<::std::sync::Mutex<#pyo3_path::impl_::freelist::PyObjectFreeList>> = #pyo3_path::sync::GILOnceCell::new(); - // If there's a race to fill the cell, the object created - // by the losing thread will be deallocated via RAII - &FREELIST.get_or_init(py, || { - ::std::sync::Mutex::new(#pyo3_path::impl_::freelist::PyObjectFreeList::with_capacity(#freelist)) - }) + static FREELIST: #pyo3_path::sync::PyOnceLock<::std::sync::Mutex<#pyo3_path::impl_::freelist::PyObjectFreeList>> = #pyo3_path::sync::PyOnceLock::new(); + &FREELIST.get_or_init(py, || ::std::sync::Mutex::new(#pyo3_path::impl_::freelist::PyObjectFreeList::with_capacity(#freelist))) } } } diff --git a/src/conversions/bigdecimal.rs b/src/conversions/bigdecimal.rs index 809cd167329..af976431894 100644 --- a/src/conversions/bigdecimal.rs +++ b/src/conversions/bigdecimal.rs @@ -54,7 +54,7 @@ use std::str::FromStr; use crate::types::PyTuple; use crate::{ exceptions::PyValueError, - sync::GILOnceCell, + sync::PyOnceLock, types::{PyAnyMethods, PyStringMethods, PyType}, Bound, FromPyObject, IntoPyObject, Py, PyAny, PyErr, PyResult, Python, }; @@ -62,12 +62,12 @@ use bigdecimal::BigDecimal; use num_bigint::Sign; fn get_decimal_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { - static DECIMAL_CLS: GILOnceCell> = GILOnceCell::new(); + static DECIMAL_CLS: PyOnceLock> = PyOnceLock::new(); DECIMAL_CLS.import(py, "decimal", "Decimal") } fn get_invalid_operation_error_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { - static INVALID_OPERATION_CLS: GILOnceCell> = GILOnceCell::new(); + static INVALID_OPERATION_CLS: PyOnceLock> = PyOnceLock::new(); INVALID_OPERATION_CLS.import(py, "decimal", "InvalidOperation") } diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 634c1a1d212..5ea8904fca2 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -52,7 +52,7 @@ use crate::types::{PyDateAccess, PyDeltaAccess, PyTimeAccess}; #[cfg(feature = "chrono-local")] use crate::{ exceptions::PyRuntimeError, - sync::GILOnceCell, + sync::PyOnceLock, types::{PyString, PyStringMethods}, Py, }; @@ -449,7 +449,7 @@ impl<'py> IntoPyObject<'py> for Local { type Error = PyErr; fn into_pyobject(self, py: Python<'py>) -> Result { - static LOCAL_TZ: GILOnceCell> = GILOnceCell::new(); + static LOCAL_TZ: PyOnceLock> = PyOnceLock::new(); let tz = LOCAL_TZ .get_or_try_init(py, || { let iana_name = iana_time_zone::get_timezone().map_err(|e| { diff --git a/src/conversions/num_rational.rs b/src/conversions/num_rational.rs index 8371c48f838..25aac24aa7a 100644 --- a/src/conversions/num_rational.rs +++ b/src/conversions/num_rational.rs @@ -45,7 +45,7 @@ use crate::conversion::IntoPyObject; use crate::ffi; -use crate::sync::GILOnceCell; +use crate::sync::PyOnceLock; use crate::types::any::PyAnyMethods; use crate::types::PyType; use crate::{Bound, FromPyObject, Py, PyAny, PyErr, PyResult, Python}; @@ -54,7 +54,7 @@ use crate::{Bound, FromPyObject, Py, PyAny, PyErr, PyResult, Python}; use num_bigint::BigInt; use num_rational::Ratio; -static FRACTION_CLS: GILOnceCell> = GILOnceCell::new(); +static FRACTION_CLS: PyOnceLock> = PyOnceLock::new(); fn get_fraction_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { FRACTION_CLS.import(py, "fractions", "Fraction") diff --git a/src/conversions/rust_decimal.rs b/src/conversions/rust_decimal.rs index 597f469d21a..b533f97c5a5 100644 --- a/src/conversions/rust_decimal.rs +++ b/src/conversions/rust_decimal.rs @@ -51,7 +51,7 @@ use crate::conversion::IntoPyObject; use crate::exceptions::PyValueError; -use crate::sync::GILOnceCell; +use crate::sync::PyOnceLock; use crate::types::any::PyAnyMethods; use crate::types::string::PyStringMethods; use crate::types::PyType; @@ -74,7 +74,7 @@ impl FromPyObject<'_> for Decimal { } } -static DECIMAL_CLS: GILOnceCell> = GILOnceCell::new(); +static DECIMAL_CLS: PyOnceLock> = PyOnceLock::new(); fn get_decimal_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { DECIMAL_CLS.import(py, "decimal", "Decimal") diff --git a/src/conversions/std/ipaddr.rs b/src/conversions/std/ipaddr.rs index 699013c53e2..7e6453ed654 100644 --- a/src/conversions/std/ipaddr.rs +++ b/src/conversions/std/ipaddr.rs @@ -3,7 +3,7 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use crate::conversion::IntoPyObject; use crate::exceptions::PyValueError; use crate::instance::Bound; -use crate::sync::GILOnceCell; +use crate::sync::PyOnceLock; use crate::types::any::PyAnyMethods; use crate::types::string::PyStringMethods; use crate::types::PyType; @@ -35,7 +35,7 @@ impl<'py> IntoPyObject<'py> for Ipv4Addr { type Error = PyErr; fn into_pyobject(self, py: Python<'py>) -> Result { - static IPV4_ADDRESS: GILOnceCell> = GILOnceCell::new(); + static IPV4_ADDRESS: PyOnceLock> = PyOnceLock::new(); IPV4_ADDRESS .import(py, "ipaddress", "IPv4Address")? .call1((u32::from_be_bytes(self.octets()),)) @@ -59,7 +59,7 @@ impl<'py> IntoPyObject<'py> for Ipv6Addr { type Error = PyErr; fn into_pyobject(self, py: Python<'py>) -> Result { - static IPV6_ADDRESS: GILOnceCell> = GILOnceCell::new(); + static IPV6_ADDRESS: PyOnceLock> = PyOnceLock::new(); IPV6_ADDRESS .import(py, "ipaddress", "IPv6Address")? .call1((u128::from_be_bytes(self.octets()),)) diff --git a/src/conversions/std/path.rs b/src/conversions/std/path.rs index ef51367e03c..17cad8f2694 100644 --- a/src/conversions/std/path.rs +++ b/src/conversions/std/path.rs @@ -1,7 +1,7 @@ use crate::conversion::IntoPyObject; use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Bound; -use crate::sync::GILOnceCell; +use crate::sync::PyOnceLock; use crate::types::any::PyAnyMethods; use crate::{ffi, FromPyObject, Py, PyAny, PyErr, PyResult, Python}; use std::borrow::Cow; @@ -25,7 +25,7 @@ impl<'py> IntoPyObject<'py> for &Path { #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { - static PY_PATH: GILOnceCell> = GILOnceCell::new(); + static PY_PATH: PyOnceLock> = PyOnceLock::new(); PY_PATH .import(py, "pathlib", "Path")? .call((self.as_os_str(),), None) diff --git a/src/conversions/std/time.rs b/src/conversions/std/time.rs index 62ee9693dc3..d2e2ed7c237 100644 --- a/src/conversions/std/time.rs +++ b/src/conversions/std/time.rs @@ -2,7 +2,7 @@ use crate::conversion::IntoPyObject; use crate::exceptions::{PyOverflowError, PyValueError}; #[cfg(Py_LIMITED_API)] use crate::intern; -use crate::sync::GILOnceCell; +use crate::sync::PyOnceLock; use crate::types::any::PyAnyMethods; #[cfg(not(Py_LIMITED_API))] use crate::types::PyDeltaAccess; @@ -125,7 +125,7 @@ impl<'py> IntoPyObject<'py> for &SystemTime { } fn unix_epoch_py(py: Python<'_>) -> PyResult> { - static UNIX_EPOCH: GILOnceCell> = GILOnceCell::new(); + static UNIX_EPOCH: PyOnceLock> = PyOnceLock::new(); Ok(UNIX_EPOCH .get_or_try_init(py, || { let utc = PyTzInfo::utc(py)?; diff --git a/src/conversions/uuid.rs b/src/conversions/uuid.rs index 2227617ba48..37d9aa768eb 100644 --- a/src/conversions/uuid.rs +++ b/src/conversions/uuid.rs @@ -68,13 +68,13 @@ use uuid::Uuid; use crate::conversion::IntoPyObject; use crate::exceptions::PyTypeError; use crate::instance::Bound; -use crate::sync::GILOnceCell; +use crate::sync::PyOnceLock; use crate::types::any::PyAnyMethods; use crate::types::PyType; use crate::{intern, FromPyObject, Py, PyAny, PyErr, PyResult, Python}; fn get_uuid_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { - static UUID_CLS: GILOnceCell> = GILOnceCell::new(); + static UUID_CLS: PyOnceLock> = PyOnceLock::new(); UUID_CLS.import(py, "uuid", "UUID") } diff --git a/src/coroutine/waker.rs b/src/coroutine/waker.rs index 0afb024a9ab..f425545346d 100644 --- a/src/coroutine/waker.rs +++ b/src/coroutine/waker.rs @@ -1,4 +1,4 @@ -use crate::sync::GILOnceCell; +use crate::sync::PyOnceLock; use crate::types::any::PyAnyMethods; use crate::types::PyCFunction; use crate::{intern, wrap_pyfunction, Bound, Py, PyAny, PyResult, Python}; @@ -14,11 +14,11 @@ use std::task::Wake; /// /// [1]: AsyncioWaker::initialize_future /// [2]: AsyncioWaker::wake -pub struct AsyncioWaker(GILOnceCell>); +pub struct AsyncioWaker(PyOnceLock>); impl AsyncioWaker { pub(super) fn new() -> Self { - Self(GILOnceCell::new()) + Self(PyOnceLock::new()) } pub(super) fn reset(&mut self) { @@ -58,7 +58,7 @@ struct LoopAndFuture { impl LoopAndFuture { fn new(py: Python<'_>) -> PyResult { - static GET_RUNNING_LOOP: GILOnceCell> = GILOnceCell::new(); + static GET_RUNNING_LOOP: PyOnceLock> = PyOnceLock::new(); let import = || -> PyResult<_> { let module = py.import("asyncio")?; Ok(module.getattr("get_running_loop")?.into()) @@ -69,7 +69,7 @@ impl LoopAndFuture { } fn set_result(&self, py: Python<'_>) -> PyResult<()> { - static RELEASE_WAITER: GILOnceCell> = GILOnceCell::new(); + static RELEASE_WAITER: PyOnceLock> = PyOnceLock::new(); let release_waiter = RELEASE_WAITER.get_or_try_init(py, || { wrap_pyfunction!(release_waiter, py).map(Bound::unbind) })?; diff --git a/src/err/err_state.rs b/src/err/err_state.rs index 8616979310f..d2285a7a1ca 100644 --- a/src/err/err_state.rs +++ b/src/err/err_state.rs @@ -368,13 +368,13 @@ fn raise_lazy(py: Python<'_>, lazy: Box) { mod tests { use crate::{ - exceptions::PyValueError, sync::GILOnceCell, Py, PyAny, PyErr, PyErrArguments, Python, + exceptions::PyValueError, sync::PyOnceLock, Py, PyAny, PyErr, PyErrArguments, Python, }; #[test] #[should_panic(expected = "Re-entrant normalization of PyErrState detected")] fn test_reentrant_normalization() { - static ERR: GILOnceCell = GILOnceCell::new(); + static ERR: PyOnceLock = PyOnceLock::new(); struct RecursiveArgs; @@ -398,7 +398,7 @@ mod tests { #[test] #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled fn test_no_deadlock_thread_switch() { - static ERR: GILOnceCell = GILOnceCell::new(); + static ERR: PyOnceLock = PyOnceLock::new(); struct GILSwitchArgs; diff --git a/src/exceptions.rs b/src/exceptions.rs index d3b9f994dd5..fca1cd63766 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -252,9 +252,9 @@ macro_rules! create_exception_type_object { impl $name { fn type_object_raw(py: $crate::Python<'_>) -> *mut $crate::ffi::PyTypeObject { - use $crate::sync::GILOnceCell; - static TYPE_OBJECT: GILOnceCell<$crate::Py<$crate::types::PyType>> = - GILOnceCell::new(); + use $crate::sync::PyOnceLock; + static TYPE_OBJECT: PyOnceLock<$crate::Py<$crate::types::PyType>> = + PyOnceLock::new(); TYPE_OBJECT .get_or_init(py, || diff --git a/src/impl_/exceptions.rs b/src/impl_/exceptions.rs index 15b6f53bbe2..cb15e04e4b9 100644 --- a/src/impl_/exceptions.rs +++ b/src/impl_/exceptions.rs @@ -1,7 +1,7 @@ -use crate::{sync::GILOnceCell, types::PyType, Bound, Py, Python}; +use crate::{sync::PyOnceLock, types::PyType, Bound, Py, PyErr, Python}; pub struct ImportedExceptionTypeObject { - imported_value: GILOnceCell>, + imported_value: PyOnceLock>, module: &'static str, name: &'static str, } @@ -9,7 +9,7 @@ pub struct ImportedExceptionTypeObject { impl ImportedExceptionTypeObject { pub const fn new(module: &'static str, name: &'static str) -> Self { Self { - imported_value: GILOnceCell::new(), + imported_value: PyOnceLock::new(), module, name, } @@ -18,7 +18,7 @@ impl ImportedExceptionTypeObject { pub fn get<'py>(&self, py: Python<'py>) -> &Bound<'py, PyType> { self.imported_value .import(py, self.module, self.name) - .unwrap_or_else(|e| { + .unwrap_or_else(|e: PyErr| { panic!( "failed to import exception {}.{}: {}", self.module, self.name, e diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 1228c2ea758..29a967e8d2b 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -994,8 +994,8 @@ unsafe fn bpo_35810_workaround(py: Python<'_>, ty: *mut ffi::PyTypeObject) { { // Must check version at runtime for abi3 wheels - they could run against a higher version // than the build config suggests. - use crate::sync::GILOnceCell; - static IS_PYTHON_3_8: GILOnceCell = GILOnceCell::new(); + use crate::sync::PyOnceLock; + static IS_PYTHON_3_8: PyOnceLock = PyOnceLock::new(); if *IS_PYTHON_3_8.get_or_init(py, || py.version_info() >= (3, 8)) { // No fix needed - the wheel is running on a sufficiently new interpreter. diff --git a/src/impl_/pyclass/lazy_type_object.rs b/src/impl_/pyclass/lazy_type_object.rs index c58f4978451..408d00ad6d7 100644 --- a/src/impl_/pyclass/lazy_type_object.rs +++ b/src/impl_/pyclass/lazy_type_object.rs @@ -6,6 +6,8 @@ use std::{ #[cfg(Py_3_14)] use crate::err::error_on_minusone; +#[allow(deprecated)] +use crate::sync::GILOnceCell; #[cfg(Py_3_14)] use crate::types::PyTypeMethods; use crate::{ @@ -13,7 +15,6 @@ use crate::{ ffi, impl_::{pyclass::MaybeRuntimePyMethodDef, pymethods::PyMethodDefType}, pyclass::{create_type_object, PyClassTypeObject}, - sync::GILOnceCell, types::PyType, Bound, Py, PyAny, PyClass, PyErr, PyResult, Python, }; @@ -28,10 +29,12 @@ pub struct LazyTypeObject(LazyTypeObjectInner, PhantomData); // Non-generic inner of LazyTypeObject to keep code size down struct LazyTypeObjectInner { + #[allow(deprecated)] value: GILOnceCell, // Threads which have begun initialization of the `tp_dict`. Used for // reentrant initialization detection. initializing_threads: Mutex>, + #[allow(deprecated)] fully_initialized_type: GILOnceCell>, } @@ -41,8 +44,10 @@ impl LazyTypeObject { pub const fn new() -> Self { LazyTypeObject( LazyTypeObjectInner { + #[allow(deprecated)] value: GILOnceCell::new(), initializing_threads: Mutex::new(Vec::new()), + #[allow(deprecated)] fully_initialized_type: GILOnceCell::new(), }, PhantomData, diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index a9736672bc7..027ce2982ee 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -25,7 +25,7 @@ use crate::PyErr; use crate::{ ffi, impl_::pymethods::PyMethodDef, - sync::GILOnceCell, + sync::PyOnceLock, types::{PyCFunction, PyModule, PyModuleMethods}, Bound, Py, PyClass, PyResult, PyTypeInfo, Python, }; @@ -43,7 +43,7 @@ pub struct ModuleDef { ))] interpreter: AtomicI64, /// Initialized module object, cached to avoid reinitialization. - module: GILOnceCell>, + module: PyOnceLock>, /// Whether or not the module supports running without the GIL gil_used: AtomicBool, } @@ -89,7 +89,7 @@ impl ModuleDef { not(all(windows, Py_LIMITED_API, not(Py_3_10))) ))] interpreter: AtomicI64::new(-1), - module: GILOnceCell::new(), + module: PyOnceLock::new(), gil_used: AtomicBool::new(true), } } diff --git a/src/internal/get_slot.rs b/src/internal/get_slot.rs index f938fc8a6ac..9d1633bed6d 100644 --- a/src/internal/get_slot.rs +++ b/src/internal/get_slot.rs @@ -134,9 +134,9 @@ impl_slots! { #[cfg(all(Py_LIMITED_API, not(Py_3_10)))] fn is_runtime_3_10(py: crate::Python<'_>) -> bool { - use crate::sync::GILOnceCell; + use crate::sync::PyOnceLock; - static IS_RUNTIME_3_10: GILOnceCell = GILOnceCell::new(); + static IS_RUNTIME_3_10: PyOnceLock = PyOnceLock::new(); *IS_RUNTIME_3_10.get_or_init(py, || py.version_info() >= (3, 10)) } diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index 1a6dd032925..b9bc1a7df96 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -540,8 +540,8 @@ fn bpo_45315_workaround(py: Python<'_>, class_name: CString) { { // Must check version at runtime for abi3 wheels - they could run against a higher version // than the build config suggests. - use crate::sync::GILOnceCell; - static IS_PYTHON_3_11: GILOnceCell = GILOnceCell::new(); + use crate::sync::PyOnceLock; + static IS_PYTHON_3_11: PyOnceLock = PyOnceLock::new(); if *IS_PYTHON_3_11.get_or_init(py, || py.version_info() >= (3, 11)) { // No fix needed - the wheel is running on a sufficiently new interpreter. diff --git a/src/sync.rs b/src/sync.rs index 54cafe3c1e4..c1e3ff177e2 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -17,9 +17,13 @@ use std::{ sync::{Once, OnceState}, }; +pub(crate) mod once_lock; + #[cfg(not(Py_GIL_DISABLED))] use crate::PyVisit; +pub use self::once_lock::PyOnceLock; + /// Value with concurrent access protected by the GIL. /// /// This is a synchronization primitive based on Python's global interpreter lock (GIL). @@ -113,6 +117,7 @@ unsafe impl Sync for GILProtected where T: Send {} /// between threads: /// /// ``` +/// #![allow(deprecated)] /// use pyo3::sync::GILOnceCell; /// use pyo3::prelude::*; /// use pyo3::types::PyList; @@ -126,6 +131,10 @@ unsafe impl Sync for GILProtected where T: Send {} /// } /// # Python::attach(|py| assert_eq!(get_shared_list(py).len(), 0)); /// ``` +#[deprecated( + since = "0.26.0", + note = "Prefer `pyo3::sync::PyOnceLock`, which avoids the possibility of racing during initialization." +)] pub struct GILOnceCell { once: Once, data: UnsafeCell>, @@ -135,6 +144,7 @@ pub struct GILOnceCell { /// `PhantomData` to make sure dropck understands we're dropping T in our Drop impl. /// /// ```compile_error,E0597 + /// #![allow(deprecated)] /// use pyo3::Python; /// use pyo3::sync::GILOnceCell; /// @@ -153,6 +163,7 @@ pub struct GILOnceCell { _marker: PhantomData, } +#[allow(deprecated)] impl Default for GILOnceCell { fn default() -> Self { Self::new() @@ -162,9 +173,12 @@ impl Default for GILOnceCell { // T: Send is needed for Sync because the thread which drops the GILOnceCell can be different // to the thread which fills it. (e.g. think scoped thread which fills the cell and then exits, // leaving the cell to be dropped by the main thread). +#[allow(deprecated)] unsafe impl Sync for GILOnceCell {} +#[allow(deprecated)] unsafe impl Send for GILOnceCell {} +#[allow(deprecated)] impl GILOnceCell { /// Create a `GILOnceCell` which does not yet contain a value. pub const fn new() -> Self { @@ -298,6 +312,7 @@ impl GILOnceCell { } } +#[allow(deprecated)] impl GILOnceCell> { /// Creates a new cell that contains a new Python reference to the same contained object. /// @@ -315,6 +330,7 @@ impl GILOnceCell> { } } +#[allow(deprecated)] impl GILOnceCell> where T: PyTypeCheck, @@ -327,6 +343,7 @@ where /// /// `GILOnceCell` can be used to avoid importing a class multiple times: /// ``` + /// #![allow(deprecated)] /// # use pyo3::prelude::*; /// # use pyo3::sync::GILOnceCell; /// # use pyo3::types::{PyDict, PyType}; @@ -364,6 +381,7 @@ where } } +#[allow(deprecated)] impl Drop for GILOnceCell { fn drop(&mut self) { if self.once.is_completed() { @@ -420,12 +438,12 @@ macro_rules! intern { /// Implementation detail for `intern!` macro. #[doc(hidden)] -pub struct Interned(&'static str, GILOnceCell>); +pub struct Interned(&'static str, PyOnceLock>); impl Interned { /// Creates an empty holder for an interned `str`. pub const fn new(value: &'static str) -> Self { - Interned(value, GILOnceCell::new()) + Interned(value, PyOnceLock::new()) } /// Gets or creates the interned `str` value. @@ -536,7 +554,7 @@ mod once_lock_ext_sealed { impl Sealed for std::sync::OnceLock {} } -/// Helper trait for `Once` to help avoid deadlocking when using a `Once` when attached to a +/// Extension trait for [`Once`] to help avoid deadlocking when using a [`Once`] when attached to a /// Python thread. pub trait OnceExt: Sealed { ///The state of `Once` @@ -816,9 +834,6 @@ where // into the C API. let ts_guard = unsafe { SuspendAttach::new() }; - // this trait is guarded by a rustc version config - // so clippy's MSRV check is wrong - #[allow(clippy::incompatible_msrv)] // By having detached here, we guarantee that `.get_or_init` cannot deadlock with // the Python interpreter let value = lock.get_or_init(move || { @@ -875,6 +890,7 @@ mod tests { } #[test] + #[allow(deprecated)] fn test_once_cell() { Python::attach(|py| { let mut cell = GILOnceCell::new(); @@ -900,6 +916,7 @@ mod tests { } #[test] + #[allow(deprecated)] fn test_once_cell_drop() { #[derive(Debug)] struct RecordDrop<'a>(&'a mut bool); diff --git a/src/sync/once_lock.rs b/src/sync/once_lock.rs new file mode 100644 index 00000000000..aaaeb31dfd1 --- /dev/null +++ b/src/sync/once_lock.rs @@ -0,0 +1,270 @@ +use crate::{ + internal::state::SuspendAttach, types::any::PyAnyMethods, Bound, Py, PyResult, PyTypeCheck, + Python, +}; + +/// An equivalent to [`std::sync::OnceLock`] for initializing objects while attached to +/// the Python interpreter. +/// +/// Unlike `OnceLock`, this type will not deadlock with the interpreter. +/// Before blocking calls the cell will detach from the runtime and then +/// re-attach once the cell is unblocked. +/// +/// # Re-entrant initialization +/// +/// Like `OnceLock`, it is an error to re-entrantly initialize a `PyOnceLock`. The exact +/// behavior in this case is not guaranteed, it may either deadlock or panic. +/// +/// # Examples +/// +/// The following example shows how to use `PyOnceLock` to share a reference to a Python list +/// between threads: +/// +/// ``` +/// use pyo3::sync::PyOnceLock; +/// use pyo3::prelude::*; +/// use pyo3::types::PyList; +/// +/// static LIST_CELL: PyOnceLock> = PyOnceLock::new(); +/// +/// pub fn get_shared_list(py: Python<'_>) -> &Bound<'_, PyList> { +/// LIST_CELL +/// .get_or_init(py, || PyList::empty(py).unbind()) +/// .bind(py) +/// } +/// # Python::attach(|py| assert_eq!(get_shared_list(py).len(), 0)); +/// ``` +#[derive(Default)] +pub struct PyOnceLock { + inner: once_cell::sync::OnceCell, +} + +impl PyOnceLock { + /// Create a `PyOnceLock` which does not yet contain a value. + pub const fn new() -> Self { + Self { + inner: once_cell::sync::OnceCell::new(), + } + } + + /// Get a reference to the contained value, or `None` if hte cell has not yet been written. + #[inline] + pub fn get(&self, _py: Python<'_>) -> Option<&T> { + self.inner.get() + } + + /// Get a reference to the contained value, initializing it if needed using the provided + /// closure. + /// + /// See the type-level documentation for detail on re-entrancy and concurrent initialization. + #[inline] + pub fn get_or_init(&self, py: Python<'_>, f: F) -> &T + where + F: FnOnce() -> T, + { + self.inner + .get() + .unwrap_or_else(|| init_once_cell_py_attached(&self.inner, py, f)) + } + + /// Like `get_or_init`, but accepts a fallible initialization function. If it fails, the cell + /// is left uninitialized. + /// + /// See the type-level documentation for detail on re-entrancy and concurrent initialization. + pub fn get_or_try_init(&self, py: Python<'_>, f: F) -> Result<&T, E> + where + F: FnOnce() -> Result, + { + self.inner + .get() + .map_or_else(|| try_init_once_cell_py_attached(&self.inner, py, f), Ok) + } + + /// Get the contents of the cell mutably. This is only possible if the reference to the cell is + /// unique. + pub fn get_mut(&mut self) -> Option<&mut T> { + self.inner.get_mut() + } + + /// Set the value in the cell. + /// + /// If the cell has already been written, `Err(value)` will be returned containing the new + /// value which was not written. + pub fn set(&self, _py: Python<'_>, value: T) -> Result<(), T> { + self.inner.set(value) + } + + /// Takes the value out of the cell, moving it back to an uninitialized state. + /// + /// Has no effect and returns None if the cell has not yet been written. + pub fn take(&mut self) -> Option { + self.inner.take() + } + + /// Consumes the cell, returning the wrapped value. + /// + /// Returns None if the cell has not yet been written. + pub fn into_inner(self) -> Option { + self.inner.into_inner() + } +} + +impl PyOnceLock> { + /// Creates a new cell that contains a new Python reference to the same contained object. + /// + /// Returns an uninitialized cell if `self` has not yet been initialized. + pub fn clone_ref(&self, py: Python<'_>) -> Self { + let cloned = PyOnceLock::new(); + if let Some(value) = self.get(py) { + let _ = cloned.set(py, value.clone_ref(py)); + } + cloned + } +} + +impl PyOnceLock> +where + T: PyTypeCheck, +{ + /// This is a shorthand method for `get_or_init` which imports the type from Python on init. + /// + /// # Example: Using `PyOnceLock` to store a class in a static variable. + /// + /// `PyOnceLock` can be used to avoid importing a class multiple times: + /// ``` + /// # use pyo3::prelude::*; + /// # use pyo3::sync::PyOnceLock; + /// # use pyo3::types::{PyDict, PyType}; + /// # use pyo3::intern; + /// # + /// #[pyfunction] + /// fn create_ordered_dict<'py>(py: Python<'py>, dict: Bound<'py, PyDict>) -> PyResult> { + /// // Even if this function is called multiple times, + /// // the `OrderedDict` class will be imported only once. + /// static ORDERED_DICT: PyOnceLock> = PyOnceLock::new(); + /// ORDERED_DICT + /// .import(py, "collections", "OrderedDict")? + /// .call1((dict,)) + /// } + /// + /// # Python::attach(|py| { + /// # let dict = PyDict::new(py); + /// # dict.set_item(intern!(py, "foo"), 42).unwrap(); + /// # let fun = wrap_pyfunction!(create_ordered_dict, py).unwrap(); + /// # let ordered_dict = fun.call1((&dict,)).unwrap(); + /// # assert!(dict.eq(ordered_dict).unwrap()); + /// # }); + /// ``` + pub fn import<'py>( + &self, + py: Python<'py>, + module_name: &str, + attr_name: &str, + ) -> PyResult<&Bound<'py, T>> { + self.get_or_try_init(py, || { + let type_object = py + .import(module_name)? + .getattr(attr_name)? + .downcast_into()?; + Ok(type_object.unbind()) + }) + .map(|ty| ty.bind(py)) + } +} + +#[cold] +fn init_once_cell_py_attached<'a, F, T>( + cell: &'a once_cell::sync::OnceCell, + _py: Python<'_>, + f: F, +) -> &'a T +where + F: FnOnce() -> T, +{ + // SAFETY: detach from the runtime right before a possibly blocking call + // then reattach when the blocking call completes and before calling + // into the C API. + let ts_guard = unsafe { SuspendAttach::new() }; + + // By having detached here, we guarantee that `.get_or_init` cannot deadlock with + // the Python interpreter + cell.get_or_init(move || { + drop(ts_guard); + f() + }) +} + +#[cold] +fn try_init_once_cell_py_attached<'a, F, T, E>( + cell: &'a once_cell::sync::OnceCell, + _py: Python<'_>, + f: F, +) -> Result<&'a T, E> +where + F: FnOnce() -> Result, +{ + // SAFETY: detach from the runtime right before a possibly blocking call + // then reattach when the blocking call completes and before calling + // into the C API. + let ts_guard = unsafe { SuspendAttach::new() }; + + // By having detached here, we guarantee that `.get_or_init` cannot deadlock with + // the Python interpreter + cell.get_or_try_init(move || { + drop(ts_guard); + f() + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_once_cell() { + Python::attach(|py| { + let mut cell = PyOnceLock::new(); + + assert!(cell.get(py).is_none()); + + assert_eq!(cell.get_or_try_init(py, || Err(5)), Err(5)); + assert!(cell.get(py).is_none()); + + assert_eq!(cell.get_or_try_init(py, || Ok::<_, ()>(2)), Ok(&2)); + assert_eq!(cell.get(py), Some(&2)); + + assert_eq!(cell.get_or_try_init(py, || Err(5)), Ok(&2)); + + assert_eq!(cell.take(), Some(2)); + assert_eq!(cell.into_inner(), None); + + let cell_py = PyOnceLock::new(); + assert!(cell_py.clone_ref(py).get(py).is_none()); + cell_py.get_or_init(py, || py.None()); + assert!(cell_py.clone_ref(py).get(py).unwrap().is_none(py)); + }) + } + + #[test] + fn test_once_cell_drop() { + #[derive(Debug)] + struct RecordDrop<'a>(&'a mut bool); + + impl Drop for RecordDrop<'_> { + fn drop(&mut self) { + *self.0 = true; + } + } + + Python::attach(|py| { + let mut dropped = false; + let cell = PyOnceLock::new(); + cell.set(py, RecordDrop(&mut dropped)).unwrap(); + let drop_container = cell.get(py).unwrap(); + + assert!(!*drop_container.0); + drop(cell); + assert!(dropped); + }); + } +} diff --git a/src/types/code.rs b/src/types/code.rs index 450e586abc9..dbd12ee6075 100644 --- a/src/types/code.rs +++ b/src/types/code.rs @@ -30,8 +30,8 @@ impl crate::PyTypeCheck for PyCode { fn type_check(object: &Bound<'_, PyAny>) -> bool { let py = object.py(); - static TYPE: crate::sync::GILOnceCell> = - crate::sync::GILOnceCell::new(); + static TYPE: crate::sync::PyOnceLock> = + crate::sync::PyOnceLock::new(); TYPE.import(py, "types", "CodeType") .and_then(|ty| object.is_instance(ty)) diff --git a/src/types/datetime.rs b/src/types/datetime.rs index d519d8a69d0..adca98e9878 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -21,7 +21,7 @@ use crate::ffi::{PyDateTime_DATE_GET_TZINFO, PyDateTime_TIME_GET_TZINFO, Py_IsNo use crate::types::{any::PyAnyMethods, PyString, PyType}; #[cfg(not(Py_LIMITED_API))] use crate::{ffi_ptr_ext::FfiPtrExt, py_result_ext::PyResultExt, types::PyTuple, BoundObject}; -use crate::{sync::GILOnceCell, Py}; +use crate::{sync::PyOnceLock, Py}; #[cfg(Py_LIMITED_API)] use crate::{types::IntoPyDict, PyTypeCheck}; use crate::{Borrowed, Bound, IntoPyObject, PyAny, PyErr, Python}; @@ -63,7 +63,7 @@ impl DatetimeTypes { } fn try_get(py: Python<'_>) -> PyResult<&Self> { - static TYPES: GILOnceCell = GILOnceCell::new(); + static TYPES: PyOnceLock = PyOnceLock::new(); TYPES.get_or_try_init(py, || { let datetime = py.import("datetime")?; Ok::<_, PyErr>(Self { @@ -801,7 +801,7 @@ impl PyTzInfo { #[cfg(Py_LIMITED_API)] { - static UTC: GILOnceCell> = GILOnceCell::new(); + static UTC: PyOnceLock> = PyOnceLock::new(); UTC.get_or_try_init(py, || { Ok(DatetimeTypes::get(py) .timezone @@ -819,7 +819,7 @@ impl PyTzInfo { where T: IntoPyObject<'py, Target = PyString>, { - static ZONE_INFO: GILOnceCell> = GILOnceCell::new(); + static ZONE_INFO: PyOnceLock> = PyOnceLock::new(); let zoneinfo = ZONE_INFO.import(py, "zoneinfo", "ZoneInfo"); diff --git a/src/types/mapping.rs b/src/types/mapping.rs index f0d039d4028..8887f19db9e 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -3,7 +3,7 @@ use crate::err::PyResult; use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Bound; use crate::py_result_ext::PyResultExt; -use crate::sync::GILOnceCell; +use crate::sync::PyOnceLock; use crate::type_object::PyTypeInfo; use crate::types::any::PyAnyMethods; use crate::types::{PyAny, PyDict, PyList, PyType}; @@ -161,7 +161,7 @@ impl<'py> PyMappingMethods<'py> for Bound<'py, PyMapping> { } fn get_mapping_abc(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { - static MAPPING_ABC: GILOnceCell> = GILOnceCell::new(); + static MAPPING_ABC: PyOnceLock> = PyOnceLock::new(); MAPPING_ABC.import(py, "collections.abc", "Mapping") } diff --git a/src/types/module.rs b/src/types/module.rs index ba4aa9428f7..4d3f3f0364c 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -80,7 +80,7 @@ impl PyModule { /// ``` /// /// If you want to import a class, you can store a reference to it with - /// [`GILOnceCell::import`][crate::sync::GILOnceCell#method.import]. + /// [`PyOnceLock::import`][crate::sync::PyOnceLock#method.import]. pub fn import<'py, N>(py: Python<'py>, name: N) -> PyResult> where N: IntoPyObject<'py, Target = PyString>, diff --git a/src/types/sequence.rs b/src/types/sequence.rs index de6c25a193c..59849fdd7b2 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -6,7 +6,7 @@ use crate::inspect::types::TypeInfo; use crate::instance::Bound; use crate::internal_tricks::get_ssize_index; use crate::py_result_ext::PyResultExt; -use crate::sync::GILOnceCell; +use crate::sync::PyOnceLock; use crate::type_object::PyTypeInfo; use crate::types::{any::PyAnyMethods, PyAny, PyList, PyString, PyTuple, PyType}; use crate::{ @@ -370,7 +370,7 @@ where } fn get_sequence_abc(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { - static SEQUENCE_ABC: GILOnceCell> = GILOnceCell::new(); + static SEQUENCE_ABC: PyOnceLock> = PyOnceLock::new(); SEQUENCE_ABC.import(py, "collections.abc", "Sequence") } diff --git a/tests/test_class_new.rs b/tests/test_class_new.rs index ce1d9b83ef1..7f56ebabc23 100644 --- a/tests/test_class_new.rs +++ b/tests/test_class_new.rs @@ -2,7 +2,7 @@ use pyo3::exceptions::PyValueError; use pyo3::prelude::*; -use pyo3::sync::GILOnceCell; +use pyo3::sync::PyOnceLock; use pyo3::types::IntoPyDict; #[pyclass] @@ -216,7 +216,7 @@ struct NewExisting { impl NewExisting { #[new] fn new(py: pyo3::Python<'_>, val: usize) -> pyo3::Py { - static PRE_BUILT: GILOnceCell<[pyo3::Py; 2]> = GILOnceCell::new(); + static PRE_BUILT: PyOnceLock<[pyo3::Py; 2]> = PyOnceLock::new(); let existing = PRE_BUILT.get_or_init(py, || { [ pyo3::Py::new(py, NewExisting { num: 0 }).unwrap(), From d6aecc123be3778419e0d3dcdae5d223ee3ad640 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 20 Aug 2025 21:45:59 +0100 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- guide/src/free-threading.md | 2 -- src/impl_/exceptions.rs | 2 +- src/sync/once_lock.rs | 2 +- src/types/module.rs | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index 8ca72714bfa..8cff3b03384 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -409,8 +409,6 @@ interpreter. [`Once`]: https://doc.rust-lang.org/stable/std/sync/struct.Once.html [`Once::call_once`]: https://doc.rust-lang.org/stable/std/sync/struct.Once.html#method.call_once [`Once::call_once_force`]: https://doc.rust-lang.org/stable/std/sync/struct.Once.html#method.call_once_force -[`OnceCell`]: https://docs.rs/once_cell/latest/once_cell/sync/struct.OnceCell.html -[`OnceCellExt`]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceCellExt.html [`OnceExt`]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceExt.html [`OnceExt::call_once_py_attached`]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceExt.html#tymethod.call_once_py_attached [`OnceExt::call_once_force_py_attached`]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceExt.html#tymethod.call_once_force_py_attached diff --git a/src/impl_/exceptions.rs b/src/impl_/exceptions.rs index cb15e04e4b9..1f1c2d36a6f 100644 --- a/src/impl_/exceptions.rs +++ b/src/impl_/exceptions.rs @@ -18,7 +18,7 @@ impl ImportedExceptionTypeObject { pub fn get<'py>(&self, py: Python<'py>) -> &Bound<'py, PyType> { self.imported_value .import(py, self.module, self.name) - .unwrap_or_else(|e: PyErr| { + .unwrap_or_else(|e| { panic!( "failed to import exception {}.{}: {}", self.module, self.name, e diff --git a/src/sync/once_lock.rs b/src/sync/once_lock.rs index aaaeb31dfd1..fd608215049 100644 --- a/src/sync/once_lock.rs +++ b/src/sync/once_lock.rs @@ -47,7 +47,7 @@ impl PyOnceLock { } } - /// Get a reference to the contained value, or `None` if hte cell has not yet been written. + /// Get a reference to the contained value, or `None` if the cell has not yet been written. #[inline] pub fn get(&self, _py: Python<'_>) -> Option<&T> { self.inner.get() diff --git a/src/types/module.rs b/src/types/module.rs index 4d3f3f0364c..72ba2d341d9 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -80,7 +80,7 @@ impl PyModule { /// ``` /// /// If you want to import a class, you can store a reference to it with - /// [`PyOnceLock::import`][crate::sync::PyOnceLock#method.import]. + /// [`PyOnceLock::import`][crate::sync::PyOnceLock::import]. pub fn import<'py, N>(py: Python<'py>, name: N) -> PyResult> where N: IntoPyObject<'py, Target = PyString>, From 2ae76b6b7a24af3d4711f492b841f094d6a2d213 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 21 Aug 2025 19:22:17 +0100 Subject: [PATCH 3/3] Update src/impl_/exceptions.rs Co-authored-by: Nathan Goldbaum --- src/impl_/exceptions.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/impl_/exceptions.rs b/src/impl_/exceptions.rs index 1f1c2d36a6f..546f7e2f318 100644 --- a/src/impl_/exceptions.rs +++ b/src/impl_/exceptions.rs @@ -1,4 +1,4 @@ -use crate::{sync::PyOnceLock, types::PyType, Bound, Py, PyErr, Python}; +use crate::{sync::PyOnceLock, types::PyType, Bound, Py, Python}; pub struct ImportedExceptionTypeObject { imported_value: PyOnceLock>,