Skip to content

Commit 66c3774

Browse files
authored
Warnings wrappers to use from C++ (#5291)
* Add warning wrappers that allow to call warnings from pybind level * Add missing include for warnings.h * Change messages on failed checks, extend testing * clang-tidy fix * Refactor tests for warnings * Move handle before check * Remove unnecessary parametrized
1 parent 65f4266 commit 66c3774

File tree

6 files changed

+194
-2
lines changed

6 files changed

+194
-2
lines changed

CMakeLists.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,8 @@ set(PYBIND11_HEADERS
160160
include/pybind11/stl_bind.h
161161
include/pybind11/stl/filesystem.h
162162
include/pybind11/type_caster_pyobject_ptr.h
163-
include/pybind11/typing.h)
163+
include/pybind11/typing.h
164+
include/pybind11/warnings.h)
164165

165166
# Compare with grep and warn if mismatched
166167
if(PYBIND11_MASTER_PROJECT)

include/pybind11/warnings.h

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
pybind11/warnings.h: Python warnings wrappers.
3+
4+
Copyright (c) 2024 Jan Iwaszkiewicz <[email protected]>
5+
6+
All rights reserved. Use of this source code is governed by a
7+
BSD-style license that can be found in the LICENSE file.
8+
*/
9+
10+
#pragma once
11+
12+
#include "pybind11.h"
13+
#include "detail/common.h"
14+
15+
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
16+
17+
PYBIND11_NAMESPACE_BEGIN(detail)
18+
19+
inline bool PyWarning_Check(PyObject *obj) {
20+
int result = PyObject_IsSubclass(obj, PyExc_Warning);
21+
if (result == 1) {
22+
return true;
23+
}
24+
if (result == -1) {
25+
raise_from(PyExc_SystemError,
26+
"pybind11::detail::PyWarning_Check(): PyObject_IsSubclass() call failed.");
27+
throw error_already_set();
28+
}
29+
return false;
30+
}
31+
32+
PYBIND11_NAMESPACE_END(detail)
33+
34+
PYBIND11_NAMESPACE_BEGIN(warnings)
35+
36+
inline object
37+
new_warning_type(handle scope, const char *name, handle base = PyExc_RuntimeWarning) {
38+
if (!detail::PyWarning_Check(base.ptr())) {
39+
pybind11_fail("pybind11::warnings::new_warning_type(): cannot create custom warning, base "
40+
"must be a subclass of "
41+
"PyExc_Warning!");
42+
}
43+
if (hasattr(scope, name)) {
44+
pybind11_fail("pybind11::warnings::new_warning_type(): an attribute with name \""
45+
+ std::string(name) + "\" exists already.");
46+
}
47+
std::string full_name = scope.attr("__name__").cast<std::string>() + std::string(".") + name;
48+
handle h(PyErr_NewException(full_name.c_str(), base.ptr(), nullptr));
49+
if (!h) {
50+
raise_from(PyExc_SystemError,
51+
"pybind11::warnings::new_warning_type(): PyErr_NewException() call failed.");
52+
throw error_already_set();
53+
}
54+
auto obj = reinterpret_steal<object>(h);
55+
scope.attr(name) = obj;
56+
return obj;
57+
}
58+
59+
// Similar to Python `warnings.warn()`
60+
inline void
61+
warn(const char *message, handle category = PyExc_RuntimeWarning, int stack_level = 2) {
62+
if (!detail::PyWarning_Check(category.ptr())) {
63+
pybind11_fail(
64+
"pybind11::warnings::warn(): cannot raise warning, category must be a subclass of "
65+
"PyExc_Warning!");
66+
}
67+
68+
if (PyErr_WarnEx(category.ptr(), message, stack_level) == -1) {
69+
throw error_already_set();
70+
}
71+
}
72+
73+
PYBIND11_NAMESPACE_END(warnings)
74+
75+
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)

tests/CMakeLists.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,8 @@ set(PYBIND11_TEST_FILES
154154
test_unnamed_namespace_a
155155
test_unnamed_namespace_b
156156
test_vector_unique_ptr_member
157-
test_virtual_functions)
157+
test_virtual_functions
158+
test_warnings)
158159

159160
# Invoking cmake with something like:
160161
# cmake -DPYBIND11_TEST_OVERRIDE="test_callbacks.cpp;test_pickling.cpp" ..

tests/extra_python_package/test_files.py

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"include/pybind11/stl_bind.h",
4848
"include/pybind11/type_caster_pyobject_ptr.h",
4949
"include/pybind11/typing.h",
50+
"include/pybind11/warnings.h",
5051
}
5152

