Add `#[classattr]` methods to define Python class attributes

This commit is contained in:
scalexm 2020-05-05 21:54:09 +02:00
parent 626268d28a
commit 7b5a076680
6 changed files with 115 additions and 6 deletions

View File

@ -29,6 +29,7 @@ pub enum FnType {
FnCall,
FnClass,
FnStatic,
ClassAttribute,
/// For methods taht have `self_: &PyCell<Self>` instead of self receiver
PySelfRef(syn::TypeReference),
/// For methods taht have `self_: PyRef<Self>` or `PyRefMut<Self>` instead of self receiver
@ -139,6 +140,15 @@ impl<'a> FnSpec<'a> {
};
}
if let FnType::ClassAttribute = &fn_type {
if self_.is_some() || !arguments.is_empty() {
return Err(syn::Error::new_spanned(
name,
"Class attribute methods cannot take arguments",
));
}
}
// "Tweak" getter / setter names: strip off set_ and get_ if needed
if let FnType::Getter | FnType::Setter = &fn_type {
if python_name.is_none() {
@ -178,7 +188,7 @@ impl<'a> FnSpec<'a> {
"text_signature not allowed on __new__; if you want to add a signature on \
__new__, put it on the struct definition instead",
)?,
FnType::FnCall | FnType::Getter | FnType::Setter => {
FnType::FnCall | FnType::Getter | FnType::Setter | FnType::ClassAttribute => {
parse_erroneous_text_signature("text_signature not allowed with this attribute")?
}
};
@ -331,6 +341,8 @@ fn parse_method_attributes(
res = Some(FnType::FnClass)
} else if name.is_ident("staticmethod") {
res = Some(FnType::FnStatic)
} else if name.is_ident("classattr") {
res = Some(FnType::ClassAttribute)
} else if name.is_ident("setter") || name.is_ident("getter") {
if let syn::AttrStyle::Inner(_) = attr.style {
return Err(syn::Error::new_spanned(

View File

@ -30,6 +30,9 @@ pub fn gen_py_method(
FnType::FnCall => impl_py_method_def_call(&spec, &impl_wrap(cls, &spec, false)),
FnType::FnClass => impl_py_method_def_class(&spec, &impl_wrap_class(cls, &spec)),
FnType::FnStatic => impl_py_method_def_static(&spec, &impl_wrap_static(cls, &spec)),
FnType::ClassAttribute => {
impl_py_class_attribute(&spec, &impl_wrap_class_attribute(cls, &spec))
}
FnType::Getter => impl_py_getter_def(
&spec.python_name,
&spec.doc,
@ -246,6 +249,19 @@ pub fn impl_wrap_static(cls: &syn::Type, spec: &FnSpec<'_>) -> TokenStream {
}
}
/// Generate a wrapper for initialization of a class attribute.
/// To be called in `pyo3::pyclass::initialize_type_object`.
pub fn impl_wrap_class_attribute(cls: &syn::Type, spec: &FnSpec<'_>) -> TokenStream {
let name = &spec.name;
let cb = quote! { #cls::#name() };
quote! {
fn __wrap(py: pyo3::Python<'_>) -> pyo3::PyObject {
pyo3::IntoPy::into_py(#cb, py)
}
}
}
fn impl_call_getter(spec: &FnSpec) -> syn::Result<TokenStream> {
let (py_arg, args) = split_off_python_arg(&spec.args);
if !args.is_empty() {
@ -615,6 +631,20 @@ pub fn impl_py_method_def_static(spec: &FnSpec, wrapper: &TokenStream) -> TokenS
}
}
pub fn impl_py_class_attribute(spec: &FnSpec<'_>, wrapper: &TokenStream) -> TokenStream {
let python_name = &spec.python_name;
quote! {
pyo3::class::PyMethodDefType::ClassAttribute({
#wrapper
pyo3::class::PyClassAttributeDef {
name: stringify!(#python_name),
meth: __wrap,
}
})
}
}
pub fn impl_py_method_def_call(spec: &FnSpec, wrapper: &TokenStream) -> TokenStream {
let python_name = &spec.python_name;
let doc = &spec.doc;

View File

@ -1,8 +1,9 @@
// Copyright (c) 2017-present PyO3 Project and Contributors
use crate::ffi;
use crate::{ffi, PyObject, Python};
use libc::c_int;
use std::ffi::CString;
use std::fmt;
/// `PyMethodDefType` represents different types of Python callable objects.
/// It is used by the `#[pymethods]` and `#[pyproto]` annotations.
@ -18,6 +19,8 @@ pub enum PyMethodDefType {
Static(PyMethodDef),
/// Represents normal method
Method(PyMethodDef),
/// Represents class attribute, used by `#[attribute]`
ClassAttribute(PyClassAttributeDef),
/// Represents getter descriptor, used by `#[getter]`
Getter(PyGetterDef),
/// Represents setter descriptor, used by `#[setter]`
@ -40,6 +43,12 @@ pub struct PyMethodDef {
pub ml_doc: &'static str,
}
#[derive(Copy, Clone)]
pub struct PyClassAttributeDef {
pub name: &'static str,
pub meth: for<'p> fn(Python<'p>) -> PyObject,
}
#[derive(Copy, Clone, Debug)]
pub struct PyGetterDef {
pub name: &'static str,
@ -85,6 +94,16 @@ impl PyMethodDef {
}
}
// Manual implementation because `Python<'_>` does not implement `Debug` and
// trait bounds on `fn` compiler-generated derive impls are too restrictive.
impl fmt::Debug for PyClassAttributeDef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PyClassAttributeDef")
.field("name", &self.name)
.finish()
}
}
impl PyGetterDef {
/// Copy descriptor information to `ffi::PyGetSetDef`
pub fn copy_to(&self, dst: &mut ffi::PyGetSetDef) {

View File

@ -24,7 +24,9 @@ pub use self::descr::PyDescrProtocol;
pub use self::gc::{PyGCProtocol, PyTraverseError, PyVisit};
pub use self::iter::PyIterProtocol;
pub use self::mapping::PyMappingProtocol;
pub use self::methods::{PyGetterDef, PyMethodDef, PyMethodDefType, PyMethodType, PySetterDef};
pub use self::methods::{
PyClassAttributeDef, PyGetterDef, PyMethodDef, PyMethodDefType, PyMethodType, PySetterDef,
};
pub use self::number::PyNumberProtocol;
pub use self::pyasync::PyAsyncProtocol;
pub use self::sequence::PySequenceProtocol;

View File

@ -1,7 +1,9 @@
//! `PyClass` trait
use crate::class::methods::{PyMethodDefType, PyMethodsImpl};
use crate::class::methods::{PyClassAttributeDef, PyMethodDefType, PyMethodsImpl};
use crate::conversion::{IntoPyPointer, ToPyObject};
use crate::pyclass_slots::{PyClassDict, PyClassWeakRef};
use crate::type_object::{type_flags, PyLayout};
use crate::types::PyDict;
use crate::{class, ffi, PyCell, PyErr, PyNativeType, PyResult, PyTypeInfo, Python};
use std::ffi::CString;
use std::os::raw::c_void;
@ -165,13 +167,23 @@ where
// buffer protocol
type_object.tp_as_buffer = to_ptr(<T as class::buffer::PyBufferProtocolImpl>::tp_as_buffer());
let (new, call, mut methods, attrs) = py_class_method_defs::<T>();
// normal methods
let (new, call, mut methods) = py_class_method_defs::<T>();
if !methods.is_empty() {
methods.push(ffi::PyMethodDef_INIT);
type_object.tp_methods = Box::into_raw(methods.into_boxed_slice()) as *mut _;
}
// class attributes
if !attrs.is_empty() {
let dict = PyDict::new(py);
for attr in attrs {
dict.set_item(attr.name, (attr.meth)(py))?;
}
type_object.tp_dict = dict.to_object(py).into_ptr();
}
// __new__ method
type_object.tp_new = new;
// __call__ method
@ -219,8 +231,10 @@ fn py_class_method_defs<T: PyMethodsImpl>() -> (
Option<ffi::newfunc>,
Option<ffi::PyCFunctionWithKeywords>,
Vec<ffi::PyMethodDef>,
Vec<PyClassAttributeDef>,
) {
let mut defs = Vec::new();
let mut attrs = Vec::new();
let mut call = None;
let mut new = None;
@ -243,6 +257,9 @@ fn py_class_method_defs<T: PyMethodsImpl>() -> (
| PyMethodDefType::Static(ref def) => {
defs.push(def.as_method_def());
}
PyMethodDefType::ClassAttribute(def) => {
attrs.push(def);
}
_ => (),
}
}
@ -265,7 +282,7 @@ fn py_class_method_defs<T: PyMethodsImpl>() -> (
py_class_async_methods::<T>(&mut defs);
(new, call, defs)
(new, call, defs, attrs)
}
fn py_class_async_methods<T>(defs: &mut Vec<ffi::PyMethodDef>) {

View File

@ -0,0 +1,29 @@
use pyo3::prelude::*;
mod common;
#[pyclass]
struct Foo {}
#[pymethods]
impl Foo {
#[classattr]
fn a() -> i32 {
5
}
#[classattr]
#[name = "B"]
fn b() -> String {
"bar".to_string()
}
}
#[test]
fn class_attributes() {
let gil = Python::acquire_gil();
let py = gil.python();
let foo_obj = py.get_type::<Foo>();
py_assert!(py, foo_obj, "foo_obj.a == 5");
py_assert!(py, foo_obj, "foo_obj.B == 'bar'");
}