From 336b1c982b081aff45468ac64eac374cd79f3683 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 31 Mar 2024 20:55:31 +0100 Subject: [PATCH] add `import_exception_bound!` macro (#4027) * add `import_exception_bound!` macro * newsfragment and tidy up --- newsfragments/4027.added.md | 1 + src/exceptions.rs | 111 +++++++++++++++++++++++------------- src/impl_.rs | 1 + src/impl_/exceptions.rs | 28 +++++++++ src/sync.rs | 8 ++- 5 files changed, 107 insertions(+), 42 deletions(-) create mode 100644 newsfragments/4027.added.md create mode 100644 src/impl_/exceptions.rs diff --git a/newsfragments/4027.added.md b/newsfragments/4027.added.md new file mode 100644 index 00000000..ccf32952 --- /dev/null +++ b/newsfragments/4027.added.md @@ -0,0 +1 @@ +Add `import_exception_bound!` macro to import exception types without generating GIL Ref functionality for them. diff --git a/src/exceptions.rs b/src/exceptions.rs index 66b5b57d..c650d7af 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -29,18 +29,7 @@ macro_rules! impl_exception_boilerplate { } } - impl $name { - /// Creates a new [`PyErr`] of this type. - /// - /// [`PyErr`]: https://docs.rs/pyo3/latest/pyo3/struct.PyErr.html "PyErr in pyo3" - #[inline] - pub fn new_err(args: A) -> $crate::PyErr - where - A: $crate::PyErrArguments + ::std::marker::Send + ::std::marker::Sync + 'static, - { - $crate::PyErr::new::<$name, A>(args) - } - } + $crate::impl_exception_boilerplate_bound!($name); impl ::std::error::Error for $name { fn source(&self) -> ::std::option::Option<&(dyn ::std::error::Error + 'static)> { @@ -59,6 +48,25 @@ macro_rules! impl_exception_boilerplate { }; } +#[doc(hidden)] +#[macro_export] +macro_rules! impl_exception_boilerplate_bound { + ($name: ident) => { + impl $name { + /// Creates a new [`PyErr`] of this type. + /// + /// [`PyErr`]: https://docs.rs/pyo3/latest/pyo3/struct.PyErr.html "PyErr in pyo3" + #[inline] + pub fn new_err(args: A) -> $crate::PyErr + where + A: $crate::PyErrArguments + ::std::marker::Send + ::std::marker::Sync + 'static, + { + $crate::PyErr::new::<$name, A>(args) + } + } + }; +} + /// Defines a Rust type for an exception defined in Python code. /// /// # Syntax @@ -105,34 +113,57 @@ macro_rules! import_exception { impl $name { fn type_object_raw(py: $crate::Python<'_>) -> *mut $crate::ffi::PyTypeObject { - use $crate::sync::GILOnceCell; - use $crate::prelude::PyTracebackMethods; - use $crate::prelude::PyAnyMethods; - static TYPE_OBJECT: GILOnceCell<$crate::Py<$crate::types::PyType>> = - GILOnceCell::new(); + use $crate::types::PyTypeMethods; + static TYPE_OBJECT: $crate::impl_::exceptions::ImportedExceptionTypeObject = + $crate::impl_::exceptions::ImportedExceptionTypeObject::new(stringify!($module), stringify!($name)); + TYPE_OBJECT.get(py).as_type_ptr() + } + } + }; +} - TYPE_OBJECT - .get_or_init(py, || { - let imp = py - .import_bound(stringify!($module)) - .unwrap_or_else(|err| { - let traceback = err - .traceback_bound(py) - .map(|tb| tb.format().expect("raised exception will have a traceback")) - .unwrap_or_default(); - ::std::panic!("Can not import module {}: {}\n{}", stringify!($module), err, traceback); - }); - let cls = imp.getattr(stringify!($name)).expect(concat!( - "Can not load exception class: ", - stringify!($module), - ".", - stringify!($name) - )); +/// Variant of [`import_exception`](crate::import_exception) that does not emit code needed to +/// use the imported exception type as a GIL Ref. +/// +/// This is useful only during migration as a way to avoid generating needless code. +#[macro_export] +macro_rules! import_exception_bound { + ($module: expr, $name: ident) => { + /// A Rust type representing an exception defined in Python code. + /// + /// This type was created by the [`pyo3::import_exception_bound!`] macro - see its documentation + /// for more information. + /// + /// [`pyo3::import_exception_bound!`]: https://docs.rs/pyo3/latest/pyo3/macro.import_exception.html "import_exception in pyo3" + #[repr(transparent)] + #[allow(non_camel_case_types)] // E.g. `socket.herror` + pub struct $name($crate::PyAny); - cls.extract() - .expect("Imported exception should be a type object") - }) - .as_ptr() as *mut _ + $crate::impl_exception_boilerplate_bound!($name); + + // FIXME remove this: was necessary while `PyTypeInfo` requires `HasPyGilRef`, + // should change in 0.22. + unsafe impl $crate::type_object::HasPyGilRef for $name { + type AsRefTarget = $crate::PyAny; + } + + $crate::pyobject_native_type_info!( + $name, + $name::type_object_raw, + ::std::option::Option::Some(stringify!($module)) + ); + + impl $crate::types::DerefToPyAny for $name {} + + impl $name { + fn type_object_raw(py: $crate::Python<'_>) -> *mut $crate::ffi::PyTypeObject { + use $crate::types::PyTypeMethods; + static TYPE_OBJECT: $crate::impl_::exceptions::ImportedExceptionTypeObject = + $crate::impl_::exceptions::ImportedExceptionTypeObject::new( + stringify!($module), + stringify!($name), + ); + TYPE_OBJECT.get(py).as_type_ptr() } } }; @@ -849,8 +880,8 @@ mod tests { use crate::types::{IntoPyDict, PyDict}; use crate::{PyErr, PyNativeType}; - import_exception!(socket, gaierror); - import_exception!(email.errors, MessageError); + import_exception_bound!(socket, gaierror); + import_exception_bound!(email.errors, MessageError); #[test] fn test_check_exception() { diff --git a/src/impl_.rs b/src/impl_.rs index ea71b257..71ba397c 100644 --- a/src/impl_.rs +++ b/src/impl_.rs @@ -9,6 +9,7 @@ #[cfg(feature = "experimental-async")] pub mod coroutine; pub mod deprecations; +pub mod exceptions; pub mod extract_argument; pub mod freelist; pub mod frompyobject; diff --git a/src/impl_/exceptions.rs b/src/impl_/exceptions.rs new file mode 100644 index 00000000..eafac1ed --- /dev/null +++ b/src/impl_/exceptions.rs @@ -0,0 +1,28 @@ +use crate::{sync::GILOnceCell, types::PyType, Bound, Py, Python}; + +pub struct ImportedExceptionTypeObject { + imported_value: GILOnceCell>, + module: &'static str, + name: &'static str, +} + +impl ImportedExceptionTypeObject { + pub const fn new(module: &'static str, name: &'static str) -> Self { + Self { + imported_value: GILOnceCell::new(), + module, + name, + } + } + + pub fn get<'py>(&self, py: Python<'py>) -> &Bound<'py, PyType> { + self.imported_value + .get_or_try_init_type_ref(py, self.module, self.name) + .unwrap_or_else(|e| { + panic!( + "failed to import exception {}.{}: {}", + self.module, self.name, e + ) + }) + } +} diff --git a/src/sync.rs b/src/sync.rs index 5af49404..856ba84d 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -201,13 +201,17 @@ impl GILOnceCell> { /// /// This is a shorthand method for `get_or_init` which imports the type from Python on init. pub(crate) fn get_or_try_init_type_ref<'py>( - &'py self, + &self, py: Python<'py>, module_name: &str, attr_name: &str, ) -> PyResult<&Bound<'py, PyType>> { self.get_or_try_init(py, || { - py.import_bound(module_name)?.getattr(attr_name)?.extract() + let type_object = py + .import_bound(module_name)? + .getattr(attr_name)? + .downcast_into()?; + Ok(type_object.unbind()) }) .map(|ty| ty.bind(py)) }