Skip to main content

pyo3_macros_backend/
pyimpl.rs

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/// The mechanism used to collect `#[pymethods]` into the type object
30#[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                            // If this is a known protocol method e.g. __contains__, then allow this
187                            // symbol even though it's not an uppercase constant.
188                            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    // if this assertion trips, a slot fragment has been implemented which has not been added in the
346    // list above
347    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    // __richcmp__ special case
379    if name == "__richcmp__" {
380        // We expend into each individual method
381        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                // We remove the CompareOp arg, this is safe because the signature is always the same
387                // First the other value to compare with then the CompareOp
388                // We cant to keep the first argument type, hence this hack
389                spec.signature.arguments.pop();
390                spec.signature.python_signature.positional_parameters.pop();
391                method_introspection_code(&spec, parent, ctx)
392            })
393            .collect();
394    }
395    // We map or ignore some magic methods
396    // TODO: this might create a naming conflict
397    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    // We introduce self/cls argument and setup decorators
407    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                // special case __new__ - does not get the decorator
435                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                // special case __new__ - does not get the decorator and gets first argument
443                first_argument = Some("cls");
444            }
445        }
446        FnType::FnModule(_) => (), // TODO: not sure this can happen
447        FnType::ClassAttribute => {
448            first_argument = Some("cls");
449            // TODO: this combination only works with Python 3.9-3.11 https://docs.python.org/3.11/library/functions.html#classmethod
450            decorators.push(PyExpr::builtin("classmethod"));
451            decorators.push(PyExpr::builtin("property"));
452        }
453    }
454    let return_type = if spec.python_name == "__new__" {
455        // Hack to return Self while implementing IntoPyObject
456        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}