use super::Diagnostic;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use std::collections::HashMap;
use syn::{
Attribute, AttributeArgs, Ident, ImplItem, Index, Item, Lit, Meta, MethodSig, NestedMeta,
};
enum ClassItem {
Method {
item_ident: Ident,
py_name: String,
},
ClassMethod {
item_ident: Ident,
py_name: String,
},
Property {
item_ident: Ident,
py_name: String,
setter: bool,
},
}
fn meta_to_vec(meta: Meta) -> Result<Vec<NestedMeta>, Meta> {
match meta {
Meta::Word(_) => Ok(Vec::new()),
Meta::List(list) => Ok(list.nested.into_iter().collect()),
Meta::NameValue(_) => Err(meta),
}
}
impl ClassItem {
fn extract_from_syn(
attrs: &mut Vec<Attribute>,
sig: &MethodSig,
) -> Result<Option<ClassItem>, Diagnostic> {
let mut item = None;
let mut attr_idx = None;
for (i, meta) in attrs
.iter()
.filter_map(|attr| attr.parse_meta().ok())
.enumerate()
{
let name = meta.name();
if name == "pymethod" {
if item.is_some() {
bail_span!(
sig.ident,
"You can only have one #[py*] attribute on an impl item"
)
}
let nesteds = meta_to_vec(meta).map_err(|meta| {
err_span!(
meta,
"#[pymethod = \"...\"] cannot be a name/value, you probably meant \
#[pymethod(name = \"...\")]",
)
})?;
let mut py_name = None;
for meta in nesteds {
let meta = match meta {
NestedMeta::Meta(meta) => meta,
NestedMeta::Literal(_) => continue,
};
if let Meta::NameValue(name_value) = meta {
if name_value.ident == "name" {
if let Lit::Str(s) = &name_value.lit {
py_name = Some(s.value());
} else {
bail_span!(&sig.ident, "#[pymethod(name = ...)] must be a string");
}
}
}
}
item = Some(ClassItem::Method {
item_ident: sig.ident.clone(),
py_name: py_name.unwrap_or_else(|| sig.ident.to_string()),
});
attr_idx = Some(i);
} else if name == "pyclassmethod" {
if item.is_some() {
bail_span!(
sig.ident,
"You can only have one #[py*] attribute on an impl item"
)
}
let nesteds = meta_to_vec(meta).map_err(|meta| {
err_span!(
meta,
"#[pyclassmethod = \"...\"] cannot be a name/value, you probably meant \
#[pyclassmethod(name = \"...\")]",
)
})?;
let mut py_name = None;
for meta in nesteds {
let meta = match meta {
NestedMeta::Meta(meta) => meta,
NestedMeta::Literal(_) => continue,
};
if let Meta::NameValue(name_value) = meta {
if name_value.ident == "name" {
if let Lit::Str(s) = &name_value.lit {
py_name = Some(s.value());
} else {
bail_span!(
&sig.ident,
"#[pyclassmethod(name = ...)] must be a string"
);
}
}
}
}
item = Some(ClassItem::ClassMethod {
item_ident: sig.ident.clone(),
py_name: py_name.unwrap_or_else(|| sig.ident.to_string()),
});
attr_idx = Some(i);
} else if name == "pyproperty" {
if item.is_some() {
bail_span!(
sig.ident,
"You can only have one #[py*] attribute on an impl item"
)
}
let nesteds = meta_to_vec(meta).map_err(|meta| {
err_span!(
meta,
"#[pyproperty = \"...\"] cannot be a name/value, you probably meant \
#[pyproperty(name = \"...\")]"
)
})?;
let mut setter = false;
let mut py_name = None;
for meta in nesteds {
let meta = match meta {
NestedMeta::Meta(meta) => meta,
NestedMeta::Literal(_) => continue,
};
match meta {
Meta::NameValue(name_value) => {
if name_value.ident == "name" {
if let Lit::Str(s) = &name_value.lit {
py_name = Some(s.value());
} else {
bail_span!(
&sig.ident,
"#[pyproperty(name = ...)] must be a string"
);
}
}
}
Meta::Word(ident) => {
if ident == "setter" {
setter = true;
}
}
_ => {}
}
}
let py_name = match py_name {
Some(py_name) => py_name,
None => {
let item_ident = sig.ident.to_string();
if setter {
if item_ident.starts_with("set_") {
let name = &item_ident["set_".len()..];
if name.is_empty() {
bail_span!(
&sig.ident,
"A #[pyproperty(setter)] fn with a set_* name must \
have something after \"set_\""
)
} else {
name.to_string()
}
} else {
bail_span!(
&sig.ident,
"A #[pyproperty(setter)] fn must either have a `name` \
parameter or a fn name along the lines of \"set_*\""
)
}
} else {
item_ident
}
}
};
item = Some(ClassItem::Property {
py_name,
item_ident: sig.ident.clone(),
setter,
});
attr_idx = Some(i);
}
}
if let Some(attr_idx) = attr_idx {
attrs.remove(attr_idx);
}
Ok(item)
}
}
pub fn impl_pyimpl(_attr: AttributeArgs, item: Item) -> Result<TokenStream2, Diagnostic> {
let mut imp = if let Item::Impl(imp) = item {
imp
} else {
return Ok(quote!(#item));
};
let mut diagnostics: Vec<Diagnostic> = Vec::new();
let items = imp
.items
.iter_mut()
.filter_map(|item| {
if let ImplItem::Method(meth) = item {
ClassItem::extract_from_syn(&mut meth.attrs, &meth.sig)
.map_err(|err| diagnostics.push(err))
.unwrap_or_default()
} else {
None
}
})
.collect::<Vec<_>>();
let ty = &imp.self_ty;
let mut properties: HashMap<&str, (Option<&Ident>, Option<&Ident>)> = HashMap::new();
for item in items.iter() {
if let ClassItem::Property {
ref item_ident,
ref py_name,
setter,
} = item
{
let entry = properties.entry(py_name).or_default();
let func = if *setter { &mut entry.1 } else { &mut entry.0 };
if func.is_some() {
bail_span!(
item_ident,
"Multiple property accessors with name {:?}",
py_name
)
}
*func = Some(item_ident);
}
}
let methods = items.iter().filter_map(|item| match item {
ClassItem::Method {
item_ident,
py_name,
} => Some(quote! {
class.set_str_attr(#py_name, ctx.new_rustfunc(Self::#item_ident));
}),
ClassItem::ClassMethod {
item_ident,
py_name,
} => Some(quote! {
class.set_str_attr(#py_name, ctx.new_classmethod(Self::#item_ident));
}),
_ => None,
});
let properties = properties
.iter()
.map(|(name, prop)| {
let getter = match prop.0 {
Some(getter) => getter,
None => {
push_err_span!(
diagnostics,
prop.1.unwrap(),
"Property {:?} is missing a getter",
name
);
return TokenStream2::new();
}
};
let add_setter = prop.1.map(|setter| quote!(.add_setter(Self::#setter)));
quote! {
class.set_str_attr(
#name,
::rustpython_vm::obj::objproperty::PropertyBuilder::new(ctx)
.add_getter(Self::#getter)
#add_setter
.create(),
);
}
})
.collect::<Vec<_>>();
Diagnostic::from_vec(diagnostics)?;
let ret = quote! {
#imp
impl ::rustpython_vm::pyobject::PyClassImpl for #ty {
fn impl_extend_class(
ctx: &::rustpython_vm::pyobject::PyContext,
class: &::rustpython_vm::obj::objtype::PyClassRef,
) {
#(#methods)*
#(#properties)*
}
}
};
Ok(ret)
}
fn generate_class_def(
ident: &Ident,
attr_name: &'static str,
attr: AttributeArgs,
attrs: &[Attribute],
) -> Result<TokenStream2, Diagnostic> {
let mut class_name = None;
for attr in attr {
if let NestedMeta::Meta(meta) = attr {
if let Meta::NameValue(name_value) = meta {
if name_value.ident == "name" {
if let Lit::Str(s) = name_value.lit {
class_name = Some(s.value());
} else {
bail_span!(
name_value.lit,
"#[{}(name = ...)] must be a string",
attr_name
);
}
}
}
}
}
let class_name = class_name.unwrap_or_else(|| ident.to_string());
let mut doc: Option<Vec<String>> = None;
for attr in attrs.iter() {
if attr.path.is_ident("doc") {
let meta = attr.parse_meta().expect("expected doc attr to be a meta");
if let Meta::NameValue(name_value) = meta {
if let Lit::Str(s) = name_value.lit {
let val = s.value().trim().to_string();
match doc {
Some(ref mut doc) => doc.push(val),
None => doc = Some(vec![val]),
}
}
}
}
}
let doc = match doc {
Some(doc) => {
let doc = doc.join("\n");
quote!(Some(#doc))
}
None => quote!(None),
};
let ret = quote! {
impl ::rustpython_vm::pyobject::PyClassDef for #ident {
const NAME: &'static str = #class_name;
const DOC: Option<&'static str> = #doc;
}
};
Ok(ret)
}
pub fn impl_pyclass(attr: AttributeArgs, item: Item) -> Result<TokenStream2, Diagnostic> {
let (item, ident, attrs) = match item {
Item::Struct(struc) => (quote!(#struc), struc.ident, struc.attrs),
Item::Enum(enu) => (quote!(#enu), enu.ident, enu.attrs),
other => bail_span!(
other,
"#[pyclass] can only be on a struct or enum declaration"
),
};
let class_def = generate_class_def(&ident, "pyclass", attr, &attrs)?;
let ret = quote! {
#item
#class_def
};
Ok(ret)
}
pub fn impl_pystruct_sequence(attr: AttributeArgs, item: Item) -> Result<TokenStream2, Diagnostic> {
let struc = if let Item::Struct(struc) = item {
struc
} else {
bail_span!(
item,
"#[pystruct_sequence] can only be on a struct declaration"
)
};
let class_def = generate_class_def(&struc.ident, "pystruct_sequence", attr, &struc.attrs)?;
let mut properties = Vec::new();
let mut field_names = Vec::new();
for (i, field) in struc.fields.iter().enumerate() {
let idx = Index::from(i);
if let Some(ref field_name) = field.ident {
let field_name_str = field_name.to_string();
let property = quote! {
class.set_str_attr(
#field_name_str,
::rustpython_vm::obj::objproperty::PropertyBuilder::new(ctx)
.add_getter(|zelf: &::rustpython_vm::obj::objtuple::PyTuple,
_vm: &::rustpython_vm::vm::VirtualMachine|
zelf.fast_getitem(#idx))
.create(),
);
};
properties.push(property);
field_names.push(quote!(#field_name));
} else {
field_names.push(quote!(#idx));
}
}
let ty = &struc.ident;
let ret = quote! {
#struc
#class_def
impl #ty {
fn into_struct_sequence(&self,
vm: &::rustpython_vm::vm::VirtualMachine,
cls: ::rustpython_vm::obj::objtype::PyClassRef,
) -> ::rustpython_vm::pyobject::PyResult<::rustpython_vm::obj::objtuple::PyTupleRef> {
let tuple = ::rustpython_vm::obj::objtuple::PyTuple::from(
vec![#(::rustpython_vm::pyobject::IntoPyObject::into_pyobject(
::std::clone::Clone::clone(&self.#field_names),
vm,
)?),*],
);
::rustpython_vm::pyobject::PyValue::into_ref_with_type(tuple, vm, cls)
}
}
impl ::rustpython_vm::pyobject::PyClassImpl for #ty {
fn impl_extend_class(
ctx: &::rustpython_vm::pyobject::PyContext,
class: &::rustpython_vm::obj::objtype::PyClassRef,
) {
#(#properties)*
}
fn make_class(
ctx: &::rustpython_vm::pyobject::PyContext
) -> ::rustpython_vm::obj::objtype::PyClassRef {
let py_class = ctx.new_class(<Self as ::rustpython_vm::pyobject::PyClassDef>::NAME, ctx.tuple_type());
Self::extend_class(ctx, &py_class);
py_class
}
}
};
Ok(ret)
}