Skip to content

Commit 79ba367

Browse files
committed
Improve native object initialization
Before this change, classes inheriting the base object was a special case where `object.__new__` is not called, and inheriting from other base classes requires use of the unlimited API. Previously this was not very limiting, but with <PyO3#4678> it will be possible to inherit from native base classes with the limited API and possibly from dynamically imported native base classes which may require `__new__` arguments to reach them.
1 parent 603a55f commit 79ba367

File tree

9 files changed

+237
-64
lines changed

9 files changed

+237
-64
lines changed

newsfragments/4798.fixed.md

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
fixes several limitations with base native type initialization. Required for extending native base types
2+
with the limited API and extending base native types that require arguments passed to `__new__`.

pyo3-macros-backend/src/method.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -830,12 +830,14 @@ impl<'a> FnSpec<'a> {
830830
_kwargs: *mut #pyo3_path::ffi::PyObject
831831
) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> {
832832
use #pyo3_path::impl_::callback::IntoPyCallbackOutput;
833+
let raw_args = _args;
834+
let raw_kwargs = _kwargs;
833835
let function = #rust_name; // Shadow the function name to avoid #3017
834836
#arg_convert
835837
#init_holders
836838
let result = #call;
837839
let initializer: #pyo3_path::PyClassInitializer::<#cls> = result.convert(py)?;
838-
#pyo3_path::impl_::pymethods::tp_new_impl(py, initializer, _slf)
840+
#pyo3_path::impl_::pymethods::tp_new_impl(py, initializer, _slf, raw_args, raw_kwargs)
839841
}
840842
}
841843
}

src/impl_/pyclass.rs

+3
Original file line numberDiff line numberDiff line change
@@ -1138,6 +1138,9 @@ pub trait PyClassBaseType: Sized {
11381138
type BaseNativeType;
11391139
type Initializer: PyObjectInit<Self>;
11401140
type PyClassMutability: PyClassMutability;
1141+
/// Whether `__new__` ([ffi::PyTypeObject::tp_new]) for this type accepts arguments other
1142+
/// than the type of object to create.
1143+
const NEW_ACCEPTS_ARGUMENTS: bool = true;
11411144
}
11421145

11431146
/// Implementation of tp_dealloc for pyclasses without gc

src/impl_/pyclass_init.rs

+46-43
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
//! Contains initialization utilities for `#[pyclass]`.
22
use crate::ffi_ptr_ext::FfiPtrExt;
3-
use crate::internal::get_slot::TP_ALLOC;
4-
use crate::types::PyType;
5-
use crate::{ffi, Borrowed, PyErr, PyResult, Python};
3+
use crate::internal::get_slot::TP_NEW;
4+
use crate::types::{PyDict, PyTuple, PyType};
5+
use crate::{ffi, Borrowed, Bound, PyErr, PyResult, Python};
66
use crate::{ffi::PyTypeObject, sealed::Sealed, type_object::PyTypeInfo};
77
use std::marker::PhantomData;
88

9+
use super::pyclass::PyClassBaseType;
10+
911
/// Initializer for Python types.
1012
///
1113
/// This trait is intended to use internally for distinguishing `#[pyclass]` and
@@ -17,69 +19,70 @@ pub trait PyObjectInit<T>: Sized + Sealed {
1719
self,
1820
py: Python<'_>,
1921
subtype: *mut PyTypeObject,
22+
args: &Bound<'_, PyTuple>,
23+
kwargs: Option<&Bound<'_, PyDict>>,
2024
) -> PyResult<*mut ffi::PyObject>;
2125

2226
#[doc(hidden)]
2327
fn can_be_subclassed(&self) -> bool;
2428
}
2529

26-
/// Initializer for Python native types, like `PyDict`.
27-
pub struct PyNativeTypeInitializer<T: PyTypeInfo>(pub PhantomData<T>);
30+
/// Initializer for Python native types, like [PyDict].
31+
pub struct PyNativeTypeInitializer<T: PyTypeInfo + PyClassBaseType>(pub PhantomData<T>);
2832

29-
impl<T: PyTypeInfo> PyObjectInit<T> for PyNativeTypeInitializer<T> {
33+
impl<T: PyTypeInfo + PyClassBaseType> PyObjectInit<T> for PyNativeTypeInitializer<T> {
34+
/// call `__new__` ([ffi::PyTypeObject::tp_new]) for the native base type.
35+
/// This will allocate a new python object and initialize the part relating to the native base type.
3036
unsafe fn into_new_object(
3137
self,
3238
py: Python<'_>,
3339
subtype: *mut PyTypeObject,
40+
args: &Bound<'_, PyTuple>,
41+
kwargs: Option<&Bound<'_, PyDict>>,
3442
) -> PyResult<*mut ffi::PyObject> {
3543
unsafe fn inner(
3644
py: Python<'_>,
37-
type_object: *mut PyTypeObject,
45+
native_base_type: *mut PyTypeObject,
3846
subtype: *mut PyTypeObject,
47+
args: &Bound<'_, PyTuple>,
48+
kwargs: Option<&Bound<'_, PyDict>>,
49+
new_accepts_arguments: bool,
3950
) -> PyResult<*mut ffi::PyObject> {
40-
// HACK (due to FIXME below): PyBaseObject_Type's tp_new isn't happy with NULL arguments
41-
let is_base_object = type_object == std::ptr::addr_of_mut!(ffi::PyBaseObject_Type);
42-
let subtype_borrowed: Borrowed<'_, '_, PyType> = subtype
51+
let native_base_type_borrowed: Borrowed<'_, '_, PyType> = native_base_type
4352
.cast::<ffi::PyObject>()
4453
.assume_borrowed_unchecked(py)
4554
.downcast_unchecked();
55+
let tp_new = native_base_type_borrowed
56+
.get_slot(TP_NEW)
57+
.unwrap_or(ffi::PyType_GenericNew);
4658

47-
if is_base_object {
48-
let alloc = subtype_borrowed
49-
.get_slot(TP_ALLOC)
50-
.unwrap_or(ffi::PyType_GenericAlloc);
51-
52-
let obj = alloc(subtype, 0);
53-
return if obj.is_null() {
54-
Err(PyErr::fetch(py))
55-
} else {
56-
Ok(obj)
57-
};
58-
}
59-
60-
#[cfg(Py_LIMITED_API)]
61-
unreachable!("subclassing native types is not possible with the `abi3` feature");
59+
let obj = if new_accepts_arguments {
60+
tp_new(
61+
subtype,
62+
args.as_ptr(),
63+
kwargs
64+
.map(|obj| obj.as_ptr())
65+
.unwrap_or(std::ptr::null_mut()),
66+
)
67+
} else {
68+
let args = PyTuple::empty(py);
69+
tp_new(subtype, args.as_ptr(), std::ptr::null_mut())
70+
};
6271

63-
#[cfg(not(Py_LIMITED_API))]
64-
{
65-
match (*type_object).tp_new {
66-
// FIXME: Call __new__ with actual arguments
67-
Some(newfunc) => {
68-
let obj = newfunc(subtype, std::ptr::null_mut(), std::ptr::null_mut());
69-
if obj.is_null() {
70-
Err(PyErr::fetch(py))
71-
} else {
72-
Ok(obj)
73-
}
74-
}
75-
None => Err(crate::exceptions::PyTypeError::new_err(
76-
"base type without tp_new",
77-
)),
78-
}
72+
if obj.is_null() {
73+
Err(PyErr::fetch(py))
74+
} else {
75+
Ok(obj)
7976
}
8077
}
81-
let type_object = T::type_object_raw(py);
82-
inner(py, type_object, subtype)
78+
inner(
79+
py,
80+
T::type_object_raw(py),
81+
subtype,
82+
args,
83+
kwargs,
84+
T::NEW_ACCEPTS_ARGUMENTS,
85+
)
8386
}
8487

8588
#[inline]

src/impl_/pymethods.rs

+18-7
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ use crate::pycell::impl_::PyClassBorrowChecker as _;
88
use crate::pycell::{PyBorrowError, PyBorrowMutError};
99
use crate::pyclass::boolean_struct::False;
1010
use crate::types::any::PyAnyMethods;
11-
use crate::types::PyType;
11+
use crate::types::{PyDict, PyTuple, PyType};
1212
use crate::{
13-
ffi, Bound, DowncastError, Py, PyAny, PyClass, PyClassInitializer, PyErr, PyObject, PyRef,
14-
PyRefMut, PyResult, PyTraverseError, PyTypeCheck, PyVisit, Python,
13+
ffi, Borrowed, Bound, DowncastError, Py, PyAny, PyClass, PyClassInitializer, PyErr, PyObject,
14+
PyRef, PyRefMut, PyResult, PyTraverseError, PyTypeCheck, PyVisit, Python,
1515
};
1616
use std::ffi::CStr;
1717
use std::fmt;
@@ -696,13 +696,24 @@ impl<'py, T> std::ops::Deref for BoundRef<'_, 'py, T> {
696696
}
697697
}
698698

699-
pub unsafe fn tp_new_impl<T: PyClass>(
700-
py: Python<'_>,
699+
pub unsafe fn tp_new_impl<'py, T: PyClass>(
700+
py: Python<'py>,
701701
initializer: PyClassInitializer<T>,
702-
target_type: *mut ffi::PyTypeObject,
702+
most_derived_type: *mut ffi::PyTypeObject,
703+
args: *mut ffi::PyObject,
704+
kwargs: *mut ffi::PyObject,
703705
) -> PyResult<*mut ffi::PyObject> {
706+
// Safety:
707+
// - `args` is known to be a tuple
708+
// - `kwargs` is known to be a dict or null
709+
// - we both have the GIL and can borrow these input references for the `'py` lifetime.
710+
let args: Borrowed<'py, 'py, PyTuple> =
711+
Borrowed::from_ptr(py, args).downcast_unchecked::<PyTuple>();
712+
let kwargs: Option<Borrowed<'py, 'py, PyDict>> =
713+
Borrowed::from_ptr_or_opt(py, kwargs).map(|kwargs| kwargs.downcast_unchecked());
714+
704715
initializer
705-
.create_class_object_of_type(py, target_type)
716+
.create_class_object_of_type(py, most_derived_type, &args, kwargs.as_deref())
706717
.map(Bound::into_ptr)
707718
}
708719

src/internal/get_slot.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,11 @@ macro_rules! impl_slots {
122122

123123
// Slots are implemented on-demand as needed.)
124124
impl_slots! {
125-
TP_ALLOC: (Py_tp_alloc, tp_alloc) -> Option<ffi::allocfunc>,
126125
TP_BASE: (Py_tp_base, tp_base) -> *mut ffi::PyTypeObject,
127126
TP_CLEAR: (Py_tp_clear, tp_clear) -> Option<ffi::inquiry>,
128127
TP_DESCR_GET: (Py_tp_descr_get, tp_descr_get) -> Option<ffi::descrgetfunc>,
129128
TP_FREE: (Py_tp_free, tp_free) -> Option<ffi::freefunc>,
129+
TP_NEW: (Py_tp_new, tp_new) -> Option<ffi::newfunc>,
130130
TP_TRAVERSE: (Py_tp_traverse, tp_traverse) -> Option<ffi::traverseproc>,
131131
}
132132

0 commit comments

Comments
 (0)