From 509dfd54dfbcc5da7d7c8b195dac23d57b3c52bb Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Wed, 16 Nov 2022 17:29:33 +0100 Subject: [PATCH] Export warning classes and add PyErr::warn_explicit() Fixes #2741 --- newsfragments/2742.added.md | 1 + pyo3-ffi/src/pyerrors.rs | 2 + src/err/mod.rs | 128 +++++++++++++++++++++++++++++++++++- src/exceptions.rs | 81 +++++++++++++++++++++-- 4 files changed, 205 insertions(+), 7 deletions(-) create mode 100644 newsfragments/2742.added.md diff --git a/newsfragments/2742.added.md b/newsfragments/2742.added.md new file mode 100644 index 00000000000..0d2871ea012 --- /dev/null +++ b/newsfragments/2742.added.md @@ -0,0 +1 @@ +Added exports for all built-in `Warning` classes as well as `PyErr::warn_explicit`. diff --git a/pyo3-ffi/src/pyerrors.rs b/pyo3-ffi/src/pyerrors.rs index 57b1b42446d..5e5c836f20d 100644 --- a/pyo3-ffi/src/pyerrors.rs +++ b/pyo3-ffi/src/pyerrors.rs @@ -245,6 +245,8 @@ extern "C" { pub static mut PyExc_BytesWarning: *mut PyObject; #[cfg_attr(PyPy, link_name = "PyPyExc_ResourceWarning")] pub static mut PyExc_ResourceWarning: *mut PyObject; + #[cfg(Py_3_10)] + pub static mut PyExc_EncodingWarning: *mut PyObject; } extern "C" { diff --git a/src/err/mod.rs b/src/err/mod.rs index ea622ed5ac1..abacef07e8c 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -477,7 +477,26 @@ impl PyErr { } /// Issues a warning message. - /// May return a `PyErr` if warnings-as-errors is enabled. + /// + /// May return an `Err(PyErr)` if warnings-as-errors is enabled. + /// + /// The `category` should be one of the `Warning` classes available in + /// [`pyo3::exceptions`](crate::exceptions), or a subclass. The Python + /// object can be retrieved using [`PyTypeInfo::type_object()`]. + /// + /// Example: + /// ```rust + /// use pyo3::prelude::*; + /// use pyo3::PyTypeInfo; + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let user_warning = pyo3::exceptions::PyUserWarning::type_object(py); + /// PyErr::warn(py, user_warning, "I am warning you", 0)?; + /// Ok(()) + /// }) + /// # } + /// ``` pub fn warn(py: Python<'_>, category: &PyAny, message: &str, stacklevel: i32) -> PyResult<()> { let message = CString::new(message)?; unsafe { @@ -492,6 +511,48 @@ impl PyErr { } } + #[cfg(not(PyPy))] + /// Issues a warning message, with more control over the warning attributes. + /// + /// May return a `PyErr` if warnings-as-errors is enabled. + /// + /// The `category` should be one of the `Warning` classes available in + /// [`pyo3::exceptions`](crate::exceptions), or a subclass. + pub fn warn_explicit( + py: Python<'_>, + category: &PyAny, + message: &str, + filename: &str, + lineno: i32, + module: Option<&str>, + registry: Option<&PyAny>, + ) -> PyResult<()> { + let message = CString::new(message)?; + let filename = CString::new(filename)?; + let module = module.map(|s| CString::new(s)).transpose()?; + let module_ptr = match module { + None => std::ptr::null_mut(), + Some(s) => s.as_ptr(), + }; + let registry: *mut ffi::PyObject = match registry { + None => std::ptr::null_mut(), + Some(obj) => obj.as_ptr(), + }; + unsafe { + error_on_minusone( + py, + ffi::PyErr_WarnExplicit( + category.as_ptr(), + message.as_ptr(), + filename.as_ptr(), + lineno, + module_ptr, + registry, + ), + ) + } + } + /// Clone the PyErr. This requires the GIL, which is why PyErr does not implement Clone. /// /// # Examples @@ -769,7 +830,7 @@ fn exceptions_must_derive_from_base_exception(py: Python<'_>) -> PyErr { mod tests { use super::PyErrState; use crate::exceptions; - use crate::{AsPyPointer, PyErr, Python}; + use crate::{AsPyPointer, PyErr, PyTypeInfo, Python}; #[test] fn no_error() { @@ -938,4 +999,67 @@ mod tests { ); }); } + + #[test] + fn warnings() { + // Note: although the warning filter is interpreter global, keeping the + // GIL locked should prevent effects to be visible to other testing + // threads. + Python::with_gil(|py| { + let cls = exceptions::PyUserWarning::type_object(py); + + // Reset warning filter to default state + let warnings = py.import("warnings").unwrap(); + warnings.call_method0("resetwarnings").unwrap(); + + // First, test with ignoring the warning + warnings + .call_method1("simplefilter", ("ignore", cls)) + .unwrap(); + PyErr::warn(py, cls, "I am warning you", 0).unwrap(); + + // Test with raising + warnings + .call_method1("simplefilter", ("error", cls)) + .unwrap(); + PyErr::warn(py, cls, "I am warning you", 0).unwrap_err(); + + // Test with explicit module and specific filter + #[cfg(not(PyPy))] + { + warnings.call_method0("resetwarnings").unwrap(); + warnings + .call_method1("simplefilter", ("ignore", cls)) + .unwrap(); + warnings + .call_method1("filterwarnings", ("error", "", cls, "pyo3test")) + .unwrap(); + + // This has the wrong module and will not raise + PyErr::warn(py, cls, "I am warning you", 0).unwrap(); + + let err = PyErr::warn_explicit( + py, + cls, + "I am warning you", + "pyo3test.py", + 427, + None, + None, + ) + .unwrap_err(); + assert!(err + .value(py) + .getattr("args") + .unwrap() + .get_item(0) + .unwrap() + .eq("I am warning you") + .unwrap()); + } + + // Finally, reset filter again + warnings.call_method0("resetwarnings").unwrap(); + }); + } } diff --git a/src/exceptions.rs b/src/exceptions.rs index c9543a280a7..d5c1420f1dc 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -1,12 +1,15 @@ // Copyright (c) 2017-present PyO3 Project and Contributors -//! Exception types defined by Python. +//! Exception and warning types defined by Python. //! -//! The structs in this module represent Python's built-in exceptions, while the modules comprise -//! structs representing errors defined in Python code. +//! The structs in this module represent Python's built-in exceptions and +//! warnings, while the modules comprise structs representing errors defined in +//! Python code. //! -//! The latter are created with the [`import_exception`](crate::import_exception) macro, which you -//! can use yourself to import Python exceptions. +//! The latter are created with the +//! [`import_exception`](crate::import_exception) macro, which you can use +//! yourself to import Python classes that are ultimately derived from +//! `BaseException`. use crate::{ffi, PyResult, Python}; use std::ffi::CStr; @@ -660,6 +663,61 @@ impl PyUnicodeDecodeError { } } +impl_native_exception!(PyWarning, PyExc_Warning, native_doc!("Warning")); +impl_native_exception!(PyUserWarning, PyExc_UserWarning, native_doc!("UserWarning")); +impl_native_exception!( + PyDeprecationWarning, + PyExc_DeprecationWarning, + native_doc!("DeprecationWarning") +); +impl_native_exception!( + PyPendingDeprecationWarning, + PyExc_PendingDeprecationWarning, + native_doc!("PendingDeprecationWarning") +); +impl_native_exception!( + PySyntaxWarning, + PyExc_SyntaxWarning, + native_doc!("SyntaxWarning") +); +impl_native_exception!( + PyRuntimeWarning, + PyExc_RuntimeWarning, + native_doc!("RuntimeWarning") +); +impl_native_exception!( + PyFutureWarning, + PyExc_FutureWarning, + native_doc!("FutureWarning") +); +impl_native_exception!( + PyImportWarning, + PyExc_ImportWarning, + native_doc!("ImportWarning") +); +impl_native_exception!( + PyUnicodeWarning, + PyExc_UnicodeWarning, + native_doc!("UnicodeWarning") +); +impl_native_exception!( + PyBytesWarning, + PyExc_BytesWarning, + native_doc!("BytesWarning") +); +impl_native_exception!( + PyResourceWarning, + PyExc_ResourceWarning, + native_doc!("ResourceWarning") +); + +#[cfg(Py_3_10)] +impl_native_exception!( + PyEncodingWarning, + PyExc_EncodingWarning, + native_doc!("EncodingWarning") +); + #[cfg(test)] macro_rules! test_exception { ($exc_ty:ident $(, $constructor:expr)?) => { @@ -1017,4 +1075,17 @@ mod tests { test_exception!(PyIOError); #[cfg(windows)] test_exception!(PyWindowsError); + + test_exception!(PyWarning); + test_exception!(PyUserWarning); + test_exception!(PyDeprecationWarning); + test_exception!(PyPendingDeprecationWarning); + test_exception!(PySyntaxWarning); + test_exception!(PyRuntimeWarning); + test_exception!(PyFutureWarning); + test_exception!(PyImportWarning); + test_exception!(PyUnicodeWarning); + test_exception!(PyBytesWarning); + #[cfg(Py_3_10)] + test_exception!(PyEncodingWarning); }