Merge pull request #2027 from mejrs/exception_docstring
Allow user defined exceptions to have docstrings
This commit is contained in:
commit
397555fd67
|
@ -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
|
||||
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
};
|
||||
}
|
||||
|
||||
|
|
10
src/panic.rs
10
src/panic.rs
|
@ -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
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue