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");
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(
py,
.get_or_init(py, ||
$crate::PyErr::new_type(
py,
concat!(stringify!($module), ".", stringify!($name)),
$doc,
::std::option::Option::Some(py.get_type::<$base>()),
::std::option::Option::None,
)
.as_ptr() as *mut $crate::ffi::PyObject,
)
})
.as_ptr() as *mut _
).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));
};
}