From c5e2f758d57fde72606b09bf4fd3de3ce50ae776 Mon Sep 17 00:00:00 2001 From: Matthew Broadway Date: Sat, 14 Dec 2024 19:42:28 +0000 Subject: [PATCH] add option to override tp_new to handle PyBaseObject_Type --- src/impl_/pyclass.rs | 5 ++--- src/impl_/pyclass_init.rs | 43 ++++++++++++++++++++------------------- src/pyclass_init.rs | 6 +++--- src/types/any.rs | 12 +++++++++-- 4 files changed, 37 insertions(+), 29 deletions(-) diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index fa4f1e3b5c1..695312bd53e 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1138,9 +1138,8 @@ pub trait PyClassBaseType: Sized { type BaseNativeType; type Initializer: PyObjectInit; type PyClassMutability: PyClassMutability; - /// Whether `__new__` ([ffi::PyTypeObject::tp_new]) for this type accepts arguments other - /// than the type of object to create. - const NEW_ACCEPTS_ARGUMENTS: bool = true; + /// Use the provided function instead of [ffi::PyTypeObject::tp_new] when creating an object of this type + const OVERRIDE_TP_NEW: Option = None; } /// Implementation of tp_dealloc for pyclasses without gc diff --git a/src/impl_/pyclass_init.rs b/src/impl_/pyclass_init.rs index bfd85476eb7..805759ebedf 100644 --- a/src/impl_/pyclass_init.rs +++ b/src/impl_/pyclass_init.rs @@ -1,8 +1,9 @@ //! Contains initialization utilities for `#[pyclass]`. +use crate::exceptions::PyTypeError; use crate::ffi_ptr_ext::FfiPtrExt; use crate::internal::get_slot::TP_NEW; use crate::types::{PyDict, PyTuple, PyType}; -use crate::{ffi, Borrowed, Bound, PyErr, PyResult, Python}; +use crate::{ffi, Bound, PyErr, PyResult, Python}; use crate::{ffi::PyTypeObject, sealed::Sealed, type_object::PyTypeInfo}; use std::marker::PhantomData; @@ -46,29 +47,29 @@ impl PyObjectInit for PyNativeTypeInitialize subtype: *mut PyTypeObject, args: &Bound<'_, PyTuple>, kwargs: Option<&Bound<'_, PyDict>>, - new_accepts_arguments: bool, + override_tp_new: Option, ) -> PyResult<*mut ffi::PyObject> { - let native_base_type_borrowed: Borrowed<'_, '_, PyType> = native_base_type - .cast::() - .assume_borrowed_unchecked(py) - .downcast_unchecked(); - let tp_new = native_base_type_borrowed - .get_slot(TP_NEW) - .unwrap_or(ffi::PyType_GenericNew); - - let obj = if new_accepts_arguments { - tp_new( - subtype, - args.as_ptr(), - kwargs - .map(|obj| obj.as_ptr()) - .unwrap_or(std::ptr::null_mut()), - ) + let tp_new = if let Some(tp_new) = override_tp_new { + tp_new } else { - let args = PyTuple::empty(py); - tp_new(subtype, args.as_ptr(), std::ptr::null_mut()) + native_base_type + .cast::() + .assume_borrowed_unchecked(py) + .downcast_unchecked::() + .get_slot(TP_NEW) + .ok_or_else(|| { + PyTypeError::new_err("cannot construct type that does not define __new__") + })? }; + let obj = tp_new( + subtype, + args.as_ptr(), + kwargs + .map(|obj| obj.as_ptr()) + .unwrap_or(std::ptr::null_mut()), + ); + if obj.is_null() { Err(PyErr::fetch(py)) } else { @@ -81,7 +82,7 @@ impl PyObjectInit for PyNativeTypeInitialize subtype, args, kwargs, - T::NEW_ACCEPTS_ARGUMENTS, + T::OVERRIDE_TP_NEW, ) } diff --git a/src/pyclass_init.rs b/src/pyclass_init.rs index 52f55b766a9..7970e0afb24 100644 --- a/src/pyclass_init.rs +++ b/src/pyclass_init.rs @@ -29,9 +29,9 @@ //! Initialization of a pyo3 `#[pyclass] struct MyClass;` //! - `MyClass(*args, **kwargs) == MyClass.__call__(*args, **kwargs) == type.__call__(*args, **kwargs)` //! - Calls `obj = MyClass.__new__(MyClass, *args, **kwargs)` -//! - Calls user defined `#[new]` function, returning a [IntoPyCallbackOutput] which has +//! - Calls user defined `#[new]` function, returning a [`IntoPyCallbackOutput`] which has //! instances of each user defined struct in the inheritance hierarchy. -//! - Calls [PyClassInitializer::create_class_object_of_type] +//! - Calls `PyClassInitializer::create_class_object_of_type` //! - Recursively calls back to the base native type. //! - At the base native type, [PyObjectInit::into_new_object] calls `__new__` for the base native type //! (passing the [ffi::PyTypeObject] of the most derived type) @@ -43,7 +43,7 @@ //! //! ## Notes: //! - pyo3 classes annotated with `#[pyclass(dict)]` have a `__dict__` attribute. When using the `tp_dictoffset` -//! mechanism instead of `Py_TPFLAGS_MANAGED_DICT` to enable this, the dict is stored in the [PyClassObjectContents] +//! mechanism instead of `Py_TPFLAGS_MANAGED_DICT` to enable this, the dict is stored in the `PyClassObjectContents` //! of the most derived type and is set to NULL at construction and initialized to a new dictionary by //! [ffi::PyObject_GenericGetDict] when first accessed. //! - The python documentation also mentions 'static' classes which define their [ffi::PyTypeObject] in static/global diff --git a/src/types/any.rs b/src/types/any.rs index 689caf42bde..c49a46b5097 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -51,8 +51,16 @@ impl crate::impl_::pyclass::PyClassBaseType for PyAny { type BaseNativeType = PyAny; type Initializer = crate::impl_::pyclass_init::PyNativeTypeInitializer; type PyClassMutability = crate::pycell::impl_::ImmutableClass; - // `object.__new__` should be called with only the type of object to create, without any other arguments. - const NEW_ACCEPTS_ARGUMENTS: bool = false; + /// `object.__new__` ([ffi::PyBaseObject_Type::tp_new]) has some unique behaviour that must be worked around: + /// - it does not accept any arguments (if args or kwargs are non-empty a `TypeError` is raised) and in the current + /// implementation, arguments are propagated to the native base class `__new__` function. + /// - it initializes `__dict__` if applicable (`#[pyclass(dict)]`). Currently this dict will leak because + /// the pointer to it will be overwritten when the rust-managed data is written to the pyobject. + /// - it checks for any abstract methods and refuses to construct if any are found. This is generally not applicable + /// to pyo3 classes. + /// + /// on the other hand, [ffi::PyType_GenericNew] simply allocates space for the object without initializing anything. + const OVERRIDE_TP_NEW: Option = Some(ffi::PyType_GenericNew); } /// This trait represents the Python APIs which are usable on all Python objects.