1use std::collections::HashSet;
2
3use crate::combine_errors::CombineErrors;
4#[cfg(feature = "experimental-inspect")]
5use crate::introspection::{attribute_introspection_code, function_introspection_code};
6#[cfg(feature = "experimental-inspect")]
7use crate::method::{FnSpec, FnType};
8#[cfg(feature = "experimental-inspect")]
9use crate::py_expr::PyExpr;
10use crate::utils::{has_attribute, has_attribute_with_namespace, Ctx, PyO3CratePath};
11use crate::{
12 attributes::{take_pyo3_options, CrateAttribute},
13 konst::{ConstAttributes, ConstSpec},
14 pyfunction::PyFunctionOptions,
15 pymethod::{
16 self, is_proto_method, GeneratedPyMethod, MethodAndMethodDef, MethodAndSlotDef, PyMethod,
17 },
18};
19use proc_macro2::TokenStream;
20use quote::{format_ident, quote};
21use syn::{
22 parse::{Parse, ParseStream},
23 spanned::Spanned,
24 ImplItemFn, Result,
25};
26#[cfg(feature = "experimental-inspect")]
27use syn::{parse_quote, Ident};
28
29#[derive(Copy, Clone)]
31pub enum PyClassMethodsType {
32 Specialization,
33 Inventory,
34}
35
36enum PyImplPyO3Option {
37 Crate(CrateAttribute),
38}
39
40impl Parse for PyImplPyO3Option {
41 fn parse(input: ParseStream<'_>) -> Result<Self> {
42 let lookahead = input.lookahead1();
43 if lookahead.peek(syn::Token![crate]) {
44 input.parse().map(PyImplPyO3Option::Crate)
45 } else {
46 Err(lookahead.error())
47 }
48 }
49}
50
51#[derive(Default)]
52pub struct PyImplOptions {
53 krate: Option<CrateAttribute>,
54}
55
56impl PyImplOptions {
57 pub fn from_attrs(attrs: &mut Vec<syn::Attribute>) -> Result<Self> {
58 let mut options: PyImplOptions = Default::default();
59
60 for option in take_pyo3_options(attrs)? {
61 match option {
62 PyImplPyO3Option::Crate(path) => options.set_crate(path)?,
63 }
64 }
65
66 Ok(options)
67 }
68
69 fn set_crate(&mut self, path: CrateAttribute) -> Result<()> {
70 ensure_spanned!(
71 self.krate.is_none(),
72 path.span() => "`crate` may only be specified once"
73 );
74
75 self.krate = Some(path);
76 Ok(())
77 }
78}
79
80pub fn build_py_methods(
81 ast: &mut syn::ItemImpl,
82 methods_type: PyClassMethodsType,
83) -> syn::Result<TokenStream> {
84 if let Some((_, path, _)) = &ast.trait_ {
85 bail_spanned!(path.span() => "#[pymethods] cannot be used on trait impl blocks");
86 } else if ast.generics != Default::default() {
87 bail_spanned!(
88 ast.generics.span() =>
89 "#[pymethods] cannot be used with lifetime parameters or generics"
90 );
91 } else {
92 let options = PyImplOptions::from_attrs(&mut ast.attrs)?;
93 impl_methods(&ast.self_ty, &mut ast.items, methods_type, options)
94 }
95}
96
97fn check_pyfunction(pyo3_path: &PyO3CratePath, meth: &mut ImplItemFn) -> syn::Result<()> {
98 let mut error = None;
99
100 meth.attrs.retain(|attr| {
101 let attrs = [attr.clone()];
102
103 if has_attribute(&attrs, "pyfunction")
104 || has_attribute_with_namespace(&attrs, Some(pyo3_path), &["pyfunction"])
105 || has_attribute_with_namespace(&attrs, Some(pyo3_path), &["prelude", "pyfunction"]) {
106 error = Some(err_spanned!(meth.sig.span() => "functions inside #[pymethods] do not need to be annotated with #[pyfunction]"));
107 false
108 } else {
109 true
110 }
111 });
112
113 error.map_or(Ok(()), Err)
114}
115
116pub fn impl_methods(
117 ty: &syn::Type,
118 impls: &mut [syn::ImplItem],
119 methods_type: PyClassMethodsType,
120 options: PyImplOptions,
121) -> syn::Result<TokenStream> {
122 let mut extra_fragments = Vec::new();
123 let mut proto_impls = Vec::new();
124 let mut methods = Vec::new();
125 let mut associated_methods = Vec::new();
126
127 let mut implemented_proto_fragments = HashSet::new();
128
129 let _: Vec<()> = impls
130 .iter_mut()
131 .map(|iimpl| {
132 match iimpl {
133 syn::ImplItem::Fn(meth) => {
134 let ctx = &Ctx::new(&options.krate, Some(&meth.sig));
135 let mut fun_options = PyFunctionOptions::from_attrs(&mut meth.attrs)?;
136 fun_options.krate = fun_options.krate.or_else(|| options.krate.clone());
137
138 check_pyfunction(&ctx.pyo3_path, meth)?;
139 let method = PyMethod::parse(&mut meth.sig, &mut meth.attrs, fun_options)?;
140 #[cfg(feature = "experimental-inspect")]
141 extra_fragments.push(method_introspection_code(&method.spec, ty, ctx));
142 match pymethod::gen_py_method(ty, method, &meth.attrs, ctx)? {
143 GeneratedPyMethod::Method(MethodAndMethodDef {
144 associated_method,
145 method_def,
146 }) => {
147 let attrs = get_cfg_attributes(&meth.attrs);
148 associated_methods.push(quote!(#(#attrs)* #associated_method));
149 methods.push(quote!(#(#attrs)* #method_def));
150 }
151 GeneratedPyMethod::SlotTraitImpl(method_name, token_stream) => {
152 implemented_proto_fragments.insert(method_name);
153 let attrs = get_cfg_attributes(&meth.attrs);
154 extra_fragments.push(quote!(#(#attrs)* #token_stream));
155 }
156 GeneratedPyMethod::Proto(MethodAndSlotDef {
157 associated_method,
158 slot_def,
159 }) => {
160 let attrs = get_cfg_attributes(&meth.attrs);
161 proto_impls.push(quote!(#(#attrs)* #slot_def));
162 associated_methods.push(quote!(#(#attrs)* #associated_method));
163 }
164 }
165 }
166 syn::ImplItem::Const(konst) => {
167 let ctx = &Ctx::new(&options.krate, None);
168 let attributes = ConstAttributes::from_attrs(&mut konst.attrs)?;
169 if attributes.is_class_attr {
170 let spec = ConstSpec {
171 rust_ident: konst.ident.clone(),
172 attributes,
173 #[cfg(feature = "experimental-inspect")]
174 expr: Some(konst.expr.clone()),
175 #[cfg(feature = "experimental-inspect")]
176 ty: konst.ty.clone(),
177 };
178 let attrs = get_cfg_attributes(&konst.attrs);
179 let MethodAndMethodDef {
180 associated_method,
181 method_def,
182 } = gen_py_const(ty, &spec, ctx);
183 methods.push(quote!(#(#attrs)* #method_def));
184 associated_methods.push(quote!(#(#attrs)* #associated_method));
185 if is_proto_method(&spec.python_name().to_string()) {
186 konst
189 .attrs
190 .push(syn::parse_quote!(#[allow(non_upper_case_globals)]));
191 }
192 }
193 }
194 syn::ImplItem::Macro(m) => bail_spanned!(
195 m.span() =>
196 "macros cannot be used as items in `#[pymethods]` impl blocks\n\
197 = note: this was previously accepted and ignored"
198 ),
199 _ => {}
200 }
201 Ok(())
202 })
203 .try_combine_syn_errors()?;
204
205 let ctx = &Ctx::new(&options.krate, None);
206
207 add_shared_proto_slots(ty, &mut proto_impls, implemented_proto_fragments, ctx);
208
209 let items = match methods_type {
210 PyClassMethodsType::Specialization => impl_py_methods(ty, methods, proto_impls, ctx),
211 PyClassMethodsType::Inventory => submit_methods_inventory(ty, methods, proto_impls, ctx),
212 };
213
214 Ok(quote! {
215 #(#extra_fragments)*
216
217 #items
218
219 #[doc(hidden)]
220 #[allow(non_snake_case)]
221 impl #ty {
222 #(#associated_methods)*
223 }
224 })
225}
226
227pub fn gen_py_const(cls: &syn::Type, spec: &ConstSpec, ctx: &Ctx) -> MethodAndMethodDef {
228 let member = &spec.rust_ident;
229 let wrapper_ident = format_ident!("__pymethod_{}__", member);
230 let python_name = spec.null_terminated_python_name();
231 let Ctx { pyo3_path, .. } = ctx;
232
233 let associated_method = quote! {
234 fn #wrapper_ident(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::Py<#pyo3_path::PyAny>> {
235 #pyo3_path::IntoPyObjectExt::into_py_any(#cls::#member, py)
236 }
237 };
238
239 let method_def = quote! {
240 #pyo3_path::impl_::pymethods::PyMethodDefType::ClassAttribute({
241 #pyo3_path::impl_::pymethods::PyClassAttributeDef::new(
242 #python_name,
243 #cls::#wrapper_ident
244 )
245 })
246 };
247
248 #[cfg_attr(not(feature = "experimental-inspect"), allow(unused_mut))]
249 let mut def = MethodAndMethodDef {
250 associated_method,
251 method_def,
252 };
253
254 #[cfg(feature = "experimental-inspect")]
255 def.add_introspection(attribute_introspection_code(
256 &ctx.pyo3_path,
257 Some(cls),
258 spec.python_name().to_string(),
259 spec.expr
260 .as_ref()
261 .map_or_else(PyExpr::ellipsis, PyExpr::constant_from_expression),
262 spec.ty.clone(),
263 true,
264 ));
265
266 def
267}
268
269fn impl_py_methods(
270 ty: &syn::Type,
271 methods: Vec<TokenStream>,
272 proto_impls: Vec<TokenStream>,
273 ctx: &Ctx,
274) -> TokenStream {
275 let Ctx { pyo3_path, .. } = ctx;
276 quote! {
277 #[allow(unknown_lints, non_local_definitions)]
278 impl #pyo3_path::impl_::pyclass::PyMethods<#ty>
279 for #pyo3_path::impl_::pyclass::PyClassImplCollector<#ty>
280 {
281 fn py_methods(self) -> &'static #pyo3_path::impl_::pyclass::PyClassItems {
282 static ITEMS: #pyo3_path::impl_::pyclass::PyClassItems = #pyo3_path::impl_::pyclass::PyClassItems {
283 methods: &[#(#methods),*],
284 slots: &[#(#proto_impls),*]
285 };
286 &ITEMS
287 }
288 }
289 }
290}
291
292fn add_shared_proto_slots(
293 ty: &syn::Type,
294 proto_impls: &mut Vec<TokenStream>,
295 mut implemented_proto_fragments: HashSet<String>,
296 ctx: &Ctx,
297) {
298 let Ctx { pyo3_path, .. } = ctx;
299 macro_rules! try_add_shared_slot {
300 ($slot:ident, $($fragments:literal),*) => {{
301 let mut implemented = false;
302 $(implemented |= implemented_proto_fragments.remove($fragments));*;
303 if implemented {
304 proto_impls.push(quote! { #pyo3_path::impl_::pyclass::$slot!(#ty) })
305 }
306 }};
307 }
308
309 try_add_shared_slot!(
310 generate_pyclass_getattro_slot,
311 "__getattribute__",
312 "__getattr__"
313 );
314 try_add_shared_slot!(generate_pyclass_setattr_slot, "__setattr__", "__delattr__");
315 try_add_shared_slot!(generate_pyclass_setdescr_slot, "__set__", "__delete__");
316 try_add_shared_slot!(generate_pyclass_setitem_slot, "__setitem__", "__delitem__");
317 try_add_shared_slot!(generate_pyclass_add_slot, "__add__", "__radd__");
318 try_add_shared_slot!(generate_pyclass_sub_slot, "__sub__", "__rsub__");
319 try_add_shared_slot!(generate_pyclass_mul_slot, "__mul__", "__rmul__");
320 try_add_shared_slot!(generate_pyclass_mod_slot, "__mod__", "__rmod__");
321 try_add_shared_slot!(generate_pyclass_divmod_slot, "__divmod__", "__rdivmod__");
322 try_add_shared_slot!(generate_pyclass_lshift_slot, "__lshift__", "__rlshift__");
323 try_add_shared_slot!(generate_pyclass_rshift_slot, "__rshift__", "__rrshift__");
324 try_add_shared_slot!(generate_pyclass_and_slot, "__and__", "__rand__");
325 try_add_shared_slot!(generate_pyclass_or_slot, "__or__", "__ror__");
326 try_add_shared_slot!(generate_pyclass_xor_slot, "__xor__", "__rxor__");
327 try_add_shared_slot!(generate_pyclass_matmul_slot, "__matmul__", "__rmatmul__");
328 try_add_shared_slot!(generate_pyclass_truediv_slot, "__truediv__", "__rtruediv__");
329 try_add_shared_slot!(
330 generate_pyclass_floordiv_slot,
331 "__floordiv__",
332 "__rfloordiv__"
333 );
334 try_add_shared_slot!(generate_pyclass_pow_slot, "__pow__", "__rpow__");
335 try_add_shared_slot!(
336 generate_pyclass_richcompare_slot,
337 "__lt__",
338 "__le__",
339 "__eq__",
340 "__ne__",
341 "__gt__",
342 "__ge__"
343 );
344
345 assert!(implemented_proto_fragments.is_empty());
348}
349
350fn submit_methods_inventory(
351 ty: &syn::Type,
352 methods: Vec<TokenStream>,
353 proto_impls: Vec<TokenStream>,
354 ctx: &Ctx,
355) -> TokenStream {
356 let Ctx { pyo3_path, .. } = ctx;
357 quote! {
358 #pyo3_path::inventory::submit! {
359 type Inventory = <#ty as #pyo3_path::impl_::pyclass::PyClassImpl>::Inventory;
360 Inventory::new(#pyo3_path::impl_::pyclass::PyClassItems { methods: &[#(#methods),*], slots: &[#(#proto_impls),*] })
361 }
362 }
363}
364
365pub(crate) fn get_cfg_attributes(attrs: &[syn::Attribute]) -> Vec<&syn::Attribute> {
366 attrs
367 .iter()
368 .filter(|attr| attr.path().is_ident("cfg"))
369 .collect()
370}
371
372#[cfg(feature = "experimental-inspect")]
373pub fn method_introspection_code(spec: &FnSpec<'_>, parent: &syn::Type, ctx: &Ctx) -> TokenStream {
374 let Ctx { pyo3_path, .. } = ctx;
375
376 let name = spec.python_name.to_string();
377
378 if name == "__richcmp__" {
380 return ["__eq__", "__ne__", "__lt__", "__le__", "__gt__", "__ge__"]
382 .into_iter()
383 .map(|method_name| {
384 let mut spec = (*spec).clone();
385 spec.python_name = Ident::new(method_name, spec.python_name.span());
386 spec.signature.arguments.pop();
390 spec.signature.python_signature.positional_parameters.pop();
391 method_introspection_code(&spec, parent, ctx)
392 })
393 .collect();
394 }
395 let name = match name.as_str() {
398 "__concat__" => "__add__".into(),
399 "__repeat__" => "__mul__".into(),
400 "__inplace_concat__" => "__iadd__".into(),
401 "__inplace_repeat__" => "__imul__".into(),
402 "__getbuffer__" | "__releasebuffer__" | "__traverse__" | "__clear__" => return quote! {},
403 _ => name,
404 };
405
406 let mut first_argument = None;
408 let mut decorators = Vec::new();
409 match &spec.tp {
410 FnType::Getter(_) => {
411 first_argument = Some("self");
412 decorators.push(PyExpr::builtin("property"));
413 }
414 FnType::Setter(_) => {
415 first_argument = Some("self");
416 decorators.push(PyExpr::attribute(
417 PyExpr::attribute(PyExpr::from_type(parent.clone(), None), name.clone()),
418 "setter",
419 ));
420 }
421 FnType::Deleter(_) => {
422 first_argument = Some("self");
423 decorators.push(PyExpr::attribute(
424 PyExpr::attribute(PyExpr::from_type(parent.clone(), None), name.clone()),
425 "deleter",
426 ));
427 }
428 FnType::Fn(_) => {
429 first_argument = Some("self");
430 }
431 FnType::FnClass(_) => {
432 first_argument = Some("cls");
433 if spec.python_name != "__new__" {
434 decorators.push(PyExpr::builtin("classmethod"));
436 }
437 }
438 FnType::FnStatic => {
439 if spec.python_name != "__new__" {
440 decorators.push(PyExpr::builtin("staticmethod"));
441 } else {
442 first_argument = Some("cls");
444 }
445 }
446 FnType::FnModule(_) => (), FnType::ClassAttribute => {
448 first_argument = Some("cls");
449 decorators.push(PyExpr::builtin("classmethod"));
451 decorators.push(PyExpr::builtin("property"));
452 }
453 }
454 let return_type = if spec.python_name == "__new__" {
455 parse_quote!(-> #pyo3_path::PyRef<Self>)
457 } else {
458 spec.output.clone()
459 };
460 function_introspection_code(
461 pyo3_path,
462 None,
463 &name,
464 &spec.signature,
465 first_argument,
466 return_type,
467 decorators,
468 spec.asyncness.is_some(),
469 Some(parent),
470 )
471}