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

View file

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