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 all commits
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
5 changes: 4 additions & 1 deletion CHANGELOG.md
Expand Up @@ -35,7 +35,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `into_instance` -> `into_value`
- Deprecate `PyType::is_instance`; it is inconsistent with other `is_instance` methods in PyO3. Instead of `typ.is_instance(obj)`, use `obj.is_instance(typ)`. [#2031](https://github.com/PyO3/pyo3/pull/2031)
- Optional parameters of `#[pymethods]` and `#[pyfunction]`s cannot be followed by required parameters, i.e. `fn opt_first(a: Option<i32>, b: i32) {}` is not allowed, while `fn opt_last(a:i32, b: Option<i32>) {}` is. [#2041](https://github.com/PyO3/pyo3/pull/2041)

- `PyErr::new_type` now takes an optional docstring and now returns `PyResult<Py<PyType>>` rather than a `ffi::PyTypeObject` pointer.
- The `create_exception!` macro can now take an optional docstring. This docstring, if supplied, is visible to users (with `.__doc__` and `help()`) and
accompanies your error type in your crate's documentation.

### Removed

- Remove all functionality deprecated in PyO3 0.14. [#2007](https://github.com/PyO3/pyo3/pull/2007)
Expand Down
51 changes: 35 additions & 16 deletions src/err/mod.rs
Expand Up @@ -13,9 +13,7 @@ use crate::{
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;

mod err_state;
mod impls;
Expand Down Expand Up @@ -337,17 +335,27 @@ 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 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
pub fn new_type<'p>(
_: Python<'p>,
/// - `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.
///
///
/// # Errors
///
/// This function returns an error if `name` is not of the form `<module>.<ExceptionName>`.
///
/// # Panics
///
/// This function will panic if `name` or `doc` cannot be converted to [`CString`]s.
pub fn new_type(
py: Python,
name: &str,
doc: Option<&str>,
base: Option<&PyType>,
dict: Option<PyObject>,
) -> NonNull<ffi::PyTypeObject> {
) -> PyResult<Py<PyType>> {
let base: *mut ffi::PyObject = match base {
None => std::ptr::null_mut(),
Some(obj) => obj.as_ptr(),
Expand All @@ -358,16 +366,27 @@ 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 null_terminated_doc =
doc.map(|d| CString::new(d).expect("Failed to initialize nul terminated docstring"));

NonNull::new_unchecked(ffi::PyErr_NewException(
null_terminated_name.as_ptr() as *mut c_char,
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)
}
)
};

unsafe { Py::from_owned_ptr_or_err(py, ptr) }
}

/// Prints a standard traceback to `sys.stderr`.
Expand Down
166 changes: 135 additions & 31 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 base class 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 locals = pyo3::types::PyDict::new(py);
/// # locals.set_item("MyError", py.get_type::<MyError>())?;
/// # locals.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.'",
/// # None,
/// # Some(locals),
/// # )?;
/// #
/// # 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 @@ -189,19 +238,15 @@ macro_rules! create_exception_type_object {
GILOnceCell::new();

TYPE_OBJECT
.get_or_init(py, || unsafe {
$crate::Py::from_owned_ptr(
.get_or_init(py, ||
$crate::PyErr::new_type(
py,
$crate::PyErr::new_type(
py,
concat!(stringify!($module), ".", stringify!($name)),
::std::option::Option::Some(py.get_type::<$base>()),
::std::option::Option::None,
)
.as_ptr() as *mut $crate::ffi::PyObject,
)
})
.as_ptr() as *mut _
concat!(stringify!($module), ".", stringify!($name)),
$doc,
::std::option::Option::Some(py.get_type::<$base>()),
::std::option::Option::None,
).expect("Failed to initialize new exception type.")
).as_ptr() as *mut $crate::ffi::PyTypeObject
}
}
};
Expand Down Expand Up @@ -746,6 +791,65 @@ mod tests {
Some(ctx),
)
.unwrap();
py.run("assert CustomError.__doc__ is None", None, Some(ctx))
.unwrap();
});
}

#[test]
fn custom_exception_doc() {
create_exception!(mymodule, CustomError, PyException, "Some docs");

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'>");
py.run(
"assert CustomError('oops').args == ('oops',)",
None,
Some(ctx),
)
.unwrap();
py.run("assert CustomError.__doc__ == 'Some docs'", None, Some(ctx))
.unwrap();
});
}

#[test]
fn custom_exception_doc_expr() {
create_exception!(
mymodule,
CustomError,
PyException,
concat!("Some", " more ", stringify!(docs))
);

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'>");
py.run(
"assert CustomError('oops').args == ('oops',)",
None,
Some(ctx),
)
.unwrap();
py.run(
"assert CustomError.__doc__ == 'Some more docs'",
None,
Some(ctx),
)
.unwrap();
});
}

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