Vendor deps.
This commit is contained in:
1 parent
294527085e
commit
91e18fde3a
1714 files changed
+556667
-7
No files matched your search
Vendored
+284
@@ -0,0 +1,284 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use crate::{
|
||||
ffi,
|
||||
types::{PyType, PyTypeMethods},
|
||||
Borrowed, Bound,
|
||||
};
|
||||
use core::ffi::c_int;
|
||||
|
||||
impl Bound<'_, PyType> {
|
||||
#[inline]
|
||||
pub(crate) fn get_slot<const S: c_int>(&self, slot: Slot<S>) -> <Slot<S> as GetSlotImpl>::Type
|
||||
where
|
||||
Slot<S>: GetSlotImpl,
|
||||
{
|
||||
// SAFETY: `self` is a valid type object.
|
||||
unsafe {
|
||||
slot.get_slot(
|
||||
self.as_type_ptr(),
|
||||
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))]
|
||||
is_runtime_3_10(self.py()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Borrowed<'_, '_, PyType> {
|
||||
#[inline]
|
||||
pub(crate) fn get_slot<const S: c_int>(self, slot: Slot<S>) -> <Slot<S> as GetSlotImpl>::Type
|
||||
where
|
||||
Slot<S>: GetSlotImpl,
|
||||
{
|
||||
// SAFETY: `self` is a valid type object.
|
||||
unsafe {
|
||||
slot.get_slot(
|
||||
self.as_type_ptr(),
|
||||
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))]
|
||||
is_runtime_3_10(self.py()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a slot from a raw FFI pointer.
|
||||
///
|
||||
/// Safety:
|
||||
/// - `ty` must be a valid non-null pointer to a `PyTypeObject`.
|
||||
/// - The Python runtime must be initialized
|
||||
pub(crate) unsafe fn get_slot<const S: c_int>(
|
||||
ty: *mut ffi::PyTypeObject,
|
||||
slot: Slot<S>,
|
||||
) -> <Slot<S> as GetSlotImpl>::Type
|
||||
where
|
||||
Slot<S>: GetSlotImpl,
|
||||
{
|
||||
unsafe {
|
||||
slot.get_slot(
|
||||
ty,
|
||||
// SAFETY: the Python runtime is initialized
|
||||
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))]
|
||||
is_runtime_3_10(crate::Python::assume_attached()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait GetSlotImpl {
|
||||
type Type;
|
||||
|
||||
/// Gets the requested slot from a type object.
|
||||
///
|
||||
/// Safety:
|
||||
/// - `ty` must be a valid non-null pointer to a `PyTypeObject`.
|
||||
/// - `is_runtime_3_10` must be `false` if the runtime is not Python 3.10 or later.
|
||||
unsafe fn get_slot(
|
||||
self,
|
||||
ty: *mut ffi::PyTypeObject,
|
||||
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))] is_runtime_3_10: bool,
|
||||
) -> Self::Type;
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub(crate) struct Slot<const S: c_int>;
|
||||
|
||||
macro_rules! impl_slots {
|
||||
($($name:ident: ($slot:ident, $field:ident) -> $tp:ty),+ $(,)?) => {
|
||||
$(
|
||||
pub (crate) const $name: Slot<{ ffi::$slot }> = Slot;
|
||||
|
||||
impl GetSlotImpl for Slot<{ ffi::$slot }> {
|
||||
type Type = $tp;
|
||||
|
||||
#[inline]
|
||||
unsafe fn get_slot(
|
||||
self,
|
||||
ty: *mut ffi::PyTypeObject,
|
||||
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))] is_runtime_3_10: bool
|
||||
) -> Self::Type {
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
{
|
||||
unsafe {(*ty).$field }
|
||||
}
|
||||
|
||||
#[cfg(Py_LIMITED_API)]
|
||||
{
|
||||
#[cfg(not(Py_3_10))]
|
||||
{
|
||||
// Calling PyType_GetSlot on static types is not valid before Python 3.10
|
||||
// ... so the workaround is to first do a runtime check for these versions
|
||||
// (3.8, 3.9) and then look in the type object anyway. This is only ok
|
||||
// because we know that the interpreter is not going to change the size
|
||||
// of the type objects for these historical versions.
|
||||
if !is_runtime_3_10 && unsafe {ffi::PyType_HasFeature(ty, ffi::Py_TPFLAGS_HEAPTYPE)} == 0
|
||||
{
|
||||
return unsafe {(*ty.cast::<PyTypeObject39Snapshot>()).$field};
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: slot type is set carefully to be valid
|
||||
unsafe {core::mem::transmute(ffi::PyType_GetSlot(ty, ffi::$slot))}
|
||||
}
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
// Slots are implemented on-demand as needed.)
|
||||
impl_slots! {
|
||||
TP_NEW: (Py_tp_new, tp_new) -> Option<ffi::newfunc>,
|
||||
TP_DEALLOC: (Py_tp_dealloc, tp_dealloc) -> Option<ffi::destructor>,
|
||||
TP_BASE: (Py_tp_base, tp_base) -> *mut ffi::PyTypeObject,
|
||||
TP_CLEAR: (Py_tp_clear, tp_clear) -> Option<ffi::inquiry>,
|
||||
TP_DESCR_GET: (Py_tp_descr_get, tp_descr_get) -> Option<ffi::descrgetfunc>,
|
||||
TP_FREE: (Py_tp_free, tp_free) -> Option<ffi::freefunc>,
|
||||
TP_TRAVERSE: (Py_tp_traverse, tp_traverse) -> Option<ffi::traverseproc>,
|
||||
}
|
||||
|
||||
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))]
|
||||
fn is_runtime_3_10(py: crate::Python<'_>) -> bool {
|
||||
use crate::sync::PyOnceLock;
|
||||
|
||||
static IS_RUNTIME_3_10: PyOnceLock<bool> = PyOnceLock::new();
|
||||
*IS_RUNTIME_3_10.get_or_init(py, || py.version_info() >= (3, 10))
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))]
|
||||
pub struct PyNumberMethods39Snapshot {
|
||||
pub nb_add: Option<ffi::binaryfunc>,
|
||||
pub nb_subtract: Option<ffi::binaryfunc>,
|
||||
pub nb_multiply: Option<ffi::binaryfunc>,
|
||||
pub nb_remainder: Option<ffi::binaryfunc>,
|
||||
pub nb_divmod: Option<ffi::binaryfunc>,
|
||||
pub nb_power: Option<ffi::ternaryfunc>,
|
||||
pub nb_negative: Option<ffi::unaryfunc>,
|
||||
pub nb_positive: Option<ffi::unaryfunc>,
|
||||
pub nb_absolute: Option<ffi::unaryfunc>,
|
||||
pub nb_bool: Option<ffi::inquiry>,
|
||||
pub nb_invert: Option<ffi::unaryfunc>,
|
||||
pub nb_lshift: Option<ffi::binaryfunc>,
|
||||
pub nb_rshift: Option<ffi::binaryfunc>,
|
||||
pub nb_and: Option<ffi::binaryfunc>,
|
||||
pub nb_xor: Option<ffi::binaryfunc>,
|
||||
pub nb_or: Option<ffi::binaryfunc>,
|
||||
pub nb_int: Option<ffi::unaryfunc>,
|
||||
pub nb_reserved: *mut core::ffi::c_void,
|
||||
pub nb_float: Option<ffi::unaryfunc>,
|
||||
pub nb_inplace_add: Option<ffi::binaryfunc>,
|
||||
pub nb_inplace_subtract: Option<ffi::binaryfunc>,
|
||||
pub nb_inplace_multiply: Option<ffi::binaryfunc>,
|
||||
pub nb_inplace_remainder: Option<ffi::binaryfunc>,
|
||||
pub nb_inplace_power: Option<ffi::ternaryfunc>,
|
||||
pub nb_inplace_lshift: Option<ffi::binaryfunc>,
|
||||
pub nb_inplace_rshift: Option<ffi::binaryfunc>,
|
||||
pub nb_inplace_and: Option<ffi::binaryfunc>,
|
||||
pub nb_inplace_xor: Option<ffi::binaryfunc>,
|
||||
pub nb_inplace_or: Option<ffi::binaryfunc>,
|
||||
pub nb_floor_divide: Option<ffi::binaryfunc>,
|
||||
pub nb_true_divide: Option<ffi::binaryfunc>,
|
||||
pub nb_inplace_floor_divide: Option<ffi::binaryfunc>,
|
||||
pub nb_inplace_true_divide: Option<ffi::binaryfunc>,
|
||||
pub nb_index: Option<ffi::unaryfunc>,
|
||||
pub nb_matrix_multiply: Option<ffi::binaryfunc>,
|
||||
pub nb_inplace_matrix_multiply: Option<ffi::binaryfunc>,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))]
|
||||
pub struct PySequenceMethods39Snapshot {
|
||||
pub sq_length: Option<ffi::lenfunc>,
|
||||
pub sq_concat: Option<ffi::binaryfunc>,
|
||||
pub sq_repeat: Option<ffi::ssizeargfunc>,
|
||||
pub sq_item: Option<ffi::ssizeargfunc>,
|
||||
pub was_sq_slice: *mut core::ffi::c_void,
|
||||
pub sq_ass_item: Option<ffi::ssizeobjargproc>,
|
||||
pub was_sq_ass_slice: *mut core::ffi::c_void,
|
||||
pub sq_contains: Option<ffi::objobjproc>,
|
||||
pub sq_inplace_concat: Option<ffi::binaryfunc>,
|
||||
pub sq_inplace_repeat: Option<ffi::ssizeargfunc>,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))]
|
||||
pub struct PyMappingMethods39Snapshot {
|
||||
pub mp_length: Option<ffi::lenfunc>,
|
||||
pub mp_subscript: Option<ffi::binaryfunc>,
|
||||
pub mp_ass_subscript: Option<ffi::objobjargproc>,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))]
|
||||
pub struct PyAsyncMethods39Snapshot {
|
||||
pub am_await: Option<ffi::unaryfunc>,
|
||||
pub am_aiter: Option<ffi::unaryfunc>,
|
||||
pub am_anext: Option<ffi::unaryfunc>,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))]
|
||||
pub struct PyBufferProcs39Snapshot {
|
||||
// not available in limited api, but structure needs to have the right size
|
||||
pub bf_getbuffer: *mut core::ffi::c_void,
|
||||
pub bf_releasebuffer: *mut core::ffi::c_void,
|
||||
}
|
||||
|
||||
/// Snapshot of the structure of PyTypeObject for Python 3.8 through 3.9.
|
||||
///
|
||||
/// This is used as a fallback for static types in abi3 when the Python version is less than 3.10;
|
||||
/// this is a bit of a hack but there's no better option and the structure of the type object is
|
||||
/// not going to change for those historical versions.
|
||||
#[repr(C)]
|
||||
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))]
|
||||
struct PyTypeObject39Snapshot {
|
||||
pub ob_base: ffi::PyVarObject,
|
||||
pub tp_name: *const core::ffi::c_char,
|
||||
pub tp_basicsize: ffi::Py_ssize_t,
|
||||
pub tp_itemsize: ffi::Py_ssize_t,
|
||||
pub tp_dealloc: Option<ffi::destructor>,
|
||||
pub tp_vectorcall_offset: ffi::Py_ssize_t,
|
||||
pub tp_getattr: Option<ffi::getattrfunc>,
|
||||
pub tp_setattr: Option<ffi::setattrfunc>,
|
||||
pub tp_as_async: *mut PyAsyncMethods39Snapshot,
|
||||
pub tp_repr: Option<ffi::reprfunc>,
|
||||
pub tp_as_number: *mut PyNumberMethods39Snapshot,
|
||||
pub tp_as_sequence: *mut PySequenceMethods39Snapshot,
|
||||
pub tp_as_mapping: *mut PyMappingMethods39Snapshot,
|
||||
pub tp_hash: Option<ffi::hashfunc>,
|
||||
pub tp_call: Option<ffi::ternaryfunc>,
|
||||
pub tp_str: Option<ffi::reprfunc>,
|
||||
pub tp_getattro: Option<ffi::getattrofunc>,
|
||||
pub tp_setattro: Option<ffi::setattrofunc>,
|
||||
pub tp_as_buffer: *mut PyBufferProcs39Snapshot,
|
||||
pub tp_flags: core::ffi::c_ulong,
|
||||
pub tp_doc: *const core::ffi::c_char,
|
||||
pub tp_traverse: Option<ffi::traverseproc>,
|
||||
pub tp_clear: Option<ffi::inquiry>,
|
||||
pub tp_richcompare: Option<ffi::richcmpfunc>,
|
||||
pub tp_weaklistoffset: ffi::Py_ssize_t,
|
||||
pub tp_iter: Option<ffi::getiterfunc>,
|
||||
pub tp_iternext: Option<ffi::iternextfunc>,
|
||||
pub tp_methods: *mut ffi::PyMethodDef,
|
||||
pub tp_members: *mut ffi::PyMemberDef,
|
||||
pub tp_getset: *mut ffi::PyGetSetDef,
|
||||
pub tp_base: *mut ffi::PyTypeObject,
|
||||
pub tp_dict: *mut ffi::PyObject,
|
||||
pub tp_descr_get: Option<ffi::descrgetfunc>,
|
||||
pub tp_descr_set: Option<ffi::descrsetfunc>,
|
||||
pub tp_dictoffset: ffi::Py_ssize_t,
|
||||
pub tp_init: Option<ffi::initproc>,
|
||||
pub tp_alloc: Option<ffi::allocfunc>,
|
||||
pub tp_new: Option<ffi::newfunc>,
|
||||
pub tp_free: Option<ffi::freefunc>,
|
||||
pub tp_is_gc: Option<ffi::inquiry>,
|
||||
pub tp_bases: *mut ffi::PyObject,
|
||||
pub tp_mro: *mut ffi::PyObject,
|
||||
pub tp_cache: *mut ffi::PyObject,
|
||||
pub tp_subclasses: *mut ffi::PyObject,
|
||||
pub tp_weaklist: *mut ffi::PyObject,
|
||||
pub tp_del: Option<ffi::destructor>,
|
||||
pub tp_version_tag: core::ffi::c_uint,
|
||||
pub tp_finalize: Option<ffi::destructor>,
|
||||
pub tp_vectorcall: Option<ffi::vectorcallfunc>,
|
||||
}
|
||||
Vendored
+90
@@ -0,0 +1,90 @@
|
||||
/// Polyfill of `cfg_select` for MSRV < 1.95
|
||||
///
|
||||
/// Note that this polyfill does not work in type position, so it is not as powerful as the
|
||||
/// real `cfg_select` macro. That is a limitation that PyO3 can live with for now.
|
||||
#[doc(hidden)]
|
||||
#[cfg(not(cfg_select))]
|
||||
macro_rules! cfg_select {
|
||||
// Final arm with _ to match any non-existing clauses
|
||||
(
|
||||
@parsed($($clauses:meta),*)
|
||||
_ => $final_arm:expr $(,)?
|
||||
) => {
|
||||
#[cfg(not(any($($clauses),+)))]
|
||||
{
|
||||
$final_arm
|
||||
}
|
||||
};
|
||||
|
||||
// Final arm has a clause, handle it plus a compile error if no clauses match
|
||||
// Need to allow for trailing comma on final expression to be optional
|
||||
(
|
||||
@parsed($($clauses:meta),*)
|
||||
$cfg:meta => $final_arm:expr $(,)?
|
||||
) => {
|
||||
#[cfg(all($cfg, not(any($($clauses),*))))]
|
||||
{
|
||||
$final_arm
|
||||
}
|
||||
|
||||
#[cfg(not(any($($clauses,)* $cfg)))]
|
||||
compile_error!(
|
||||
"cfg_select! requires a final `_ => expr` arm to be used as a fallback when no other cfg matches"
|
||||
);
|
||||
};
|
||||
|
||||
// Non-terminating expression arm requires trailing comma
|
||||
(
|
||||
@parsed($($clauses:meta),*)
|
||||
$cfg:meta => $arm:expr, $($rest:tt)*
|
||||
) => {
|
||||
#[cfg(all($cfg, not(any($($clauses),*))))]
|
||||
{
|
||||
$arm
|
||||
}
|
||||
|
||||
cfg_select! {
|
||||
@parsed($($clauses,)* $cfg)
|
||||
$($rest)*
|
||||
}
|
||||
};
|
||||
|
||||
// Non-terminating block doesn't require trailing comma
|
||||
(
|
||||
@parsed($($clauses:meta),*)
|
||||
$cfg:meta => $arm:block $($rest:tt)*
|
||||
) => {
|
||||
#[cfg(all($cfg, not(any($($clauses),*))))]
|
||||
$arm
|
||||
|
||||
cfg_select! {
|
||||
@parsed($($clauses,)* $cfg)
|
||||
$($rest)*
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
$cfg:meta => $($rest:tt)*
|
||||
) => {
|
||||
{
|
||||
cfg_select! {
|
||||
@parsed()
|
||||
$cfg => $($rest)*
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
#[cfg(not(cfg_select))]
|
||||
fn test_cfg_select_polyfill_short_circuit() {
|
||||
cfg_select! {
|
||||
all() => {},
|
||||
all() => {
|
||||
unreachable!("the first arm should be selected, so this arm should not be evaluated");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
//! Contains initialization utilities for `#[pyclass]`.
|
||||
use crate::exceptions::PyTypeError;
|
||||
use crate::ffi_ptr_ext::FfiPtrExt;
|
||||
use crate::impl_::pyclass::PyClassBaseType;
|
||||
use crate::internal::get_slot::TP_NEW;
|
||||
use crate::types::{PyTuple, PyType, PyTypeMethods};
|
||||
use crate::{
|
||||
ffi, IntoPyObject, IntoPyObjectExt, PyClass, PyClassInitializer, PyErr, PyResult, Python,
|
||||
};
|
||||
use crate::{ffi::PyTypeObject, type_object::PyTypeInfo};
|
||||
use core::marker::PhantomData;
|
||||
|
||||
/// Initializer for Python types.
|
||||
///
|
||||
/// This trait is used internally for distinguishing `#[pyclass]` and
|
||||
/// Python native types.
|
||||
pub(crate) trait PyObjectInit<T>: Sized {
|
||||
/// # Safety
|
||||
/// - `subtype` must be a valid pointer to a type object of T or a subclass.
|
||||
unsafe fn into_new_object(
|
||||
self,
|
||||
py: Python<'_>,
|
||||
subtype: *mut PyTypeObject,
|
||||
) -> PyResult<*mut ffi::PyObject>;
|
||||
}
|
||||
|
||||
/// Initializer for Python native types, like `PyDict`.
|
||||
pub struct PyNativeTypeInitializer<T: PyTypeInfo>(pub PhantomData<T>);
|
||||
|
||||
impl<T: PyTypeInfo> PyObjectInit<T> for PyNativeTypeInitializer<T> {
|
||||
unsafe fn into_new_object(
|
||||
self,
|
||||
py: Python<'_>,
|
||||
subtype: *mut PyTypeObject,
|
||||
) -> PyResult<*mut ffi::PyObject> {
|
||||
unsafe fn inner(
|
||||
py: Python<'_>,
|
||||
type_ptr: *mut PyTypeObject,
|
||||
subtype: *mut PyTypeObject,
|
||||
) -> PyResult<*mut ffi::PyObject> {
|
||||
let tp_new = unsafe {
|
||||
type_ptr
|
||||
.cast::<ffi::PyObject>()
|
||||
.assume_borrowed_unchecked(py)
|
||||
.cast_unchecked::<PyType>()
|
||||
.get_slot(TP_NEW)
|
||||
.ok_or_else(|| PyTypeError::new_err("base type without tp_new"))?
|
||||
};
|
||||
|
||||
// TODO: make it possible to provide real arguments to the base tp_new
|
||||
let obj =
|
||||
unsafe { tp_new(subtype, PyTuple::empty(py).as_ptr(), core::ptr::null_mut()) };
|
||||
if obj.is_null() {
|
||||
Err(PyErr::fetch(py))
|
||||
} else {
|
||||
Ok(obj)
|
||||
}
|
||||
}
|
||||
unsafe { inner(py, T::type_object_raw(py), subtype) }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TpNewValueTypeResolver<ClassT, ValueT>(TpNewTupleResolver<ClassT, ValueT>);
|
||||
pub struct TpNewTupleResolver<ClassT, ValueT>(
|
||||
ResolveToArbitraryObject,
|
||||
PhantomData<(ClassT, ValueT)>,
|
||||
);
|
||||
pub struct ResolveToArbitraryObject(());
|
||||
|
||||
/// First step of machinery for resolving the type of `#[new]` return values, which is used to
|
||||
/// implement specialization for the various cases.
|
||||
///
|
||||
/// Call `.resolve()` on the returned tag to get the final tag type.
|
||||
///
|
||||
/// This resolution step is necessary in order to encode the preference to go via `PyClassInitializer<T>`
|
||||
/// for new instances of `ClassT`. Without this step, the fallback to `IntoPyObject` conflicts for
|
||||
/// `ClassT` because that implementation ignores the `cls` parameter for `PyClassInit` (and would
|
||||
/// therefore be incorrect when instantiating subclasses).
|
||||
pub fn tp_new_resolver<ClassT, ValueT>(_: &ValueT) -> TpNewValueTypeResolver<ClassT, ValueT> {
|
||||
TpNewValueTypeResolver(TpNewTupleResolver(
|
||||
ResolveToArbitraryObject(()),
|
||||
PhantomData,
|
||||
))
|
||||
}
|
||||
|
||||
impl<ClassT, ValueT> core::ops::Deref for TpNewValueTypeResolver<ClassT, ValueT> {
|
||||
type Target = TpNewTupleResolver<ClassT, ValueT>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<ClassT, ValueT> core::ops::Deref for TpNewTupleResolver<ClassT, ValueT> {
|
||||
type Target = ResolveToArbitraryObject;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Second stage of machinery for resolving the type of `#[new]` return values. This
|
||||
/// is the specialized implementation for the case where the return type should be
|
||||
/// used to build a new class object (`T`, `PyClassInitializer<T>`, `(S, B)` tuples).
|
||||
#[expect(private_bounds, reason = "internal trait implementation")]
|
||||
impl<ClassT, ValueT> TpNewValueTypeResolver<ClassT, ValueT>
|
||||
where
|
||||
ClassT: PyClass,
|
||||
ValueT: IntoPyClassInitializer<ClassT>,
|
||||
{
|
||||
pub fn resolve(&self, value: ValueT) -> PyClassInitializer<ClassT> {
|
||||
value.into_pyclass_initializer()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, B> TpNewTupleResolver<S, (S, B)>
|
||||
where
|
||||
S: PyClass<BaseType = B>,
|
||||
B: PyClass + PyClassBaseType<Initializer = PyClassInitializer<B>>,
|
||||
B::BaseType: PyClassBaseType<Initializer = PyNativeTypeInitializer<B::BaseType>>,
|
||||
{
|
||||
#[deprecated(
|
||||
since = "0.29.0",
|
||||
note = "Tuple syntax for super class initialization is phased out. Use `PyClassInitializer` instead."
|
||||
)]
|
||||
pub fn resolve(&self, value: (S, B)) -> PyClassInitializer<S> {
|
||||
value.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// All other conversions fall back to IntoPyObject via the deref implementation
|
||||
impl ResolveToArbitraryObject {
|
||||
pub fn resolve<ValueT>(&self, value: ValueT) -> ValueT {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
#[diagnostic::on_unimplemented(
|
||||
message = "`{Self}` cannot be used as the return value for `#[new]` methods",
|
||||
note = "all types which implement `IntoPyObject` are suitable return values for `#[new]` methods",
|
||||
note = "`PyClassInitializer<T: PyClass>` may also be used and is necessary for `#[pyclass(extends = '...')]` types",
|
||||
label = "must be a type which implements `IntoPyObject`, a `PyClassInitializer<{Self}>`, or a `Result<T>` wrapping such types"
|
||||
)]
|
||||
pub(crate) trait PyClassInit<'py, T> {
|
||||
/// # Safety
|
||||
/// - `cls` must be the type object for `T` (or a subclass)
|
||||
unsafe fn init(
|
||||
self,
|
||||
cls: crate::Borrowed<'_, 'py, crate::types::PyType>,
|
||||
) -> PyResult<crate::Bound<'py, crate::PyAny>>;
|
||||
}
|
||||
|
||||
impl<'py, ClassT, T> PyClassInit<'py, ClassT> for T
|
||||
where
|
||||
T: IntoPyObject<'py>,
|
||||
{
|
||||
unsafe fn init(
|
||||
self,
|
||||
cls: crate::Borrowed<'_, 'py, crate::types::PyType>,
|
||||
) -> PyResult<crate::Bound<'py, crate::PyAny>> {
|
||||
self.into_bound_py_any(cls.py())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, T> PyClassInit<'py, T> for PyClassInitializer<T>
|
||||
where
|
||||
T: PyClass,
|
||||
{
|
||||
unsafe fn init(
|
||||
self,
|
||||
cls: crate::Borrowed<'_, 'py, crate::types::PyType>,
|
||||
) -> PyResult<crate::Bound<'py, crate::PyAny>> {
|
||||
// SAFETY: caller has guaranteed that `cls` is correct object
|
||||
unsafe { self.create_class_object_of_type(cls.py(), cls.as_type_ptr()) }
|
||||
.map(crate::Bound::into_any)
|
||||
}
|
||||
}
|
||||
|
||||
/// Analogous to `Into<PyClassInitializer<T>>`, but just an internal equivalent
|
||||
/// to avoid allowing user code to define custom return types from `#[new]`.
|
||||
trait IntoPyClassInitializer<T: PyClass>: Sized {
|
||||
fn into_pyclass_initializer(self) -> PyClassInitializer<T>;
|
||||
}
|
||||
|
||||
impl<T: PyClass> IntoPyClassInitializer<T> for PyClassInitializer<T> {
|
||||
fn into_pyclass_initializer(self) -> PyClassInitializer<T> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoPyClassInitializer<T> for T
|
||||
where
|
||||
T: PyClass,
|
||||
T::BaseType: PyClassBaseType<Initializer = PyNativeTypeInitializer<T::BaseType>>,
|
||||
{
|
||||
fn into_pyclass_initializer(self) -> PyClassInitializer<T> {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
Vendored
+602
@@ -0,0 +1,602 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
//! Interaction with attachment of the current thread to the Python interpreter.
|
||||
|
||||
#[cfg(pyo3_disable_reference_pool)]
|
||||
use crate::impl_::panic::PanicTrap;
|
||||
use crate::{ffi, Python};
|
||||
|
||||
use core::cell::Cell;
|
||||
#[cfg_attr(pyo3_disable_reference_pool, allow(unused_imports))]
|
||||
use core::{mem, ptr::NonNull};
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
std::thread_local! {
|
||||
/// This is an internal counter in pyo3 monitoring whether this thread is attached to the interpreter.
|
||||
///
|
||||
/// It will be incremented whenever an AttachGuard is created, and decremented whenever
|
||||
/// they are dropped.
|
||||
///
|
||||
/// As a result, if this thread is attached to the interpreter, ATTACH_COUNT is greater than zero.
|
||||
///
|
||||
/// Additionally, we sometimes need to prevent safe access to the Python interpreter,
|
||||
/// e.g. when implementing `__traverse__`, which is represented by a negative value.
|
||||
static ATTACH_COUNT: Cell<isize> = const { Cell::new(0) };
|
||||
}
|
||||
|
||||
const ATTACH_FORBIDDEN_DURING_TRAVERSE: isize = -1;
|
||||
|
||||
/// Checks whether the thread is attached to the Python interpreter.
|
||||
///
|
||||
/// Note: This uses pyo3's internal count rather than PyGILState_Check for two reasons:
|
||||
/// 1) for performance
|
||||
/// 2) PyGILState_Check always returns 1 if the sub-interpreter APIs have ever been called,
|
||||
/// which could lead to incorrect conclusions that the thread is attached.
|
||||
#[inline(always)]
|
||||
pub(crate) fn thread_is_attached() -> bool {
|
||||
ATTACH_COUNT.try_with(|c| c.get() > 0).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// RAII type that represents thread attachment to the interpreter.
|
||||
pub(crate) enum AttachGuard {
|
||||
/// Indicates the thread was already attached when this AttachGuard was acquired.
|
||||
Assumed,
|
||||
/// Indicates that we attached when this AttachGuard was acquired
|
||||
Ensured { gstate: ffi::PyGILState_STATE },
|
||||
}
|
||||
|
||||
/// Possible error when calling `try_attach()`
|
||||
pub(crate) enum AttachError {
|
||||
/// Forbidden during GC traversal.
|
||||
ForbiddenDuringTraverse,
|
||||
/// The interpreter is not initialized.
|
||||
NotInitialized,
|
||||
#[cfg(Py_3_13)]
|
||||
/// The interpreter is finalizing.
|
||||
Finalizing,
|
||||
}
|
||||
|
||||
impl AttachGuard {
|
||||
/// PyO3 internal API for attaching to the Python interpreter. The public API is Python::attach.
|
||||
///
|
||||
/// If the thread was already attached via PyO3, this returns
|
||||
/// `AttachGuard::Assumed`. Otherwise, the thread will attach now and
|
||||
/// `AttachGuard::Ensured` will be returned.
|
||||
pub(crate) fn attach() -> Self {
|
||||
match Self::try_attach() {
|
||||
Ok(guard) => guard,
|
||||
Err(AttachError::ForbiddenDuringTraverse) => {
|
||||
panic!("{}", ForbidAttaching::FORBIDDEN_DURING_TRAVERSE)
|
||||
}
|
||||
Err(AttachError::NotInitialized) => {
|
||||
// try to initialize the interpreter and try again
|
||||
crate::interpreter_lifecycle::ensure_initialized();
|
||||
unsafe { Self::do_attach_unchecked() }
|
||||
}
|
||||
#[cfg(Py_3_13)]
|
||||
Err(AttachError::Finalizing) => {
|
||||
panic!("Cannot attach to the Python interpreter while it is finalizing.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Variant of the above which will will return gracefully if the interpreter cannot be attached to.
|
||||
pub(crate) fn try_attach() -> Result<Self, AttachError> {
|
||||
match ATTACH_COUNT.try_with(|c| c.get()) {
|
||||
Ok(i) if i > 0 => {
|
||||
// SAFETY: We just checked that the thread is already attached.
|
||||
return Ok(unsafe { Self::assume() });
|
||||
}
|
||||
// Cannot attach during GC traversal.
|
||||
Ok(ATTACH_FORBIDDEN_DURING_TRAVERSE) => {
|
||||
return Err(AttachError::ForbiddenDuringTraverse)
|
||||
}
|
||||
// other cases handled below
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// SAFETY: always safe to call this
|
||||
if unsafe { ffi::Py_IsInitialized() } == 0 {
|
||||
return Err(AttachError::NotInitialized);
|
||||
}
|
||||
|
||||
// Py_IsInitialized() can return 1 while Py_InitializeEx is still
|
||||
// running (e.g. importing site.py). Block until any in-progress PyO3
|
||||
// initialization has fully completed.
|
||||
crate::interpreter_lifecycle::wait_for_initialization();
|
||||
|
||||
// Calling `PyGILState_Ensure` while finalizing may crash CPython in unpredictable
|
||||
// ways, we'll make a best effort attempt here to avoid that. (There's a time of
|
||||
// check to time-of-use issue, but it's better than nothing.)
|
||||
//
|
||||
// SAFETY: always safe to call this
|
||||
#[cfg(Py_3_13)]
|
||||
if unsafe { ffi::Py_IsFinalizing() } != 0 {
|
||||
// If the interpreter is not initialized, we cannot attach.
|
||||
return Err(AttachError::Finalizing);
|
||||
}
|
||||
|
||||
// SAFETY: We have done everything reasonable to ensure we're in a safe state to
|
||||
// attach to the Python interpreter.
|
||||
Ok(unsafe { Self::do_attach_unchecked() })
|
||||
}
|
||||
|
||||
/// Acquires the `AttachGuard` without performing any state checking.
|
||||
///
|
||||
/// This can be called in "unsafe" contexts where the normal interpreter state
|
||||
/// checking performed by `AttachGuard::try_attach` may fail. This includes calling
|
||||
/// as part of multi-phase interpreter initialization.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that the Python interpreter is sufficiently initialized
|
||||
/// for a thread to be able to attach to it.
|
||||
pub(crate) unsafe fn attach_unchecked() -> Self {
|
||||
if thread_is_attached() {
|
||||
return unsafe { Self::assume() };
|
||||
}
|
||||
|
||||
unsafe { Self::do_attach_unchecked() }
|
||||
}
|
||||
|
||||
/// Attach to the interpreter, without a fast-path to check if the thread is already attached.
|
||||
#[cold]
|
||||
unsafe fn do_attach_unchecked() -> Self {
|
||||
// SAFETY: interpreter is sufficiently initialized to attach a thread.
|
||||
let gstate = unsafe { ffi::PyGILState_Ensure() };
|
||||
increment_attach_count();
|
||||
// SAFETY: just attached to the interpreter
|
||||
drop_deferred_references(unsafe { Python::assume_attached() });
|
||||
AttachGuard::Ensured { gstate }
|
||||
}
|
||||
|
||||
/// Acquires the `AttachGuard` while assuming that the thread is already attached
|
||||
/// to the interpreter.
|
||||
pub(crate) unsafe fn assume() -> Self {
|
||||
increment_attach_count();
|
||||
// SAFETY: invariant of calling this function
|
||||
drop_deferred_references(unsafe { Python::assume_attached() });
|
||||
AttachGuard::Assumed
|
||||
}
|
||||
|
||||
/// Gets the Python token associated with this [`AttachGuard`].
|
||||
#[inline]
|
||||
pub(crate) fn python(&self) -> Python<'_> {
|
||||
// SAFETY: this guard guarantees the thread is attached
|
||||
unsafe { Python::assume_attached() }
|
||||
}
|
||||
}
|
||||
|
||||
/// The Drop implementation for `AttachGuard` will decrement the attach count (and potentially detach).
|
||||
impl Drop for AttachGuard {
|
||||
fn drop(&mut self) {
|
||||
match self {
|
||||
AttachGuard::Assumed => {}
|
||||
AttachGuard::Ensured { gstate } => unsafe {
|
||||
// Drop the objects in the pool before attempting to release the thread state
|
||||
ffi::PyGILState_Release(*gstate);
|
||||
},
|
||||
}
|
||||
decrement_attach_count();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
type PyObjVec = Vec<NonNull<ffi::PyObject>>;
|
||||
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
/// Thread-safe storage for objects which were dec_ref while not attached.
|
||||
struct ReferencePool {
|
||||
pending_decrefs: Mutex<PyObjVec>,
|
||||
}
|
||||
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
impl ReferencePool {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
pending_decrefs: Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn register_decref(&self, obj: NonNull<ffi::PyObject>) {
|
||||
self.pending_decrefs.lock().unwrap().push(obj);
|
||||
}
|
||||
|
||||
fn drop_deferred_references(&self, _py: Python<'_>) {
|
||||
let mut pending_decrefs = self.pending_decrefs.lock().unwrap();
|
||||
if pending_decrefs.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let decrefs = mem::take(&mut *pending_decrefs);
|
||||
drop(pending_decrefs);
|
||||
|
||||
for ptr in decrefs {
|
||||
unsafe { ffi::Py_DECREF(ptr.as_ptr()) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
unsafe impl Send for ReferencePool {}
|
||||
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
unsafe impl Sync for ReferencePool {}
|
||||
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
static POOL: OnceLock<ReferencePool> = OnceLock::new();
|
||||
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
fn get_pool() -> &'static ReferencePool {
|
||||
POOL.get_or_init(ReferencePool::new)
|
||||
}
|
||||
|
||||
#[cfg_attr(pyo3_disable_reference_pool, inline(always))]
|
||||
#[cfg_attr(pyo3_disable_reference_pool, allow(unused_variables))]
|
||||
fn drop_deferred_references(py: Python<'_>) {
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
if let Some(pool) = POOL.get() {
|
||||
pool.drop_deferred_references(py);
|
||||
}
|
||||
}
|
||||
|
||||
/// A guard which can be used to temporarily detach from the interpreter and restore on `Drop`.
|
||||
pub(crate) struct SuspendAttach {
|
||||
count: isize,
|
||||
tstate: *mut ffi::PyThreadState,
|
||||
}
|
||||
|
||||
impl SuspendAttach {
|
||||
pub(crate) unsafe fn new() -> Self {
|
||||
let count = ATTACH_COUNT.with(|c| c.replace(0));
|
||||
let tstate = unsafe { ffi::PyEval_SaveThread() };
|
||||
|
||||
Self { count, tstate }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SuspendAttach {
|
||||
fn drop(&mut self) {
|
||||
ATTACH_COUNT.with(|c| c.set(self.count));
|
||||
unsafe {
|
||||
ffi::PyEval_RestoreThread(self.tstate);
|
||||
|
||||
// Update counts of `Py<T>` that were dropped while not attached.
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
if let Some(pool) = POOL.get() {
|
||||
pool.drop_deferred_references(Python::assume_attached());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to lock safe access to the interpreter
|
||||
pub(crate) struct ForbidAttaching {
|
||||
count: isize,
|
||||
}
|
||||
|
||||
impl ForbidAttaching {
|
||||
const FORBIDDEN_DURING_TRAVERSE: &'static str = "Attaching a thread to the interpreter is prohibited while a __traverse__ implementation is running.";
|
||||
|
||||
/// Lock access to the interpreter while an implementation of `__traverse__` is running
|
||||
pub fn during_traverse() -> Self {
|
||||
Self::new(ATTACH_FORBIDDEN_DURING_TRAVERSE)
|
||||
}
|
||||
|
||||
fn new(reason: isize) -> Self {
|
||||
let count = ATTACH_COUNT.with(|c| c.replace(reason));
|
||||
|
||||
Self { count }
|
||||
}
|
||||
|
||||
#[cold]
|
||||
fn bail(current: isize) {
|
||||
match current {
|
||||
ATTACH_FORBIDDEN_DURING_TRAVERSE => panic!("{}", Self::FORBIDDEN_DURING_TRAVERSE),
|
||||
_ => panic!("Attaching a thread to the interpreter is currently prohibited."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ForbidAttaching {
|
||||
fn drop(&mut self) {
|
||||
ATTACH_COUNT.with(|c| c.set(self.count));
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers a Python object pointer inside the release pool, to have its reference count decreased
|
||||
/// the next time the thread is attached in pyo3.
|
||||
///
|
||||
/// If the thread is attached, the reference count will be decreased immediately instead of being queued
|
||||
/// for later.
|
||||
///
|
||||
/// # Safety
|
||||
/// - The object must be an owned Python reference.
|
||||
/// - The reference must not be used after calling this function.
|
||||
#[inline]
|
||||
pub unsafe fn register_decref(obj: NonNull<ffi::PyObject>) {
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
{
|
||||
get_pool().register_decref(obj);
|
||||
}
|
||||
#[cfg(all(
|
||||
pyo3_disable_reference_pool,
|
||||
not(pyo3_leak_on_drop_without_reference_pool)
|
||||
))]
|
||||
{
|
||||
let _trap = PanicTrap::new("Aborting the process to avoid panic-from-drop.");
|
||||
panic!("Cannot drop pointer into Python heap without the thread being attached.");
|
||||
}
|
||||
}
|
||||
|
||||
/// Private helper function to check if we are currently in a GC traversal (as detected by PyO3).
|
||||
#[cfg(any(not(Py_LIMITED_API), Py_3_11))]
|
||||
pub(crate) fn is_in_gc_traversal() -> bool {
|
||||
ATTACH_COUNT
|
||||
.try_with(|c| c.get() == ATTACH_FORBIDDEN_DURING_TRAVERSE)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Increments pyo3's internal attach count - to be called whenever an AttachGuard is created.
|
||||
#[inline(always)]
|
||||
fn increment_attach_count() {
|
||||
// Ignores the error in case this function called from `atexit`.
|
||||
let _ = ATTACH_COUNT.try_with(|c| {
|
||||
let current = c.get();
|
||||
if current < 0 {
|
||||
ForbidAttaching::bail(current);
|
||||
}
|
||||
c.set(current + 1);
|
||||
});
|
||||
}
|
||||
|
||||
/// Decrements pyo3's internal attach count - to be called whenever AttachGuard is dropped.
|
||||
#[inline(always)]
|
||||
fn decrement_attach_count() {
|
||||
// Ignores the error in case this function called from `atexit`.
|
||||
let _ = ATTACH_COUNT.try_with(|c| {
|
||||
let current = c.get();
|
||||
debug_assert!(
|
||||
current > 0,
|
||||
"Negative attach count detected. Please report this error to the PyO3 repo as a bug."
|
||||
);
|
||||
c.set(current - 1);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::{Py, PyAny, Python};
|
||||
|
||||
fn get_object(py: Python<'_>) -> Py<PyAny> {
|
||||
py.eval(c"object()", None, None).unwrap().unbind()
|
||||
}
|
||||
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
fn pool_dec_refs_does_not_contain(obj: &Py<PyAny>) -> bool {
|
||||
!get_pool()
|
||||
.pending_decrefs
|
||||
.lock()
|
||||
.unwrap()
|
||||
.contains(&unsafe { NonNull::new_unchecked(obj.as_ptr()) })
|
||||
}
|
||||
|
||||
// With free-threading, threads can empty the POOL at any time, so this
|
||||
// function does not test anything meaningful
|
||||
#[cfg(not(any(pyo3_disable_reference_pool, Py_GIL_DISABLED)))]
|
||||
fn pool_dec_refs_contains(obj: &Py<PyAny>) -> bool {
|
||||
get_pool()
|
||||
.pending_decrefs
|
||||
.lock()
|
||||
.unwrap()
|
||||
.contains(&unsafe { NonNull::new_unchecked(obj.as_ptr()) })
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pyobject_drop_attached_decreases_refcnt() {
|
||||
Python::attach(|py| {
|
||||
let obj = get_object(py);
|
||||
|
||||
// Create a reference to drop while attached.
|
||||
let reference = obj.clone_ref(py);
|
||||
|
||||
assert_eq!(obj._get_refcnt(py), 2);
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
assert!(pool_dec_refs_does_not_contain(&obj));
|
||||
|
||||
// While attached, reference count will be decreased immediately.
|
||||
drop(reference);
|
||||
|
||||
assert_eq!(obj._get_refcnt(py), 1);
|
||||
#[cfg(not(any(pyo3_disable_reference_pool)))]
|
||||
assert!(pool_dec_refs_does_not_contain(&obj));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(all(not(pyo3_disable_reference_pool), not(target_arch = "wasm32")))] // We are building wasm Python with pthreads disabled
|
||||
fn test_pyobject_drop_detached_doesnt_decrease_refcnt() {
|
||||
let obj = Python::attach(|py| {
|
||||
let obj = get_object(py);
|
||||
// Create a reference to drop while detached.
|
||||
let reference = obj.clone_ref(py);
|
||||
|
||||
assert_eq!(obj._get_refcnt(py), 2);
|
||||
assert!(pool_dec_refs_does_not_contain(&obj));
|
||||
|
||||
// Drop reference in a separate (detached) thread.
|
||||
std::thread::spawn(move || drop(reference)).join().unwrap();
|
||||
|
||||
// The reference count should not have changed, it is remembered
|
||||
// to release later.
|
||||
assert_eq!(obj._get_refcnt(py), 2);
|
||||
#[cfg(not(Py_GIL_DISABLED))]
|
||||
assert!(pool_dec_refs_contains(&obj));
|
||||
obj
|
||||
});
|
||||
|
||||
// On next attach, the reference is released
|
||||
#[allow(unused)]
|
||||
Python::attach(|py| {
|
||||
// With free-threading, another thread could still be processing
|
||||
// DECREFs after releasing the lock on the POOL, so the
|
||||
// refcnt could still be 2 when this assert happens
|
||||
#[cfg(not(Py_GIL_DISABLED))]
|
||||
assert_eq!(obj._get_refcnt(py), 1);
|
||||
assert!(pool_dec_refs_does_not_contain(&obj));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_attach_counts() {
|
||||
// Check `attach` and AttachGuard both increase counts correctly
|
||||
let get_attach_count = || ATTACH_COUNT.with(|c| c.get());
|
||||
|
||||
assert_eq!(get_attach_count(), 0);
|
||||
Python::attach(|_| {
|
||||
assert_eq!(get_attach_count(), 1);
|
||||
|
||||
let pool = unsafe { AttachGuard::assume() };
|
||||
assert_eq!(get_attach_count(), 2);
|
||||
|
||||
let pool2 = unsafe { AttachGuard::assume() };
|
||||
assert_eq!(get_attach_count(), 3);
|
||||
|
||||
drop(pool);
|
||||
assert_eq!(get_attach_count(), 2);
|
||||
|
||||
Python::attach(|_| {
|
||||
// nested `attach` updates attach count
|
||||
assert_eq!(get_attach_count(), 3);
|
||||
});
|
||||
assert_eq!(get_attach_count(), 2);
|
||||
|
||||
drop(pool2);
|
||||
assert_eq!(get_attach_count(), 1);
|
||||
});
|
||||
assert_eq!(get_attach_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detach() {
|
||||
assert!(!thread_is_attached());
|
||||
|
||||
Python::attach(|py| {
|
||||
assert!(thread_is_attached());
|
||||
|
||||
py.detach(move || {
|
||||
assert!(!thread_is_attached());
|
||||
|
||||
Python::attach(|_| assert!(thread_is_attached()));
|
||||
|
||||
assert!(!thread_is_attached());
|
||||
});
|
||||
|
||||
assert!(thread_is_attached());
|
||||
});
|
||||
|
||||
assert!(!thread_is_attached());
|
||||
}
|
||||
|
||||
#[cfg(feature = "py-clone")]
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_detach_updates_refcounts() {
|
||||
Python::attach(|py| {
|
||||
// Make a simple object with 1 reference
|
||||
let obj = get_object(py);
|
||||
assert_eq!(obj._get_refcnt(py), 1);
|
||||
// Cloning the object when detached should panic
|
||||
py.detach(|| obj.clone());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recursive_attach_ok() {
|
||||
Python::attach(|py| {
|
||||
let obj = Python::attach(|_| py.eval(c"object()", None, None).unwrap());
|
||||
assert_eq!(obj._get_refcnt(), 1);
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "py-clone")]
|
||||
#[test]
|
||||
fn test_clone_attached() {
|
||||
Python::attach(|py| {
|
||||
let obj = get_object(py);
|
||||
let count = obj._get_refcnt(py);
|
||||
|
||||
// Cloning when attached should increase reference count immediately
|
||||
#[expect(clippy::redundant_clone)]
|
||||
let c = obj.clone();
|
||||
assert_eq!(count + 1, c._get_refcnt(py));
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
fn test_drop_deferred_references_does_not_deadlock() {
|
||||
// drop_deferred_references can run arbitrary Python code during Py_DECREF.
|
||||
// if the locking is implemented incorrectly, it will deadlock.
|
||||
|
||||
use crate::ffi;
|
||||
|
||||
Python::attach(|py| {
|
||||
let obj = get_object(py);
|
||||
|
||||
unsafe extern "C" fn capsule_drop(capsule: *mut ffi::PyObject) {
|
||||
// This line will implicitly call drop_deferred_references
|
||||
// -> and so cause deadlock if drop_deferred_references is not handling recursion correctly.
|
||||
let pool = unsafe { AttachGuard::assume() };
|
||||
|
||||
// Rebuild obj so that it can be dropped
|
||||
unsafe {
|
||||
use crate::Bound;
|
||||
|
||||
Bound::from_owned_ptr(
|
||||
pool.python(),
|
||||
ffi::PyCapsule_GetPointer(capsule, core::ptr::null()) as _,
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
let ptr = obj.into_ptr();
|
||||
|
||||
let capsule =
|
||||
unsafe { ffi::PyCapsule_New(ptr as _, core::ptr::null(), Some(capsule_drop)) };
|
||||
|
||||
get_pool().register_decref(NonNull::new(capsule).unwrap());
|
||||
|
||||
// Updating the counts will call decref on the capsule, which calls capsule_drop
|
||||
get_pool().drop_deferred_references(py);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
fn test_attach_guard_drop_deferred_references() {
|
||||
Python::attach(|py| {
|
||||
let obj = get_object(py);
|
||||
|
||||
// For AttachGuard::attach
|
||||
|
||||
get_pool().register_decref(NonNull::new(obj.clone_ref(py).into_ptr()).unwrap());
|
||||
#[cfg(not(Py_GIL_DISABLED))]
|
||||
assert!(pool_dec_refs_contains(&obj));
|
||||
let _guard = AttachGuard::attach();
|
||||
assert!(pool_dec_refs_does_not_contain(&obj));
|
||||
|
||||
// For AttachGuard::assume
|
||||
|
||||
get_pool().register_decref(NonNull::new(obj.clone_ref(py).into_ptr()).unwrap());
|
||||
#[cfg(not(Py_GIL_DISABLED))]
|
||||
assert!(pool_dec_refs_contains(&obj));
|
||||
let _guard2 = unsafe { AttachGuard::assume() };
|
||||
assert!(pool_dec_refs_does_not_contain(&obj));
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user