1use crate::attributes::KeywordAttribute;
2use crate::combine_errors::CombineErrors;
3#[cfg(feature = "experimental-inspect")]
4use crate::introspection::{function_introspection_code, introspection_id_const};
5#[cfg(feature = "experimental-inspect")]
6use crate::py_expr::PyExpr;
7#[cfg(feature = "experimental-inspect")]
8use crate::utils::get_doc;
9use crate::utils::Ctx;
10use crate::{
11 attributes::{
12 self, get_pyo3_options, take_attributes, take_pyo3_options, CrateAttribute,
13 FromPyWithAttribute, NameAttribute, TextSignatureAttribute,
14 },
15 method::{self, CallingConvention, ClassMethodReceiver, FnArg, SelfConversionPolicy},
16 pymethod::check_generic,
17};
18use proc_macro2::{Span, TokenStream};
19use quote::{format_ident, quote, ToTokens};
20use std::cmp::PartialEq;
21use std::ffi::CString;
22#[cfg(feature = "experimental-inspect")]
23use std::iter::empty;
24use syn::parse::{Parse, ParseStream};
25use syn::punctuated::Punctuated;
26#[cfg(feature = "experimental-inspect")]
27use syn::ReturnType;
28use syn::{ext::IdentExt, spanned::Spanned, LitCStr, LitStr, Path, Result, Token};
29
30mod signature;
31
32pub use self::signature::{ConstructorAttribute, FunctionSignature, SignatureAttribute};
33
34#[derive(Clone, Debug)]
35pub struct PyFunctionArgPyO3Attributes {
36 pub from_py_with: Option<FromPyWithAttribute>,
37 pub cancel_handle: Option<attributes::kw::cancel_handle>,
38}
39
40enum PyFunctionArgPyO3Attribute {
41 FromPyWith(FromPyWithAttribute),
42 CancelHandle(attributes::kw::cancel_handle),
43}
44
45impl Parse for PyFunctionArgPyO3Attribute {
46 fn parse(input: ParseStream<'_>) -> Result<Self> {
47 let lookahead = input.lookahead1();
48 if lookahead.peek(attributes::kw::cancel_handle) {
49 input.parse().map(PyFunctionArgPyO3Attribute::CancelHandle)
50 } else if lookahead.peek(attributes::kw::from_py_with) {
51 input.parse().map(PyFunctionArgPyO3Attribute::FromPyWith)
52 } else {
53 Err(lookahead.error())
54 }
55 }
56}
57
58impl PyFunctionArgPyO3Attributes {
59 pub fn from_attrs(attrs: &mut Vec<syn::Attribute>) -> syn::Result<Self> {
61 let mut attributes = PyFunctionArgPyO3Attributes {
62 from_py_with: None,
63 cancel_handle: None,
64 };
65 take_attributes(attrs, |attr| {
66 if let Some(pyo3_attrs) = get_pyo3_options(attr)? {
67 for attr in pyo3_attrs {
68 match attr {
69 PyFunctionArgPyO3Attribute::FromPyWith(from_py_with) => {
70 ensure_spanned!(
71 attributes.from_py_with.is_none(),
72 from_py_with.span() => "`from_py_with` may only be specified once per argument"
73 );
74 attributes.from_py_with = Some(from_py_with);
75 }
76 PyFunctionArgPyO3Attribute::CancelHandle(cancel_handle) => {
77 ensure_spanned!(
78 attributes.cancel_handle.is_none(),
79 cancel_handle.span() => "`cancel_handle` may only be specified once per argument"
80 );
81 attributes.cancel_handle = Some(cancel_handle);
82 }
83 }
84 ensure_spanned!(
85 attributes.from_py_with.is_none() || attributes.cancel_handle.is_none(),
86 attributes.cancel_handle.unwrap().span() => "`from_py_with` and `cancel_handle` cannot be specified together"
87 );
88 }
89 Ok(true)
90 } else {
91 Ok(false)
92 }
93 })?;
94 Ok(attributes)
95 }
96}
97
98type PyFunctionWarningMessageAttribute = KeywordAttribute<attributes::kw::message, LitStr>;
99type PyFunctionWarningCategoryAttribute = KeywordAttribute<attributes::kw::category, Path>;
100
101pub struct PyFunctionWarningAttribute {
102 pub message: PyFunctionWarningMessageAttribute,
103 pub category: Option<PyFunctionWarningCategoryAttribute>,
104 pub span: Span,
105}
106
107#[derive(PartialEq, Clone)]
108pub enum PyFunctionWarningCategory {
109 Path(Path),
110 UserWarning,
111 DeprecationWarning, }
113
114#[derive(Clone)]
115pub struct PyFunctionWarning {
116 pub message: LitStr,
117 pub category: PyFunctionWarningCategory,
118 pub span: Span,
119}
120
121impl From<PyFunctionWarningAttribute> for PyFunctionWarning {
122 fn from(value: PyFunctionWarningAttribute) -> Self {
123 Self {
124 message: value.message.value,
125 category: value
126 .category
127 .map_or(PyFunctionWarningCategory::UserWarning, |cat| {
128 PyFunctionWarningCategory::Path(cat.value)
129 }),
130 span: value.span,
131 }
132 }
133}
134
135pub trait WarningFactory {
136 fn build_py_warning(&self, ctx: &Ctx) -> TokenStream;
137 fn span(&self) -> Span;
138}
139
140impl WarningFactory for PyFunctionWarning {
141 fn build_py_warning(&self, ctx: &Ctx) -> TokenStream {
142 let message = &self.message.value();
143 let c_message = LitCStr::new(
144 &CString::new(message.clone()).unwrap(),
145 Spanned::span(&message),
146 );
147 let pyo3_path = &ctx.pyo3_path;
148 let category = match &self.category {
149 PyFunctionWarningCategory::Path(path) => quote! {#path},
150 PyFunctionWarningCategory::UserWarning => {
151 quote! {#pyo3_path::exceptions::PyUserWarning}
152 }
153 PyFunctionWarningCategory::DeprecationWarning => {
154 quote! {#pyo3_path::exceptions::PyDeprecationWarning}
155 }
156 };
157 quote! {
158 #pyo3_path::PyErr::warn(py, &<#category as #pyo3_path::PyTypeInfo>::type_object(py), #c_message, 1)?;
159 }
160 }
161
162 fn span(&self) -> Span {
163 self.span
164 }
165}
166
167impl<T: WarningFactory> WarningFactory for Vec<T> {
168 fn build_py_warning(&self, ctx: &Ctx) -> TokenStream {
169 let warnings = self.iter().map(|warning| warning.build_py_warning(ctx));
170
171 quote! {
172 #(#warnings)*
173 }
174 }
175
176 fn span(&self) -> Span {
177 self.iter()
178 .map(|val| val.span())
179 .reduce(|acc, span| acc.join(span).unwrap_or(acc))
180 .unwrap()
181 }
182}
183
184impl Parse for PyFunctionWarningAttribute {
185 fn parse(input: ParseStream<'_>) -> Result<Self> {
186 let mut message: Option<PyFunctionWarningMessageAttribute> = None;
187 let mut category: Option<PyFunctionWarningCategoryAttribute> = None;
188
189 let span = input.parse::<attributes::kw::warn>()?.span();
190
191 let content;
192 syn::parenthesized!(content in input);
193
194 while !content.is_empty() {
195 let lookahead = content.lookahead1();
196
197 if lookahead.peek(attributes::kw::message) {
198 message = content
199 .parse::<PyFunctionWarningMessageAttribute>()
200 .map(Some)?;
201 } else if lookahead.peek(attributes::kw::category) {
202 category = content
203 .parse::<PyFunctionWarningCategoryAttribute>()
204 .map(Some)?;
205 } else {
206 return Err(lookahead.error());
207 }
208
209 if content.peek(Token![,]) {
210 content.parse::<Token![,]>()?;
211 }
212 }
213
214 Ok(PyFunctionWarningAttribute {
215 message: message.ok_or(syn::Error::new(
216 content.span(),
217 "missing `message` in `warn` attribute",
218 ))?,
219 category,
220 span,
221 })
222 }
223}
224
225impl ToTokens for PyFunctionWarningAttribute {
226 fn to_tokens(&self, tokens: &mut TokenStream) {
227 let message_tokens = self.message.to_token_stream();
228 let category_tokens = self
229 .category
230 .as_ref()
231 .map_or(quote! {}, |cat| cat.to_token_stream());
232
233 let token_stream = quote! {
234 warn(#message_tokens, #category_tokens)
235 };
236
237 tokens.extend(token_stream);
238 }
239}
240
241#[derive(Default)]
242pub struct PyFunctionOptions {
243 pub pass_module: Option<attributes::kw::pass_module>,
244 pub name: Option<NameAttribute>,
245 pub signature: Option<SignatureAttribute>,
246 pub text_signature: Option<TextSignatureAttribute>,
247 pub krate: Option<CrateAttribute>,
248 pub warnings: Vec<PyFunctionWarning>,
249}
250
251impl Parse for PyFunctionOptions {
252 fn parse(input: ParseStream<'_>) -> Result<Self> {
253 let mut options = PyFunctionOptions::default();
254
255 let attrs = Punctuated::<PyFunctionOption, syn::Token![,]>::parse_terminated(input)?;
256 options.add_attributes(attrs)?;
257
258 Ok(options)
259 }
260}
261
262pub enum PyFunctionOption {
263 Name(NameAttribute),
264 PassModule(attributes::kw::pass_module),
265 Signature(SignatureAttribute),
266 TextSignature(TextSignatureAttribute),
267 Crate(CrateAttribute),
268 Warning(PyFunctionWarningAttribute),
269}
270
271impl Parse for PyFunctionOption {
272 fn parse(input: ParseStream<'_>) -> Result<Self> {
273 let lookahead = input.lookahead1();
274 if lookahead.peek(attributes::kw::name) {
275 input.parse().map(PyFunctionOption::Name)
276 } else if lookahead.peek(attributes::kw::pass_module) {
277 input.parse().map(PyFunctionOption::PassModule)
278 } else if lookahead.peek(attributes::kw::signature) {
279 input.parse().map(PyFunctionOption::Signature)
280 } else if lookahead.peek(attributes::kw::text_signature) {
281 input.parse().map(PyFunctionOption::TextSignature)
282 } else if lookahead.peek(syn::Token![crate]) {
283 input.parse().map(PyFunctionOption::Crate)
284 } else if lookahead.peek(attributes::kw::warn) {
285 input.parse().map(PyFunctionOption::Warning)
286 } else {
287 Err(lookahead.error())
288 }
289 }
290}
291
292impl PyFunctionOptions {
293 pub fn from_attrs(attrs: &mut Vec<syn::Attribute>) -> syn::Result<Self> {
294 let mut options = PyFunctionOptions::default();
295 options.add_attributes(take_pyo3_options(attrs)?)?;
296 Ok(options)
297 }
298
299 pub fn add_attributes(
300 &mut self,
301 attrs: impl IntoIterator<Item = PyFunctionOption>,
302 ) -> Result<()> {
303 macro_rules! set_option {
304 ($key:ident) => {
305 {
306 ensure_spanned!(
307 self.$key.is_none(),
308 $key.span() => concat!("`", stringify!($key), "` may only be specified once")
309 );
310 self.$key = Some($key);
311 }
312 };
313 }
314 for attr in attrs {
315 match attr {
316 PyFunctionOption::Name(name) => set_option!(name),
317 PyFunctionOption::PassModule(pass_module) => set_option!(pass_module),
318 PyFunctionOption::Signature(signature) => set_option!(signature),
319 PyFunctionOption::TextSignature(text_signature) => set_option!(text_signature),
320 PyFunctionOption::Crate(krate) => set_option!(krate),
321 PyFunctionOption::Warning(warning) => {
322 self.warnings.push(warning.into());
323 }
324 }
325 }
326 Ok(())
327 }
328}
329
330pub fn build_py_function(
331 ast: &mut syn::ItemFn,
332 mut options: PyFunctionOptions,
333) -> syn::Result<TokenStream> {
334 options.add_attributes(take_pyo3_options(&mut ast.attrs)?)?;
335 impl_wrap_pyfunction(ast, options)
336}
337
338pub fn impl_wrap_pyfunction(
341 func: &mut syn::ItemFn,
342 options: PyFunctionOptions,
343) -> syn::Result<TokenStream> {
344 check_generic(&func.sig)?;
345 let PyFunctionOptions {
346 pass_module,
347 name,
348 signature,
349 text_signature,
350 krate,
351 warnings,
352 } = options;
353
354 let ctx = &Ctx::new(&krate, Some(&func.sig));
355 let Ctx { pyo3_path, .. } = &ctx;
356
357 let python_name = name
358 .as_ref()
359 .map_or_else(|| &func.sig.ident, |name| &name.value.0)
360 .unraw();
361
362 let tp = if pass_module.is_some() {
363 let span = match func.sig.inputs.first() {
364 Some(syn::FnArg::Typed(first_arg)) => first_arg.ty.span(),
365 Some(syn::FnArg::Receiver(_)) | None => bail_spanned!(
366 func.sig.paren_token.span.join() => "expected `&PyModule` or `Py<PyModule>` as first argument with `pass_module`"
367 ),
368 };
369 method::FnType::FnModule(span)
370 } else {
371 method::FnType::FnStatic
372 };
373
374 let arguments = func
375 .sig
376 .inputs
377 .iter_mut()
378 .skip(if tp.skip_first_rust_argument_in_python_signature() {
379 1
380 } else {
381 0
382 })
383 .map(FnArg::parse)
384 .try_combine_syn_errors()?;
385
386 let signature = if let Some(signature) = signature {
387 FunctionSignature::from_arguments_and_attribute(arguments, signature)?
388 } else {
389 FunctionSignature::from_arguments(arguments)
390 };
391
392 let spec = method::FnSpec {
393 tp,
394 name: &func.sig.ident,
395 python_name,
396 signature,
397 text_signature,
398 asyncness: func.sig.asyncness,
399 unsafety: func.sig.unsafety,
400 warnings,
401 output: func.sig.output.clone(),
402 };
403
404 let vis = &func.vis;
405 let name = &func.sig.ident;
406
407 #[cfg(feature = "experimental-inspect")]
408 let introspection = function_introspection_code(
409 pyo3_path,
410 Some(name),
411 &spec.python_name.to_string(),
413 &spec.signature,
414 None,
415 match &func.sig.output {
416 ReturnType::Type(_, t) => PyExpr::from_return_type((**t).clone(), None),
417 ReturnType::Default => PyExpr::none(),
418 },
419 empty(),
420 func.sig.asyncness.is_some(),
421 false,
422 get_doc(&func.attrs, None).as_ref(),
423 None,
424 );
425 #[cfg(not(feature = "experimental-inspect"))]
426 let introspection = quote! {};
427 #[cfg(feature = "experimental-inspect")]
428 let introspection_id = introspection_id_const();
429 #[cfg(not(feature = "experimental-inspect"))]
430 let introspection_id = quote! {};
431
432 let wrapper_ident = format_ident!("__pyfunction_{}", spec.name);
433 if spec.asyncness.is_some() {
434 ensure_spanned!(
435 cfg!(feature = "experimental-async"),
436 spec.asyncness.span() => "async functions are only supported with the `experimental-async` feature"
437 );
438 }
439 let calling_convention = CallingConvention::from_signature(&spec.signature);
440 let wrapper = spec.get_wrapper_function(
441 &wrapper_ident,
442 None,
443 calling_convention,
444 SelfConversionPolicy::checked(),
445 ClassMethodReceiver::Class,
446 ctx,
447 )?;
448 let methoddef = spec.get_methoddef(
449 wrapper_ident,
450 spec.get_doc(&func.attrs).as_ref(),
451 calling_convention,
452 ctx,
453 )?;
454
455 let wrapped_pyfunction = quote! {
456 #[doc(hidden)]
459 #vis mod #name {
460 pub(crate) struct MakeDef;
461 pub static _PYO3_DEF: #pyo3_path::impl_::pyfunction::PyFunctionDef = MakeDef::_PYO3_DEF;
462 #introspection_id
463 }
464
465 #[allow(unknown_lints, non_local_definitions)]
470 impl #name::MakeDef {
471 #[allow(clippy::declare_interior_mutable_const)]
473 const _PYO3_DEF: #pyo3_path::impl_::pyfunction::PyFunctionDef =
474 #pyo3_path::impl_::pyfunction::PyFunctionDef::from_method_def(#methoddef);
475 }
476
477 #[allow(non_snake_case)]
478 #wrapper
479
480 #introspection
481 };
482 Ok(wrapped_pyfunction)
483}