diff --git a/guide/src/class.md b/guide/src/class.md index 84fccd072f7..9e4ed3eb043 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -604,6 +604,20 @@ be mutated at all: pyo3::py_run!(py, my_class, "my_class.my_attribute = 'foo'") ``` +If the class attribute is defined with `const` code only, one can also annotate associated +constants: + +```rust +# use pyo3::prelude::*; +# #[pyclass] +# struct MyClass {} +#[pymethods] +impl MyClass { + #[classattr] + const MY_CONST_ATTRIBUTE: &'static str = "foobar"; +} +``` + ## Callable objects To specify a custom `__call__` method for a custom class, the method needs to be annotated with diff --git a/pyo3-derive-backend/src/konst.rs b/pyo3-derive-backend/src/konst.rs new file mode 100644 index 00000000000..608339a7d6e --- /dev/null +++ b/pyo3-derive-backend/src/konst.rs @@ -0,0 +1,34 @@ +use crate::pyfunction::parse_name_attribute; +use syn::ext::IdentExt; + +#[derive(Clone, PartialEq, Debug)] +pub struct ConstSpec { + pub is_class_attr: bool, + pub python_name: syn::Ident, +} + +impl ConstSpec { + // For now, the only valid attribute is `#[classattr]`. + pub fn parse(name: &syn::Ident, attrs: &mut Vec) -> syn::Result { + let mut new_attrs = Vec::new(); + let mut is_class_attr = false; + + for attr in attrs.iter() { + if let syn::Meta::Path(name) = attr.parse_meta()? { + if name.is_ident("classattr") { + is_class_attr = true; + continue; + } + } + new_attrs.push(attr.clone()); + } + + attrs.clear(); + attrs.extend(new_attrs); + + Ok(ConstSpec { + is_class_attr, + python_name: parse_name_attribute(attrs)?.unwrap_or_else(|| name.unraw()), + }) + } +} diff --git a/pyo3-derive-backend/src/lib.rs b/pyo3-derive-backend/src/lib.rs index 9571a52cb73..cd1b4c3ba54 100644 --- a/pyo3-derive-backend/src/lib.rs +++ b/pyo3-derive-backend/src/lib.rs @@ -5,6 +5,7 @@ mod defs; mod func; +mod konst; mod method; mod module; mod pyclass; diff --git a/pyo3-derive-backend/src/pyimpl.rs b/pyo3-derive-backend/src/pyimpl.rs index 8c71188cdc8..c2a68e6b0ea 100644 --- a/pyo3-derive-backend/src/pyimpl.rs +++ b/pyo3-derive-backend/src/pyimpl.rs @@ -24,9 +24,18 @@ pub fn impl_methods(ty: &syn::Type, impls: &mut Vec) -> syn::Resu let mut methods = Vec::new(); let mut cfg_attributes = Vec::new(); for iimpl in impls.iter_mut() { - if let syn::ImplItem::Method(ref mut meth) = iimpl { - methods.push(pymethod::gen_py_method(ty, &mut meth.sig, &mut meth.attrs)?); - cfg_attributes.push(get_cfg_attributes(&meth.attrs)); + match iimpl { + syn::ImplItem::Method(meth) => { + methods.push(pymethod::gen_py_method(ty, &mut meth.sig, &mut meth.attrs)?); + cfg_attributes.push(get_cfg_attributes(&meth.attrs)); + } + syn::ImplItem::Const(konst) => { + if let Some(meth) = pymethod::gen_py_const(ty, &konst.ident, &mut konst.attrs)? { + methods.push(meth); + } + cfg_attributes.push(get_cfg_attributes(&konst.attrs)); + } + _ => (), } } diff --git a/pyo3-derive-backend/src/pymethod.rs b/pyo3-derive-backend/src/pymethod.rs index 6782a6908ef..670b0938847 100644 --- a/pyo3-derive-backend/src/pymethod.rs +++ b/pyo3-derive-backend/src/pymethod.rs @@ -1,4 +1,5 @@ // Copyright (c) 2017-present PyO3 Project and Contributors +use crate::konst::ConstSpec; use crate::method::{FnArg, FnSpec, FnType}; use crate::utils; use proc_macro2::{Span, TokenStream}; @@ -31,7 +32,7 @@ pub fn gen_py_method( 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)) + impl_py_class_attribute(&spec.python_name, &impl_wrap_class_attribute(cls, &spec)) } FnType::Getter => impl_py_getter_def( &spec.python_name, @@ -62,6 +63,23 @@ fn check_generic(sig: &syn::Signature) -> syn::Result<()> { Ok(()) } +pub fn gen_py_const( + cls: &syn::Type, + name: &syn::Ident, + attrs: &mut Vec, +) -> syn::Result> { + let spec = ConstSpec::parse(name, attrs)?; + if spec.is_class_attr { + let wrapper = quote! { + fn __wrap(py: pyo3::Python<'_>) -> pyo3::PyObject { + pyo3::IntoPy::into_py(#cls::#name, py) + } + }; + return Ok(Some(impl_py_class_attribute(&spec.python_name, &wrapper))); + } + Ok(None) +} + /// Generate function wrapper (PyCFunction, PyCFunctionWithKeywords) pub fn impl_wrap(cls: &syn::Type, spec: &FnSpec<'_>, noargs: bool) -> TokenStream { let body = impl_call(cls, &spec); @@ -249,7 +267,8 @@ pub fn impl_wrap_static(cls: &syn::Type, spec: &FnSpec<'_>) -> TokenStream { } } -/// Generate a wrapper for initialization of a class attribute. +/// Generate a wrapper for initialization of a class attribute from a method +/// annotated with `#[classattr]`. /// 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; @@ -631,8 +650,7 @@ 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; +pub fn impl_py_class_attribute(python_name: &syn::Ident, wrapper: &TokenStream) -> TokenStream { quote! { pyo3::class::PyMethodDefType::ClassAttribute({ #wrapper diff --git a/tests/test_class_attributes.rs b/tests/test_class_attributes.rs index 692a6629bc1..085e2faf43a 100644 --- a/tests/test_class_attributes.rs +++ b/tests/test_class_attributes.rs @@ -16,6 +16,9 @@ struct Bar { #[pymethods] impl Foo { + #[classattr] + const MY_CONST: &'static str = "foobar"; + #[classattr] fn a() -> i32 { 5 @@ -53,6 +56,7 @@ fn class_attributes() { let foo_obj = py.get_type::(); py_assert!(py, foo_obj, "foo_obj.a == 5"); py_assert!(py, foo_obj, "foo_obj.B == 'bar'"); + py_assert!(py, foo_obj, "foo_obj.MY_CONST == 'foobar'"); } #[test]