Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend PyType to create metaclasses #4621

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading