1 | use proc_macro2::{Span, TokenStream}; |
2 | use quote::ToTokens; |
3 | use syn::{punctuated::Punctuated, spanned::Spanned, Token}; |
4 | |
5 | use crate::attributes::{CrateAttribute, RenamingRule}; |
6 | |
7 | /// Macro inspired by `anyhow::anyhow!` to create a compiler error with the given span. |
8 | macro_rules! err_spanned { |
9 | ($span:expr => $msg:expr) => { |
10 | syn::Error::new($span, $msg) |
11 | }; |
12 | } |
13 | |
14 | /// Macro inspired by `anyhow::bail!` to return a compiler error with the given span. |
15 | macro_rules! bail_spanned { |
16 | ($span:expr => $msg:expr) => { |
17 | return Err(err_spanned!($span => $msg)) |
18 | }; |
19 | } |
20 | |
21 | /// Macro inspired by `anyhow::ensure!` to return a compiler error with the given span if the |
22 | /// specified condition is not met. |
23 | macro_rules! ensure_spanned { |
24 | ($condition:expr, $span:expr => $msg:expr) => { |
25 | if !($condition) { |
26 | bail_spanned!($span => $msg); |
27 | } |
28 | } |
29 | } |
30 | |
31 | /// Check if the given type `ty` is `pyo3::Python`. |
32 | pub fn is_python(ty: &syn::Type) -> bool { |
33 | match unwrap_ty_group(ty) { |
34 | syn::Type::Path(typath: &TypePath) => typath |
35 | .path |
36 | .segments |
37 | .last() |
38 | .map(|seg| seg.ident == "Python" ) |
39 | .unwrap_or(default:false), |
40 | _ => false, |
41 | } |
42 | } |
43 | |
44 | /// If `ty` is `Option<T>`, return `Some(T)`, else `None`. |
45 | pub fn option_type_argument(ty: &syn::Type) -> Option<&syn::Type> { |
46 | if let syn::Type::Path(syn::TypePath { path: &Path, .. }) = ty { |
47 | let seg: &PathSegment = path.segments.last().filter(|s: &&PathSegment| s.ident == "Option" )?; |
48 | if let syn::PathArguments::AngleBracketed(params: &AngleBracketedGenericArguments) = &seg.arguments { |
49 | if let syn::GenericArgument::Type(ty: &Type) = params.args.first()? { |
50 | return Some(ty); |
51 | } |
52 | } |
53 | } |
54 | None |
55 | } |
56 | |
57 | /// A syntax tree which evaluates to a nul-terminated docstring for Python. |
58 | /// |
59 | /// Typically the tokens will just be that string, but if the original docs included macro |
60 | /// expressions then the tokens will be a concat!("...", "\n", "\0") expression of the strings and |
61 | /// macro parts. |
62 | /// contents such as parse the string contents. |
63 | #[derive (Clone)] |
64 | pub struct PythonDoc(TokenStream); |
65 | |
66 | /// Collects all #[doc = "..."] attributes into a TokenStream evaluating to a null-terminated string. |
67 | /// |
68 | /// If this doc is for a callable, the provided `text_signature` can be passed to prepend |
69 | /// this to the documentation suitable for Python to extract this into the `__text_signature__` |
70 | /// attribute. |
71 | pub fn get_doc(attrs: &[syn::Attribute], mut text_signature: Option<String>) -> PythonDoc { |
72 | // insert special divider between `__text_signature__` and doc |
73 | // (assume text_signature is itself well-formed) |
74 | if let Some(text_signature) = &mut text_signature { |
75 | text_signature.push_str(" \n-- \n\n" ); |
76 | } |
77 | |
78 | let mut parts = Punctuated::<TokenStream, Token![,]>::new(); |
79 | let mut first = true; |
80 | let mut current_part = text_signature.unwrap_or_default(); |
81 | |
82 | for attr in attrs { |
83 | if attr.path().is_ident("doc" ) { |
84 | if let Ok(nv) = attr.meta.require_name_value() { |
85 | if !first { |
86 | current_part.push(' \n' ); |
87 | } else { |
88 | first = false; |
89 | } |
90 | if let syn::Expr::Lit(syn::ExprLit { |
91 | lit: syn::Lit::Str(lit_str), |
92 | .. |
93 | }) = &nv.value |
94 | { |
95 | // Strip single left space from literal strings, if needed. |
96 | // e.g. `/// Hello world` expands to #[doc = " Hello world"] |
97 | let doc_line = lit_str.value(); |
98 | current_part.push_str(doc_line.strip_prefix(' ' ).unwrap_or(&doc_line)); |
99 | } else { |
100 | // This is probably a macro doc from Rust 1.54, e.g. #[doc = include_str!(...)] |
101 | // Reset the string buffer, write that part, and then push this macro part too. |
102 | parts.push(current_part.to_token_stream()); |
103 | current_part.clear(); |
104 | parts.push(nv.value.to_token_stream()); |
105 | } |
106 | } |
107 | } |
108 | } |
109 | |
110 | if !parts.is_empty() { |
111 | // Doc contained macro pieces - return as `concat!` expression |
112 | if !current_part.is_empty() { |
113 | parts.push(current_part.to_token_stream()); |
114 | } |
115 | |
116 | let mut tokens = TokenStream::new(); |
117 | |
118 | syn::Ident::new("concat" , Span::call_site()).to_tokens(&mut tokens); |
119 | syn::token::Not(Span::call_site()).to_tokens(&mut tokens); |
120 | syn::token::Bracket(Span::call_site()).surround(&mut tokens, |tokens| { |
121 | parts.to_tokens(tokens); |
122 | syn::token::Comma(Span::call_site()).to_tokens(tokens); |
123 | syn::LitStr::new(" \0" , Span::call_site()).to_tokens(tokens); |
124 | }); |
125 | |
126 | PythonDoc(tokens) |
127 | } else { |
128 | // Just a string doc - return directly with nul terminator |
129 | current_part.push(' \0' ); |
130 | PythonDoc(current_part.to_token_stream()) |
131 | } |
132 | } |
133 | |
134 | impl quote::ToTokens for PythonDoc { |
135 | fn to_tokens(&self, tokens: &mut TokenStream) { |
136 | self.0.to_tokens(tokens) |
137 | } |
138 | } |
139 | |
140 | pub fn ensure_not_async_fn(sig: &syn::Signature) -> syn::Result<()> { |
141 | if let Some(asyncness: &Async) = &sig.asyncness { |
142 | bail_spanned!( |
143 | asyncness.span() => "`async fn` is not yet supported for Python functions. \n\n\ |
144 | Additional crates such as `pyo3-asyncio` can be used to integrate async Rust and \ |
145 | Python. For more information, see https://github.com/PyO3/pyo3/issues/1632" |
146 | ); |
147 | }; |
148 | Ok(()) |
149 | } |
150 | |
151 | pub fn unwrap_ty_group(mut ty: &syn::Type) -> &syn::Type { |
152 | while let syn::Type::Group(g: &TypeGroup) = ty { |
153 | ty = &*g.elem; |
154 | } |
155 | ty |
156 | } |
157 | |
158 | /// Extract the path to the pyo3 crate, or use the default (`::pyo3`). |
159 | pub(crate) fn get_pyo3_crate(attr: &Option<CrateAttribute>) -> syn::Path { |
160 | attrOption.as_ref() |
161 | .map(|p: &KeywordAttribute| p.value.0.clone()) |
162 | .unwrap_or_else(|| syn::parse_str("::pyo3" ).unwrap()) |
163 | } |
164 | |
165 | pub fn apply_renaming_rule(rule: RenamingRule, name: &str) -> String { |
166 | use heck::*; |
167 | |
168 | match rule { |
169 | RenamingRule::CamelCase => name.to_lower_camel_case(), |
170 | RenamingRule::KebabCase => name.to_kebab_case(), |
171 | RenamingRule::Lowercase => name.to_lowercase(), |
172 | RenamingRule::PascalCase => name.to_upper_camel_case(), |
173 | RenamingRule::ScreamingKebabCase => name.to_shouty_kebab_case(), |
174 | RenamingRule::ScreamingSnakeCase => name.to_shouty_snake_case(), |
175 | RenamingRule::SnakeCase => name.to_snake_case(), |
176 | RenamingRule::Uppercase => name.to_uppercase(), |
177 | } |
178 | } |
179 | |
180 | pub(crate) fn is_abi3() -> bool { |
181 | pyo3_build_config::get().abi3 |
182 | } |
183 | |