From e8f595bb85b95317ce4d1dbb3d04951d3f56e89a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 25 Aug 2024 23:35:03 -0400 Subject: [PATCH 1/6] chore(deps): bump actions/attest-build-provenance in the actions group (#5335) Bumps the actions group with 1 update: [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance). Updates `actions/attest-build-provenance` from 1.4.1 to 1.4.2 - [Release notes](https://github.com/actions/attest-build-provenance/releases) - [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md) - [Commits](https://github.com/actions/attest-build-provenance/compare/310b0a4a3b0b78ef57ecda988ee04b132db73ef8...6149ea5740be74af77f260b9db67e633f6b0a9a1) --- updated-dependencies: - dependency-name: actions/attest-build-provenance dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pip.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pip.yml b/.github/workflows/pip.yml index 3edfa612..af397166 100644 --- a/.github/workflows/pip.yml +++ b/.github/workflows/pip.yml @@ -102,7 +102,7 @@ jobs: - uses: actions/download-artifact@v4 - name: Generate artifact attestation for sdist and wheel - uses: actions/attest-build-provenance@310b0a4a3b0b78ef57ecda988ee04b132db73ef8 # v1.4.1 + uses: actions/attest-build-provenance@6149ea5740be74af77f260b9db67e633f6b0a9a1 # v1.4.2 with: subject-path: "*/pybind11*" From 3fb16ad17587a7ad7fbe18c043e790d68ee7fab4 Mon Sep 17 00:00:00 2001 From: ObeliskGate <96614155+ObeliskGate@users.noreply.github.com> Date: Mon, 26 Aug 2024 11:36:03 +0800 Subject: [PATCH 2/6] fix: using `__cpp_nontype_template_args` instead of `__cpp_nontype_template_parameter_class` (#5330) * fix: use `__cpp_nontype_template_args` instead of gnu extensions * fix: add feature test value * fix: change `PYBIND11_TYPING_H_HAS_STRING_LITERAL` skip reason --- include/pybind11/typing.h | 4 +--- tests/test_pytypes.py | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/include/pybind11/typing.h b/include/pybind11/typing.h index b0feb946..84aaf9f7 100644 --- a/include/pybind11/typing.h +++ b/include/pybind11/typing.h @@ -100,9 +100,7 @@ class Never : public none { using none::none; }; -#if defined(__cpp_nontype_template_parameter_class) \ - && (/* See #5201 */ !defined(__GNUC__) \ - || (__GNUC__ > 10 || (__GNUC__ == 10 && __GNUC_MINOR__ >= 3))) +#if defined(__cpp_nontype_template_args) && __cpp_nontype_template_args >= 201911L # define PYBIND11_TYPING_H_HAS_STRING_LITERAL template struct StringLiteral { diff --git a/tests/test_pytypes.py b/tests/test_pytypes.py index 1c6335f7..6f015eec 100644 --- a/tests/test_pytypes.py +++ b/tests/test_pytypes.py @@ -1026,7 +1026,7 @@ def test_optional_object_annotations(doc): @pytest.mark.skipif( not m.defined_PYBIND11_TYPING_H_HAS_STRING_LITERAL, - reason="C++20 feature not available.", + reason="C++20 non-type template args feature not available.", ) def test_literal(doc): assert ( @@ -1037,7 +1037,7 @@ def test_literal(doc): @pytest.mark.skipif( not m.defined_PYBIND11_TYPING_H_HAS_STRING_LITERAL, - reason="C++20 feature not available.", + reason="C++20 non-type template args feature not available.", ) def test_typevar(doc): assert ( From 5ed381daac5035cad9745824f96d04e7a35924e7 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 27 Aug 2024 00:23:51 +0700 Subject: [PATCH 3/6] Replace all SMART_HOLDER_WIP comments with reminders, notes, or pointers. (#5336) The SMART_HOLDER_WIP comments are mostly from 2021. In the meantime, the smart_holder code was extremely thoroughly tested in the Google codebase (production code). Additionally, testing via PyCLIF-pybind11 provided thousands of diverse use cases that needed to satisfy many million unit tests (the success rate was about 99.999%). --- include/pybind11/detail/struct_smart_holder.h | 15 +++++++++------ include/pybind11/detail/type_caster_base.h | 9 ++++----- include/pybind11/trampoline_self_life_support.h | 5 ++--- ...test_class_sh_trampoline_shared_ptr_cpp_arg.py | 2 +- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/include/pybind11/detail/struct_smart_holder.h b/include/pybind11/detail/struct_smart_holder.h index b1e24d7b..980fc369 100644 --- a/include/pybind11/detail/struct_smart_holder.h +++ b/include/pybind11/detail/struct_smart_holder.h @@ -215,7 +215,7 @@ struct smart_holder { // race conditions, but in the context of Python it is a bug (elsewhere) // if the Global Interpreter Lock (GIL) is not being held when this code // is reached. - // SMART_HOLDER_WIP: IMPROVABLE: assert(GIL is held). + // PYBIND11:REMINDER: This may need to be protected by a mutex in free-threaded Python. if (vptr.use_count() != 1) { throw std::invalid_argument(std::string("Cannot disown use_count != 1 (") + context + ")."); @@ -277,29 +277,32 @@ struct smart_holder { return hld; } - // Caller is responsible for ensuring preconditions (SMART_HOLDER_WIP: details). + // Caller is responsible for ensuring the complex preconditions + // (see `smart_holder_type_caster_support::load_helper`). void disown() { reset_vptr_deleter_armed_flag(false); is_disowned = true; } - // Caller is responsible for ensuring preconditions (SMART_HOLDER_WIP: details). + // Caller is responsible for ensuring the complex preconditions + // (see `smart_holder_type_caster_support::load_helper`). void reclaim_disowned() { reset_vptr_deleter_armed_flag(true); is_disowned = false; } - // Caller is responsible for ensuring preconditions (SMART_HOLDER_WIP: details). + // Caller is responsible for ensuring the complex preconditions + // (see `smart_holder_type_caster_support::load_helper`). void release_disowned() { vptr.reset(); } - // SMART_HOLDER_WIP: review this function. void ensure_can_release_ownership(const char *context = "ensure_can_release_ownership") const { ensure_is_not_disowned(context); ensure_vptr_is_using_builtin_delete(context); ensure_use_count_1(context); } - // Caller is responsible for ensuring preconditions (SMART_HOLDER_WIP: details). + // Caller is responsible for ensuring the complex preconditions + // (see `smart_holder_type_caster_support::load_helper`). void release_ownership() { reset_vptr_deleter_armed_flag(false); release_disowned(); diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index f1a5d0d8..7cdeabdc 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -475,7 +475,7 @@ inline PyObject *make_new_instance(PyTypeObject *type); #ifdef PYBIND11_HAS_INTERNALS_WITH_SMART_HOLDER_SUPPORT -// SMART_HOLDER_WIP: Needs refactoring of existing pybind11 code. +// PYBIND11:REMINDER: Needs refactoring of existing pybind11 code. inline bool deregister_instance(instance *self, void *valptr, const type_info *tinfo); PYBIND11_NAMESPACE_BEGIN(smart_holder_type_caster_support) @@ -568,8 +568,7 @@ handle smart_holder_from_unique_ptr(std::unique_ptr &&src, if (static_cast(src.get()) == src_raw_void_ptr) { // This is a multiple-inheritance situation that is incompatible with the current - // shared_from_this handling (see PR #3023). - // SMART_HOLDER_WIP: IMPROVABLE: Is there a better solution? + // shared_from_this handling (see PR #3023). Is there a better solution? src_raw_void_ptr = nullptr; } auto smhldr = smart_holder::from_unique_ptr(std::move(src), src_raw_void_ptr); @@ -623,8 +622,8 @@ handle smart_holder_from_shared_ptr(const std::shared_ptr &src, void *src_raw_void_ptr = static_cast(src_raw_ptr); const detail::type_info *tinfo = st.second; if (handle existing_inst = find_registered_python_instance(src_raw_void_ptr, tinfo)) { - // SMART_HOLDER_WIP: MISSING: Enforcement of consistency with existing smart_holder. - // SMART_HOLDER_WIP: MISSING: keep_alive. + // PYBIND11:REMINDER: MISSING: Enforcement of consistency with existing smart_holder. + // PYBIND11:REMINDER: MISSING: keep_alive. return existing_inst; } diff --git a/include/pybind11/trampoline_self_life_support.h b/include/pybind11/trampoline_self_life_support.h index cef69632..a30ffda3 100644 --- a/include/pybind11/trampoline_self_life_support.h +++ b/include/pybind11/trampoline_self_life_support.h @@ -15,14 +15,13 @@ PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) PYBIND11_NAMESPACE_BEGIN(detail) -// SMART_HOLDER_WIP: Needs refactoring of existing pybind11 code. +// PYBIND11:REMINDER: Needs refactoring of existing pybind11 code. inline bool deregister_instance(instance *self, void *valptr, const type_info *tinfo); PYBIND11_NAMESPACE_END(detail) // The original core idea for this struct goes back to PyCLIF: // https://github.com/google/clif/blob/07f95d7e69dca2fcf7022978a55ef3acff506c19/clif/python/runtime.cc#L37 -// URL provided here mainly to give proper credit. To fully explain the `HoldPyObj` feature, more -// context is needed (SMART_HOLDER_WIP). +// URL provided here mainly to give proper credit. struct trampoline_self_life_support { detail::value_and_holder v_h; diff --git a/tests/test_class_sh_trampoline_shared_ptr_cpp_arg.py b/tests/test_class_sh_trampoline_shared_ptr_cpp_arg.py index 13daeee2..85977542 100644 --- a/tests/test_class_sh_trampoline_shared_ptr_cpp_arg.py +++ b/tests/test_class_sh_trampoline_shared_ptr_cpp_arg.py @@ -62,7 +62,7 @@ def test_shared_ptr_arg_identity(): del obj pytest.gc_collect() - # SMART_HOLDER_WIP: the behavior below is DIFFERENT from PR #2839 + # NOTE: the behavior below is DIFFERENT from PR #2839 # python reference is gone because it is not an Alias instance assert objref() is None assert tester.has_python_instance() is False From f99ffd7e03001810a3e722bf48ad1a9e08415d7d Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 27 Aug 2024 01:56:00 +0700 Subject: [PATCH 4/6] Remove test_classh_mock.cpp,py (#5338) --- tests/CMakeLists.txt | 1 - tests/test_classh_mock.cpp | 73 -------------------------------------- tests/test_classh_mock.py | 13 ------- 3 files changed, 87 deletions(-) delete mode 100644 tests/test_classh_mock.cpp delete mode 100644 tests/test_classh_mock.py diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 99843ee4..53de1cd9 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -132,7 +132,6 @@ set(PYBIND11_TEST_FILES test_class_sh_unique_ptr_custom_deleter test_class_sh_unique_ptr_member test_class_sh_virtual_py_cpp_mix - test_classh_mock test_const_name test_constants_and_functions test_copy_move diff --git a/tests/test_classh_mock.cpp b/tests/test_classh_mock.cpp deleted file mode 100644 index 51be4ae0..00000000 --- a/tests/test_classh_mock.cpp +++ /dev/null @@ -1,73 +0,0 @@ -#include "pybind11_tests.h" - -// The main purpose of this test was to ensure that the suggested -// BOILERPLATE code block (NOW DEPRECATED!) block below is correct. - -// Copy this block of code into your project. -// Replace FOOEXT with the name of your project. -// BOILERPLATE BEGIN DEPRECATED DEPRECATED DEPRECATED DEPRECATED DEPRECATED -#ifdef FOOEXT_USING_PYBIND11_SMART_HOLDER -# include -#else -# include -PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) -# ifndef PYBIND11_HAS_INTERNALS_WITH_SMART_HOLDER_SUPPORT -template -using classh = class_; -# endif -PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) -# ifndef PYBIND11_SH_AVL -# define PYBIND11_SH_AVL(...) std::shared_ptr<__VA_ARGS__> // "Smart_Holder if AVaiLable" -# endif -# ifndef PYBIND11_SH_DEF -# define PYBIND11_SH_DEF(...) std::shared_ptr<__VA_ARGS__> // "Smart_Holder if DEFault" -# endif -# ifndef PYBIND11_SMART_HOLDER_TYPE_CASTERS -# define PYBIND11_SMART_HOLDER_TYPE_CASTERS(...) -# endif -# ifndef PYBIND11_TYPE_CASTER_BASE_HOLDER -# define PYBIND11_TYPE_CASTER_BASE_HOLDER(...) -# endif -#endif -// BOILERPLATE END DEPRECATED DEPRECATED DEPRECATED DEPRECATED DEPRECATED - -namespace { -struct FooUc {}; -struct FooUp {}; -struct FooSa {}; -struct FooSc {}; -struct FooSp {}; -} // namespace - -PYBIND11_SMART_HOLDER_TYPE_CASTERS(FooUp) // DEPRECATED -PYBIND11_SMART_HOLDER_TYPE_CASTERS(FooSp) // DEPRECATED - -PYBIND11_TYPE_CASTER_BASE_HOLDER(FooSa, std::shared_ptr) - -TEST_SUBMODULE(classh_mock, m) { - // Please see README_smart_holder.rst, in particular section - // Classic / Conservative / Progressive cross-module compatibility - - // Uses std::unique_ptr as holder in Classic or Conservative mode, py::smart_holder in - // Progressive mode. - py::class_(m, "FooUc").def(py::init<>()); - - // Uses std::unique_ptr as holder in Classic mode, py::smart_holder in Conservative or - // Progressive mode. - py::classh(m, "FooUp").def(py::init<>()); - - // Always uses std::shared_ptr as holder. - py::class_>(m, "FooSa").def(py::init<>()); - - // Uses std::shared_ptr as holder in Classic or Conservative mode, py::smart_holder in - // Progressive mode. - py::class_(m, "FooSc").def(py::init<>()); - // -------------- std::shared_ptr -- same length by design, to not disturb the - // indentation of existing code. - - // Uses std::shared_ptr as holder in Classic mode, py::smart_holder in Conservative or - // Progressive mode. - py::class_(m, "FooSp").def(py::init<>()); - // -------------- std::shared_ptr -- same length by design, to not disturb the - // indentation of existing code. -} diff --git a/tests/test_classh_mock.py b/tests/test_classh_mock.py deleted file mode 100644 index b05cd0c5..00000000 --- a/tests/test_classh_mock.py +++ /dev/null @@ -1,13 +0,0 @@ -from __future__ import annotations - -from pybind11_tests import classh_mock as m - - -def test_foobar(): - # Not really testing anything in particular. The main purpose of this test is to ensure the - # suggested BOILERPLATE code block in test_classh_mock.cpp is correct. - assert m.FooUc() - assert m.FooUp() - assert m.FooSa() - assert m.FooSc() - assert m.FooSp() From 65f4266cefc48401fadd4623f5af430359a6866d Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 28 Aug 2024 11:04:48 +0700 Subject: [PATCH 5/6] Add `while True` & `top` method to FAQ. (#5340) --- docs/faq.rst | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/docs/faq.rst b/docs/faq.rst index 92777b5b..31e33f8b 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -247,6 +247,50 @@ been received, you must either explicitly interrupt execution by throwing }); } +What is a highly conclusive and simple way to find memory leaks (e.g. in pybind11 bindings)? +============================================================================================ + +Use ``while True`` & ``top`` (Linux, macOS). + +For example, locally change tests/test_type_caster_pyobject_ptr.py like this: + +.. code-block:: diff + + def test_return_list_pyobject_ptr_reference(): + + while True: + vec_obj = m.return_list_pyobject_ptr_reference(ValueHolder) + assert [e.value for e in vec_obj] == [93, 186] + # Commenting out the next `assert` will leak the Python references. + # An easy way to see evidence of the leaks: + # Insert `while True:` as the first line of this function and monitor the + # process RES (Resident Memory Size) with the Unix top command. + - assert m.dec_ref_each_pyobject_ptr(vec_obj) == 2 + + # assert m.dec_ref_each_pyobject_ptr(vec_obj) == 2 + +Then run the test as you would normally do, which will go into the infinite loop. + +**In another shell, but on the same machine** run: + +.. code-block:: bash + + top + +This will show: + +.. code-block:: + + PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND + 1266095 rwgk 20 0 5207496 611372 45696 R 100.0 0.3 0:08.01 test_type_caste + +Look for the number under ``RES`` there. You'll see it going up very quickly. + +**Don't forget to Ctrl-C the test command** before your machine becomes +unresponsive due to swapping. + +This method only takes a couple minutes of effort and is very conclusive. +What you want to see is that the ``RES`` number is stable after a couple +seconds. + CMake doesn't detect the right Python version ============================================= From 66c3774a6402224b1724329c81c880e76633a92b Mon Sep 17 00:00:00 2001 From: Jan Iwaszkiewicz Date: Thu, 29 Aug 2024 05:55:50 +0200 Subject: [PATCH 6/6] 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 --- CMakeLists.txt | 3 +- include/pybind11/warnings.h | 75 ++++++++++++++++++++++++ tests/CMakeLists.txt | 3 +- tests/extra_python_package/test_files.py | 1 + tests/test_warnings.cpp | 46 +++++++++++++++ tests/test_warnings.py | 68 +++++++++++++++++++++ 6 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 include/pybind11/warnings.h create mode 100644 tests/test_warnings.cpp create mode 100644 tests/test_warnings.py diff --git a/CMakeLists.txt b/CMakeLists.txt index ed297194..a641925e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -160,7 +160,8 @@ set(PYBIND11_HEADERS include/pybind11/stl_bind.h include/pybind11/stl/filesystem.h include/pybind11/type_caster_pyobject_ptr.h - include/pybind11/typing.h) + include/pybind11/typing.h + include/pybind11/warnings.h) # Compare with grep and warn if mismatched if(PYBIND11_MASTER_PROJECT) diff --git a/include/pybind11/warnings.h b/include/pybind11/warnings.h new file mode 100644 index 00000000..263b2990 --- /dev/null +++ b/include/pybind11/warnings.h @@ -0,0 +1,75 @@ +/* + pybind11/warnings.h: Python warnings wrappers. + + Copyright (c) 2024 Jan Iwaszkiewicz + + 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(".") + 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(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) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index cd94ef3e..d2156f46 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -154,7 +154,8 @@ set(PYBIND11_TEST_FILES test_unnamed_namespace_a test_unnamed_namespace_b test_vector_unique_ptr_member - test_virtual_functions) + test_virtual_functions + test_warnings) # Invoking cmake with something like: # cmake -DPYBIND11_TEST_OVERRIDE="test_callbacks.cpp;test_pickling.cpp" .. diff --git a/tests/extra_python_package/test_files.py b/tests/extra_python_package/test_files.py index 0a5db901..7ad9b806 100644 --- a/tests/extra_python_package/test_files.py +++ b/tests/extra_python_package/test_files.py @@ -47,6 +47,7 @@ "include/pybind11/stl_bind.h", "include/pybind11/type_caster_pyobject_ptr.h", "include/pybind11/typing.h", + "include/pybind11/warnings.h", } detail_headers = { diff --git a/tests/test_warnings.cpp b/tests/test_warnings.cpp new file mode 100644 index 00000000..e76f2124 --- /dev/null +++ b/tests/test_warnings.cpp @@ -0,0 +1,46 @@ +/* + tests/test_warnings.cpp -- usage of warnings::warn() and warnings categories. + + Copyright (c) 2024 Jan Iwaszkiewicz + + All rights reserved. Use of this source code is governed by a + BSD-style license that can be found in the LICENSE file. +*/ + +#include + +#include "pybind11_tests.h" + +#include + +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 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); }); +} diff --git a/tests/test_warnings.py b/tests/test_warnings.py new file mode 100644 index 00000000..4313432c --- /dev/null +++ b/tests/test_warnings.py @@ -0,0 +1,68 @@ +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 + + 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 str(excinfo[0].message) == "This is warning from Python!" + + +def test_warning_custom(): + with pytest.warns(m.CustomWarning) as excinfo: + value = m.warn_with_custom_type() + + assert issubclass(excinfo[0].category, DeprecationWarning) + assert str(excinfo[0].message) == "This is CustomWarning" + assert value == 37