Skip to main content

pyo3/
pyclass_init.rs

1// TODO https://github.com/PyO3/pyo3/issues/5487
2#![allow(clippy::undocumented_unsafe_blocks)]
3
4//! Contains initialization utilities for `#[pyclass]`.
5use crate::ffi_ptr_ext::FfiPtrExt;
6use crate::impl_::pyclass::{PyClassBaseType, PyClassImpl};
7use crate::impl_::pyclass_init::PyNativeTypeInitializer;
8use crate::internal::pyclass_init::PyObjectInit;
9use crate::pycell::impl_::PyClassObjectLayout;
10use crate::{ffi, Bound, PyClass, PyResult, Python};
11use crate::{ffi::PyTypeObject, pycell::impl_::PyClassObjectContents};
12use core::marker::PhantomData;
13
14/// Initializer for our `#[pyclass]` system.
15///
16/// You can use this type to initialize complicatedly nested `#[pyclass]`.
17///
18/// # Examples
19///
20/// ```
21/// # use pyo3::prelude::*;
22/// # use pyo3::py_run;
23/// #[pyclass(subclass)]
24/// struct BaseClass {
25///     #[pyo3(get)]
26///     basename: &'static str,
27/// }
28/// #[pyclass(extends=BaseClass, subclass)]
29/// struct SubClass {
30///     #[pyo3(get)]
31///     subname: &'static str,
32/// }
33/// #[pyclass(extends=SubClass)]
34/// struct SubSubClass {
35///     #[pyo3(get)]
36///     subsubname: &'static str,
37/// }
38///
39/// #[pymethods]
40/// impl SubSubClass {
41///     #[new]
42///     fn new() -> PyClassInitializer<Self> {
43///         PyClassInitializer::from(BaseClass { basename: "base" })
44///             .add_subclass(SubClass { subname: "sub" })
45///             .add_subclass(SubSubClass {
46///                 subsubname: "subsub",
47///             })
48///     }
49/// }
50/// Python::attach(|py| {
51///     let typeobj = py.get_type::<SubSubClass>();
52///     let sub_sub_class = typeobj.call((), None).unwrap();
53///     py_run!(
54///         py,
55///         sub_sub_class,
56///         r#"
57///  assert sub_sub_class.basename == 'base'
58///  assert sub_sub_class.subname == 'sub'
59///  assert sub_sub_class.subsubname == 'subsub'"#
60///     );
61/// });
62/// ```
63pub struct PyClassInitializer<T: PyClass> {
64    init: T,
65    super_init: <T::BaseType as PyClassBaseType>::Initializer,
66}
67
68impl<T: PyClass> PyClassInitializer<T> {
69    /// Constructs a new initializer from value `T` and base class' initializer.
70    ///
71    /// It is recommended to use `add_subclass` instead of this method for most usage.
72    #[track_caller]
73    #[inline]
74    pub fn new(init: T, super_init: <T::BaseType as PyClassBaseType>::Initializer) -> Self {
75        Self { init, super_init }
76    }
77
78    /// Constructs a new initializer from an initializer for the base class.
79    ///
80    /// # Examples
81    /// ```
82    /// use pyo3::prelude::*;
83    ///
84    /// #[pyclass(subclass)]
85    /// struct BaseClass {
86    ///     #[pyo3(get)]
87    ///     value: i32,
88    /// }
89    ///
90    /// impl BaseClass {
91    ///     fn new(value: i32) -> PyResult<Self> {
92    ///         Ok(Self { value })
93    ///     }
94    /// }
95    ///
96    /// #[pyclass(extends=BaseClass)]
97    /// struct SubClass {}
98    ///
99    /// #[pymethods]
100    /// impl SubClass {
101    ///     #[new]
102    ///     fn new(value: i32) -> PyResult<PyClassInitializer<Self>> {
103    ///         let base_init = PyClassInitializer::from(BaseClass::new(value)?);
104    ///         Ok(base_init.add_subclass(SubClass {}))
105    ///     }
106    /// }
107    ///
108    /// fn main() -> PyResult<()> {
109    ///     Python::attach(|py| {
110    ///         let m = PyModule::new(py, "example")?;
111    ///         m.add_class::<SubClass>()?;
112    ///         m.add_class::<BaseClass>()?;
113    ///
114    ///         let instance = m.getattr("SubClass")?.call1((92,))?;
115    ///
116    ///         // `SubClass` does not have a `value` attribute, but `BaseClass` does.
117    ///         let n = instance.getattr("value")?.extract::<i32>()?;
118    ///         assert_eq!(n, 92);
119    ///
120    ///         Ok(())
121    ///     })
122    /// }
123    /// ```
124    #[track_caller]
125    #[inline]
126    pub fn add_subclass<S>(self, subclass_value: S) -> PyClassInitializer<S>
127    where
128        T: PyClassBaseType<Initializer = Self>,
129        S: PyClass<BaseType = T>,
130    {
131        PyClassInitializer::new(subclass_value, self)
132    }
133
134    /// Creates a new class object and initializes it.
135    pub(crate) fn create_class_object(self, py: Python<'_>) -> PyResult<Bound<'_, T>>
136    where
137        T: PyClass,
138    {
139        unsafe { self.create_class_object_of_type(py, T::type_object_raw(py)) }
140    }
141
142    /// Creates a new class object and initializes it given a typeobject `subtype`.
143    ///
144    /// # Safety
145    /// `subtype` must be a valid pointer to the type object of T or a subclass.
146    pub(crate) unsafe fn create_class_object_of_type(
147        self,
148        py: Python<'_>,
149        target_type: *mut crate::ffi::PyTypeObject,
150    ) -> PyResult<Bound<'_, T>>
151    where
152        T: PyClass,
153    {
154        let obj = unsafe { self.super_init.into_new_object(py, target_type)? };
155
156        // SAFETY: `obj` is constructed using `T::Layout` but has not been initialized yet
157        let contents = unsafe { <T as PyClassImpl>::Layout::contents_uninit(obj) };
158
159        let new_contents = PyClassObjectContents::new(self.init);
160
161        // CPython 3.11 and 3.12 eagerly create the instance dict for types with a nonzero
162        // `tp_dictoffset` in `_PyObject_InitializeDict`, storing an owned reference in
163        // the `__dict__` slot, which lives inside `contents`. Carry that value over
164        // instead of clobbering it below, otherwise it leaks. Python 3.13 returned to
165        // creating the instance dict lazily
166        //
167        // The condition is `not(Py_3_13)` rather than `all(Py_3_11, not(Py_3_13))`
168        // because an abi3 build with a lower minimum version can still run on 3.11 and
169        // 3.12; on 3.10 and older this is a harmless no-op (the slot is always null
170        // there).
171        #[cfg(not(Py_3_13))]
172        let new_contents = {
173            let mut new_contents = new_contents;
174            if eagerly_created_dict_possible::<T>(py) {
175                // SAFETY: `tp_alloc` zero-initializes the object, so the slot contains either
176                // zeroes (a valid empty slot value) or a valid owned pointer stored by the base
177                // `tp_new` through the type's `tp_dictoffset`.
178                unsafe {
179                    let contents_ptr = (*contents).as_mut_ptr();
180                    let dict_ptr = &raw const (*contents_ptr).dict;
181                    new_contents.dict = core::ptr::read(dict_ptr);
182                }
183            }
184            new_contents
185        };
186
187        // SAFETY: `contents` is a non-null pointer to the space allocated for our
188        // `PyClassObjectContents` (either statically in Rust or dynamically by Python)
189        unsafe { (*contents).write(new_contents) };
190
191        // Safety: obj is a valid pointer to an object of type `target_type`, which` is a known
192        // subclass of `T`
193        Ok(unsafe { obj.assume_owned(py).cast_into_unchecked() })
194    }
195}
196
197/// Whether the running interpreter may have eagerly created an instance dict for `T`
198/// during `tp_new` (CPython 3.11 and 3.12 only).
199///
200/// For native builds the compile-time `not(Py_3_13)` gate at the call site is exact; abi3
201/// builds with a minimum version below 3.13 must check the interpreter version at runtime
202#[cfg(not(Py_3_13))]
203#[inline]
204fn eagerly_created_dict_possible<T: PyClassImpl>(py: Python<'_>) -> bool {
205    if core::mem::size_of::<T::Dict>() == 0 {
206        return false;
207    }
208    cfg_select! {
209        Py_LIMITED_API =>
210        {
211            use crate::sync::PyOnceLock;
212            static IS_PYTHON_3_11_OR_3_12: PyOnceLock<bool> = PyOnceLock::new();
213            *IS_PYTHON_3_11_OR_3_12.get_or_init(py, || {
214                let version_info = py.version_info();
215                matches!((version_info.major, version_info.minor), (3, 11) | (3, 12))
216            })
217        }
218        not(Py_LIMITED_API) =>
219        {
220            let _ = py;
221            cfg!(Py_3_11)
222        }
223    }
224}
225
226impl<T: PyClass> PyObjectInit<T> for PyClassInitializer<T> {
227    unsafe fn into_new_object(
228        self,
229        py: Python<'_>,
230        subtype: *mut PyTypeObject,
231    ) -> PyResult<*mut ffi::PyObject> {
232        unsafe {
233            self.create_class_object_of_type(py, subtype)
234                .map(Bound::into_ptr)
235        }
236    }
237}
238
239impl<T> From<T> for PyClassInitializer<T>
240where
241    T: PyClass,
242    T::BaseType: PyClassBaseType<Initializer = PyNativeTypeInitializer<T::BaseType>>,
243{
244    #[inline]
245    fn from(value: T) -> PyClassInitializer<T> {
246        Self::new(value, PyNativeTypeInitializer(PhantomData))
247    }
248}
249
250impl<S, B> From<(S, B)> for PyClassInitializer<S>
251where
252    S: PyClass<BaseType = B>,
253    B: PyClass + PyClassBaseType<Initializer = PyClassInitializer<B>>,
254    B::BaseType: PyClassBaseType<Initializer = PyNativeTypeInitializer<B::BaseType>>,
255{
256    #[track_caller]
257    #[inline]
258    fn from(sub_and_base: (S, B)) -> PyClassInitializer<S> {
259        let (sub, base) = sub_and_base;
260        PyClassInitializer::from(base).add_subclass(sub)
261    }
262}