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` - `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) - 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) - 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 ### Removed

View file

@ -13,9 +13,7 @@ use crate::{
use std::borrow::Cow; use std::borrow::Cow;
use std::cell::UnsafeCell; use std::cell::UnsafeCell;
use std::ffi::CString; use std::ffi::CString;
use std::os::raw::c_char;
use std::os::raw::c_int; use std::os::raw::c_int;
use std::ptr::NonNull;
mod err_state; mod err_state;
mod impls; mod impls;
@ -337,17 +335,27 @@ impl PyErr {
} }
} }
/// Creates a new exception type with the given name, which must be of the form /// Creates a new exception type with the given name and docstring.
/// `<module>.<ExceptionName>`, as required by `PyErr_NewException`.
/// ///
/// `base` can be an existing exception type to subclass, or a tuple of classes /// - `base` can be an existing exception type to subclass, or a tuple of classes.
/// `dict` specifies an optional dictionary of class variables and methods /// - `dict` specifies an optional dictionary of class variables and methods.
pub fn new_type<'p>( /// - `doc` will be the docstring seen by python users.
_: Python<'p>, ///
///
/// # 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, name: &str,
doc: Option<&str>,
base: Option<&PyType>, base: Option<&PyType>,
dict: Option<PyObject>, dict: Option<PyObject>,
) -> NonNull<ffi::PyTypeObject> { ) -> PyResult<Py<PyType>> {
let base: *mut ffi::PyObject = match base { let base: *mut ffi::PyObject = match base {
None => std::ptr::null_mut(), None => std::ptr::null_mut(),
Some(obj) => obj.as_ptr(), Some(obj) => obj.as_ptr(),
@ -358,16 +366,27 @@ impl PyErr {
Some(obj) => obj.as_ptr(), Some(obj) => obj.as_ptr(),
}; };
unsafe { let null_terminated_name =
let null_terminated_name = CString::new(name).expect("Failed to initialize nul terminated exception name");
CString::new(name).expect("Failed to initialize nul terminated exception name");
NonNull::new_unchecked(ffi::PyErr_NewException( let null_terminated_doc =
null_terminated_name.as_ptr() as *mut c_char, 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, base,
dict, dict,
) as *mut ffi::PyTypeObject) )
} };
unsafe { Py::from_owned_ptr_or_err(py, ptr) }
} }
/// Prints a standard traceback to `sys.stderr`. /// Prints a standard traceback to `sys.stderr`.

View file

@ -129,33 +129,67 @@ macro_rules! import_exception {
/// ///
/// # Syntax /// # Syntax
/// ///
/// ```create_exception!(module, MyError, BaseException)```
///
/// * `module` is the name of the containing module. /// * `module` is the name of the containing module.
/// * `MyError` is the name of the new exception type. /// * `name` is the name of the new exception type.
/// * `BaseException` is the superclass of `MyError`, usually `pyo3::exceptions::PyException`. /// * `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 /// # Examples
///
/// ``` /// ```
/// use pyo3::prelude::*; /// use pyo3::prelude::*;
/// use pyo3::create_exception; /// use pyo3::create_exception;
/// use pyo3::types::IntoPyDict;
/// use pyo3::exceptions::PyException; /// use pyo3::exceptions::PyException;
/// ///
/// create_exception!(mymodule, CustomError, PyException); /// create_exception!(my_module, MyError, PyException, "Some description.");
/// ///
/// Python::with_gil(|py| { /// #[pyfunction]
/// let error_type = py.get_type::<CustomError>(); /// fn raise_myerror() -> PyResult<()>{
/// let ctx = [("CustomError", error_type)].into_py_dict(py); /// let err = MyError::new_err("Some error happened.");
/// let type_description: String = py /// Err(err)
/// .eval("str(CustomError)", None, Some(&ctx)) /// }
/// .unwrap() ///
/// .extract() /// #[pymodule]
/// .unwrap(); /// fn my_module(py: Python, m: &PyModule) -> PyResult<()> {
/// assert_eq!(type_description, "<class 'mymodule.CustomError'>"); /// m.add("MyError", py.get_type::<MyError>())?;
/// pyo3::py_run!(py, *ctx, "assert CustomError('oops').args == ('oops',)"); /// 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_export]
macro_rules! create_exception { macro_rules! create_exception {
($module: ident, $name: ident, $base: ty) => { ($module: ident, $name: ident, $base: ty) => {
@ -165,7 +199,22 @@ macro_rules! create_exception {
$crate::impl_exception_boilerplate!($name); $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)] #[doc(hidden)]
#[macro_export] #[macro_export]
macro_rules! create_exception_type_object { 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!( $crate::pyobject_native_type_core!(
$name, $name,
*$name::type_object_raw($crate::Python::assume_gil_acquired()), *$name::type_object_raw($crate::Python::assume_gil_acquired()),
@ -189,19 +238,15 @@ macro_rules! create_exception_type_object {
GILOnceCell::new(); GILOnceCell::new();
TYPE_OBJECT TYPE_OBJECT
.get_or_init(py, || unsafe { .get_or_init(py, ||
$crate::Py::from_owned_ptr( $crate::PyErr::new_type(
py, py,
$crate::PyErr::new_type( concat!(stringify!($module), ".", stringify!($name)),
py, $doc,
concat!(stringify!($module), ".", stringify!($name)), ::std::option::Option::Some(py.get_type::<$base>()),
::std::option::Option::Some(py.get_type::<$base>()), ::std::option::Option::None,
::std::option::Option::None, ).expect("Failed to initialize new exception type.")
) ).as_ptr() as *mut $crate::ffi::PyTypeObject
.as_ptr() as *mut $crate::ffi::PyObject,
)
})
.as_ptr() as *mut _
} }
} }
}; };
@ -746,6 +791,65 @@ mod tests {
Some(ctx), Some(ctx),
) )
.unwrap(); .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::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!( 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 Like SystemExit, this exception is derived from BaseException so that
it will typically propagate all the way through the stack and cause the it will typically propagate all the way through the stack and cause the
Python interpreter to exit. Python interpreter to exit.
", ",
PanicException, PanicException,
PyBaseException PyBaseException
); );