-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
feat: warnings wrappers to use from C++ #5291
Changes from 1 commit
0afa32f
3233262
0dcd917
1f50050
76d9e10
f4cf539
68cfd13
f98b5d4
24cebfa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
/* | ||
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, | ||
"PyWarning_Check(): internal error of Python C API while " | ||
"checking a subclass of the object!"); | ||
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("warning(): cannot create custom warning, base must be a subclass of " | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
? To make it easier to pin-point this code based on the error message. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Applied, also I have followed the |
||
"PyExc_Warning!"); | ||
} | ||
if (hasattr(scope, "__dict__") && scope.attr("__dict__").contains(name)) { | ||
pybind11_fail("Error during initialization: multiple incompatible " | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe
? I'm also wondering, could the condition simply be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are right, it works the same. Also added test case for it. For the message same as #5291 (comment) |
||
"definitions with name \"" | ||
+ std::string(name) + "\""); | ||
} | ||
std::string full_name = scope.attr("__name__").cast<std::string>() + std::string(".") + name; | ||
handle h(PyErr_NewException(const_cast<char *>(full_name.c_str()), base.ptr(), nullptr)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe the
I think that'd be sufficient, although There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree on that, I have modified it and move creation of handle after the check -- so any null should be verified first. About the docs... yes, they are "very vague" about error messages so referring to them and cpython code this is the best that can be done. However, I am not able to think of a way of testing it. Seems like there is no way to break it. To break it down:
I can only imagine if somehow There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's very common that testing of error conditions is incomplete. It's often just too difficult. What I usually do: temporarily change e.g. |
||
object obj = reinterpret_steal<object>(h); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I saw clang-tidy wants |
||
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("raise_warning(): cannot raise warning, category must be a subclass of " | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as #5291 (comment) |
||
"PyExc_Warning!"); | ||
} | ||
|
||
if (PyErr_WarnEx(category.ptr(), message, stack_level) == -1) { | ||
throw error_already_set(); | ||
} | ||
} | ||
|
||
PYBIND11_NAMESPACE_END(warnings) | ||
|
||
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
/* | ||
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> | ||
|
||
namespace warning_helpers { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you please put this in a namespace? Usually, in new code, I'd have this here:
This is because we're linking most tests together into one extension (something I wanted to change for years, but it's still like that). Without using namespaces we might get accidental ODR violations (in the link step). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See #5291 (comment) |
||
void warn_function(py::module &m, const char *name, py::handle category, const char *message) { | ||
m.def(name, [category, message]() { py::warnings::warn(message, category); }); | ||
} | ||
} // namespace warning_helpers | ||
|
||
class CustomWarning {}; | ||
|
||
TEST_SUBMODULE(warnings_, m) { | ||
|
||
// Test warning mechanism base | ||
m.def("raise_and_return", []() { | ||
std::string message = "Warning was raised!"; | ||
py::warnings::warn(message.c_str(), PyExc_Warning); | ||
return 21; | ||
}); | ||
|
||
m.def("raise_default", []() { py::warnings::warn("RuntimeWarning is raised!"); }); | ||
|
||
m.def("raise_from_cpython", | ||
[]() { py::warnings::warn("UnicodeWarning is raised!", PyExc_UnicodeWarning); }); | ||
|
||
m.def("raise_and_fail", | ||
[]() { py::warnings::warn("RuntimeError should be raised!", PyExc_Exception); }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This confused me:
Suggestion to change to:
I'd also change Actually: is "raise" a good term to use here (all test functions)? Because there is no "raise" in Maybe There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see the motivation, I hope now names and messages are more self-explainatory/clear. |
||
|
||
// 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("raise_custom", []() { | ||
py::warnings::warn("CustomWarning was raised!", ex_storage.get_stored()); | ||
return 37; | ||
}); | ||
|
||
// Bind warning categories | ||
warning_helpers::warn_function(m, "raise_base_warning", PyExc_Warning, "This is Warning!"); | ||
warning_helpers::warn_function( | ||
m, "raise_bytes_warning", PyExc_BytesWarning, "This is BytesWarning!"); | ||
warning_helpers::warn_function( | ||
m, "raise_deprecation_warning", PyExc_DeprecationWarning, "This is DeprecationWarning!"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd delete all warning below: mostly because it doesn't exercise anything specific in your implementation, but also because it will look old after a few years, because we're likely to not keep up with new warning types, and the wall of repetitive code is distracting from potentially more important things. I think it's best to only keep There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have removed them all as other functions like |
||
warning_helpers::warn_function( | ||
m, "raise_future_warning", PyExc_FutureWarning, "This is FutureWarning!"); | ||
warning_helpers::warn_function( | ||
m, "raise_import_warning", PyExc_ImportWarning, "This is ImportWarning!"); | ||
warning_helpers::warn_function(m, | ||
"raise_pending_deprecation_warning", | ||
PyExc_PendingDeprecationWarning, | ||
"This is PendingDeprecationWarning!"); | ||
warning_helpers::warn_function( | ||
m, "raise_resource_warning", PyExc_ResourceWarning, "This is ResourceWarning!"); | ||
warning_helpers::warn_function( | ||
m, "raise_runtime_warning", PyExc_RuntimeWarning, "This is RuntimeWarning!"); | ||
warning_helpers::warn_function( | ||
m, "raise_syntax_warning", PyExc_SyntaxWarning, "This is SyntaxWarning!"); | ||
warning_helpers::warn_function( | ||
m, "raise_unicode_warning", PyExc_UnicodeWarning, "This is UnicodeWarning!"); | ||
warning_helpers::warn_function( | ||
m, "raise_user_warning", PyExc_UserWarning, "This is UserWarning!"); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
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, "Warning was raised!", 21, m.raise_and_return), | ||
(RuntimeWarning, "RuntimeWarning is raised!", None, m.raise_default), | ||
(UnicodeWarning, "UnicodeWarning is raised!", None, m.raise_from_cpython), | ||
], | ||
) | ||
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_fail(): | ||
with pytest.raises(Exception) as excinfo: | ||
m.raise_and_fail() | ||
|
||
assert issubclass(excinfo.type, RuntimeError) | ||
assert ( | ||
str(excinfo.value) | ||
== "raise_warning(): cannot raise warning, category must be a subclass of PyExc_Warning!" | ||
) | ||
|
||
|
||
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", | ||
), | ||
[ | ||
( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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! |
||
m.CustomWarning, | ||
DeprecationWarning, | ||
"CustomWarning was raised!", | ||
37, | ||
m.raise_custom, | ||
), | ||
], | ||
) | ||
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 | ||
|
||
|
||
@pytest.mark.parametrize( | ||
("expected_category", "module_function"), | ||
[ | ||
(Warning, m.raise_base_warning), | ||
(BytesWarning, m.raise_bytes_warning), | ||
(DeprecationWarning, m.raise_deprecation_warning), | ||
(FutureWarning, m.raise_future_warning), | ||
(ImportWarning, m.raise_import_warning), | ||
(PendingDeprecationWarning, m.raise_pending_deprecation_warning), | ||
(ResourceWarning, m.raise_resource_warning), | ||
(RuntimeWarning, m.raise_runtime_warning), | ||
(SyntaxWarning, m.raise_syntax_warning), | ||
(UnicodeWarning, m.raise_unicode_warning), | ||
(UserWarning, m.raise_user_warning), | ||
], | ||
) | ||
def test_warning_categories(expected_category, module_function): | ||
with pytest.warns(Warning) as excinfo: | ||
module_function() | ||
|
||
assert issubclass(excinfo[0].category, expected_category) | ||
assert str(excinfo[0].message) == f"This is {expected_category.__name__}!" |
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.
Maybe a little simpler?
I think the fully-qualified function name is important here, otherwise people will think is a C Python function.
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.
I see the point, applied.