From bdbcd6a65f405c8a80fd304932768c8910df981c Mon Sep 17 00:00:00 2001 From: Matthew Broadway Date: Sun, 6 Oct 2024 22:13:42 +0100 Subject: [PATCH] add the ability to extend `PyType` to create metaclasses --- newsfragments/4621.added.md | 1 + src/impl_/pyclass.rs | 1 - src/type_object.rs | 2 +- src/types/typeobject.rs | 73 ++++++++++++++++++++++++++++++++++ tests/test_inheritance.rs | 78 +++++++++++++++++++++++++++++++------ 5 files changed, 141 insertions(+), 14 deletions(-) create mode 100644 newsfragments/4621.added.md diff --git a/newsfragments/4621.added.md b/newsfragments/4621.added.md new file mode 100644 index 00000000000..34c5225919e --- /dev/null +++ b/newsfragments/4621.added.md @@ -0,0 +1 @@ +Added the ability to extend `PyType` to create metaclasses. \ No newline at end of file diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 53c8dacff35..b33f0b01d86 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1110,7 +1110,6 @@ impl PyClassThreadChecker for ThreadCheckerImpl { } /// Trait denoting that this class is suitable to be used as a base type for PyClass. - #[cfg_attr( all(diagnostic_namespace, Py_LIMITED_API), diagnostic::on_unimplemented( diff --git a/src/type_object.rs b/src/type_object.rs index b7cad4ab3b2..368320d4056 100644 --- a/src/type_object.rs +++ b/src/type_object.rs @@ -16,7 +16,7 @@ use crate::{ffi, Bound, Python}; /// This trait must only be implemented for types which represent valid layouts of Python objects. pub unsafe trait PyLayout {} -/// `T: PySizedLayout` represents that `T` is not a instance of +/// `T: PySizedLayout` represents that `T` is not an instance of /// [`PyVarObject`](https://docs.python.org/3/c-api/structures.html#c.PyVarObject). /// /// In addition, that `T` is a concrete representation of `U`. diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index 7a66b7ad0df..9e3f705231b 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -8,6 +8,9 @@ use crate::{ffi, Bound, PyAny, PyTypeInfo, Python}; use super::PyString; +#[cfg(not(Py_LIMITED_API))] +use super::PyDict; + /// Represents a reference to a Python `type` object. /// /// Values of this type are accessed via PyO3's smart pointers, e.g. as @@ -20,6 +23,17 @@ pub struct PyType(PyAny); pyobject_native_type_core!(PyType, pyobject_native_static_type_object!(ffi::PyType_Type), #checkfunction=ffi::PyType_Check); +#[cfg(not(Py_LIMITED_API))] +pyobject_native_type_sized!(PyType, ffi::PyHeapTypeObject); + +#[cfg(not(Py_LIMITED_API))] +impl crate::impl_::pyclass::PyClassBaseType for PyType { + type LayoutAsBase = crate::impl_::pycell::PyClassObjectBase; + type BaseNativeType = PyType; + type Initializer = crate::impl_::pyclass_init::PyNativeTypeInitializer; + type PyClassMutability = crate::pycell::impl_::ImmutableClass; +} + impl PyType { /// Creates a new type object. #[inline] @@ -50,6 +64,29 @@ impl PyType { .downcast_unchecked() .to_owned() } + + /// Creates a new type object (class). The resulting type/class will inherit the given metaclass `T` + /// + /// Equivalent to calling `type(name, bases, dict, **kwds)` + /// + #[cfg(not(Py_LIMITED_API))] + pub fn new_type<'py, T: PyTypeInfo>( + py: Python<'py>, + args: &Bound<'py, PyTuple>, + kwargs: Option<&Bound<'py, PyDict>>, + ) -> PyResult> { + let new_fn = unsafe { + ffi::PyType_Type + .tp_new + .expect("PyType_Type.tp_new should be present") + }; + let raw_type = T::type_object_raw(py); + let raw_args = args.as_ptr(); + let raw_kwargs = kwargs.map(|v| v.as_ptr()).unwrap_or(std::ptr::null_mut()); + let obj_ptr = unsafe { new_fn(raw_type, raw_args, raw_kwargs) }; + let borrowed_obj = unsafe { Borrowed::from_ptr_or_err(py, obj_ptr) }?; + Ok(borrowed_obj.downcast()?.to_owned()) + } } /// Implementation of functionality for [`PyType`]. @@ -390,4 +427,40 @@ class OuterClass: ); }); } + + #[test] + #[cfg(all(not(Py_LIMITED_API), feature = "macros"))] + fn test_new_type() { + use crate::{ + types::{PyDict, PyList, PyString}, + IntoPy, + }; + + Python::with_gil(|py| { + #[allow(non_snake_case)] + let ListType = py.get_type::(); + let name = PyString::new(py, "MyClass"); + let bases = PyTuple::new(py, [ListType]).unwrap(); + let dict = PyDict::new(py); + dict.set_item("foo", 123_i32.into_py(py)).unwrap(); + let args = PyTuple::new(py, [name.as_any(), bases.as_any(), dict.as_any()]).unwrap(); + #[allow(non_snake_case)] + let MyClass = PyType::new_type::(py, &args, None).unwrap(); + + assert_eq!(MyClass.name().unwrap(), "MyClass"); + assert_eq!(MyClass.qualname().unwrap(), "MyClass"); + + crate::py_run!( + py, + MyClass, + r#" + assert type(MyClass) is type + assert MyClass.__bases__ == (list,) + assert issubclass(MyClass, list) + assert MyClass.foo == 123 + assert not hasattr(MyClass, "__module__") + "# + ); + }); + } } diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index 7190dd49555..f811a2d5373 100644 --- a/tests/test_inheritance.rs +++ b/tests/test_inheritance.rs @@ -89,9 +89,9 @@ fn call_base_and_sub_methods() { py, obj, r#" - assert obj.base_method(10) == 100 - assert obj.sub_method(10) == 50 -"# + assert obj.base_method(10) == 100 + assert obj.sub_method(10) == 50 + "# ); }); } @@ -163,14 +163,14 @@ fn handle_result_in_new() { py, subclass, r#" -try: - subclass(-10) - assert Fals -except ValueError as e: - pass -except Exception as e: - raise e -"# + try: + subclass(-10) + assert Fals + except ValueError as e: + pass + except Exception as e: + raise e + "# ); }); } @@ -180,7 +180,7 @@ except Exception as e: mod inheriting_native_type { use super::*; use pyo3::exceptions::PyException; - use pyo3::types::PyDict; + use pyo3::types::{PyDict, PyTuple}; #[cfg(not(PyPy))] #[test] @@ -300,6 +300,60 @@ mod inheriting_native_type { ) }) } + + #[cfg(not(Py_LIMITED_API))] + #[test] + fn inherit_type() { + use pyo3::types::PyType; + + #[pyclass(extends=PyType)] + #[derive(Debug)] + struct Metaclass {} + + #[pymethods] + impl Metaclass { + #[new] + #[pyo3(signature = (*args, **kwds))] + fn new<'py>( + py: Python<'py>, + args: &Bound<'py, PyTuple>, + kwds: Option<&Bound<'py, PyDict>>, + ) -> PyResult> { + let type_object = PyType::new_type::(py, args, kwds)?; + type_object.setattr("some_var", 123)?; + Ok(type_object) + } + + fn __getitem__(&self, item: u64) -> u64 { + item + 1 + } + } + + Python::with_gil(|py| { + #[allow(non_snake_case)] + let Metaclass = py.get_type::(); + + // checking base is `type` + py_run!(py, Metaclass, r#"assert Metaclass.__bases__ == (type,)"#); + + // check can be used as a metaclass + py_run!( + py, + Metaclass, + r#" + class Foo(metaclass=Metaclass): + pass + assert type(Foo) is Metaclass + assert isinstance(Foo, Metaclass) + assert Foo.some_var == 123 + assert Foo[100] == 101 + FooDynamic = Metaclass("FooDynamic", (), {}) + assert FooDynamic.some_var == 123 + assert FooDynamic[100] == 101 + "# + ); + }); + } } #[pyclass(subclass)]