Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow user defined exceptions to have docstrings #2027

Merged
merged 4 commits into from Dec 14, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
85 changes: 73 additions & 12 deletions src/err/mod.rs
Expand Up @@ -11,7 +11,6 @@ use crate::{AsPyPointer, IntoPy, Py, PyAny, PyObject, Python, ToBorrowedObject,
use std::borrow::Cow;
use std::cell::UnsafeCell;
use std::ffi::CString;
use std::os::raw::c_char;
use std::os::raw::c_int;
use std::ptr::NonNull;

Expand Down Expand Up @@ -306,13 +305,20 @@ impl PyErr {
}
}

/// Creates a new exception type with the given name, which must be of the form
/// `<module>.<ExceptionName>`, as required by `PyErr_NewException`.
/// Creates a new exception type with the given name.
///
/// `base` can be an existing exception type to subclass, or a tuple of classes
/// `dict` specifies an optional dictionary of class variables and methods
/// - `base` can be an existing exception type to subclass, or a tuple of classes.
/// - `dict` specifies an optional dictionary of class variables and methods.
///
/// For a version of this function that also takes a docstring, see [`PyErr::new_type_with_doc`].
///
/// # Panics
///
/// This function will panic if:
/// - `name` is not of the form `<module>.<ExceptionName>`.
/// - `name` cannot be converted to [`CString`]s.
pub fn new_type<'p>(
_: Python<'p>,
py: Python<'p>,
name: &str,
base: Option<&PyType>,
dict: Option<PyObject>,
Expand All @@ -327,15 +333,70 @@ impl PyErr {
Some(obj) => obj.as_ptr(),
};

unsafe {
let null_terminated_name =
CString::new(name).expect("Failed to initialize nul terminated exception name");
let null_terminated_name =
CString::new(name).expect("Failed to initialize nul terminated exception name");

let ptr = unsafe {
ffi::PyErr_NewException(null_terminated_name.as_ptr(), base, dict)
as *mut ffi::PyTypeObject
};
match NonNull::new(ptr) {
Some(not_null) => not_null,
None => panic!("{}", PyErr::fetch(py)),
davidhewitt marked this conversation as resolved.
Show resolved Hide resolved
}
}

NonNull::new_unchecked(ffi::PyErr_NewException(
null_terminated_name.as_ptr() as *mut c_char,
/// Creates a new exception type with the given name and docstring.
///
/// - `base` can be an existing exception type to subclass, or a tuple of classes.
/// - `dict` specifies an optional dictionary of class variables and methods.
/// - `doc` will be the docstring seen by python users.
///
/// # Panics
///
/// This function will panic if:
/// - `name` is not of the form `<module>.<ExceptionName>`.
/// - `name` or `doc` cannot be converted to [`CString`]s.
pub fn new_type_with_doc<'p>(
davidhewitt marked this conversation as resolved.
Show resolved Hide resolved
py: Python<'p>,
name: &str,
doc: Option<&str>,
base: Option<&PyType>,
dict: Option<PyObject>,
) -> NonNull<ffi::PyTypeObject> {
let base: *mut ffi::PyObject = match base {
None => std::ptr::null_mut(),
Some(obj) => obj.as_ptr(),
};

let dict: *mut ffi::PyObject = match dict {
None => std::ptr::null_mut(),
Some(obj) => obj.as_ptr(),
};

let null_terminated_name =
CString::new(name).expect("Failed to initialize nul terminated exception name");

let null_terminated_doc =
doc.map(|d| CString::new(d).expect("Failed to initialize nul terminated docstring"));

let null_terminated_doc_ptr = match null_terminated_doc.as_ref() {
Some(c) => c.as_ptr(),
None => std::ptr::null(),
};

let ptr = unsafe {
ffi::PyErr_NewExceptionWithDoc(
null_terminated_name.as_ptr(),
null_terminated_doc_ptr,
base,
dict,
) as *mut ffi::PyTypeObject)
) as *mut ffi::PyTypeObject
};

match NonNull::new(ptr) {
Some(not_null) => not_null,
None => panic!("{}", PyErr::fetch(py)),
}
}

