-
Notifications
You must be signed in to change notification settings - Fork 2.2k
feat: warnings wrappers to use from C++ #5291
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 7 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
0afa32f
Add warning wrappers that allow to call warnings from pybind level
jiwaszki 3233262
Add missing include for warnings.h
jiwaszki 0dcd917
Change messages on failed checks, extend testing
jiwaszki 1f50050
clang-tidy fix
jiwaszki 76d9e10
Refactor tests for warnings
jiwaszki f4cf539
Move handle before check
jiwaszki 68cfd13
Merge remote-tracking branch 'upstream/master' into jiwaszki/warnings…
jiwaszki f98b5d4
Remove unnecessary parametrized
jiwaszki 24cebfa
Merge remote-tracking branch 'upstream/master' into jiwaszki/warnings…
jiwaszki File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
/* | ||
pybind11/warnings.h: Python warnings wrappers. | ||
|
||
Copyright (c) 2024 Jan Iwaszkiewicz <[email protected]> | ||
|
||
All rights reserved. Use of this source code is governed by a | ||
BSD-style license that can be found in the LICENSE file. | ||
*/ | ||
|
||
#pragma once | ||
|
||
#include "pybind11.h" | ||
#include "detail/common.h" | ||
|
||
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) | ||
|
||
PYBIND11_NAMESPACE_BEGIN(detail) | ||
|
||
inline bool PyWarning_Check(PyObject *obj) { | ||
int result = PyObject_IsSubclass(obj, PyExc_Warning); | ||
if (result == 1) { | ||
return true; | ||
} | ||
if (result == -1) { | ||
raise_from(PyExc_SystemError, | ||
"pybind11::detail::PyWarning_Check(): PyObject_IsSubclass() call failed."); | ||
throw error_already_set(); | ||
} | ||
return false; | ||
} | ||
|
||
PYBIND11_NAMESPACE_END(detail) | ||
|
||
PYBIND11_NAMESPACE_BEGIN(warnings) | ||
|
||
inline object | ||
new_warning_type(handle scope, const char *name, handle base = PyExc_RuntimeWarning) { | ||
if (!detail::PyWarning_Check(base.ptr())) { | ||
pybind11_fail("pybind11::warnings::new_warning_type(): cannot create custom warning, base " | ||
"must be a subclass of " | ||
"PyExc_Warning!"); | ||
} | ||
if (hasattr(scope, name)) { | ||
pybind11_fail("pybind11::warnings::new_warning_type(): an attribute with name \"" | ||
+ std::string(name) + "\" exists already."); | ||
} | ||
std::string full_name = scope.attr("__name__").cast<std::string>() + std::string(".") + name; | ||
handle h(PyErr_NewException(full_name.c_str(), base.ptr(), nullptr)); | ||
if (!h) { | ||
raise_from(PyExc_SystemError, | ||
"pybind11::warnings::new_warning_type(): PyErr_NewException() call failed."); | ||
throw error_already_set(); | ||
} | ||
auto obj = reinterpret_steal<object>(h); | ||
scope.attr(name) = obj; | ||
return obj; | ||
} | ||
|
||
// Similar to Python `warnings.warn()` | ||
inline void | ||
warn(const char *message, handle category = PyExc_RuntimeWarning, int stack_level = 2) { | ||
if (!detail::PyWarning_Check(category.ptr())) { | ||
pybind11_fail( | ||
"pybind11::warnings::warn(): cannot raise warning, category must be a subclass of " | ||
"PyExc_Warning!"); | ||
} | ||
|
||
if (PyErr_WarnEx(category.ptr(), message, stack_level) == -1) { | ||
throw error_already_set(); | ||
} | ||
} | ||
|
||
PYBIND11_NAMESPACE_END(warnings) | ||
|
||
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
/* | ||
tests/test_warnings.cpp -- usage of warnings::warn() and warnings categories. | ||
|
||
Copyright (c) 2024 Jan Iwaszkiewicz <[email protected]> | ||
|
||
All rights reserved. Use of this source code is governed by a | ||
BSD-style license that can be found in the LICENSE file. | ||
*/ | ||
|
||
#include <pybind11/warnings.h> | ||
|
||
#include "pybind11_tests.h" | ||
|
||
#include <utility> | ||
|
||
TEST_SUBMODULE(warnings_, m) { | ||
|
||
// Test warning mechanism base | ||
m.def("warn_and_return_value", []() { | ||
std::string message = "This is simple warning"; | ||
py::warnings::warn(message.c_str(), PyExc_Warning); | ||
return 21; | ||
}); | ||
|
||
m.def("warn_with_default_category", []() { py::warnings::warn("This is RuntimeWarning"); }); | ||
|
||
m.def("warn_with_different_category", | ||
[]() { py::warnings::warn("This is FutureWarning", PyExc_FutureWarning); }); | ||
|
||
m.def("warn_with_invalid_category", | ||
[]() { py::warnings::warn("Invalid category", PyExc_Exception); }); | ||
|
||
// Test custom warnings | ||
PYBIND11_CONSTINIT static py::gil_safe_call_once_and_store<py::object> ex_storage; | ||
ex_storage.call_once_and_store_result([&]() { | ||
return py::warnings::new_warning_type(m, "CustomWarning", PyExc_DeprecationWarning); | ||
}); | ||
|
||
m.def("warn_with_custom_type", []() { | ||
py::warnings::warn("This is CustomWarning", ex_storage.get_stored()); | ||
return 37; | ||
}); | ||
|
||
m.def("register_duplicate_warning", | ||
[m]() { py::warnings::new_warning_type(m, "CustomWarning", PyExc_RuntimeWarning); }); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
from __future__ import annotations | ||
|
||
import warnings | ||
|
||
import pytest | ||
|
||
import pybind11_tests # noqa: F401 | ||
from pybind11_tests import warnings_ as m | ||
|
||
|
||
@pytest.mark.parametrize( | ||
("expected_category", "expected_message", "expected_value", "module_function"), | ||
[ | ||
(Warning, "This is simple warning", 21, m.warn_and_return_value), | ||
(RuntimeWarning, "This is RuntimeWarning", None, m.warn_with_default_category), | ||
(FutureWarning, "This is FutureWarning", None, m.warn_with_different_category), | ||
], | ||
) | ||
def test_warning_simple( | ||
expected_category, expected_message, expected_value, module_function | ||
): | ||
with pytest.warns(Warning) as excinfo: | ||
value = module_function() | ||
|
||
assert issubclass(excinfo[0].category, expected_category) | ||
assert str(excinfo[0].message) == expected_message | ||
assert value == expected_value | ||
|
||
|
||
def test_warning_wrong_subclass_fail(): | ||
with pytest.raises(Exception) as excinfo: | ||
m.warn_with_invalid_category() | ||
|
||
assert issubclass(excinfo.type, RuntimeError) | ||
assert ( | ||
str(excinfo.value) | ||
== "pybind11::warnings::warn(): cannot raise warning, category must be a subclass of PyExc_Warning!" | ||
) | ||
|
||
|
||
def test_warning_double_register_fail(): | ||
with pytest.raises(Exception) as excinfo: | ||
m.register_duplicate_warning() | ||
|
||
assert issubclass(excinfo.type, RuntimeError) | ||
assert ( | ||
str(excinfo.value) | ||
== 'pybind11::warnings::new_warning_type(): an attribute with name "CustomWarning" exists already.' | ||
) | ||
|
||
|
||
def test_warning_register(): | ||
assert m.CustomWarning is not None | ||
assert issubclass(m.CustomWarning, DeprecationWarning) | ||
|
||
with pytest.warns(m.CustomWarning) as excinfo: | ||
warnings.warn("This is warning from Python!", m.CustomWarning, stacklevel=1) | ||
|
||
assert issubclass(excinfo[0].category, DeprecationWarning) | ||
assert issubclass(excinfo[0].category, m.CustomWarning) | ||
assert str(excinfo[0].message) == "This is warning from Python!" | ||
|
||
|
||
@pytest.mark.parametrize( | ||
( | ||
"expected_category", | ||
"expected_base", | ||
"expected_message", | ||
"expected_value", | ||
"module_function", | ||
), | ||
[ | ||
( | ||
m.CustomWarning, | ||
DeprecationWarning, | ||
"This is CustomWarning", | ||
37, | ||
m.warn_with_custom_type, | ||
), | ||
], | ||
) | ||
def test_warning_custom( | ||
expected_category, expected_base, expected_message, expected_value, module_function | ||
): | ||
with pytest.warns(expected_category) as excinfo: | ||
value = module_function() | ||
|
||
assert issubclass(excinfo[0].category, expected_base) | ||
assert issubclass(excinfo[0].category, expected_category) | ||
assert str(excinfo[0].message) == expected_message | ||
assert value == expected_value |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to be the only one list item.
I'd either remove the
@pytest.mark.parametrize()
here and just assign the variables directly (e.g.expected_value = 37
) or try to think if there could be another test we could meaningfully add here.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I guess it's a leftover from previous iterations (not sure but looks like one:)). Removed!