Skip to content

Commit

Permalink
Merge pull request #914 from scalexm/const
Browse files Browse the repository at this point in the history
Allow `#[classattr]` on associated constants
  • Loading branch information
kngwyu committed May 9, 2020
2 parents ab374b4 + 76c7f9e commit 818ebf7
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- `PyAny::dir`. [#886](https://github.com/PyO3/pyo3/pull/886)
- All builtin types (`PyList`, `PyTuple`, `PyDict`) etc. now implement `Deref<Target = PyAny>`. [#911](https://github.com/PyO3/pyo3/pull/911)
- `PyCell<T>` now implements `Deref<Target = PyAny>`. [#911](https://github.com/PyO3/pyo3/pull/911)
- Methods and associated constants can now be annotated with `#[classattr]` to define class attributes. [#905](https://github.com/PyO3/pyo3/pull/905) [#914](https://github.com/PyO3/pyo3/pull/914)

### Changed
- Panics from Rust will now be caught and raised as Python errors. [#797](https://github.com/PyO3/pyo3/pull/797)
Expand Down
14 changes: 14 additions & 0 deletions guide/src/class.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 34 additions & 0 deletions pyo3-derive-backend/src/konst.rs
Original file line number Diff line number Diff line change
@@ -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::Attribute>) -> syn::Result<ConstSpec> {
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()),
})
}
}
1 change: 1 addition & 0 deletions pyo3-derive-backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

mod defs;
mod func;
mod konst;
mod method;
mod module;
mod pyclass;
Expand Down
15 changes: 12 additions & 3 deletions pyo3-derive-backend/src/pyimpl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,18 @@ pub fn impl_methods(ty: &syn::Type, impls: &mut Vec<syn::ImplItem>) -> 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));
}
_ => (),
}
}

Expand Down
39 changes: 36 additions & 3 deletions pyo3-derive-backend/src/pymethod.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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_method_class_attribute(&spec, &impl_wrap_class_attribute(cls, &spec))
}
FnType::Getter => impl_py_getter_def(
&spec.python_name,
Expand Down Expand Up @@ -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::Attribute>,
) -> syn::Result<Option<TokenStream>> {
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_const_class_attribute(&spec, &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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -630,7 +649,21 @@ pub fn impl_py_method_def_static(spec: &FnSpec, wrapper: &TokenStream) -> TokenS
}
}

pub fn impl_py_class_attribute(spec: &FnSpec<'_>, wrapper: &TokenStream) -> TokenStream {
pub fn impl_py_method_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_const_class_attribute(spec: &ConstSpec, wrapper: &TokenStream) -> TokenStream {
let python_name = &spec.python_name;
quote! {
pyo3::class::PyMethodDefType::ClassAttribute({
Expand Down
4 changes: 4 additions & 0 deletions tests/test_class_attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ struct Bar {

#[pymethods]
impl Foo {
#[classattr]
const MY_CONST: &'static str = "foobar";

#[classattr]
fn a() -> i32 {
5
Expand Down Expand Up @@ -53,6 +56,7 @@ fn class_attributes() {
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'");
py_assert!(py, foo_obj, "foo_obj.MY_CONST == 'foobar'");
}

#[test]
Expand Down

0 comments on commit 818ebf7

Please sign in to comment.