From 86ddfbc080054ebe38832f7813ce0dc770658fd9 Mon Sep 17 00:00:00 2001 From: scalexm Date: Tue, 5 May 2020 21:54:09 +0200 Subject: [PATCH] Add `#[attribute]` methods to define Python class attributes --- pyo3-derive-backend/src/method.rs | 14 ++++++++++++- pyo3-derive-backend/src/pymethod.rs | 32 +++++++++++++++++++++++++++++ src/class/methods.rs | 27 +++++++++++++++++++++++- src/class/mod.rs | 4 +++- src/pyclass.rs | 29 +++++++++++++++++++++++--- tests/test_class_attributes.rs | 29 ++++++++++++++++++++++++++ 6 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 tests/test_class_attributes.rs diff --git a/pyo3-derive-backend/src/method.rs b/pyo3-derive-backend/src/method.rs index aa02a90be97..ea44039b099 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("attribute") { + 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 64bf7b9a02a..b33279723ee 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,21 @@ 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() }; + let into_result = quote! { pyo3::derive_utils::IntoPyResult::into_py_result }; + + quote! { + fn __wrap(py: pyo3::Python<'_>) -> pyo3::PyResult<*mut pyo3::ffi::PyObject> { + let result = #into_result(#cb); + pyo3::callback::convert(py, result) + } + } +} + fn impl_call_getter(spec: &FnSpec) -> syn::Result { let (py_arg, args) = split_off_python_arg(&spec.args); if !args.is_empty() { @@ -615,6 +633,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 1240c97296c..2f4ed1e3a5d 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, PyResult, 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>) -> PyResult<*mut ffi::PyObject>, +} + #[derive(Copy, Clone, Debug)] pub struct PyGetterDef { pub name: &'static str, @@ -85,6 +94,22 @@ impl PyMethodDef { } } +impl PyClassAttributeDef { + pub fn name_cstring(&self) -> CString { + CString::new(self.name).expect("Attribute name must not contain NULL byte") + } +} + +// 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 46114023866..df828ecb75c 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 829cf3dbb47..4fa6aeaf0a8 100644 --- a/src/pyclass.rs +++ b/src/pyclass.rs @@ -1,5 +1,5 @@ //! `PyClass` trait -use crate::class::methods::{PyMethodDefType, PyMethodsImpl}; +use crate::class::methods::{PyClassAttributeDef, PyMethodDefType, PyMethodsImpl}; use crate::pyclass_slots::{PyClassDict, PyClassWeakRef}; use crate::type_object::{type_flags, PyLayout}; use crate::{class, ffi, PyCell, PyErr, PyNativeType, PyResult, PyTypeInfo, Python}; @@ -165,13 +165,31 @@ 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() { + unsafe { + type_object.tp_dict = ffi::PyDict_New(); + if type_object.tp_dict.is_null() { + return Err(PyErr::fetch(py)); + } + for attr in attrs { + let name = attr.name_cstring().into_raw(); + let value = (attr.meth)(py)?; + if ffi::PyDict_SetItemString(type_object.tp_dict, name, value) != 0 { + return Err(PyErr::fetch(py)); + } + } + } + } + // __new__ method type_object.tp_new = new; // __call__ method @@ -219,8 +237,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 +263,9 @@ fn py_class_method_defs() -> ( | PyMethodDefType::Static(ref def) => { defs.push(def.as_method_def()); } + PyMethodDefType::ClassAttribute(def) => { + attrs.push(def); + } _ => (), } } @@ -265,7 +288,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 00000000000..5c162c3bd7b --- /dev/null +++ b/tests/test_class_attributes.rs @@ -0,0 +1,29 @@ +use pyo3::prelude::*; + +mod common; + +#[pyclass] +struct Foo { } + +#[pymethods] +impl Foo { + #[attribute] + fn foo() -> i32 { + 5 + } + + #[attribute] + #[name = "BAR"] + fn bar() -> String { + "bar".to_string() + } +} + +#[test] +fn class_attributes() { + let gil = Python::acquire_gil(); + let py = gil.python(); + let typeobj = py.get_type::(); + py_assert!(py, typeobj, "typeobj.foo == 5"); + py_assert!(py, typeobj, "typeobj.BAR == 'bar'"); +}