| 1 | use super::Signature; |
| 2 | use proc_macro2::TokenTree; |
| 3 | use quote::ToTokens; |
| 4 | use syn::{Attribute, Expr, ExprLit, Ident, Lit, Meta, MetaList, Result}; |
| 5 | |
| 6 | pub fn extract_documents(attrs: &[Attribute]) -> Vec<String> { |
| 7 | let mut docs = Vec::new(); |
| 8 | for attr in attrs { |
| 9 | // `#[doc = "..."]` case |
| 10 | if attr.path().is_ident("doc" ) { |
| 11 | if let Meta::NameValue(syn::MetaNameValue { |
| 12 | value: |
| 13 | Expr::Lit(ExprLit { |
| 14 | lit: Lit::Str(doc), .. |
| 15 | }), |
| 16 | .. |
| 17 | }) = &attr.meta |
| 18 | { |
| 19 | let doc = doc.value(); |
| 20 | // Remove head space |
| 21 | // |
| 22 | // ``` |
| 23 | // /// This is special document! |
| 24 | // ^ This space is trimmed here |
| 25 | // ``` |
| 26 | docs.push(if !doc.is_empty() && doc.starts_with(' ' ) { |
| 27 | doc[1..].to_string() |
| 28 | } else { |
| 29 | doc |
| 30 | }); |
| 31 | } |
| 32 | } |
| 33 | } |
| 34 | docs |
| 35 | } |
| 36 | |
| 37 | /// `#[pyo3(...)]` style attributes appear in `#[pyclass]` and `#[pymethods]` proc-macros |
| 38 | /// |
| 39 | /// As the reference of PyO3 says: |
| 40 | /// |
| 41 | /// https://docs.rs/pyo3/latest/pyo3/attr.pyclass.html |
| 42 | /// > All of these parameters can either be passed directly on the `#[pyclass(...)]` annotation, |
| 43 | /// > or as one or more accompanying `#[pyo3(...)]` annotations, |
| 44 | /// |
| 45 | /// `#[pyclass(name = "MyClass", module = "MyModule")]` will be decomposed into |
| 46 | /// `#[pyclass]` + `#[pyo3(name = "MyClass")]` + `#[pyo3(module = "MyModule")]`, |
| 47 | /// i.e. two `Attr`s will be created for this case. |
| 48 | /// |
| 49 | #[derive (Debug, Clone, PartialEq)] |
| 50 | pub enum Attr { |
| 51 | // Attributes appears in `#[pyo3(...)]` form or its equivalence |
| 52 | Name(String), |
| 53 | Get, |
| 54 | GetAll, |
| 55 | Module(String), |
| 56 | Signature(Signature), |
| 57 | |
| 58 | // Attributes appears in components within `#[pymethods]` |
| 59 | // <https://docs.rs/pyo3/latest/pyo3/attr.pymethods.html> |
| 60 | New, |
| 61 | Getter(Option<String>), |
| 62 | StaticMethod, |
| 63 | ClassMethod, |
| 64 | } |
| 65 | |
| 66 | pub fn parse_pyo3_attrs(attrs: &[Attribute]) -> Result<Vec<Attr>> { |
| 67 | let mut out: Vec = Vec::new(); |
| 68 | for attr: &Attribute in attrs { |
| 69 | let mut new: Vec = parse_pyo3_attr(attr)?; |
| 70 | out.append(&mut new); |
| 71 | } |
| 72 | Ok(out) |
| 73 | } |
| 74 | |
| 75 | pub fn parse_pyo3_attr(attr: &Attribute) -> Result<Vec<Attr>> { |
| 76 | let mut pyo3_attrs = Vec::new(); |
| 77 | let path = attr.path(); |
| 78 | if path.is_ident("pyclass" ) |
| 79 | || path.is_ident("pymethods" ) |
| 80 | || path.is_ident("pyfunction" ) |
| 81 | || path.is_ident("pyo3" ) |
| 82 | { |
| 83 | // Inner tokens of `#[pyo3(...)]` may not be nested meta |
| 84 | // which can be parsed by `Attribute::parse_nested_meta` |
| 85 | // due to the case of `#[pyo3(signature = (...))]`. |
| 86 | // https://pyo3.rs/v0.19.1/function/signature |
| 87 | if let Meta::List(MetaList { tokens, .. }) = &attr.meta { |
| 88 | use TokenTree::*; |
| 89 | let tokens: Vec<TokenTree> = tokens.clone().into_iter().collect(); |
| 90 | // Since `(...)` part with `signature` becomes `TokenTree::Group`, |
| 91 | // we can split entire stream by `,` first, and then pattern match to each cases. |
| 92 | for tt in tokens.split(|tt| { |
| 93 | if let Punct(p) = tt { |
| 94 | p.as_char() == ',' |
| 95 | } else { |
| 96 | false |
| 97 | } |
| 98 | }) { |
| 99 | match tt { |
| 100 | [Ident(ident)] => { |
| 101 | if ident == "get" { |
| 102 | pyo3_attrs.push(Attr::Get); |
| 103 | } |
| 104 | if ident == "get_all" { |
| 105 | pyo3_attrs.push(Attr::GetAll); |
| 106 | } |
| 107 | } |
| 108 | [Ident(ident), Punct(_), Literal(lit)] => { |
| 109 | if ident == "name" { |
| 110 | pyo3_attrs |
| 111 | .push(Attr::Name(lit.to_string().trim_matches('"' ).to_string())); |
| 112 | } |
| 113 | if ident == "module" { |
| 114 | pyo3_attrs |
| 115 | .push(Attr::Module(lit.to_string().trim_matches('"' ).to_string())); |
| 116 | } |
| 117 | } |
| 118 | [Ident(ident), Punct(_), Group(group)] => { |
| 119 | if ident == "signature" { |
| 120 | pyo3_attrs.push(Attr::Signature(syn::parse2(group.to_token_stream())?)); |
| 121 | } |
| 122 | } |
| 123 | _ => {} |
| 124 | } |
| 125 | } |
| 126 | } |
| 127 | } else if path.is_ident("new" ) { |
| 128 | pyo3_attrs.push(Attr::New); |
| 129 | } else if path.is_ident("staticmethod" ) { |
| 130 | pyo3_attrs.push(Attr::StaticMethod); |
| 131 | } else if path.is_ident("classmethod" ) { |
| 132 | pyo3_attrs.push(Attr::ClassMethod); |
| 133 | } else if path.is_ident("getter" ) { |
| 134 | if let Ok(inner) = attr.parse_args::<Ident>() { |
| 135 | pyo3_attrs.push(Attr::Getter(Some(inner.to_string()))); |
| 136 | } else { |
| 137 | pyo3_attrs.push(Attr::Getter(None)); |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | Ok(pyo3_attrs) |
| 142 | } |
| 143 | |
| 144 | #[cfg (test)] |
| 145 | mod test { |
| 146 | use super::*; |
| 147 | use syn::{parse_str, Fields, ItemStruct}; |
| 148 | |
| 149 | #[test ] |
| 150 | fn test_parse_pyo3_attr() -> Result<()> { |
| 151 | let item: ItemStruct = parse_str( |
| 152 | r#" |
| 153 | #[pyclass(mapping, module = "my_module", name = "Placeholder")] |
| 154 | pub struct PyPlaceholder { |
| 155 | #[pyo3(get)] |
| 156 | pub name: String, |
| 157 | } |
| 158 | "# , |
| 159 | )?; |
| 160 | // `#[pyclass]` part |
| 161 | let attrs = parse_pyo3_attr(&item.attrs[0])?; |
| 162 | assert_eq!( |
| 163 | attrs, |
| 164 | vec![ |
| 165 | Attr::Module("my_module" .to_string()), |
| 166 | Attr::Name("Placeholder" .to_string()) |
| 167 | ] |
| 168 | ); |
| 169 | |
| 170 | // `#[pyo3(get)]` part |
| 171 | if let Fields::Named(fields) = item.fields { |
| 172 | let attrs = parse_pyo3_attr(&fields.named[0].attrs[0])?; |
| 173 | assert_eq!(attrs, vec![Attr::Get]); |
| 174 | } else { |
| 175 | unreachable!() |
| 176 | } |
| 177 | Ok(()) |
| 178 | } |
| 179 | } |
| 180 | |