Expand Down
90 changes: 70 additions & 20 deletions src/exceptions.rs
Expand Up @@ -129,33 +129,67 @@ macro_rules! import_exception {
///
/// # Syntax
///
/// ```create_exception!(module, MyError, BaseException)```
///
/// * `module` is the name of the containing module.
/// * `MyError` is the name of the new exception type.
/// * `BaseException` is the superclass of `MyError`, usually `pyo3::exceptions::PyException`.
/// * `name` is the name of the new exception type.
/// * `base` is the superclass of `MyError`, usually [`PyException`].
/// * `doc` (optional) is the docstring visible to users (with `.__doc__` and `help()`) and
/// accompanies your error type in your crate's documentation.
///
/// # Examples
///
/// ```
/// use pyo3::prelude::*;
/// use pyo3::create_exception;
/// use pyo3::types::IntoPyDict;
/// use pyo3::exceptions::PyException;
///
/// create_exception!(mymodule, CustomError, PyException);
/// create_exception!(my_module, MyError, PyException, "Some description.");
///
/// Python::with_gil(|py| {
/// let error_type = py.get_type::<CustomError>();
/// let ctx = [("CustomError", error_type)].into_py_dict(py);
/// let type_description: String = py
/// .eval("str(CustomError)", None, Some(&ctx))
/// .unwrap()
/// .extract()
/// .unwrap();
/// assert_eq!(type_description, "<class 'mymodule.CustomError'>");
/// pyo3::py_run!(py, *ctx, "assert CustomError('oops').args == ('oops',)");
/// });
/// #[pyfunction]
/// fn raise_myerror() -> PyResult<()>{
/// let err = MyError::new_err("Some error happened.");
/// Err(err)
/// }
///
/// #[pymodule]
/// fn my_module(py: Python, m: &PyModule) -> PyResult<()> {
/// m.add("MyError", py.get_type::<MyError>())?;
/// m.add_function(wrap_pyfunction!(raise_myerror, py)?)?;
/// Ok(())
/// }
/// # fn main() -> PyResult<()> {
/// # Python::with_gil(|py| -> PyResult<()> {
/// # let fun = wrap_pyfunction!(raise_myerror, py)?;
/// # let globals = pyo3::types::PyDict::new(py);
/// # globals.set_item("MyError", py.get_type::<MyError>())?;
/// # globals.set_item("raise_myerror", fun)?;
/// #
/// # py.run(
/// # "try:
/// # raise_myerror()
/// # except MyError as e:
/// # assert e.__doc__ == 'Some description.'
/// # assert str(e) == 'Some error happened.'",
/// # Some(globals),
/// # None,
davidhewitt marked this conversation as resolved.
Show resolved Hide resolved
/// # )?;
/// #
/// # Ok(())
/// # })
/// # }
/// ```
///
/// Python code can handle this exception like any other exception:
///
/// ```python
/// from my_module import MyError, raise_myerror
///
/// try:
/// raise_myerror()
/// except MyError as e:
/// assert e.__doc__ == 'Some description.'
/// assert str(e) == 'Some error happened.'
/// ```
///
#[macro_export]
macro_rules! create_exception {
($module: ident, $name: ident, $base: ty) => {
Expand All @@ -165,7 +199,22 @@ macro_rules! create_exception {

$crate::impl_exception_boilerplate!($name);

$crate::create_exception_type_object!($module, $name, $base);
$crate::create_exception_type_object!($module, $name, $base, ::std::option::Option::None);
davidhewitt marked this conversation as resolved.
Show resolved Hide resolved
};
($module: ident, $name: ident, $base: ty, $doc: expr) => {
#[repr(transparent)]
#[allow(non_camel_case_types)] // E.g. `socket.herror`
#[doc = $doc]
pub struct $name($crate::PyAny);

$crate::impl_exception_boilerplate!($name);

$crate::create_exception_type_object!(
$module,
$name,
$base,
::std::option::Option::Some($doc)
);
};
}

Expand All @@ -174,7 +223,7 @@ macro_rules! create_exception {
#[doc(hidden)]
#[macro_export]
macro_rules! create_exception_type_object {
($module: ident, $name: ident, $base: ty) => {
($module: ident, $name: ident, $base: ty, $doc: expr) => {
$crate::pyobject_native_type_core!(
$name,
*$name::type_object_raw($crate::Python::assume_gil_acquired()),
Expand All @@ -192,9 +241,10 @@ macro_rules! create_exception_type_object {
.get_or_init(py, || unsafe {
$crate::Py::from_owned_ptr(
py,
$crate::PyErr::new_type(
$crate::PyErr::new_type_with_doc(
py,
concat!(stringify!($module), ".", stringify!($name)),
$doc,
::std::option::Option::Some(py.get_type::<$base>()),
::std::option::Option::None,
)
Expand Down
2 changes: 1 addition & 1 deletion src/internal_tricks.rs
Expand Up @@ -35,7 +35,7 @@ macro_rules! pyo3_exception {

$crate::impl_exception_boilerplate!($name);

$crate::create_exception_type_object!(pyo3_runtime, $name, $base);
$crate::create_exception_type_object!(pyo3_runtime, $name, $base, Some($doc));
};
}

Expand Down
10 changes: 5 additions & 5 deletions src/panic.rs
Expand Up @@ -5,12 +5,12 @@ use std::any::Any;

pyo3_exception!(
"
The exception raised when Rust code called from Python panics.
The exception raised when Rust code called from Python panics.

Like SystemExit, this exception is derived from BaseException so that
it will typically propagate all the way through the stack and cause the
Python interpreter to exit.
",
Like SystemExit, this exception is derived from BaseException so that
it will typically propagate all the way through the stack and cause the
Python interpreter to exit.
",
PanicException,
PyBaseException
);
Expand Down