Merge pull request #2027 from mejrs/exception_docstring
Allow user defined exceptions to have docstrings
This commit is contained in:
commit
397555fd67
|
@ -35,7 +35,10 @@ 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
|
||||||
|
|
||||||
- Remove all functionality deprecated in PyO3 0.14. [#2007](https://github.com/PyO3/pyo3/pull/2007)
|
- Remove all functionality deprecated in PyO3 0.14. [#2007](https://github.com/PyO3/pyo3/pull/2007)
|
||||||
|
|
|
@ -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`.
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
10
src/panic.rs
10
src/panic.rs
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue