From 813a3a4e81f88cd38b82fd0ad2647c847a7f7973 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Wed, 15 May 2024 22:54:14 +0200 Subject: [PATCH] impl `PyException` macro --- pyo3-macros-backend/src/lib.rs | 2 + pyo3-macros-backend/src/pyexception.rs | 187 +++++++++++++++++++++++++ pyo3-macros/src/lib.rs | 13 +- src/lib.rs | 2 +- src/prelude.rs | 2 +- 5 files changed, 201 insertions(+), 5 deletions(-) create mode 100644 pyo3-macros-backend/src/pyexception.rs diff --git a/pyo3-macros-backend/src/lib.rs b/pyo3-macros-backend/src/lib.rs index a9d75a2a6fe..1ec11484a57 100644 --- a/pyo3-macros-backend/src/lib.rs +++ b/pyo3-macros-backend/src/lib.rs @@ -16,6 +16,7 @@ mod method; mod module; mod params; mod pyclass; +mod pyexception; mod pyfunction; mod pyimpl; mod pymethod; @@ -24,6 +25,7 @@ mod quotes; pub use frompyobject::build_derive_from_pyobject; pub use module::{pymodule_function_impl, pymodule_module_impl, PyModuleOptions}; pub use pyclass::{build_py_class, build_py_enum, PyClassArgs}; +pub use pyexception::build_derive_pyexception; pub use pyfunction::{build_py_function, PyFunctionOptions}; pub use pyimpl::{build_py_methods, PyClassMethodsType}; pub use utils::get_doc; diff --git a/pyo3-macros-backend/src/pyexception.rs b/pyo3-macros-backend/src/pyexception.rs new file mode 100644 index 00000000000..97f66199cb2 --- /dev/null +++ b/pyo3-macros-backend/src/pyexception.rs @@ -0,0 +1,187 @@ +use crate::attributes::{self, take_pyo3_options, CrateAttribute, KeywordAttribute, NameAttribute}; +use crate::utils::Ctx; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use syn::parse::{Parse, ParseStream}; +use syn::{spanned::Spanned, Attribute, DeriveInput, Result, Token}; + +pub fn build_derive_pyexception(tokens: &DeriveInput) -> Result { + let options = ContainerOptions::from_attrs(&tokens.attrs)?; + let ctx = &Ctx::new(&options.krate); + let Ctx { pyo3_path } = &ctx; + let krate = quote!(#pyo3_path).to_string(); + + let derives = match &tokens.data { + syn::Data::Enum(en) => { + let vis = &tokens.vis; + let ident = &tokens.ident; + let python_name = ident.to_string(); + let base_exception = format_ident!("Py{}", ident); + + let mut variant_match = TokenStream::new(); + let mut variant_exceptions = TokenStream::new(); + + for variant in &en.variants { + let python_name = variant.ident.to_string(); + let exception = format_ident!("Py{}", variant.ident); + let variant = &variant.ident; + + variant_match.extend(quote! { + #ident::#variant { .. } => Self::new::<#exception, _>(::std::string::ToString::to_string(&value)), + }); + + variant_exceptions.extend(quote! { + #[#pyo3_path::pyclass(crate = #krate)] + #[pyo3(name = #python_name, extends = #base_exception, subclass)] + #[automatically_derived] + #vis struct #exception; + + #[#pyo3_path::pymethods(crate = #krate)] + #[automatically_derived] + impl #exception { + #[new] + #[pyo3(signature = (*args, **kwargs))] + pub fn new( + args: #pyo3_path::Bound<'_, #pyo3_path::types::PyTuple>, + kwargs: ::std::option::Option<#pyo3_path::Bound<'_, #pyo3_path::types::PyDict>> + ) -> #pyo3_path::PyClassInitializer { + #pyo3_path::PyClassInitializer::from(#base_exception).add_subclass(Self) + } + } + }) + } + + let (impl_generics, ty_generics, where_clause) = tokens.generics.split_for_impl(); + quote! { + #[#pyo3_path::pyclass(crate = #krate)] + #[pyo3(name = #python_name, extends = #pyo3_path::exceptions::PyException, subclass)] + #[automatically_derived] + #vis struct #base_exception; + + #[#pyo3_path::pymethods(crate = #krate)] + #[automatically_derived] + impl #base_exception { + #[new] + #[pyo3(signature = (*args, **kwargs))] + pub fn new( + args: #pyo3_path::Bound<'_, #pyo3_path::types::PyTuple>, + kwargs: ::std::option::Option<#pyo3_path::Bound<'_, #pyo3_path::types::PyDict>> + ) -> Self { + Self + } + } + + #variant_exceptions + + #[automatically_derived] + impl #impl_generics ::std::convert::From<#ident #ty_generics> for #pyo3_path::PyErr #where_clause { + fn from(value: #ident #ty_generics) -> Self { + match value { + #variant_match + } + } + } + + } + } + syn::Data::Struct(..) => { + let vis = &tokens.vis; + let name_opt = options.name.map(|KeywordAttribute { value, .. }| value.0); + let ident = &tokens.ident; + let python_name = name_opt + .as_ref() + .map(|i| i.to_string()) + .unwrap_or_else(|| ident.to_string()); + let exception = name_opt.unwrap_or_else(|| format_ident!("Py{}", ident)); + + let (impl_generics, ty_generics, where_clause) = tokens.generics.split_for_impl(); + quote! { + #[#pyo3_path::pyclass(crate = #krate)] + #[pyo3(name = #python_name, extends = #pyo3_path::exceptions::PyException, subclass)] + #[automatically_derived] + #vis struct #exception; + + #[#pyo3_path::pymethods(crate = #krate)] + #[automatically_derived] + impl #exception { + #[new] + #[pyo3(signature = (*args, **kwargs))] + pub fn new( + args: #pyo3_path::Bound<'_, #pyo3_path::types::PyTuple>, + kwargs: ::std::option::Option<#pyo3_path::Bound<'_, #pyo3_path::types::PyDict>> + ) -> Self { + Self + } + } + + #[automatically_derived] + impl #impl_generics ::std::convert::From<#ident #ty_generics> for #pyo3_path::PyErr #where_clause { + fn from(value: #ident #ty_generics) -> Self { + Self::new::<#exception, _>(::std::string::ToString::to_string(&value)) + } + } + } + } + syn::Data::Union(_) => bail_spanned!( + tokens.span() => "#[derive(PyException)] is not supported for unions" + ), + }; + + Ok(derives) +} + +#[derive(Default)] +struct ContainerOptions { + name: Option, + krate: Option, +} + +enum ContainerPyO3Attribute { + Name(NameAttribute), + Crate(CrateAttribute), +} + +impl Parse for ContainerPyO3Attribute { + fn parse(input: ParseStream<'_>) -> Result { + let lookahead = input.lookahead1(); + if lookahead.peek(Token![crate]) { + input.parse().map(ContainerPyO3Attribute::Crate) + } else if lookahead.peek(attributes::kw::name) { + input.parse().map(ContainerPyO3Attribute::Name) + } else { + Err(lookahead.error()) + } + } +} + +impl ContainerOptions { + fn from_attrs(attrs: &[Attribute]) -> Result { + let mut options = ContainerOptions::default(); + + take_pyo3_options(&mut attrs.to_vec())? + .into_iter() + .try_for_each(|option| options.set_option(option))?; + + Ok(options) + } + + fn set_option(&mut self, option: ContainerPyO3Attribute) -> syn::Result<()> { + macro_rules! set_option { + ($key:ident) => { + { + ensure_spanned!( + self.$key.is_none(), + $key.span() => concat!("`", stringify!($key), "` may only be specified once") + ); + self.$key = Some($key); + } + }; + } + + match option { + ContainerPyO3Attribute::Crate(krate) => set_option!(krate), + ContainerPyO3Attribute::Name(name) => set_option!(name), + } + Ok(()) + } +} diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index 64756a1c73b..2e3b245039f 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -5,9 +5,9 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use pyo3_macros_backend::{ - build_derive_from_pyobject, build_py_class, build_py_enum, build_py_function, build_py_methods, - pymodule_function_impl, pymodule_module_impl, PyClassArgs, PyClassMethodsType, - PyFunctionOptions, + build_derive_from_pyobject, build_derive_pyexception, build_py_class, build_py_enum, + build_py_function, build_py_methods, pymodule_function_impl, pymodule_module_impl, PyClassArgs, + PyClassMethodsType, PyFunctionOptions, }; use quote::quote; use syn::{parse::Nothing, parse_macro_input, Item}; @@ -154,6 +154,13 @@ pub fn derive_from_py_object(item: TokenStream) -> TokenStream { .into() } +#[proc_macro_derive(PyException, attributes(pyo3))] +pub fn derive_pyexception(item: TokenStream) -> TokenStream { + let ast = parse_macro_input!(item as syn::DeriveInput); + let expanded = build_derive_pyexception(&ast).unwrap_or_compile_error(); + quote!(#expanded).into() +} + fn pyclass_impl( attrs: TokenStream, mut ast: syn::ItemStruct, diff --git a/src/lib.rs b/src/lib.rs index b1d8ae6c7cf..5cf39f42e8e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -474,7 +474,7 @@ mod version; pub use crate::conversions::*; #[cfg(feature = "macros")] -pub use pyo3_macros::{pyfunction, pymethods, pymodule, FromPyObject}; +pub use pyo3_macros::{pyfunction, pymethods, pymodule, FromPyObject, PyException}; /// A proc macro used to expose Rust structs and fieldless enums as Python objects. /// diff --git a/src/prelude.rs b/src/prelude.rs index 4052f7c2d0b..62261cf0622 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -25,7 +25,7 @@ pub use crate::types::{PyAny, PyModule}; pub use crate::PyNativeType; #[cfg(feature = "macros")] -pub use pyo3_macros::{pyclass, pyfunction, pymethods, pymodule, FromPyObject}; +pub use pyo3_macros::{pyclass, pyfunction, pymethods, pymodule, FromPyObject, PyException}; #[cfg(feature = "macros")] pub use crate::{wrap_pyfunction, wrap_pyfunction_bound};