Merge pull request #2027 from mejrs/exception_docstring

Allow user defined exceptions to have docstrings
This commit is contained in:
David Hewitt 2021-12-14 20:40:06 +00:00 committed by GitHub
commit 397555fd67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 180 additions and 54 deletions

View File

@ -35,6 +35,9 @@ 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

View File

@ -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;
@ -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(),
@ -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");
NonNull::new_unchecked(ffi::PyErr_NewException(
null_terminated_name.as_ptr() as *mut c_char,
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)
}
)
};
unsafe { Py::from_owned_ptr_or_err(py, ptr) }
}
/// Prints a standard traceback to `sys.stderr`.

View File

@ -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) => {
@ -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);
};
($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)
);
};
}
@ -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()),
@ -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
}
}
};
@ -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();
});
}

View File

@ -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));
};
}

View File

@ -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
);