diff --git a/newsfragments/6209.changed.md b/newsfragments/6209.changed.md new file mode 100644 index 00000000000..bce8ccdfcf2 --- /dev/null +++ b/newsfragments/6209.changed.md @@ -0,0 +1 @@ +Improved introspection for `create_exception!` types, and stub generation now works for them. diff --git a/pyo3-macros-backend/src/exception.rs b/pyo3-macros-backend/src/exception.rs new file mode 100644 index 00000000000..ec567ec40c6 --- /dev/null +++ b/pyo3-macros-backend/src/exception.rs @@ -0,0 +1,65 @@ +use proc_macro2::TokenStream; +use syn::parse::Parse; +use syn::{Expr, Ident, Path, Token}; + +use crate::introspection::class_introspection_code; +use crate::py_expr::PyExpr; +use crate::utils::{PyO3CratePath, PythonDoc, StrOrExpr}; + +pub struct ExceptionIntrospectionArgs { + crate_path: Path, + name: Ident, + base: Path, // the super class of this exception + doc: Option, +} + +impl Parse for ExceptionIntrospectionArgs { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let crate_path = input.parse()?; + input.parse::()?; + let name = input.parse()?; + input.parse::()?; + let base = input.parse()?; + let doc = if input.parse::>()?.is_some() { + Some(input.parse()?) + } else { + None + }; + + Ok(Self { + crate_path, + name, + base, + doc, + }) + } +} + +pub fn build_exception_introspection( + ExceptionIntrospectionArgs { + crate_path, + name, + base, + doc, + }: ExceptionIntrospectionArgs, +) -> TokenStream { + let doc = doc.map(|doc| PythonDoc { + parts: vec![StrOrExpr::Expr(doc)], + }); + class_introspection_code( + &PyO3CratePath::Given(crate_path), + &name, + &name.to_string(), + Some(PyExpr::from_type( + syn::TypePath { + qself: None, + path: base, + } + .into(), + None, + )), + false, + None, + doc.as_ref(), + ) +} diff --git a/pyo3-macros-backend/src/lib.rs b/pyo3-macros-backend/src/lib.rs index a90fa73678e..1f192abcf13 100644 --- a/pyo3-macros-backend/src/lib.rs +++ b/pyo3-macros-backend/src/lib.rs @@ -11,6 +11,8 @@ mod utils; mod attributes; mod combine_errors; mod derive_attributes; +#[cfg(feature = "experimental-inspect")] +mod exception; mod frompyobject; mod intopyobject; #[cfg(feature = "experimental-inspect")] @@ -27,6 +29,8 @@ mod pyimpl; mod pymethod; mod quotes; +#[cfg(feature = "experimental-inspect")] +pub use exception::{build_exception_introspection, ExceptionIntrospectionArgs}; pub use frompyobject::build_derive_from_pyobject; pub use intopyobject::build_derive_into_pyobject; pub use module::{pymodule_function_impl, pymodule_module_impl, PyModuleOptions}; diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index bba07366b3c..12ca55c0fe6 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -123,6 +123,14 @@ pub fn pymethods(attr: TokenStream, input: TokenStream) -> TokenStream { pymethods_impl(attr, input, methods_type) } +#[doc(hidden)] +#[proc_macro] +#[cfg(feature = "experimental-inspect")] +pub fn exception_introspection_impl(input: TokenStream) -> TokenStream { + let args = parse_macro_input!(input as pyo3_macros_backend::ExceptionIntrospectionArgs); + pyo3_macros_backend::build_exception_introspection(args).into() +} + /// A proc macro used to expose Rust functions to Python. /// /// Functions annotated with `#[pyfunction]` can also be annotated with the following `#[pyo3]` diff --git a/pytests/stubs/exception.pyi b/pytests/stubs/exception.pyi index 47e86d914c5..dbd975d07a8 100644 --- a/pytests/stubs/exception.pyi +++ b/pytests/stubs/exception.pyi @@ -1,8 +1,6 @@ -from _typeshed import Incomplete -from typing import Any +class MyValueError(ValueError): ... def raise_my_value_error() -> None: ... -def return_my_value_error() -> Any: ... +def return_my_value_error() -> MyValueError: ... def return_pyerr() -> BaseException: ... def return_value_error() -> ValueError: ... -def __getattr__(name: str) -> Incomplete: ... diff --git a/src/exceptions.rs b/src/exceptions.rs index 060c8132176..fa0a81553f6 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -171,6 +171,8 @@ macro_rules! create_exception { $crate::impl_exception_boilerplate!($name); $crate::create_exception_type_object!($module, $name, $base, None); + + $crate::create_exception_introspection!($name, $base); }; ($module: expr, $name: ident, $base: ty, $doc: expr) => { #[repr(transparent)] @@ -180,6 +182,8 @@ macro_rules! create_exception { $crate::impl_exception_boilerplate!($name); $crate::create_exception_type_object!($module, $name, $base, Some($doc)); + + $crate::create_exception_introspection!($name, $base, $doc); }; } @@ -249,6 +253,22 @@ macro_rules! create_exception_type_object { }; } +#[cfg(not(all(feature = "experimental-inspect", feature = "macros")))] +#[doc(hidden)] +#[macro_export] +macro_rules! create_exception_introspection( + ($name: ident, $base: ty $(, $doc: expr)?) => {}; +); + +#[cfg(all(feature = "experimental-inspect", feature = "macros"))] +#[doc(hidden)] +#[macro_export] +macro_rules! create_exception_introspection( + ($name: ident, $base: ty $(, $doc: expr)?) => { + $crate::exception_introspection_impl!($crate, $name, $base $(, $doc)?); + }; +); + /// Adds a TYPE_HINT constant if the `experimental-inspect` feature is enabled. #[cfg(not(feature = "experimental-inspect"))] #[doc(hidden)] diff --git a/src/lib.rs b/src/lib.rs index b9fce6cf463..13b47b74722 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -469,6 +469,10 @@ pub use pyo3_macros::{ #[cfg(feature = "macros")] pub use pyo3_macros::pyclass; +#[doc(hidden)] +#[cfg(all(feature = "macros", feature = "experimental-inspect"))] +pub use pyo3_macros::exception_introspection_impl; + #[cfg(feature = "macros")] #[macro_use] mod macros;