5253
detail_headers = {

tests/test_warnings.cpp

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
tests/test_warnings.cpp -- usage of warnings::warn() and warnings categories.
3+
4+
Copyright (c) 2024 Jan Iwaszkiewicz <[email protected]>
5+
6+
All rights reserved. Use of this source code is governed by a
7+
BSD-style license that can be found in the LICENSE file.
8+
*/
9+
10+
#include <pybind11/warnings.h>
11+
12+
#include "pybind11_tests.h"
13+
14+
#include <utility>
15+
16+
TEST_SUBMODULE(warnings_, m) {
17+
18+
// Test warning mechanism base
19+
m.def("warn_and_return_value", []() {
20+
std::string message = "This is simple warning";
21+
py::warnings::warn(message.c_str(), PyExc_Warning);
22+
return 21;
23+
});
24+
25+
m.def("warn_with_default_category", []() { py::warnings::warn("This is RuntimeWarning"); });
26+
27+
m.def("warn_with_different_category",
28+
[]() { py::warnings::warn("This is FutureWarning", PyExc_FutureWarning); });
29+
30+
m.def("warn_with_invalid_category",
31+
[]() { py::warnings::warn("Invalid category", PyExc_Exception); });
32+
33+
// Test custom warnings
34+
PYBIND11_CONSTINIT static py::gil_safe_call_once_and_store<py::object> ex_storage;
35+
ex_storage.call_once_and_store_result([&]() {
36+
return py::warnings::new_warning_type(m, "CustomWarning", PyExc_DeprecationWarning);
37+
});
38+
39+
m.def("warn_with_custom_type", []() {
40+
py::warnings::warn("This is CustomWarning", ex_storage.get_stored());
41+
return 37;
42+
});
43+
44+
m.def("register_duplicate_warning",
45+
[m]() { py::warnings::new_warning_type(m, "CustomWarning", PyExc_RuntimeWarning); });
46+
}

tests/test_warnings.py

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from __future__ import annotations
2+
3+
import warnings
4+
5+
import pytest
6+
7+
import pybind11_tests # noqa: F401
8+
from pybind11_tests import warnings_ as m
9+
10+
11+
@pytest.mark.parametrize(
12+
("expected_category", "expected_message", "expected_value", "module_function"),
13+
[
14+
(Warning, "This is simple warning", 21, m.warn_and_return_value),
15+
(RuntimeWarning, "This is RuntimeWarning", None, m.warn_with_default_category),
16+
(FutureWarning, "This is FutureWarning", None, m.warn_with_different_category),
17+
],
18+
)
19+
def test_warning_simple(
20+
expected_category, expected_message, expected_value, module_function
21+
):
22+
with pytest.warns(Warning) as excinfo:
23+
value = module_function()
24+
25+
assert issubclass(excinfo[0].category, expected_category)
26+
assert str(excinfo[0].message) == expected_message
27+
assert value == expected_value
28+
29+
30+
def test_warning_wrong_subclass_fail():
31+
with pytest.raises(Exception) as excinfo:
32+
m.warn_with_invalid_category()
33+
34+
assert issubclass(excinfo.type, RuntimeError)
35+
assert (
36+
str(excinfo.value)
37+
== "pybind11::warnings::warn(): cannot raise warning, category must be a subclass of PyExc_Warning!"
38+
)
39+
40+
41+
def test_warning_double_register_fail():
42+
with pytest.raises(Exception) as excinfo:
43+
m.register_duplicate_warning()
44+
45+
assert issubclass(excinfo.type, RuntimeError)
46+
assert (
47+
str(excinfo.value)
48+
== 'pybind11::warnings::new_warning_type(): an attribute with name "CustomWarning" exists already.'
49+
)
50+
51+
52+
def test_warning_register():
53+
assert m.CustomWarning is not None
54+
55+
with pytest.warns(m.CustomWarning) as excinfo:
56+
warnings.warn("This is warning from Python!", m.CustomWarning, stacklevel=1)
57+
58+
assert issubclass(excinfo[0].category, DeprecationWarning)
59+
assert str(excinfo[0].message) == "This is warning from Python!"
60+
61+
62+
def test_warning_custom():
63+
with pytest.warns(m.CustomWarning) as excinfo:
64+
value = m.warn_with_custom_type()
65+
66+
assert issubclass(excinfo[0].category, DeprecationWarning)
67+
assert str(excinfo[0].message) == "This is CustomWarning"
68+
assert value == 37

0 commit comments

Comments
 (0)