| 1 | use std::collections::HashSet; |
| 2 | |
| 3 | use crate::utils::{has_attribute, has_attribute_with_namespace, Ctx, PyO3CratePath}; |
| 4 | use crate::{ |
| 5 | attributes::{take_pyo3_options, CrateAttribute}, |
| 6 | konst::{ConstAttributes, ConstSpec}, |
| 7 | pyfunction::PyFunctionOptions, |
| 8 | pymethod::{self, is_proto_method, MethodAndMethodDef, MethodAndSlotDef}, |
| 9 | }; |
| 10 | use proc_macro2::TokenStream; |
| 11 | use pymethod::GeneratedPyMethod; |
| 12 | use quote::{format_ident, quote}; |
| 13 | use syn::ImplItemFn; |
| 14 | use syn::{ |
| 15 | parse::{Parse, ParseStream}, |
| 16 | spanned::Spanned, |
| 17 | Result, |
| 18 | }; |
| 19 | |
| 20 | /// The mechanism used to collect `#[pymethods]` into the type object |
| 21 | #[derive (Copy, Clone)] |
| 22 | pub enum PyClassMethodsType { |
| 23 | Specialization, |
| 24 | Inventory, |
| 25 | } |
| 26 | |
| 27 | enum PyImplPyO3Option { |
| 28 | Crate(CrateAttribute), |
| 29 | } |
| 30 | |
| 31 | impl Parse for PyImplPyO3Option { |
| 32 | fn parse(input: ParseStream<'_>) -> Result<Self> { |
| 33 | let lookahead: Lookahead1<'_> = input.lookahead1(); |
| 34 | if lookahead.peek(syn::Token![crate]) { |
| 35 | input.parse().map(op:PyImplPyO3Option::Crate) |
| 36 | } else { |
| 37 | Err(lookahead.error()) |
| 38 | } |
| 39 | } |
| 40 | } |
| 41 | |
| 42 | #[derive (Default)] |
| 43 | pub struct PyImplOptions { |
| 44 | krate: Option<CrateAttribute>, |
| 45 | } |
| 46 | |
| 47 | impl PyImplOptions { |
| 48 | pub fn from_attrs(attrs: &mut Vec<syn::Attribute>) -> Result<Self> { |
| 49 | let mut options: PyImplOptions = Default::default(); |
| 50 | |
| 51 | for option: PyImplPyO3Option in take_pyo3_options(attrs)? { |
| 52 | match option { |
| 53 | PyImplPyO3Option::Crate(path: KeywordAttribute>) => options.set_crate(path)?, |
| 54 | } |
| 55 | } |
| 56 | |
| 57 | Ok(options) |
| 58 | } |
| 59 | |
| 60 | fn set_crate(&mut self, path: CrateAttribute) -> Result<()> { |
| 61 | ensure_spanned!( |
| 62 | self.krate.is_none(), |
| 63 | path.span() => "`crate` may only be specified once" |
| 64 | ); |
| 65 | |
| 66 | self.krate = Some(path); |
| 67 | Ok(()) |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | pub fn build_py_methods( |
| 72 | ast: &mut syn::ItemImpl, |
| 73 | methods_type: PyClassMethodsType, |
| 74 | ) -> syn::Result<TokenStream> { |
| 75 | if let Some((_, path: &Path, _)) = &ast.trait_ { |
| 76 | bail_spanned!(path.span() => "#[pymethods] cannot be used on trait impl blocks" ); |
| 77 | } else if ast.generics != Default::default() { |
| 78 | bail_spanned!( |
| 79 | ast.generics.span() => |
| 80 | "#[pymethods] cannot be used with lifetime parameters or generics" |
| 81 | ); |
| 82 | } else { |
| 83 | let options: PyImplOptions = PyImplOptions::from_attrs(&mut ast.attrs)?; |
| 84 | impl_methods(&ast.self_ty, &mut ast.items, methods_type, options) |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | fn check_pyfunction(pyo3_path: &PyO3CratePath, meth: &mut ImplItemFn) -> syn::Result<()> { |
| 89 | let mut error: Option = None; |
| 90 | |
| 91 | meth.attrs.retain(|attr: &Attribute| { |
| 92 | let attrs: [Attribute; 1] = [attr.clone()]; |
| 93 | |
| 94 | if has_attribute(&attrs, ident:"pyfunction" ) |
| 95 | || has_attribute_with_namespace(&attrs, crate_path:Some(pyo3_path), &["pyfunction" ]) |
| 96 | || has_attribute_with_namespace(&attrs, crate_path:Some(pyo3_path), &["prelude" , "pyfunction" ]) { |
| 97 | error = Some(err_spanned!(meth.sig.span() => "functions inside #[pymethods] do not need to be annotated with #[pyfunction]" )); |
| 98 | false |
| 99 | } else { |
| 100 | true |
| 101 | } |
| 102 | }); |
| 103 | |
| 104 | error.map_or(default:Ok(()), f:Err) |
| 105 | } |
| 106 | |
| 107 | pub fn impl_methods( |
| 108 | ty: &syn::Type, |
| 109 | impls: &mut [syn::ImplItem], |
| 110 | methods_type: PyClassMethodsType, |
| 111 | options: PyImplOptions, |
| 112 | ) -> syn::Result<TokenStream> { |
| 113 | let mut trait_impls = Vec::new(); |
| 114 | let mut proto_impls = Vec::new(); |
| 115 | let mut methods = Vec::new(); |
| 116 | let mut associated_methods = Vec::new(); |
| 117 | |
| 118 | let mut implemented_proto_fragments = HashSet::new(); |
| 119 | |
| 120 | for iimpl in impls { |
| 121 | match iimpl { |
| 122 | syn::ImplItem::Fn(meth) => { |
| 123 | let ctx = &Ctx::new(&options.krate, Some(&meth.sig)); |
| 124 | let mut fun_options = PyFunctionOptions::from_attrs(&mut meth.attrs)?; |
| 125 | fun_options.krate = fun_options.krate.or_else(|| options.krate.clone()); |
| 126 | |
| 127 | check_pyfunction(&ctx.pyo3_path, meth)?; |
| 128 | |
| 129 | match pymethod::gen_py_method(ty, &mut meth.sig, &mut meth.attrs, fun_options, ctx)? |
| 130 | { |
| 131 | GeneratedPyMethod::Method(MethodAndMethodDef { |
| 132 | associated_method, |
| 133 | method_def, |
| 134 | }) => { |
| 135 | let attrs = get_cfg_attributes(&meth.attrs); |
| 136 | associated_methods.push(quote!(#(#attrs)* #associated_method)); |
| 137 | methods.push(quote!(#(#attrs)* #method_def)); |
| 138 | } |
| 139 | GeneratedPyMethod::SlotTraitImpl(method_name, token_stream) => { |
| 140 | implemented_proto_fragments.insert(method_name); |
| 141 | let attrs = get_cfg_attributes(&meth.attrs); |
| 142 | trait_impls.push(quote!(#(#attrs)* #token_stream)); |
| 143 | } |
| 144 | GeneratedPyMethod::Proto(MethodAndSlotDef { |
| 145 | associated_method, |
| 146 | slot_def, |
| 147 | }) => { |
| 148 | let attrs = get_cfg_attributes(&meth.attrs); |
| 149 | proto_impls.push(quote!(#(#attrs)* #slot_def)); |
| 150 | associated_methods.push(quote!(#(#attrs)* #associated_method)); |
| 151 | } |
| 152 | } |
| 153 | } |
| 154 | syn::ImplItem::Const(konst) => { |
| 155 | let ctx = &Ctx::new(&options.krate, None); |
| 156 | let attributes = ConstAttributes::from_attrs(&mut konst.attrs)?; |
| 157 | if attributes.is_class_attr { |
| 158 | let spec = ConstSpec { |
| 159 | rust_ident: konst.ident.clone(), |
| 160 | attributes, |
| 161 | }; |
| 162 | let attrs = get_cfg_attributes(&konst.attrs); |
| 163 | let MethodAndMethodDef { |
| 164 | associated_method, |
| 165 | method_def, |
| 166 | } = gen_py_const(ty, &spec, ctx); |
| 167 | methods.push(quote!(#(#attrs)* #method_def)); |
| 168 | associated_methods.push(quote!(#(#attrs)* #associated_method)); |
| 169 | if is_proto_method(&spec.python_name().to_string()) { |
| 170 | // If this is a known protocol method e.g. __contains__, then allow this |
| 171 | // symbol even though it's not an uppercase constant. |
| 172 | konst |
| 173 | .attrs |
| 174 | .push(syn::parse_quote!(#[allow(non_upper_case_globals)])); |
| 175 | } |
| 176 | } |
| 177 | } |
| 178 | syn::ImplItem::Macro(m) => bail_spanned!( |
| 179 | m.span() => |
| 180 | "macros cannot be used as items in `#[pymethods]` impl blocks \n\ |
| 181 | = note: this was previously accepted and ignored" |
| 182 | ), |
| 183 | _ => {} |
| 184 | } |
| 185 | } |
| 186 | let ctx = &Ctx::new(&options.krate, None); |
| 187 | |
| 188 | add_shared_proto_slots(ty, &mut proto_impls, implemented_proto_fragments, ctx); |
| 189 | |
| 190 | let items = match methods_type { |
| 191 | PyClassMethodsType::Specialization => impl_py_methods(ty, methods, proto_impls, ctx), |
| 192 | PyClassMethodsType::Inventory => submit_methods_inventory(ty, methods, proto_impls, ctx), |
| 193 | }; |
| 194 | |
| 195 | Ok(quote! { |
| 196 | #(#trait_impls)* |
| 197 | |
| 198 | #items |
| 199 | |
| 200 | #[doc(hidden)] |
| 201 | #[allow(non_snake_case)] |
| 202 | impl #ty { |
| 203 | #(#associated_methods)* |
| 204 | } |
| 205 | }) |
| 206 | } |
| 207 | |
| 208 | pub fn gen_py_const(cls: &syn::Type, spec: &ConstSpec, ctx: &Ctx) -> MethodAndMethodDef { |
| 209 | let member = &spec.rust_ident; |
| 210 | let wrapper_ident = format_ident!("__pymethod_ {}__" , member); |
| 211 | let python_name = spec.null_terminated_python_name(ctx); |
| 212 | let Ctx { pyo3_path, .. } = ctx; |
| 213 | |
| 214 | let associated_method = quote! { |
| 215 | fn #wrapper_ident(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { |
| 216 | #pyo3_path::IntoPyObjectExt::into_py_any(#cls::#member, py) |
| 217 | } |
| 218 | }; |
| 219 | |
| 220 | let method_def = quote! { |
| 221 | #pyo3_path::impl_::pyclass::MaybeRuntimePyMethodDef::Static( |
| 222 | #pyo3_path::impl_::pymethods::PyMethodDefType::ClassAttribute({ |
| 223 | #pyo3_path::impl_::pymethods::PyClassAttributeDef::new( |
| 224 | #python_name, |
| 225 | #cls::#wrapper_ident |
| 226 | ) |
| 227 | }) |
| 228 | ) |
| 229 | }; |
| 230 | |
| 231 | MethodAndMethodDef { |
| 232 | associated_method, |
| 233 | method_def, |
| 234 | } |
| 235 | } |
| 236 | |
| 237 | fn impl_py_methods( |
| 238 | ty: &syn::Type, |
| 239 | methods: Vec<TokenStream>, |
| 240 | proto_impls: Vec<TokenStream>, |
| 241 | ctx: &Ctx, |
| 242 | ) -> TokenStream { |
| 243 | let Ctx { pyo3_path: &PyO3CratePath, .. } = ctx; |
| 244 | quote! { |
| 245 | #[allow(unknown_lints, non_local_definitions)] |
| 246 | impl #pyo3_path::impl_::pyclass::PyMethods<#ty> |
| 247 | for #pyo3_path::impl_::pyclass::PyClassImplCollector<#ty> |
| 248 | { |
| 249 | fn py_methods(self) -> &'static #pyo3_path::impl_::pyclass::PyClassItems { |
| 250 | static ITEMS: #pyo3_path::impl_::pyclass::PyClassItems = #pyo3_path::impl_::pyclass::PyClassItems { |
| 251 | methods: &[#(#methods),*], |
| 252 | slots: &[#(#proto_impls),*] |
| 253 | }; |
| 254 | &ITEMS |
| 255 | } |
| 256 | } |
| 257 | } |
| 258 | } |
| 259 | |
| 260 | fn add_shared_proto_slots( |
| 261 | ty: &syn::Type, |
| 262 | proto_impls: &mut Vec<TokenStream>, |
| 263 | mut implemented_proto_fragments: HashSet<String>, |
| 264 | ctx: &Ctx, |
| 265 | ) { |
| 266 | let Ctx { pyo3_path, .. } = ctx; |
| 267 | macro_rules! try_add_shared_slot { |
| 268 | ($slot:ident, $($fragments:literal),*) => {{ |
| 269 | let mut implemented = false; |
| 270 | $(implemented |= implemented_proto_fragments.remove($fragments));*; |
| 271 | if implemented { |
| 272 | proto_impls.push(quote! { #pyo3_path::impl_::pyclass::$slot!(#ty) }) |
| 273 | } |
| 274 | }}; |
| 275 | } |
| 276 | |
| 277 | try_add_shared_slot!( |
| 278 | generate_pyclass_getattro_slot, |
| 279 | "__getattribute__" , |
| 280 | "__getattr__" |
| 281 | ); |
| 282 | try_add_shared_slot!(generate_pyclass_setattr_slot, "__setattr__" , "__delattr__" ); |
| 283 | try_add_shared_slot!(generate_pyclass_setdescr_slot, "__set__" , "__delete__" ); |
| 284 | try_add_shared_slot!(generate_pyclass_setitem_slot, "__setitem__" , "__delitem__" ); |
| 285 | try_add_shared_slot!(generate_pyclass_add_slot, "__add__" , "__radd__" ); |
| 286 | try_add_shared_slot!(generate_pyclass_sub_slot, "__sub__" , "__rsub__" ); |
| 287 | try_add_shared_slot!(generate_pyclass_mul_slot, "__mul__" , "__rmul__" ); |
| 288 | try_add_shared_slot!(generate_pyclass_mod_slot, "__mod__" , "__rmod__" ); |
| 289 | try_add_shared_slot!(generate_pyclass_divmod_slot, "__divmod__" , "__rdivmod__" ); |
| 290 | try_add_shared_slot!(generate_pyclass_lshift_slot, "__lshift__" , "__rlshift__" ); |
| 291 | try_add_shared_slot!(generate_pyclass_rshift_slot, "__rshift__" , "__rrshift__" ); |
| 292 | try_add_shared_slot!(generate_pyclass_and_slot, "__and__" , "__rand__" ); |
| 293 | try_add_shared_slot!(generate_pyclass_or_slot, "__or__" , "__ror__" ); |
| 294 | try_add_shared_slot!(generate_pyclass_xor_slot, "__xor__" , "__rxor__" ); |
| 295 | try_add_shared_slot!(generate_pyclass_matmul_slot, "__matmul__" , "__rmatmul__" ); |
| 296 | try_add_shared_slot!(generate_pyclass_truediv_slot, "__truediv__" , "__rtruediv__" ); |
| 297 | try_add_shared_slot!( |
| 298 | generate_pyclass_floordiv_slot, |
| 299 | "__floordiv__" , |
| 300 | "__rfloordiv__" |
| 301 | ); |
| 302 | try_add_shared_slot!(generate_pyclass_pow_slot, "__pow__" , "__rpow__" ); |
| 303 | try_add_shared_slot!( |
| 304 | generate_pyclass_richcompare_slot, |
| 305 | "__lt__" , |
| 306 | "__le__" , |
| 307 | "__eq__" , |
| 308 | "__ne__" , |
| 309 | "__gt__" , |
| 310 | "__ge__" |
| 311 | ); |
| 312 | |
| 313 | // if this assertion trips, a slot fragment has been implemented which has not been added in the |
| 314 | // list above |
| 315 | assert!(implemented_proto_fragments.is_empty()); |
| 316 | } |
| 317 | |
| 318 | fn submit_methods_inventory( |
| 319 | ty: &syn::Type, |
| 320 | methods: Vec<TokenStream>, |
| 321 | proto_impls: Vec<TokenStream>, |
| 322 | ctx: &Ctx, |
| 323 | ) -> TokenStream { |
| 324 | let Ctx { pyo3_path: &PyO3CratePath, .. } = ctx; |
| 325 | quote! { |
| 326 | #pyo3_path::inventory::submit! { |
| 327 | type Inventory = <#ty as #pyo3_path::impl_::pyclass::PyClassImpl>::Inventory; |
| 328 | Inventory::new(#pyo3_path::impl_::pyclass::PyClassItems { methods: &[#(#methods),*], slots: &[#(#proto_impls),*] }) |
| 329 | } |
| 330 | } |
| 331 | } |
| 332 | |
| 333 | pub(crate) fn get_cfg_attributes(attrs: &[syn::Attribute]) -> Vec<&syn::Attribute> { |
| 334 | attrsimpl Iterator |
| 335 | .iter() |
| 336 | .filter(|attr: &&Attribute| attr.path().is_ident("cfg" )) |
| 337 | .collect() |
| 338 | } |
| 339 | |