Skip to content

Commit bdbcd6a

Browse files
committed
add the ability to extend PyType to create metaclasses
1 parent 29c6f4b commit bdbcd6a

File tree

5 files changed

+141
-14
lines changed

5 files changed

+141
-14
lines changed

newsfragments/4621.added.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added the ability to extend `PyType` to create metaclasses.

src/impl_/pyclass.rs

-1
Original file line numberDiff line numberDiff line change
@@ -1110,7 +1110,6 @@ impl<T> PyClassThreadChecker<T> for ThreadCheckerImpl {
11101110
}
11111111

11121112
/// Trait denoting that this class is suitable to be used as a base type for PyClass.
1113-
11141113
#[cfg_attr(
11151114
all(diagnostic_namespace, Py_LIMITED_API),
11161115
diagnostic::on_unimplemented(

src/type_object.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use crate::{ffi, Bound, Python};
1616
/// This trait must only be implemented for types which represent valid layouts of Python objects.
1717
pub unsafe trait PyLayout<T> {}
1818

19-
/// `T: PySizedLayout<U>` represents that `T` is not a instance of
19+
/// `T: PySizedLayout<U>` represents that `T` is not an instance of
2020
/// [`PyVarObject`](https://docs.python.org/3/c-api/structures.html#c.PyVarObject).
2121
///
2222
/// In addition, that `T` is a concrete representation of `U`.

src/types/typeobject.rs

+73
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ use crate::{ffi, Bound, PyAny, PyTypeInfo, Python};
88

99
use super::PyString;
1010

11+
#[cfg(not(Py_LIMITED_API))]
12+
use super::PyDict;
13+
1114
/// Represents a reference to a Python `type` object.
1215
///
1316
/// Values of this type are accessed via PyO3's smart pointers, e.g. as
@@ -20,6 +23,17 @@ pub struct PyType(PyAny);
2023

2124
pyobject_native_type_core!(PyType, pyobject_native_static_type_object!(ffi::PyType_Type), #checkfunction=ffi::PyType_Check);
2225

26+
#[cfg(not(Py_LIMITED_API))]
27+
pyobject_native_type_sized!(PyType, ffi::PyHeapTypeObject);
28+
29+
#[cfg(not(Py_LIMITED_API))]
30+
impl crate::impl_::pyclass::PyClassBaseType for PyType {
31+
type LayoutAsBase = crate::impl_::pycell::PyClassObjectBase<ffi::PyHeapTypeObject>;
32+
type BaseNativeType = PyType;
33+
type Initializer = crate::impl_::pyclass_init::PyNativeTypeInitializer<Self>;
34+
type PyClassMutability = crate::pycell::impl_::ImmutableClass;
35+
}
36+
2337
impl PyType {
2438
/// Creates a new type object.
2539
#[inline]
@@ -50,6 +64,29 @@ impl PyType {
5064
.downcast_unchecked()
5165
.to_owned()
5266
}
67+
68+
/// Creates a new type object (class). The resulting type/class will inherit the given metaclass `T`
69+
///
70+
/// Equivalent to calling `type(name, bases, dict, **kwds)`
71+
/// <https://docs.python.org/3/library/functions.html#type>
72+
#[cfg(not(Py_LIMITED_API))]
73+
pub fn new_type<'py, T: PyTypeInfo>(
74+
py: Python<'py>,
75+
args: &Bound<'py, PyTuple>,
76+
kwargs: Option<&Bound<'py, PyDict>>,
77+
) -> PyResult<Bound<'py, T>> {
78+
let new_fn = unsafe {
79+
ffi::PyType_Type
80+
.tp_new
81+
.expect("PyType_Type.tp_new should be present")
82+
};
83+
let raw_type = T::type_object_raw(py);
84+
let raw_args = args.as_ptr();
85+
let raw_kwargs = kwargs.map(|v| v.as_ptr()).unwrap_or(std::ptr::null_mut());
86+
let obj_ptr = unsafe { new_fn(raw_type, raw_args, raw_kwargs) };
87+
let borrowed_obj = unsafe { Borrowed::from_ptr_or_err(py, obj_ptr) }?;
88+
Ok(borrowed_obj.downcast()?.to_owned())
89+
}
5390
}
5491

