From 690a115d84f2b1217eb43d5f3c35d7be3052d8df Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 7 Aug 2023 20:48:20 -0700 Subject: [PATCH 1/7] Add `py::set_error()`, use in updated `py::exception<>` documentation (#4772) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Copy clang 17 compatibility fixes from PR #4762 to a separate PR. * static py::exception<> -> static py::handle * Add `py::set_error()` but also try the suggestion of @malfet (https://github.com/pytorch/pytorch/pull/106401#pullrequestreview-1559961407). * clang 17 compatibility fixes (#4767) * Copy clang 17 compatibility fixes from PR #4762 to a separate PR. * Add gcc:13 C++20 * Add silkeh/clang:16-bullseye C++20 * chore(deps): update pre-commit hooks (#4770) updates: - [github.com/psf/black: 23.3.0 → 23.7.0](https://github.com/psf/black/compare/23.3.0...23.7.0) - [github.com/astral-sh/ruff-pre-commit: v0.0.276 → v0.0.281](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.276...v0.0.281) - [github.com/asottile/blacken-docs: 1.14.0 → 1.15.0](https://github.com/asottile/blacken-docs/compare/1.14.0...1.15.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> * docs: Remove upper bound on pybind11 in example pyproject.toml for setuptools (#4774) * docs: Remove upper bound on pybind11 in example pyproject.toml for setuptools * Update docs/compiling.rst --------- Co-authored-by: Henry Schreiner * Provide better type hints for a variety of generic types (#4259) * Provide better type hints for a variety of generic types * Makes better documentation * tuple, dict, list, set, function * Move to py::typing * style: pre-commit fixes * Update copyright line with correct year and actual author. The author information was copy-pasted from the git log output. --------- Co-authored-by: Ralf W. Grosse-Kunstleve Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> * Use `py::set_error()` everywhere possible (only one special case, in common.h). Overload `py::set_error(py::handle, py::handle)`. Change back to `static py::handle exc = ... .release();` Deprecate `py::exception<>::operator()` * Add `PYBIND11_WARNING_DISABLE` for INTEL and MSVC (and sort alphabetically). * `PYBIND11_WARNING_DISABLE_INTEL(10441)` does not work. For ICC only, falling back to the recommended `py::set_error()` to keep the testing simple. It is troublesome to add `--diag-disable=10441` specifically for test_exceptions.cpp, even that is non-ideal because it covers the entire file, not just the one line we need it for, and the value of exercising the trivial deprecated `operator()` on this one extra platform is practically zero. * Fix silly oversight. * NVHPC 23.5.0 generates deprecation warnings. They are currently not treated as errors, but falling back to using `py::set_error()` to not have to deal with that distraction. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Keto D. Zhang Co-authored-by: Henry Schreiner Co-authored-by: Dustin Spicuzza --- docs/advanced/exceptions.rst | 23 +++---- include/pybind11/detail/class.h | 6 +- include/pybind11/detail/common.h | 2 +- include/pybind11/detail/internals.h | 2 +- include/pybind11/detail/type_caster_base.h | 2 +- include/pybind11/numpy.h | 4 +- include/pybind11/pybind11.h | 22 +++---- include/pybind11/pytypes.h | 8 +++ ...s_module_interleaved_error_already_set.cpp | 4 +- tests/pybind11_cross_module_tests.cpp | 8 +-- tests/test_exceptions.cpp | 60 +++++++++++++++---- tests/test_exceptions.h | 2 +- tests/test_exceptions.py | 10 +++- tests/test_pytypes.cpp | 2 +- tests/test_type_caster_pyobject_ptr.cpp | 4 +- 15 files changed, 105 insertions(+), 54 deletions(-) diff --git a/docs/advanced/exceptions.rst b/docs/advanced/exceptions.rst index 53981dc08..616e2d772 100644 --- a/docs/advanced/exceptions.rst +++ b/docs/advanced/exceptions.rst @@ -127,8 +127,7 @@ before a global translator is tried. Inside the translator, ``std::rethrow_exception`` should be used within a try block to re-throw the exception. One or more catch clauses to catch the appropriate exceptions should then be used with each clause using -``PyErr_SetString`` to set a Python exception or ``ex(string)`` to set -the python exception to a custom exception type (see below). +``py::set_error()`` (see below). To declare a custom Python exception type, declare a ``py::exception`` variable and use this in the associated exception translator (note: it is often useful @@ -142,14 +141,17 @@ standard python RuntimeError: .. code-block:: cpp - static py::exception exc(m, "MyCustomError"); + // This is a static object, so we must leak the Python reference: + // It is undefined when the destructor will run, possibly only after the + // Python interpreter is finalized already. + static py::handle exc = py::exception(m, "MyCustomError").release(); py::register_exception_translator([](std::exception_ptr p) { try { if (p) std::rethrow_exception(p); } catch (const MyCustomException &e) { - exc(e.what()); + py::set_error(exc, e.what()); } catch (const OtherException &e) { - PyErr_SetString(PyExc_RuntimeError, e.what()); + py::set_error(PyExc_RuntimeError, e.what()); } }); @@ -168,8 +170,7 @@ section. .. note:: - Call either ``PyErr_SetString`` or a custom exception's call - operator (``exc(string)``) for every exception caught in a custom exception + Call ``py::set_error()`` for every exception caught in a custom exception translator. Failure to do so will cause Python to crash with ``SystemError: error return without exception set``. @@ -200,7 +201,7 @@ If module1 has the following translator: try { if (p) std::rethrow_exception(p); } catch (const std::invalid_argument &e) { - PyErr_SetString("module1 handled this") + py::set_error(PyExc_ArgumentError, "module1 handled this"); } } @@ -212,7 +213,7 @@ and module2 has the following similar translator: try { if (p) std::rethrow_exception(p); } catch (const std::invalid_argument &e) { - PyErr_SetString("module2 handled this") + py::set_error(PyExc_ArgumentError, "module2 handled this"); } } @@ -312,11 +313,11 @@ error protocol, which is outlined here. After calling the Python C API, if Python returns an error, ``throw py::error_already_set();``, which allows pybind11 to deal with the exception and pass it back to the Python interpreter. This includes calls to -the error setting functions such as ``PyErr_SetString``. +the error setting functions such as ``py::set_error()``. .. code-block:: cpp - PyErr_SetString(PyExc_TypeError, "C API type error demo"); + py::set_error(PyExc_TypeError, "C API type error demo"); throw py::error_already_set(); // But it would be easier to simply... diff --git a/include/pybind11/detail/class.h b/include/pybind11/detail/class.h index bc2b40c50..3ece0643b 100644 --- a/include/pybind11/detail/class.h +++ b/include/pybind11/detail/class.h @@ -375,7 +375,7 @@ extern "C" inline PyObject *pybind11_object_new(PyTypeObject *type, PyObject *, extern "C" inline int pybind11_object_init(PyObject *self, PyObject *, PyObject *) { PyTypeObject *type = Py_TYPE(self); std::string msg = get_fully_qualified_tp_name(type) + ": No constructor defined!"; - PyErr_SetString(PyExc_TypeError, msg.c_str()); + set_error(PyExc_TypeError, msg.c_str()); return -1; } @@ -579,7 +579,7 @@ extern "C" inline int pybind11_getbuffer(PyObject *obj, Py_buffer *view, int fla if (view) { view->obj = nullptr; } - PyErr_SetString(PyExc_BufferError, "pybind11_getbuffer(): Internal error"); + set_error(PyExc_BufferError, "pybind11_getbuffer(): Internal error"); return -1; } std::memset(view, 0, sizeof(Py_buffer)); @@ -587,7 +587,7 @@ extern "C" inline int pybind11_getbuffer(PyObject *obj, Py_buffer *view, int fla if ((flags & PyBUF_WRITABLE) == PyBUF_WRITABLE && info->readonly) { delete info; // view->obj = nullptr; // Was just memset to 0, so not necessary - PyErr_SetString(PyExc_BufferError, "Writable buffer requested for readonly storage"); + set_error(PyExc_BufferError, "Writable buffer requested for readonly storage"); return -1; } view->obj = obj; diff --git a/include/pybind11/detail/common.h b/include/pybind11/detail/common.h index 7ebc01c5d..e79f7693d 100644 --- a/include/pybind11/detail/common.h +++ b/include/pybind11/detail/common.h @@ -399,7 +399,7 @@ PYBIND11_WARNING_POP return nullptr; \ } \ catch (const std::exception &e) { \ - PyErr_SetString(PyExc_ImportError, e.what()); \ + ::pybind11::set_error(PyExc_ImportError, e.what()); \ return nullptr; \ } diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index aaa7f8686..228dd764b 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -352,7 +352,7 @@ inline bool raise_err(PyObject *exc_type, const char *msg) { raise_from(exc_type, msg); return true; } - PyErr_SetString(exc_type, msg); + set_error(exc_type, msg); return false; } diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index 16387506c..fc5f1c88b 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -786,7 +786,7 @@ public: std::string tname = rtti_type ? rtti_type->name() : cast_type.name(); detail::clean_type_id(tname); std::string msg = "Unregistered type : " + tname; - PyErr_SetString(PyExc_TypeError, msg.c_str()); + set_error(PyExc_TypeError, msg.c_str()); return {nullptr, nullptr}; } diff --git a/include/pybind11/numpy.h b/include/pybind11/numpy.h index 36077ec04..02f65d740 100644 --- a/include/pybind11/numpy.h +++ b/include/pybind11/numpy.h @@ -1008,7 +1008,7 @@ protected: /// Create array from any object -- always returns a new reference static PyObject *raw_array(PyObject *ptr, int ExtraFlags = 0) { if (ptr == nullptr) { - PyErr_SetString(PyExc_ValueError, "cannot create a pybind11::array from a nullptr"); + set_error(PyExc_ValueError, "cannot create a pybind11::array from a nullptr"); return nullptr; } return detail::npy_api::get().PyArray_FromAny_( @@ -1155,7 +1155,7 @@ protected: /// Create array from any object -- always returns a new reference static PyObject *raw_array_t(PyObject *ptr) { if (ptr == nullptr) { - PyErr_SetString(PyExc_ValueError, "cannot create a pybind11::array_t from a nullptr"); + set_error(PyExc_ValueError, "cannot create a pybind11::array_t from a nullptr"); return nullptr; } return detail::npy_api::get().PyArray_FromAny_(ptr, diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 3bce1a01b..76edf7f1f 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -694,9 +694,8 @@ protected: if (overloads->is_constructor) { if (!parent || !PyObject_TypeCheck(parent.ptr(), (PyTypeObject *) overloads->scope.ptr())) { - PyErr_SetString( - PyExc_TypeError, - "__init__(self, ...) called with invalid or missing `self` argument"); + set_error(PyExc_TypeError, + "__init__(self, ...) called with invalid or missing `self` argument"); return nullptr; } @@ -1007,7 +1006,7 @@ protected: A translator may choose to do one of the following: - - catch the exception and call PyErr_SetString or PyErr_SetObject + - catch the exception and call py::set_error() to set a standard (or custom) Python exception, or - do nothing and let the exception fall through to the next translator, or - delegate translation to the next translator by throwing a new type of exception. @@ -1023,8 +1022,7 @@ protected: return nullptr; } - PyErr_SetString(PyExc_SystemError, - "Exception escaped from default exception translator!"); + set_error(PyExc_SystemError, "Exception escaped from default exception translator!"); return nullptr; } @@ -1125,7 +1123,7 @@ protected: raise_from(PyExc_TypeError, msg.c_str()); return nullptr; } - PyErr_SetString(PyExc_TypeError, msg.c_str()); + set_error(PyExc_TypeError, msg.c_str()); return nullptr; } if (!result) { @@ -1138,7 +1136,7 @@ protected: raise_from(PyExc_TypeError, msg.c_str()); return nullptr; } - PyErr_SetString(PyExc_TypeError, msg.c_str()); + set_error(PyExc_TypeError, msg.c_str()); return nullptr; } if (overloads->is_constructor && !self_value_and_holder.holder_constructed()) { @@ -2528,7 +2526,7 @@ inline void register_local_exception_translator(ExceptionTranslator &&translator /** * Wrapper to generate a new Python exception type. * - * This should only be used with PyErr_SetString for now. + * This should only be used with py::set_error() for now. * It is not (yet) possible to use as a py::base. * Template type argument is reserved for future use. */ @@ -2549,7 +2547,9 @@ public: } // Sets the current python exception to this exception object with the given message - void operator()(const char *message) { PyErr_SetString(m_ptr, message); } + PYBIND11_DEPRECATED("Please use py::set_error() instead " + "(https://github.com/pybind/pybind11/pull/4772)") + void operator()(const char *message) const { set_error(*this, message); } }; PYBIND11_NAMESPACE_BEGIN(detail) @@ -2581,7 +2581,7 @@ register_exception_impl(handle scope, const char *name, handle base, bool isLoca try { std::rethrow_exception(p); } catch (const CppException &e) { - detail::get_exception_object()(e.what()); + set_error(detail::get_exception_object(), e.what()); } }); return ex; diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 580a4658e..fa2d24984 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -334,6 +334,14 @@ public: #endif }; +inline void set_error(const handle &type, const char *message) { + PyErr_SetString(type.ptr(), message); +} + +inline void set_error(const handle &type, const handle &value) { + PyErr_SetObject(type.ptr(), value.ptr()); +} + /** \rst Holds a reference to a Python object (with reference counting) diff --git a/tests/cross_module_interleaved_error_already_set.cpp b/tests/cross_module_interleaved_error_already_set.cpp index fdd9939e4..3493a7e61 100644 --- a/tests/cross_module_interleaved_error_already_set.cpp +++ b/tests/cross_module_interleaved_error_already_set.cpp @@ -16,12 +16,12 @@ namespace { namespace py = pybind11; void interleaved_error_already_set() { - PyErr_SetString(PyExc_RuntimeError, "1st error."); + py::set_error(PyExc_RuntimeError, "1st error."); try { throw py::error_already_set(); } catch (const py::error_already_set &) { // The 2nd error could be conditional in a real application. - PyErr_SetString(PyExc_RuntimeError, "2nd error."); + py::set_error(PyExc_RuntimeError, "2nd error."); } // Here the 1st error is destroyed before the 2nd error is fetched. // The error_already_set dtor triggers a pybind11::detail::get_internals() // call via pybind11::gil_scoped_acquire. diff --git a/tests/pybind11_cross_module_tests.cpp b/tests/pybind11_cross_module_tests.cpp index 9379f3f25..ad68e9a54 100644 --- a/tests/pybind11_cross_module_tests.cpp +++ b/tests/pybind11_cross_module_tests.cpp @@ -31,11 +31,11 @@ PYBIND11_MODULE(pybind11_cross_module_tests, m) { // test_exceptions.py py::register_local_exception(m, "LocalSimpleException"); m.def("raise_runtime_error", []() { - PyErr_SetString(PyExc_RuntimeError, "My runtime error"); + py::set_error(PyExc_RuntimeError, "My runtime error"); throw py::error_already_set(); }); m.def("raise_value_error", []() { - PyErr_SetString(PyExc_ValueError, "My value error"); + py::set_error(PyExc_ValueError, "My value error"); throw py::error_already_set(); }); m.def("throw_pybind_value_error", []() { throw py::value_error("pybind11 value error"); }); @@ -49,7 +49,7 @@ PYBIND11_MODULE(pybind11_cross_module_tests, m) { std::rethrow_exception(p); } } catch (const shared_exception &e) { - PyErr_SetString(PyExc_KeyError, e.what()); + py::set_error(PyExc_KeyError, e.what()); } }); @@ -60,7 +60,7 @@ PYBIND11_MODULE(pybind11_cross_module_tests, m) { std::rethrow_exception(p); } } catch (const LocalException &e) { - PyErr_SetString(PyExc_KeyError, e.what()); + py::set_error(PyExc_KeyError, e.what()); } }); diff --git a/tests/test_exceptions.cpp b/tests/test_exceptions.cpp index 854c7e6f7..f6d9c215c 100644 --- a/tests/test_exceptions.cpp +++ b/tests/test_exceptions.cpp @@ -25,6 +25,10 @@ private: std::string message = ""; }; +class MyExceptionUseDeprecatedOperatorCall : public MyException { + using MyException::MyException; +}; + // A type that should be translated to a standard Python exception class MyException2 : public std::exception { public: @@ -109,8 +113,8 @@ TEST_SUBMODULE(exceptions, m) { m.def("throw_std_exception", []() { throw std::runtime_error("This exception was intentionally thrown."); }); - // make a new custom exception and use it as a translation target - static py::exception ex(m, "MyException"); + // PLEASE KEEP IN SYNC with docs/advanced/exceptions.rst + static py::handle ex = py::exception(m, "MyException").release(); py::register_exception_translator([](std::exception_ptr p) { try { if (p) { @@ -118,7 +122,32 @@ TEST_SUBMODULE(exceptions, m) { } } catch (const MyException &e) { // Set MyException as the active python error - ex(e.what()); + py::set_error(ex, e.what()); + } + }); + + // Same as above, but using the deprecated `py::exception<>::operator()` + // We want to be sure it still works, until it's removed. + static const auto *const exd = new py::exception( + m, "MyExceptionUseDeprecatedOperatorCall"); + py::register_exception_translator([](std::exception_ptr p) { + try { + if (p) { + std::rethrow_exception(p); + } + } catch (const MyExceptionUseDeprecatedOperatorCall &e) { +#if defined(__INTEL_COMPILER) || defined(__NVCOMPILER) + // It is not worth the trouble dealing with warning suppressions for these compilers. + // Falling back to the recommended approach to keep the test code simple. + py::set_error(*exd, e.what()); +#else + PYBIND11_WARNING_PUSH + PYBIND11_WARNING_DISABLE_CLANG("-Wdeprecated-declarations") + PYBIND11_WARNING_DISABLE_GCC("-Wdeprecated-declarations") + PYBIND11_WARNING_DISABLE_MSVC(4996) + (*exd)(e.what()); + PYBIND11_WARNING_POP +#endif } }); @@ -132,7 +161,7 @@ TEST_SUBMODULE(exceptions, m) { } } catch (const MyException2 &e) { // Translate this exception to a standard RuntimeError - PyErr_SetString(PyExc_RuntimeError, e.what()); + py::set_error(PyExc_RuntimeError, e.what()); } }); @@ -162,11 +191,16 @@ TEST_SUBMODULE(exceptions, m) { std::rethrow_exception(p); } } catch (const MyException6 &e) { - PyErr_SetString(PyExc_RuntimeError, e.what()); + py::set_error(PyExc_RuntimeError, e.what()); } }); - m.def("throws1", []() { throw MyException("this error should go to a custom type"); }); + m.def("throws1", + []() { throw MyException("this error should go to py::exception"); }); + m.def("throws1d", []() { + throw MyExceptionUseDeprecatedOperatorCall( + "this error should go to py::exception"); + }); m.def("throws2", []() { throw MyException2("this error should go to a standard Python exception"); }); m.def("throws3", []() { throw MyException3("this error cannot be translated"); }); @@ -222,7 +256,7 @@ TEST_SUBMODULE(exceptions, m) { m.def("throw_already_set", [](bool err) { if (err) { - PyErr_SetString(PyExc_ValueError, "foo"); + py::set_error(PyExc_ValueError, "foo"); } try { throw py::error_already_set(); @@ -238,7 +272,7 @@ TEST_SUBMODULE(exceptions, m) { } PyErr_Clear(); if (err) { - PyErr_SetString(PyExc_ValueError, "foo"); + py::set_error(PyExc_ValueError, "foo"); } throw py::error_already_set(); }); @@ -247,7 +281,7 @@ TEST_SUBMODULE(exceptions, m) { bool retval = false; try { PythonCallInDestructor set_dict_in_destructor(d); - PyErr_SetString(PyExc_ValueError, "foo"); + py::set_error(PyExc_ValueError, "foo"); throw py::error_already_set(); } catch (const py::error_already_set &) { retval = true; @@ -282,14 +316,14 @@ TEST_SUBMODULE(exceptions, m) { m.def("throw_should_be_translated_to_key_error", []() { throw shared_exception(); }); m.def("raise_from", []() { - PyErr_SetString(PyExc_ValueError, "inner"); + py::set_error(PyExc_ValueError, "inner"); py::raise_from(PyExc_ValueError, "outer"); throw py::error_already_set(); }); m.def("raise_from_already_set", []() { try { - PyErr_SetString(PyExc_ValueError, "inner"); + py::set_error(PyExc_ValueError, "inner"); throw py::error_already_set(); } catch (py::error_already_set &e) { py::raise_from(e, PyExc_ValueError, "outer"); @@ -306,7 +340,7 @@ TEST_SUBMODULE(exceptions, m) { }); m.def("error_already_set_what", [](const py::object &exc_type, const py::object &exc_value) { - PyErr_SetObject(exc_type.ptr(), exc_value.ptr()); + py::set_error(exc_type, exc_value); std::string what = py::error_already_set().what(); bool py_err_set_after_what = (PyErr_Occurred() != nullptr); PyErr_Clear(); @@ -321,7 +355,7 @@ TEST_SUBMODULE(exceptions, m) { }); m.def("test_error_already_set_double_restore", [](bool dry_run) { - PyErr_SetString(PyExc_ValueError, "Random error."); + py::set_error(PyExc_ValueError, "Random error."); py::error_already_set e; e.restore(); PyErr_Clear(); diff --git a/tests/test_exceptions.h b/tests/test_exceptions.h index 03684b89f..2eaa3d3d1 100644 --- a/tests/test_exceptions.h +++ b/tests/test_exceptions.h @@ -9,5 +9,5 @@ class PYBIND11_EXPORT_EXCEPTION shared_exception : public pybind11::builtin_exce public: using builtin_exception::builtin_exception; explicit shared_exception() : shared_exception("") {} - void set_error() const override { PyErr_SetString(PyExc_RuntimeError, what()); } + void set_error() const override { py::set_error(PyExc_RuntimeError, what()); } }; diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index ccac4536d..5d8e6292a 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -139,7 +139,15 @@ def test_custom(msg): # Can we catch a MyException? with pytest.raises(m.MyException) as excinfo: m.throws1() - assert msg(excinfo.value) == "this error should go to a custom type" + assert msg(excinfo.value) == "this error should go to py::exception" + + # Can we catch a MyExceptionUseDeprecatedOperatorCall? + with pytest.raises(m.MyExceptionUseDeprecatedOperatorCall) as excinfo: + m.throws1d() + assert ( + msg(excinfo.value) + == "this error should go to py::exception" + ) # Can we translate to standard Python exceptions? with pytest.raises(RuntimeError) as excinfo: diff --git a/tests/test_pytypes.cpp b/tests/test_pytypes.cpp index cb2f76c03..b03f5624e 100644 --- a/tests/test_pytypes.cpp +++ b/tests/test_pytypes.cpp @@ -25,7 +25,7 @@ PyObject *conv(PyObject *o) { ret = PyFloat_FromDouble(v); } } else { - PyErr_SetString(PyExc_TypeError, "Unexpected type"); + py::set_error(PyExc_TypeError, "Unexpected type"); } return ret; } diff --git a/tests/test_type_caster_pyobject_ptr.cpp b/tests/test_type_caster_pyobject_ptr.cpp index 1667ea126..8069f7dcd 100644 --- a/tests/test_type_caster_pyobject_ptr.cpp +++ b/tests/test_type_caster_pyobject_ptr.cpp @@ -78,14 +78,14 @@ TEST_SUBMODULE(type_caster_pyobject_ptr, m) { m.def("cast_to_pyobject_ptr_nullptr", [](bool set_error) { if (set_error) { - PyErr_SetString(PyExc_RuntimeError, "Reflective of healthy error handling."); + py::set_error(PyExc_RuntimeError, "Reflective of healthy error handling."); } PyObject *ptr = nullptr; py::cast(ptr); }); m.def("cast_to_pyobject_ptr_non_nullptr_with_error_set", []() { - PyErr_SetString(PyExc_RuntimeError, "Reflective of unhealthy error handling."); + py::set_error(PyExc_RuntimeError, "Reflective of unhealthy error handling."); py::cast(Py_None); }); From 4bf60c609a8af3868b813a27072d51c080c3e175 Mon Sep 17 00:00:00 2001 From: Pieter P Date: Tue, 8 Aug 2023 05:58:30 +0200 Subject: [PATCH 2/7] Disable strip when build type is unset (#4454) (#4780) --- tools/pybind11NewTools.cmake | 12 +++++++----- tools/pybind11Tools.cmake | 10 ++++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/tools/pybind11NewTools.cmake b/tools/pybind11NewTools.cmake index 7d7424a79..3a35a7386 100644 --- a/tools/pybind11NewTools.cmake +++ b/tools/pybind11NewTools.cmake @@ -233,11 +233,13 @@ function(pybind11_add_module target_name) endif() endif() - # Use case-insensitive comparison to match the result of $ - string(TOUPPER "${CMAKE_BUILD_TYPE}" uppercase_CMAKE_BUILD_TYPE) - if(NOT MSVC AND NOT "${uppercase_CMAKE_BUILD_TYPE}" MATCHES DEBUG|RELWITHDEBINFO) - # Strip unnecessary sections of the binary on Linux/macOS - pybind11_strip(${target_name}) + if(DEFINED CMAKE_BUILD_TYPE) # see https://github.com/pybind/pybind11/issues/4454 + # Use case-insensitive comparison to match the result of $ + string(TOUPPER "${CMAKE_BUILD_TYPE}" uppercase_CMAKE_BUILD_TYPE) + if(NOT MSVC AND NOT "${uppercase_CMAKE_BUILD_TYPE}" MATCHES DEBUG|RELWITHDEBINFO) + # Strip unnecessary sections of the binary on Linux/macOS + pybind11_strip(${target_name}) + endif() endif() if(MSVC) diff --git a/tools/pybind11Tools.cmake b/tools/pybind11Tools.cmake index 66ad00a47..67e710bda 100644 --- a/tools/pybind11Tools.cmake +++ b/tools/pybind11Tools.cmake @@ -212,10 +212,12 @@ function(pybind11_add_module target_name) endif() endif() - # Use case-insensitive comparison to match the result of $ - string(TOUPPER "${CMAKE_BUILD_TYPE}" uppercase_CMAKE_BUILD_TYPE) - if(NOT MSVC AND NOT "${uppercase_CMAKE_BUILD_TYPE}" MATCHES DEBUG|RELWITHDEBINFO) - pybind11_strip(${target_name}) + if(DEFINED CMAKE_BUILD_TYPE) # see https://github.com/pybind/pybind11/issues/4454 + # Use case-insensitive comparison to match the result of $ + string(TOUPPER "${CMAKE_BUILD_TYPE}" uppercase_CMAKE_BUILD_TYPE) + if(NOT MSVC AND NOT "${uppercase_CMAKE_BUILD_TYPE}" MATCHES DEBUG|RELWITHDEBINFO) + pybind11_strip(${target_name}) + endif() endif() if(MSVC) From 9039e6ac425e5d77dbef9b281124679e7dc421c4 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Thu, 10 Aug 2023 15:17:56 -0400 Subject: [PATCH 3/7] chore: use 2x faster black mirror (#4784) Committed via https://github.com/asottile/all-repos --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c7f596342..4102ee3b9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: types_or: [c++, c, cuda] # Black, the code formatter, natively supports pre-commit -- repo: https://github.com/psf/black +- repo: https://github.com/psf/black-pre-commit-mirror rev: "23.7.0" # Keep in sync with blacken-docs hooks: - id: black From add281a2da7a6eae78c99d20cc3034b0b5381524 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 14 Aug 2023 21:46:17 -0700 Subject: [PATCH 4/7] =?UTF-8?q?Migrate=20to=20readthedocs=20configuration?= =?UTF-8?q?=20file=20v2=C2=B6=20(#4789)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Copy of recommded readthedocs configuration file v2 * [ci skip] It is now requirements (not requirements_file) --- .readthedocs.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index c9c61617c..2625521e5 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,3 +1,15 @@ +# https://blog.readthedocs.com/migrate-configuration-v2/ + +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +sphinx: + configuration: docs/conf.py + python: - version: 3 -requirements_file: docs/requirements.txt + install: + - requirements: docs/requirements.txt From 80bcd21fb75b1dd95c0d9b095bb1657c3588d85e Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 15 Aug 2023 07:02:54 -0700 Subject: [PATCH 5/7] [ci skip] Adopt nanobind config. (#4792) --- .readthedocs.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.readthedocs.yml b/.readthedocs.yml index 2625521e5..a2b802f73 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -4,6 +4,8 @@ version: 2 build: os: ubuntu-22.04 + apt_packages: + - librsvg2-bin tools: python: "3.11" @@ -13,3 +15,6 @@ sphinx: python: install: - requirements: docs/requirements.txt + +formats: + - pdf From f47ff3280e30c275c0a633293bf08d23d3728668 Mon Sep 17 00:00:00 2001 From: Kenji Date: Tue, 15 Aug 2023 10:09:13 -0400 Subject: [PATCH 6/7] Fix grammar in functions.rst (#4791) The previous sentence had an extra "a" before "several", which isn't right. --- docs/advanced/functions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced/functions.rst b/docs/advanced/functions.rst index 69e3d8a1d..372934b09 100644 --- a/docs/advanced/functions.rst +++ b/docs/advanced/functions.rst @@ -16,7 +16,7 @@ lifetime of objects managed by them. This can lead to issues when creating bindings for functions that return a non-trivial type. Just by looking at the type information, it is not clear whether Python should take charge of the returned value and eventually free its resources, or if this is handled on the -C++ side. For this reason, pybind11 provides a several *return value policy* +C++ side. For this reason, pybind11 provides several *return value policy* annotations that can be passed to the :func:`module_::def` and :func:`class_::def` functions. The default policy is :enum:`return_value_policy::automatic`. From b9359ceadbf4d4b22509b684eea107d055b77901 Mon Sep 17 00:00:00 2001 From: Jean Elsner Date: Tue, 15 Aug 2023 16:48:59 +0200 Subject: [PATCH 7/7] Remove newlines from docstring signature (#4735) * Remove newlines from docstring signature * Jean/dev (#1) Replace newlines in arg values with spaces * style: pre-commit fixes * Don't use std::find_if for C++ 11 compatibility * Avoid implicit char to bool conversion * Test default arguments for line breaks * style: pre-commit fixes * Separate Eigen tests * style: pre-commit fixes * Fix merge * Try importing numpy * Avoid unreferenced variable in catch block * style: pre-commit fixes * Update squash function * Reduce try block * Additional test cases * style: pre-commit fixes * Put statement inside braces * Move string into function body * Rename repr for better readability. Make constr explicit. * Add multiline string default argument test case * style: pre-commit fixes * Add std namespace, do not modify string repr * Test for all space chars, test str repr not modified --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- include/pybind11/pybind11.h | 41 +++++++++++++++++++++++++++- tests/test_eigen_matrix.cpp | 17 ++++++++++++ tests/test_eigen_matrix.py | 5 ++++ tests/test_kwargs_and_defaults.cpp | 44 ++++++++++++++++++++++++++++++ tests/test_kwargs_and_defaults.py | 32 ++++++++++++++++++++++ 5 files changed, 138 insertions(+), 1 deletion(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 76edf7f1f..db91639dc 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -52,6 +52,45 @@ PYBIND11_WARNING_DISABLE_MSVC(4127) PYBIND11_NAMESPACE_BEGIN(detail) +inline std::string replace_newlines_and_squash(const char *text) { + const char *whitespaces = " \t\n\r\f\v"; + std::string result(text); + bool previous_is_whitespace = false; + + // Do not modify string representations + char first_char = result[0]; + char last_char = result[result.size() - 1]; + if (first_char == last_char && first_char == '\'') { + return result; + } + result.clear(); + + // Replace characters in whitespaces array with spaces and squash consecutive spaces + while (*text != '\0') { + if (std::strchr(whitespaces, *text)) { + if (!previous_is_whitespace) { + result += ' '; + previous_is_whitespace = true; + } + } else { + result += *text; + previous_is_whitespace = false; + } + ++text; + } + + // Strip leading and trailing whitespaces + const size_t str_begin = result.find_first_not_of(whitespaces); + if (str_begin == std::string::npos) { + return ""; + } + + const size_t str_end = result.find_last_not_of(whitespaces); + const size_t str_range = str_end - str_begin + 1; + + return result.substr(str_begin, str_range); +} + // Apply all the extensions translators from a list // Return true if one of the translators completed without raising an exception // itself. Return of false indicates that if there are other translators @@ -424,7 +463,7 @@ protected: // Write default value if available. if (!is_starred && arg_index < rec->args.size() && rec->args[arg_index].descr) { signature += " = "; - signature += rec->args[arg_index].descr; + signature += detail::replace_newlines_and_squash(rec->args[arg_index].descr); } // Separator for positional-only arguments (placed after the // argument, rather than before like * diff --git a/tests/test_eigen_matrix.cpp b/tests/test_eigen_matrix.cpp index 554cc4d7f..0c003d05e 100644 --- a/tests/test_eigen_matrix.cpp +++ b/tests/test_eigen_matrix.cpp @@ -330,6 +330,23 @@ TEST_SUBMODULE(eigen_matrix, m) { m.def("dense_c", [mat]() -> DenseMatrixC { return DenseMatrixC(mat); }); m.def("dense_copy_r", [](const DenseMatrixR &m) -> DenseMatrixR { return m; }); m.def("dense_copy_c", [](const DenseMatrixC &m) -> DenseMatrixC { return m; }); + // test_defaults + bool have_numpy = true; + try { + py::module_::import("numpy"); + } catch (const py::error_already_set &) { + have_numpy = false; + } + if (have_numpy) { + py::module_::import("numpy"); + Eigen::Matrix defaultMatrix = Eigen::Matrix3d::Identity(); + m.def( + "defaults_mat", [](const Eigen::Matrix3d &) {}, py::arg("mat") = defaultMatrix); + + Eigen::VectorXd defaultVector = Eigen::VectorXd::Ones(32); + m.def( + "defaults_vec", [](const Eigen::VectorXd &) {}, py::arg("vec") = defaultMatrix); + } // test_sparse, test_sparse_signature m.def("sparse_r", [mat]() -> SparseMatrixR { // NOLINTNEXTLINE(clang-analyzer-core.uninitialized.UndefReturn) diff --git a/tests/test_eigen_matrix.py b/tests/test_eigen_matrix.py index b2e76740b..a486c2f93 100644 --- a/tests/test_eigen_matrix.py +++ b/tests/test_eigen_matrix.py @@ -716,6 +716,11 @@ def test_dense_signature(doc): ) +def test_defaults(doc): + assert "\n" not in str(doc(m.defaults_mat)) + assert "\n" not in str(doc(m.defaults_vec)) + + def test_named_arguments(): a = np.array([[1.0, 2], [3, 4], [5, 6]]) b = np.ones((2, 1)) diff --git a/tests/test_kwargs_and_defaults.cpp b/tests/test_kwargs_and_defaults.cpp index 77e72c0c7..614c826da 100644 --- a/tests/test_kwargs_and_defaults.cpp +++ b/tests/test_kwargs_and_defaults.cpp @@ -42,6 +42,50 @@ TEST_SUBMODULE(kwargs_and_defaults, m) { m.def("kw_func_udl", kw_func, "x"_a, "y"_a = 300); m.def("kw_func_udl_z", kw_func, "x"_a, "y"_a = 0); + // test line breaks in default argument representation + struct CustomRepr { + std::string repr_string; + + explicit CustomRepr(const std::string &repr) : repr_string(repr) {} + + std::string __repr__() const { return repr_string; } + }; + + py::class_(m, "CustomRepr") + .def(py::init()) + .def("__repr__", &CustomRepr::__repr__); + + m.def( + "kw_lb_func0", + [](const CustomRepr &) {}, + py::arg("custom") = CustomRepr(" array([[A, B], [C, D]]) ")); + m.def( + "kw_lb_func1", + [](const CustomRepr &) {}, + py::arg("custom") = CustomRepr(" array([[A, B],\n[C, D]]) ")); + m.def( + "kw_lb_func2", + [](const CustomRepr &) {}, + py::arg("custom") = CustomRepr("\v\n array([[A, B], [C, D]])")); + m.def( + "kw_lb_func3", + [](const CustomRepr &) {}, + py::arg("custom") = CustomRepr("array([[A, B], [C, D]]) \f\n")); + m.def( + "kw_lb_func4", + [](const CustomRepr &) {}, + py::arg("custom") = CustomRepr("array([[A, B],\n\f\n[C, D]])")); + m.def( + "kw_lb_func5", + [](const CustomRepr &) {}, + py::arg("custom") = CustomRepr("array([[A, B],\r [C, D]])")); + m.def( + "kw_lb_func6", [](const CustomRepr &) {}, py::arg("custom") = CustomRepr(" \v\t ")); + m.def( + "kw_lb_func7", + [](const std::string &) {}, + py::arg("str_arg") = "First line.\n Second line."); + // test_args_and_kwargs m.def("args_function", [](py::args args) -> py::tuple { PYBIND11_WARNING_PUSH diff --git a/tests/test_kwargs_and_defaults.py b/tests/test_kwargs_and_defaults.py index 7174726fc..b57700197 100644 --- a/tests/test_kwargs_and_defaults.py +++ b/tests/test_kwargs_and_defaults.py @@ -23,6 +23,38 @@ def test_function_signatures(doc): doc(m.KWClass.foo1) == "foo1(self: m.kwargs_and_defaults.KWClass, x: int, y: float) -> None" ) + assert ( + doc(m.kw_lb_func0) + == "kw_lb_func0(custom: m.kwargs_and_defaults.CustomRepr = array([[A, B], [C, D]])) -> None" + ) + assert ( + doc(m.kw_lb_func1) + == "kw_lb_func1(custom: m.kwargs_and_defaults.CustomRepr = array([[A, B], [C, D]])) -> None" + ) + assert ( + doc(m.kw_lb_func2) + == "kw_lb_func2(custom: m.kwargs_and_defaults.CustomRepr = array([[A, B], [C, D]])) -> None" + ) + assert ( + doc(m.kw_lb_func3) + == "kw_lb_func3(custom: m.kwargs_and_defaults.CustomRepr = array([[A, B], [C, D]])) -> None" + ) + assert ( + doc(m.kw_lb_func4) + == "kw_lb_func4(custom: m.kwargs_and_defaults.CustomRepr = array([[A, B], [C, D]])) -> None" + ) + assert ( + doc(m.kw_lb_func5) + == "kw_lb_func5(custom: m.kwargs_and_defaults.CustomRepr = array([[A, B], [C, D]])) -> None" + ) + assert ( + doc(m.kw_lb_func6) + == "kw_lb_func6(custom: m.kwargs_and_defaults.CustomRepr = ) -> None" + ) + assert ( + doc(m.kw_lb_func7) + == "kw_lb_func7(str_arg: str = 'First line.\\n Second line.') -> None" + ) def test_named_arguments():