1use proc_macro2::{Span, TokenStream};
2use quote::ToTokens;
3use syn::{punctuated::Punctuated, spanned::Spanned, Token};
4
5use crate::attributes::{CrateAttribute, RenamingRule};
6
7/// Macro inspired by `anyhow::anyhow!` to create a compiler error with the given span.
8macro_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.
15macro_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.
23macro_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`.
32pub 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`.
45pub 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)]
64pub 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.
71pub 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
134impl quote::ToTokens for PythonDoc {
135 fn to_tokens(&self, tokens: &mut TokenStream) {
136 self.0.to_tokens(tokens)
137 }
138}
139
140pub 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
151pub 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`).
159pub(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
165pub 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
180pub(crate) fn is_abi3() -> bool {
181 pyo3_build_config::get().abi3
182}
183