diff --git a/Cargo.toml b/Cargo.toml index 3e2629bd4ca..b5d42af5d95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,7 +60,7 @@ smallvec = { version = "1.0", optional = true } uuid = { version = "1.11.0", optional = true } lock_api = { version = "0.4", optional = true } parking_lot = { version = "0.12", optional = true } -iana-time-zone = { version = "0.1", optional = true, features = ["fallback"]} +iana-time-zone = { version = "0.1", optional = true, features = ["fallback"] } [target.'cfg(not(target_has_atomic = "64"))'.dependencies] portable-atomic = "1.0" diff --git a/newsfragments/5666.added.md b/newsfragments/5666.added.md new file mode 100644 index 00000000000..2b49ec0eed9 --- /dev/null +++ b/newsfragments/5666.added.md @@ -0,0 +1 @@ +Introspection: adds support for exception created using `create_exception!` \ No newline at end of file diff --git a/pyo3-macros-backend/src/introspection.rs b/pyo3-macros-backend/src/introspection.rs index 0aa0e68fe29..b9eec7d8942 100644 --- a/pyo3-macros-backend/src/introspection.rs +++ b/pyo3-macros-backend/src/introspection.rs @@ -20,10 +20,48 @@ use std::collections::HashMap; use std::hash::{Hash, Hasher}; use std::mem::take; use std::sync::atomic::{AtomicUsize, Ordering}; -use syn::{Attribute, Ident, ReturnType, Type, TypePath}; +use syn::ext::IdentExt; +use syn::parse::{Parse, ParseStream}; +use syn::{Attribute, Ident, Path, ReturnType, Token, Type, TypePath}; static GLOBAL_COUNTER_FOR_UNIQUE_NAMES: AtomicUsize = AtomicUsize::new(0); +/// Entry point to implement introspection on exceptions +pub fn implement_class_introspection(options: ClassIntrospectionOptions) -> TokenStream { + class_introspection_code( + &PyO3CratePath::Given(options.pyo3_class_path), + &options.name, + &options.name.unraw().to_string(), + options.base.map(|t| PythonTypeHint::from_type(t, None)), + false, + ) +} + +pub struct ClassIntrospectionOptions { + pub pyo3_class_path: Path, + pub name: Ident, + pub base: Option, +} + +impl Parse for ClassIntrospectionOptions { + fn parse(input: ParseStream<'_>) -> syn::Result { + let pyo3_class_path = input.parse()?; + let _: Token![,] = input.parse()?; + let name = input.parse()?; + let base = if input.peek(Token![,]) { + let _: Token![,] = input.parse()?; + Some(input.parse()?) + } else { + None + }; + Ok(Self { + pyo3_class_path, + name, + base, + }) + } +} + pub fn module_introspection_code<'a>( pyo3_crate_path: &PyO3CratePath, name: &str, diff --git a/pyo3-macros-backend/src/lib.rs b/pyo3-macros-backend/src/lib.rs index 96510e1a8a5..8b443985488 100644 --- a/pyo3-macros-backend/src/lib.rs +++ b/pyo3-macros-backend/src/lib.rs @@ -30,6 +30,8 @@ mod type_hint; pub use frompyobject::build_derive_from_pyobject; pub use intopyobject::build_derive_into_pyobject; +#[cfg(feature = "experimental-inspect")] +pub use introspection::{implement_class_introspection, ClassIntrospectionOptions}; pub use module::{pymodule_function_impl, pymodule_module_impl, PyModuleOptions}; pub use pyclass::{build_py_class, build_py_enum, PyClassArgs}; pub use pyfunction::{build_py_function, PyFunctionOptions}; diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index 04fa83b740d..4187b83209d 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -159,32 +159,25 @@ pub fn pyfunction(attr: TokenStream, input: TokenStream) -> TokenStream { #[proc_macro_derive(IntoPyObject, attributes(pyo3))] pub fn derive_into_py_object(item: TokenStream) -> TokenStream { let ast = parse_macro_input!(item as syn::DeriveInput); - let expanded = build_derive_into_pyobject::(&ast).unwrap_or_compile_error(); - quote!( - #expanded - ) - .into() + build_derive_into_pyobject::(&ast) + .unwrap_or_compile_error() + .into() } #[proc_macro_derive(IntoPyObjectRef, attributes(pyo3))] pub fn derive_into_py_object_ref(item: TokenStream) -> TokenStream { let ast = parse_macro_input!(item as syn::DeriveInput); - let expanded = - pyo3_macros_backend::build_derive_into_pyobject::(&ast).unwrap_or_compile_error(); - quote!( - #expanded - ) - .into() + build_derive_into_pyobject::(&ast) + .unwrap_or_compile_error() + .into() } #[proc_macro_derive(FromPyObject, attributes(pyo3))] pub fn derive_from_py_object(item: TokenStream) -> TokenStream { let ast = parse_macro_input!(item as syn::DeriveInput); - let expanded = build_derive_from_pyobject(&ast).unwrap_or_compile_error(); - quote!( - #expanded - ) - .into() + build_derive_from_pyobject(&ast) + .unwrap_or_compile_error() + .into() } fn pyclass_impl( @@ -254,3 +247,11 @@ impl UnwrapOrCompileError for syn::Result { self.unwrap_or_else(|e| e.into_compile_error()) } } + +#[cfg(feature = "experimental-inspect")] +#[doc(hidden)] +#[proc_macro] +pub fn implement_class_introspection(args: TokenStream) -> TokenStream { + let options = parse_macro_input!(args as pyo3_macros_backend::ClassIntrospectionOptions); + pyo3_macros_backend::implement_class_introspection(options).into() +} diff --git a/pytests/src/exception.rs b/pytests/src/exception.rs new file mode 100644 index 00000000000..8af81c188bc --- /dev/null +++ b/pytests/src/exception.rs @@ -0,0 +1,19 @@ +use pyo3::create_exception; +use pyo3::exceptions::{PyException, PyValueError}; +use pyo3::prelude::*; + +create_exception!(pyo3_pytests.exception, CustomValueError, PyValueError); + +create_exception!(pyo3_pytests.exception, CustomException, PyException); + +#[pymodule(gil_used = false)] +pub mod exception { + #[pymodule_export] + use super::{CustomException, CustomValueError}; + use pyo3::prelude::*; + + #[pyfunction] + fn raise_custom_value_error() -> PyResult<()> { + Err(CustomValueError::new_err("custom value error")) + } +} diff --git a/pytests/src/lib.rs b/pytests/src/lib.rs index b0f8b897495..43ad9421ca9 100644 --- a/pytests/src/lib.rs +++ b/pytests/src/lib.rs @@ -9,6 +9,7 @@ mod consts; pub mod datetime; pub mod dict_iter; pub mod enums; +mod exception; pub mod misc; pub mod objstore; pub mod othermod; @@ -27,8 +28,8 @@ mod pyo3_pytests { use buf_and_str::buf_and_str; #[pymodule_export] use { - comparisons::comparisons, consts::consts, enums::enums, pyclasses::pyclasses, - pyfunctions::pyfunctions, subclassing::subclassing, + comparisons::comparisons, consts::consts, enums::enums, exception::exception, + pyclasses::pyclasses, pyfunctions::pyfunctions, subclassing::subclassing, }; // Inserting to sys.modules allows importing submodules nicely from Python diff --git a/pytests/stubs/exception.pyi b/pytests/stubs/exception.pyi new file mode 100644 index 00000000000..5ad246b89cf --- /dev/null +++ b/pytests/stubs/exception.pyi @@ -0,0 +1,6 @@ +from typing import Any + +class CustomException(Exception): ... +class CustomValueError(ValueError): ... + +def raise_custom_value_error() -> Any: ... diff --git a/src/exceptions.rs b/src/exceptions.rs index 5d5ac6b86f9..0539cab5be0 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -230,9 +230,28 @@ macro_rules! create_exception_type_object { ).as_ptr() as *mut $crate::ffi::PyTypeObject } } + + $crate::create_exception_introspection_data!($name, $base); }; } +/// Adds some introspection data for the exception if the `experimental-inspect` feature is enabled. +#[cfg(not(feature = "experimental-inspect"))] +#[doc(hidden)] +#[macro_export] +macro_rules! create_exception_introspection_data( + ($name: ident, $base: ty) => {}; +); + +#[cfg(all(feature = "experimental-inspect", feature = "macros"))] +#[doc(hidden)] +#[macro_export] +macro_rules! create_exception_introspection_data( + ($name: ident, $base: ty) => { + $crate::implement_class_introspection!($crate, $name, $base); + }; +); + macro_rules! impl_native_exception ( ($name:ident, $exc_name:ident, $python_name:expr, $doc:expr, $layout:path $(, #checkfunction=$checkfunction:path)?) => ( #[doc = $doc] diff --git a/src/lib.rs b/src/lib.rs index ed9ed27698d..0c6e1977647 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -445,6 +445,9 @@ mod version; )] pub use crate::conversions::*; +#[cfg(all(feature = "experimental-inspect", feature = "macros"))] +#[doc(hidden)] +pub use pyo3_macros::implement_class_introspection; #[cfg(feature = "macros")] pub use pyo3_macros::{ pyfunction, pymethods, pymodule, FromPyObject, IntoPyObject, IntoPyObjectRef,