5592
/// Implementation of functionality for [`PyType`].
@@ -390,4 +427,40 @@ class OuterClass:
390427
);
391428
});
392429
}
430+
431+
#[test]
432+
#[cfg(all(not(Py_LIMITED_API), feature = "macros"))]
433+
fn test_new_type() {
434+
use crate::{
435+
types::{PyDict, PyList, PyString},
436+
IntoPy,
437+
};
438+
439+
Python::with_gil(|py| {
440+
#[allow(non_snake_case)]
441+
let ListType = py.get_type::<PyList>();
442+
let name = PyString::new(py, "MyClass");
443+
let bases = PyTuple::new(py, [ListType]).unwrap();
444+
let dict = PyDict::new(py);
445+
dict.set_item("foo", 123_i32.into_py(py)).unwrap();
446+
let args = PyTuple::new(py, [name.as_any(), bases.as_any(), dict.as_any()]).unwrap();
447+
#[allow(non_snake_case)]
448+
let MyClass = PyType::new_type::<PyType>(py, &args, None).unwrap();
449+
450+
assert_eq!(MyClass.name().unwrap(), "MyClass");
451+
assert_eq!(MyClass.qualname().unwrap(), "MyClass");
452+
453+
crate::py_run!(
454+
py,
455+
MyClass,
456+
r#"
457+
assert type(MyClass) is type
458+
assert MyClass.__bases__ == (list,)
459+
assert issubclass(MyClass, list)
460+
assert MyClass.foo == 123
461+
assert not hasattr(MyClass, "__module__")
462+
"#
463+
);
464+
});
465+
}
393466
}

tests/test_inheritance.rs

+66-12
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,9 @@ fn call_base_and_sub_methods() {
8989
py,
9090
obj,
9191
r#"
92-
assert obj.base_method(10) == 100
93-
assert obj.sub_method(10) == 50
94-
"#
92+
assert obj.base_method(10) == 100
93+
assert obj.sub_method(10) == 50
94+
"#
9595
);
9696
});
9797
}
@@ -163,14 +163,14 @@ fn handle_result_in_new() {
163163
py,
164164
subclass,
165165
r#"
166-
try:
167-
subclass(-10)
168-
assert Fals
169-
except ValueError as e:
170-
pass
171-
except Exception as e:
172-
raise e
173-
"#
166+
try:
167+
subclass(-10)
168+
assert Fals
169+
except ValueError as e:
170+
pass
171+
except Exception as e:
172+
raise e
173+
"#
174174
);
175175
});
176176
}
@@ -180,7 +180,7 @@ except Exception as e:
180180
mod inheriting_native_type {
181181
use super::*;
182182
use pyo3::exceptions::PyException;
183-
use pyo3::types::PyDict;
183+
use pyo3::types::{PyDict, PyTuple};
184184

185185
#[cfg(not(PyPy))]
186186
#[test]
@@ -300,6 +300,60 @@ mod inheriting_native_type {
300300
)
301301
})
302302
}
303+
304+
#[cfg(not(Py_LIMITED_API))]
305+
#[test]
306+
fn inherit_type() {
307+
use pyo3::types::PyType;
308+
309+
#[pyclass(extends=PyType)]
310+
#[derive(Debug)]
311+
struct Metaclass {}
312+
313+
#[pymethods]
314+
impl Metaclass {
315+
#[new]
316+
#[pyo3(signature = (*args, **kwds))]
317+
fn new<'py>(
318+
py: Python<'py>,
319+
args: &Bound<'py, PyTuple>,
320+
kwds: Option<&Bound<'py, PyDict>>,
321+
) -> PyResult<Bound<'py, Self>> {
322+
let type_object = PyType::new_type::<Metaclass>(py, args, kwds)?;
323+
type_object.setattr("some_var", 123)?;
324+
Ok(type_object)
325+
}
326+
327+
fn __getitem__(&self, item: u64) -> u64 {
328+
item + 1
329+
}
330+
}
331+
332+
Python::with_gil(|py| {
333+
#[allow(non_snake_case)]
334+
let Metaclass = py.get_type::<Metaclass>();
335+
336+
// checking base is `type`
337+
py_run!(py, Metaclass, r#"assert Metaclass.__bases__ == (type,)"#);
338+
339+
// check can be used as a metaclass
340+
py_run!(
341+
py,
342+
Metaclass,
343+
r#"
344+
class Foo(metaclass=Metaclass):
345+
pass
346+
assert type(Foo) is Metaclass
347+
assert isinstance(Foo, Metaclass)
348+
assert Foo.some_var == 123
349+
assert Foo[100] == 101
350+
FooDynamic = Metaclass("FooDynamic", (), {})
351+
assert FooDynamic.some_var == 123
352+
assert FooDynamic[100] == 101
353+
"#
354+
);
355+
});
356+
}
303357
}
304358

305359
#[pyclass(subclass)]

0 commit comments

Comments
 (0)