-
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
Conversation
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.
Looks great, I think this is a useful addition.
All comments are minor.
You need to add the new header in one more file. A nice trick to pin-point where is to look for an existing header, e.g.:
$ git grep include/pybind11/typing.h
CMakeLists.txt: include/pybind11/typing.h)
tests/extra_python_package/test_files.py: "include/pybind11/typing.h",
I didn't review the tests yet (it's very late in my timezone). I'll do that in the next round.
include/pybind11/warnings.h
Outdated
+ 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 comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the PyErr_NewException()
documentation is incomplete because there is no mention of errors, but almost certainly we need at least this:
if (!h) {
throw error_already_set();
}
I think that'd be sufficient, although raise_from()
like you have above might be even better. I'm not sure how to judge cost:benefit 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.
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:
name
is always consisting one dot - it's forced byfull_name
.base
is also validated byPyWarning_Check
(plus it could also be null by design of cpython function).dict
by itself is always a nullptr here so that check in cpython is automatically "passed".
I can only imagine if somehow modulename
is broken. Maybe I am missing something but it seems to me like trying to break just to prove "that it's breakable". Do you have any thoughts on that?
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.
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. if (ptr == nullptr)
to if (ptr != nullptr)
so that the if
branch is reached, run the tests to convince myself that it works as intended, then change it back.
include/pybind11/warnings.h
Outdated
} | ||
if (result == -1) { | ||
raise_from(PyExc_SystemError, | ||
"PyWarning_Check(): internal error of Python C API while " |
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?
raise_from(PyExc_SystemError, "pybind11::detail::PyWarning_Check(): PyObject_IsSubclass() call failed.");
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.
include/pybind11/warnings.h
Outdated
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 comment
The reason will be displayed to describe this comment to others. Learn more.
pybind11_fail("warnings::new_warning_type(): ...")
?
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Applied, also I have followed the pybind11::*
convention from #5291 (comment) . I think it's better to keep it uniform across all messages, what do you think?
include/pybind11/warnings.h
Outdated
"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 comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe
pybind11_fail("new_warning_type(): an attribute with name \"" + std::string(name) + "\" exists already.");
?
I'm also wondering, could the condition simply be hasattr(scope, name)
? (I'm really not sure.)
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.
You are right, it works the same. Also added test case for it.
For the message same as #5291 (comment)
include/pybind11/warnings.h
Outdated
} | ||
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)); | ||
object obj = reinterpret_steal<object>(h); |
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 saw clang-tidy wants auto obj
here.
include/pybind11/warnings.h
Outdated
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 comment
The reason will be displayed to describe this comment to others. Learn more.
pybind11_fail("wanings::warn(): ...")
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.
Same as #5291 (comment)
include/pybind11/warnings.h
Outdated
+ std::string(name) + "\" exists already."); | ||
} | ||
std::string full_name = scope.attr("__name__").cast<std::string>() + std::string(".") + name; | ||
auto *new_ex = 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 comment
The reason will be displayed to describe this comment to others. Learn more.
Two separate things things:
-
Using
handle
here is very common around the pybind11 codebase, before anynullptr
check. Theif (!new_ex)
below can stay as is. -
Question: Is the
const_cast
still needed? Could you please try? — Recently we removed Python 3.7 support on master.
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.
- Changed back to handle.
- The
const_cast
was originally not there, so it can be removed (as my local builds and now CIs shows to be true).
tests/test_warnings.cpp
Outdated
|
||
#include <utility> | ||
|
||
namespace warning_helpers { |
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.
Could you please put this in a namespace?
Usually, in new code, I'd have this here:
namespace pybind11_tests {
namespace warnings {
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 comment
The reason will be displayed to describe this comment to others. Learn more.
See #5291 (comment)
tests/test_warnings.cpp
Outdated
[]() { 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 comment
The reason will be displayed to describe this comment to others. Learn more.
This confused me:
"RuntimeError should be raised!"
Suggestion to change to:
"Invalid category"
I'd also change raise_and_fail
to raise_invalid_category
Actually: is "raise" a good term to use here (all test functions)?
Because there is no "raise" in warnings.warn()
.
Maybe warn_with_invalid_category
etc? So that it is more clear to someone who gets here from a different context (e.g. refactoring or cleanup) what's happening.
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 motivation, I hope now names and messages are more self-explainatory/clear.
tests/test_warnings.cpp
Outdated
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 comment
The 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 PyExc_Warning
(the base class) and one randomly picked derived class.
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 have removed them all as other functions like warn_and_return_value
or warn_with_different_category
use these categories. This also result in obsolecence of namespace pybind11_tests/warnings
as no helpers are needed now (ref #5291 (comment)).
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.
Looks great! My only question is about using @pytest.mark.parametrize()
where there is only actually one test.
I only have/had a few minutes right now. I'll try to find more time later for another more careful pass.
tests/test_warnings.py
Outdated
"module_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.
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!
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.
Looks great, thanks!
I'll wait a couple days to give others a chance to look, too: @henryiii, @EthanSteinberg, @Skylion007
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 looks good to me. Thanks for the contribution!
@rwgk @EthanSteinberg thank you as well! One more question, is it okay if I add documentation later around end of this/next week? |
Yes, that will be great. I merged this PR already. Please open a new PR for the documentation. |
Description
Providing a simple interface to handle warnings on the pybind side, using Python's warning categories. Following up #601 issue and "re-do" of #4535 PR.
py::warnings
namespace.warnings::warn
that allow to throw warnings directly.warnings:: new_warning_type
to create new warning type on specific pybind's scope.Suggested changelog entry: