diff --git a/pyo3-derive-backend/src/method.rs b/pyo3-derive-backend/src/method.rs index aa02a90b..5cb9606e 100644 --- a/pyo3-derive-backend/src/method.rs +++ b/pyo3-derive-backend/src/method.rs @@ -29,6 +29,7 @@ pub enum FnType { FnCall, FnClass, FnStatic, + ClassAttribute, /// For methods taht have `self_: &PyCell` instead of self receiver PySelfRef(syn::TypeReference), /// For methods taht have `self_: PyRef` or `PyRefMut` 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( diff --git a/pyo3-derive-backend/src/pymethod.rs b/pyo3-derive-backend/src/pymethod.rs index 64bf7b9a..6782a690 100644 --- a/pyo3-derive-backend/src/pymethod.rs +++ b/pyo3-derive-backend/src/pymethod.rs @@ -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 { 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; diff --git a/src/class/methods.rs b/src/class/methods.rs index 1240c972..125e1eb2 100644 --- a/src/class/methods.rs +++ b/src/class/methods.rs @@ -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) { diff --git a/src/class/mod.rs b/src/class/mod.rs index 46114023..df828ecb 100644 --- a/src/class/mod.rs +++ b/src/class/mod.rs @@ -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; diff --git a/src/pyclass.rs b/src/pyclass.rs index 829cf3db..f0da8c35 100644 --- a/src/pyclass.rs +++ b/src/pyclass.rs @@ -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(::tp_as_buffer()); + let (new, call, mut methods, attrs) = py_class_method_defs::(); + // normal methods - let (new, call, mut methods) = py_class_method_defs::(); 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() -> ( Option, Option, Vec, + Vec, ) { 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() -> ( | 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() -> ( py_class_async_methods::(&mut defs); - (new, call, defs) + (new, call, defs, attrs) } fn py_class_async_methods(defs: &mut Vec) { diff --git a/tests/test_class_attributes.rs b/tests/test_class_attributes.rs new file mode 100644 index 00000000..c5a36a2e --- /dev/null +++ b/tests/test_class_attributes.rs @@ -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::(); + py_assert!(py, foo_obj, "foo_obj.a == 5"); + py_assert!(py, foo_obj, "foo_obj.B == 'bar'"); +}