Skip to content

Commit

Permalink
add the ability to extend PyType to create metaclasses
Browse files Browse the repository at this point in the history
  • Loading branch information
mbway committed Oct 14, 2024
1 parent 29c6f4b commit bdbcd6a
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 14 deletions.
1 change: 1 addition & 0 deletions newsfragments/4621.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added the ability to extend `PyType` to create metaclasses.
1 change: 0 additions & 1 deletion src/impl_/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1110,7 +1110,6 @@ impl<T> PyClassThreadChecker<T> 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(
Expand Down
2 changes: 1 addition & 1 deletion src/type_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {}

/// `T: PySizedLayout<U>` represents that `T` is not a instance of
/// `T: PySizedLayout<U>` 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`.
Expand Down
73 changes: 73 additions & 0 deletions src/types/typeobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<ffi::PyHeapTypeObject>;
type BaseNativeType = PyType;
type Initializer = crate::impl_::pyclass_init::PyNativeTypeInitializer<Self>;
type PyClassMutability = crate::pycell::impl_::ImmutableClass;
}

impl PyType {
/// Creates a new type object.
#[inline]
Expand Down Expand Up @@ -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)`
/// <https://docs.python.org/3/library/functions.html#type>
#[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<Bound<'py, T>> {
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`].
Expand Down Expand Up @@ -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::<PyList>();
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::<PyType>(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__")
"#
);
});
}
}
78 changes: 66 additions & 12 deletions tests/test_inheritance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
"#
);
});
}
Expand Down Expand Up @@ -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
"#
);
});
}
Expand All @@ -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]
Expand Down Expand Up @@ -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<Bound<'py, Self>> {
let type_object = PyType::new_type::<Metaclass>(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::<Metaclass>();

// 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)]
Expand Down

0 comments on commit bdbcd6a

Please sign in to comment.