Specify item key and attr name as arguments.

This commit is contained in:
Sebastian Pütz 2020-08-29 10:40:05 +02:00
parent 60fe4925f5
commit 7781bb78de
2 changed files with 87 additions and 58 deletions

View file

@ -1,7 +1,7 @@
use proc_macro2::{Span, TokenStream}; use proc_macro2::{Span, TokenStream};
use quote::quote; use quote::quote;
use syn::punctuated::Punctuated; use syn::punctuated::Punctuated;
use syn::{parse_quote, Attribute, DataEnum, DeriveInput, ExprCall, Fields, Ident, Result}; use syn::{parse_quote, Attribute, DataEnum, DeriveInput, Fields, Ident, Meta, Result};
/// Describes derivation input of an enum. /// Describes derivation input of an enum.
#[derive(Debug)] #[derive(Debug)]
@ -148,7 +148,7 @@ impl<'a> Container<'a> {
.as_ref() .as_ref()
.expect("Named fields should have identifiers"); .expect("Named fields should have identifiers");
let attr = FieldAttribute::parse_attrs(&field.attrs)? let attr = FieldAttribute::parse_attrs(&field.attrs)?
.unwrap_or_else(|| FieldAttribute::Ident(parse_quote!(getattr))); .unwrap_or_else(|| FieldAttribute::GetAttr(None));
fields.push((ident, attr)) fields.push((ident, attr))
} }
ContainerType::Struct(fields) ContainerType::Struct(fields)
@ -242,10 +242,19 @@ impl<'a> Container<'a> {
let mut fields: Punctuated<TokenStream, syn::Token![,]> = Punctuated::new(); let mut fields: Punctuated<TokenStream, syn::Token![,]> = Punctuated::new();
for (ident, attr) in tups { for (ident, attr) in tups {
let ext_fn = match attr { let ext_fn = match attr {
FieldAttribute::IdentWithArg(expr) => quote!(#expr), FieldAttribute::GetAttr(name) => {
FieldAttribute::Ident(meth) => { if let Some(name) = name.as_ref() {
let arg = ident.to_string(); quote!(getattr(#name))
quote!(#meth(#arg)) } else {
quote!(getattr(stringify!(#ident)))
}
}
FieldAttribute::GetItem(key) => {
if let Some(key) = key.as_ref() {
quote!(get_item(#key))
} else {
quote!(get_item(stringify!(#ident)))
}
} }
}; };
fields.push(quote!(#ident: obj.#ext_fn?.extract()?)); fields.push(quote!(#ident: obj.#ext_fn?.extract()?));
@ -329,9 +338,8 @@ impl ContainerAttribute {
/// Attributes for deriving FromPyObject scoped on fields. /// Attributes for deriving FromPyObject scoped on fields.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
enum FieldAttribute { enum FieldAttribute {
/// How a specific field should be extracted. GetItem(Option<syn::Lit>),
Ident(Ident), GetAttr(Option<syn::LitStr>),
IdentWithArg(ExprCall),
} }
impl FieldAttribute { impl FieldAttribute {
@ -340,68 +348,89 @@ impl FieldAttribute {
/// Currently fails if more than 1 attribute is passed in `pyo3` /// Currently fails if more than 1 attribute is passed in `pyo3`
fn parse_attrs(attrs: &[Attribute]) -> Result<Option<Self>> { fn parse_attrs(attrs: &[Attribute]) -> Result<Option<Self>> {
let list = get_pyo3_meta_list(attrs)?; let list = get_pyo3_meta_list(attrs)?;
if list.nested.is_empty() {
return Ok(None);
}
if list.nested.len() > 1 { if list.nested.len() > 1 {
return Err(syn::Error::new_spanned( return Err(syn::Error::new_spanned(
list, list,
"Only one of `item`, `attribute` can be provided, possibly as \ "Only one of `item`, `attribute` can be provided, possibly with an \
a key-value pair: `attribute = \"name\"`.", additional argument: `item(\"key\")` or `attribute(\"name\").",
)); ));
} }
let meta = if let Some(attr) = list.nested.first() { let metaitem = list.nested.into_iter().next().unwrap();
attr let meta = match metaitem {
} else { syn::NestedMeta::Meta(meta) => meta,
return Ok(None); syn::NestedMeta::Lit(lit) => {
}; return Err(syn::Error::new_spanned(
if let syn::NestedMeta::Meta(metaitem) = meta { lit,
let path = metaitem.path(); "Expected `attribute` or `item`, not a literal.",
let ident = Self::check_valid_ident(path)?; ))
match metaitem {
syn::Meta::NameValue(nv) => Self::get_ident_with_arg(ident, &nv.lit).map(Some),
syn::Meta::Path(_) => Ok(Some(FieldAttribute::Ident(parse_quote!(#ident)))),
_ => Err(syn::Error::new_spanned(
metaitem,
"`item` or `attribute` need to be passed alone or as key-value \
pairs, e.g. `attribute = \"name\"`.",
)),
} }
} else { };
Err(syn::Error::new_spanned(meta, "Unexpected literal.")) let path = meta.path();
} if path.is_ident("attribute") {
} Ok(Some(FieldAttribute::GetAttr(Self::attribute_arg(meta)?)))
} else if path.is_ident("item") {
/// Verify the attribute path and return it if it is valid. Ok(Some(FieldAttribute::GetItem(Self::item_arg(meta)?)))
fn check_valid_ident(path: &syn::Path) -> Result<Ident> {
if path.is_ident("item") {
Ok(parse_quote!(get_item))
} else if path.is_ident("attribute") {
Ok(parse_quote!(getattr))
} else { } else {
Err(syn::Error::new_spanned( Err(syn::Error::new_spanned(
path, meta,
"Expected `item` or `attribute`", "Expected `attribute` or `item`.",
)) ))
} }
} }
/// Try to build `IdentWithArg` based on identifier and literal. fn attribute_arg(meta: syn::Meta) -> syn::Result<Option<syn::LitStr>> {
fn get_ident_with_arg(ident: Ident, lit: &syn::Lit) -> Result<Self> { let arg_list = match meta {
if ident == "getattr" { syn::Meta::List(list) => list,
if let syn::Lit::Str(s) = lit { syn::Meta::Path(_) => return Ok(None),
return Ok(FieldAttribute::IdentWithArg(parse_quote!(#ident(#s)))); Meta::NameValue(nv) => {
} else { let err_msg = "Expected a string literal or no argument: `pyo3(attribute(\"name\") or `pyo3(attribute)`";
return Err(syn::Error::new_spanned(lit, "Expected string literal.")); return Err(syn::Error::new_spanned(nv, err_msg));
}
};
if arg_list.nested.len() != 1 {
return Err(syn::Error::new_spanned(
arg_list,
"Expected a single string literal.",
));
}
let first = arg_list.nested.first().unwrap();
if let syn::NestedMeta::Lit(lit) = first {
if let syn::Lit::Str(litstr) = lit {
return Ok(Some(parse_quote!(#litstr)));
} }
} }
if ident == "get_item" {
return Ok(FieldAttribute::IdentWithArg(parse_quote!(#ident(#lit))));
}
// path is already checked in the `parse_attrs` loop, returning the error here anyways.
Err(syn::Error::new_spanned( Err(syn::Error::new_spanned(
ident, first,
"Expected `item` or `attribute`.", "Expected a single string literal.",
)) ))
} }
fn item_arg(meta: syn::Meta) -> syn::Result<Option<syn::Lit>> {
let arg_list = match meta {
syn::Meta::List(list) => list,
syn::Meta::Path(_) => return Ok(None),
Meta::NameValue(nv) => {
return Err(syn::Error::new_spanned(
nv,
"Expected a literal or no argument: `pyo3(item(\"key\") or `pyo3(item)`",
))
}
};
if arg_list.nested.len() != 1 {
return Err(syn::Error::new_spanned(
arg_list,
"Expected a single literal.",
));
}
let first = arg_list.nested.first().unwrap();
if let syn::NestedMeta::Lit(lit) = first {
return Ok(Some(parse_quote!(#lit)));
}
Err(syn::Error::new_spanned(first, "Expected a literal."))
}
} }
/// Extract pyo3 metalist, flattens multiple lists into a single one. /// Extract pyo3 metalist, flattens multiple lists into a single one.

View file

@ -12,7 +12,7 @@ pub struct A<'a> {
s: String, s: String,
#[pyo3(item)] #[pyo3(item)]
t: &'a PyString, t: &'a PyString,
#[pyo3(attribute = "foo")] #[pyo3(attribute("foo"))]
p: &'a PyAny, p: &'a PyAny,
} }
@ -121,7 +121,7 @@ fn test_generic_named_fields_struct() {
#[derive(Debug, FromPyObject)] #[derive(Debug, FromPyObject)]
pub struct C { pub struct C {
#[pyo3(attribute = "test")] #[pyo3(attribute("test"))]
test: String, test: String,
} }
@ -184,7 +184,7 @@ pub enum Foo<'a> {
a: Option<String>, a: Option<String>,
}, },
StructVarGetAttrArg { StructVarGetAttrArg {
#[pyo3(attribute = "bla")] #[pyo3(attribute("bla"))]
a: bool, a: bool,
}, },
StructWithGetItem { StructWithGetItem {
@ -192,7 +192,7 @@ pub enum Foo<'a> {
a: String, a: String,
}, },
StructWithGetItemArg { StructWithGetItemArg {
#[pyo3(item = "foo")] #[pyo3(item("foo"))]
a: String, a: String,
}, },
#[pyo3(transparent)] #[pyo3(transparent)]