From 2a78abffd85904849a1c9f9c6705f2d5a0d27018 Mon Sep 17 00:00:00 2001 From: Jeremy Maitin-Shepard Date: Thu, 23 Sep 2021 10:36:25 -0700 Subject: [PATCH 1/5] Ensure PYBIND11_TLS_REPLACE_VALUE evaluates its arguments only once (#3290) --- include/pybind11/detail/internals.h | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index 7c2e49978..98d21eb98 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -84,12 +84,13 @@ inline PyObject *make_object_base_type(PyTypeObject *metaclass); // On CPython < 3.4 and on PyPy, `PyThread_set_key_value` strangely does not set // the value if it has already been set. Instead, it must first be deleted and // then set again. +inline void tls_replace_value(PYBIND11_TLS_KEY_REF key, void *value) { + PyThread_delete_key_value(key); + PyThread_set_key_value(key, value); +} # define PYBIND11_TLS_DELETE_VALUE(key) PyThread_delete_key_value(key) # define PYBIND11_TLS_REPLACE_VALUE(key, value) \ - do { \ - PyThread_delete_key_value((key)); \ - PyThread_set_key_value((key), (value)); \ - } while (false) + ::pybind11::detail::tls_replace_value((key), (value)) # else # define PYBIND11_TLS_DELETE_VALUE(key) PyThread_set_key_value((key), nullptr) # define PYBIND11_TLS_REPLACE_VALUE(key, value) PyThread_set_key_value((key), (value)) From 21282e645a638600be9bf44dd535dbae5416005e Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Thu, 23 Sep 2021 15:06:07 -0400 Subject: [PATCH 2/5] feat: reapply fixed version of #3271 (#3293) * Add make_value_iterator (#3271) * Add make_value_iterator This is the counterpart to make_key_iterator, and will allow implementing a `value` method in `bind_map` (although doing so is left for a subsequent PR). I made a few design changes to reduce copy-and-paste boilerplate. Previously detail::iterator_state had a boolean template parameter to indicate whether it was being used for make_iterator or make_key_iterator. I replaced the boolean with a class that determines how to dereference the iterator. This allows for a generic implementation of `__next__`. I also added the ValueType and Extra... parameters to the iterator_state template args, because I think it was a bug that they were missing: if make_iterator is called twice with different values of these, only the first set has effect (because the state class is only registered once). There is still a potential issue in that the *values* of the extra arguments are latched on the first call, but since most policies are empty classes this should be even less common. * Add some remove_cv_t to appease clang-tidy * Make iterator_access and friends take reference For some reason I'd accidentally made it take a const value, which caused some issues with third-party packages. * Another attempt to remove remove_cv_t from iterators Some of the return types were const (non-reference) types because of the pecularities of decltype: `decltype((*it).first)` is the *declared* type of the member of the pair, rather than the type of the expression. So if the reference type of the iterator is `pair &`, then the decltype is `const int`. Wrapping an extra set of parentheses to form `decltype(((*it).first))` would instead give `const int &`. This means that the existing make_key_iterator actually returns by value from `__next__`, rather than by reference. Since for mapping types, keys are always const, this probably hasn't been noticed, but it will affect make_value_iterator if the Python code tries to mutate the returned objects. I've changed things to use double parentheses so that make_iterator, make_key_iterator and make_value_iterator should now all return the reference type of the iterator. I'll still need to add a test for that; for now I'm just checking whether I can keep Clang-Tidy happy. * Add back some NOLINTNEXTLINE to appease Clang-Tidy This is favoured over using remove_cv_t because in some cases a const value return type is deliberate (particularly for Eigen). * Add a unit test for iterator referencing Ensure that make_iterator, make_key_iterator and make_value_iterator return references to the container elements, rather than copies. The test for make_key_iterator fails to compile on master, which gives me confidence that this branch has fixed it. * Make the iterator_access etc operator() const I'm actually a little surprised it compiled at all given that the operator() is called on a temporary, but I don't claim to fully understand all the different value types in C++11. * Attempt to work around compiler bugs https://godbolt.org/ shows an example where ICC gets the wrong result for a decltype used as the default for a template argument, and CI also showed problems with PGI. This is a shot in the dark to see if it fixes things. * Make a test constructor explicit (Clang-Tidy) * Fix unit test on GCC 4.8.5 It seems to require the arguments to the std::pair constructor to be implicitly convertible to the types in the pair, rather than just requiring is_constructible. * Remove DOXYGEN_SHOULD_SKIP_THIS guards Now that a complex decltype expression has been replaced by a simpler nested type, I'm hoping Doxygen will be able to build it without issues. * Add comment to explain iterator_state template params * fix: regression in #3271 Co-authored-by: Bruce Merry <1963944+bmerry@users.noreply.github.com> --- docs/reference.rst | 3 + include/pybind11/pybind11.h | 120 ++++++++++++++++++------- tests/test_sequences_and_iterators.cpp | 77 +++++++++++++++- tests/test_sequences_and_iterators.py | 30 +++++++ 4 files changed, 195 insertions(+), 35 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index a678d41c8..e64a03519 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -63,6 +63,9 @@ Convenience functions converting to Python types .. doxygenfunction:: make_key_iterator(Iterator, Sentinel, Extra &&...) .. doxygenfunction:: make_key_iterator(Type &, Extra&&...) +.. doxygenfunction:: make_value_iterator(Iterator, Sentinel, Extra &&...) +.. doxygenfunction:: make_value_iterator(Type &, Extra&&...) + .. _extras: Passing extra arguments to ``def`` or ``class_`` diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index b8f5a6bae..16535f195 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -1955,25 +1955,54 @@ inline std::pair all_t return res; } -template +/* There are a large number of apparently unused template arguments because + * each combination requires a separate py::class_ registration. + */ +template struct iterator_state { Iterator it; Sentinel end; bool first_or_done; }; -PYBIND11_NAMESPACE_END(detail) +// Note: these helpers take the iterator by non-const reference because some +// iterators in the wild can't be dereferenced when const. C++ needs the extra parens in decltype +// to enforce an lvalue. The & after Iterator is required for MSVC < 16.9. SFINAE cannot be +// reused for result_type due to bugs in ICC, NVCC, and PGI compilers. See PR #3293. +template ()))> +struct iterator_access { + using result_type = decltype((*std::declval())); + // NOLINTNEXTLINE(readability-const-return-type) // PR #3263 + result_type operator()(Iterator &it) const { + return *it; + } +}; -/// Makes a python iterator from a first and past-the-end C++ InputIterator. -template ()).first)) > +struct iterator_key_access { + using result_type = decltype(((*std::declval()).first)); + result_type operator()(Iterator &it) const { + return (*it).first; + } +}; + +template ()).second))> +struct iterator_value_access { + using result_type = decltype(((*std::declval()).second)); + result_type operator()(Iterator &it) const { + return (*it).second; + } +}; + +template ()), -#endif + typename ValueType, typename... Extra> -iterator make_iterator(Iterator first, Sentinel last, Extra &&... extra) { - using state = detail::iterator_state; +iterator make_iterator_impl(Iterator first, Sentinel last, Extra &&... extra) { + using state = detail::iterator_state; + // TODO: state captures only the types of Extra, not the values if (!detail::get_type_info(typeid(state), false)) { class_(handle(), "iterator", pybind11::module_local()) @@ -1987,7 +2016,7 @@ iterator make_iterator(Iterator first, Sentinel last, Extra &&... extra) { s.first_or_done = true; throw stop_iteration(); } - return *s.it; + return Access()(s.it); // NOLINTNEXTLINE(readability-const-return-type) // PR #3263 }, std::forward(extra)..., Policy); } @@ -1995,35 +2024,55 @@ iterator make_iterator(Iterator first, Sentinel last, Extra &&... extra) { return cast(state{first, last, true}); } -/// Makes an python iterator over the keys (`.first`) of a iterator over pairs from a +PYBIND11_NAMESPACE_END(detail) + +/// Makes a python iterator from a first and past-the-end C++ InputIterator. +template ::result_type, + typename... Extra> +iterator make_iterator(Iterator first, Sentinel last, Extra &&... extra) { + return detail::make_iterator_impl< + detail::iterator_access, + Policy, + Iterator, + Sentinel, + ValueType, + Extra...>(first, last, std::forward(extra)...); +} + +/// Makes a python iterator over the keys (`.first`) of a iterator over pairs from a /// first and past-the-end InputIterator. template ()).first), -#endif + typename KeyType = typename detail::iterator_key_access::result_type, typename... Extra> iterator make_key_iterator(Iterator first, Sentinel last, Extra &&...extra) { - using state = detail::iterator_state; + return detail::make_iterator_impl< + detail::iterator_key_access, + Policy, + Iterator, + Sentinel, + KeyType, + Extra...>(first, last, std::forward(extra)...); +} - if (!detail::get_type_info(typeid(state), false)) { - class_(handle(), "iterator", pybind11::module_local()) - .def("__iter__", [](state &s) -> state& { return s; }) - .def("__next__", [](state &s) -> detail::remove_cv_t { - if (!s.first_or_done) - ++s.it; - else - s.first_or_done = false; - if (s.it == s.end) { - s.first_or_done = true; - throw stop_iteration(); - } - return (*s.it).first; - }, std::forward(extra)..., Policy); - } - - return cast(state{first, last, true}); +/// Makes a python iterator over the values (`.second`) of a iterator over pairs from a +/// first and past-the-end InputIterator. +template ::result_type, + typename... Extra> +iterator make_value_iterator(Iterator first, Sentinel last, Extra &&...extra) { + return detail::make_iterator_impl< + detail::iterator_value_access, + Policy, Iterator, + Sentinel, + ValueType, + Extra...>(first, last, std::forward(extra)...); } /// Makes an iterator over values of an stl container or other container supporting @@ -2040,6 +2089,13 @@ template (std::begin(value), std::end(value), extra...); } +/// Makes an iterator over the values (`.second`) of a stl map-like container supporting +/// `std::begin()`/`std::end()` +template iterator make_value_iterator(Type &value, Extra&&... extra) { + return make_value_iterator(std::begin(value), std::end(value), extra...); +} + template void implicitly_convertible() { struct set_flag { bool &flag; diff --git a/tests/test_sequences_and_iterators.cpp b/tests/test_sequences_and_iterators.cpp index 16f8f5b23..9de69338b 100644 --- a/tests/test_sequences_and_iterators.cpp +++ b/tests/test_sequences_and_iterators.cpp @@ -15,6 +15,7 @@ #include #include +#include #ifdef PYBIND11_HAS_OPTIONAL #include @@ -37,6 +38,29 @@ bool operator==(const NonZeroIterator>& it, const NonZeroSentine return !(*it).first || !(*it).second; } +class NonCopyableInt { +public: + explicit NonCopyableInt(int value) : value_(value) {} + NonCopyableInt(const NonCopyableInt &) = delete; + NonCopyableInt(NonCopyableInt &&other) noexcept : value_(other.value_) { + other.value_ = -1; // detect when an unwanted move occurs + } + NonCopyableInt &operator=(const NonCopyableInt &) = delete; + NonCopyableInt &operator=(NonCopyableInt &&other) noexcept { + value_ = other.value_; + other.value_ = -1; // detect when an unwanted move occurs + return *this; + } + int get() const { return value_; } + void set(int value) { value_ = value; } + ~NonCopyableInt() = default; +private: + int value_; +}; +using NonCopyableIntPair = std::pair; +PYBIND11_MAKE_OPAQUE(std::vector); +PYBIND11_MAKE_OPAQUE(std::vector); + template py::list test_random_access_iterator(PythonType x) { if (x.size() < 5) @@ -288,6 +312,10 @@ TEST_SUBMODULE(sequences_and_iterators, m) { .def( "items", [](const StringMap &map) { return py::make_iterator(map.begin(), map.end()); }, + py::keep_alive<0, 1>()) + .def( + "values", + [](const StringMap &map) { return py::make_value_iterator(map.begin(), map.end()); }, py::keep_alive<0, 1>()); // test_generalized_iterators @@ -308,19 +336,62 @@ TEST_SUBMODULE(sequences_and_iterators, m) { .def("nonzero_keys", [](const IntPairs& s) { return py::make_key_iterator(NonZeroIterator>(s.begin()), NonZeroSentinel()); }, py::keep_alive<0, 1>()) + .def("nonzero_values", [](const IntPairs& s) { + return py::make_value_iterator(NonZeroIterator>(s.begin()), NonZeroSentinel()); + }, py::keep_alive<0, 1>()) + + // test single-argument make_iterator .def("simple_iterator", [](IntPairs& self) { return py::make_iterator(self); }, py::keep_alive<0, 1>()) .def("simple_keys", [](IntPairs& self) { return py::make_key_iterator(self); }, py::keep_alive<0, 1>()) + .def("simple_values", [](IntPairs& self) { + return py::make_value_iterator(self); + }, py::keep_alive<0, 1>()) - // test iterator with keep_alive (doesn't work so not used at runtime, but tests compile) - .def("make_iterator_keep_alive", [](IntPairs& self) { - return py::make_iterator(self, py::keep_alive<0, 1>()); + // Test iterator with an Extra (doesn't do anything useful, so not used + // at runtime, but tests need to be able to compile with the correct + // overload. See PR #3293. + .def("_make_iterator_extras", [](IntPairs& self) { + return py::make_iterator(self, py::call_guard()); + }, py::keep_alive<0, 1>()) + .def("_make_key_extras", [](IntPairs& self) { + return py::make_key_iterator(self, py::call_guard()); + }, py::keep_alive<0, 1>()) + .def("_make_value_extras", [](IntPairs& self) { + return py::make_value_iterator(self, py::call_guard()); }, py::keep_alive<0, 1>()) ; + // test_iterater_referencing + py::class_(m, "NonCopyableInt") + .def(py::init()) + .def("set", &NonCopyableInt::set) + .def("__int__", &NonCopyableInt::get) + ; + py::class_>(m, "VectorNonCopyableInt") + .def(py::init<>()) + .def("append", [](std::vector &vec, int value) { + vec.emplace_back(value); + }) + .def("__iter__", [](std::vector &vec) { + return py::make_iterator(vec.begin(), vec.end()); + }) + ; + py::class_>(m, "VectorNonCopyableIntPair") + .def(py::init<>()) + .def("append", [](std::vector &vec, const std::pair &value) { + vec.emplace_back(NonCopyableInt(value.first), NonCopyableInt(value.second)); + }) + .def("keys", [](std::vector &vec) { + return py::make_key_iterator(vec.begin(), vec.end()); + }) + .def("values", [](std::vector &vec) { + return py::make_value_iterator(vec.begin(), vec.end()); + }) + ; #if 0 // Obsolete: special data structure for exposing custom iterator types to python diff --git a/tests/test_sequences_and_iterators.py b/tests/test_sequences_and_iterators.py index 902f4914c..38e2ab5b7 100644 --- a/tests/test_sequences_and_iterators.py +++ b/tests/test_sequences_and_iterators.py @@ -36,6 +36,10 @@ def test_generalized_iterators(): assert list(m.IntPairs([(1, 2), (2, 0), (0, 3), (4, 5)]).nonzero_keys()) == [1] assert list(m.IntPairs([(0, 3), (1, 2), (3, 4)]).nonzero_keys()) == [] + assert list(m.IntPairs([(1, 2), (3, 4), (0, 5)]).nonzero_values()) == [2, 4] + assert list(m.IntPairs([(1, 2), (2, 0), (0, 3), (4, 5)]).nonzero_values()) == [2] + assert list(m.IntPairs([(0, 3), (1, 2), (3, 4)]).nonzero_values()) == [] + # __next__ must continue to raise StopIteration it = m.IntPairs([(0, 0)]).nonzero() for _ in range(3): @@ -55,6 +59,31 @@ def test_generalized_iterators_simple(): (0, 5), ] assert list(m.IntPairs([(1, 2), (3, 4), (0, 5)]).simple_keys()) == [1, 3, 0] + assert list(m.IntPairs([(1, 2), (3, 4), (0, 5)]).simple_values()) == [2, 4, 5] + + +def test_iterator_referencing(): + """Test that iterators reference rather than copy their referents.""" + vec = m.VectorNonCopyableInt() + vec.append(3) + vec.append(5) + assert [int(x) for x in vec] == [3, 5] + # Increment everything to make sure the referents can be mutated + for x in vec: + x.set(int(x) + 1) + assert [int(x) for x in vec] == [4, 6] + + vec = m.VectorNonCopyableIntPair() + vec.append([3, 4]) + vec.append([5, 7]) + assert [int(x) for x in vec.keys()] == [3, 5] + assert [int(x) for x in vec.values()] == [4, 7] + for x in vec.keys(): + x.set(int(x) + 1) + for x in vec.values(): + x.set(int(x) + 10) + assert [int(x) for x in vec.keys()] == [4, 6] + assert [int(x) for x in vec.values()] == [14, 17] def test_sliceable(): @@ -160,6 +189,7 @@ def test_map_iterator(): assert sm[k] == expected[k] for k, v in sm.items(): assert v == expected[k] + assert list(sm.values()) == [expected[k] for k in sm] it = iter(m.StringMap({})) for _ in range(3): # __next__ must continue to raise StopIteration From 6ad3f874a797ed554b2fea82dd2b798826a83fa8 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Thu, 23 Sep 2021 15:42:16 -0400 Subject: [PATCH 3/5] fix(build): avoid a possible warning about shadowed variables and changing behaviors (#3220) --- tools/pybind11Tools.cmake | 42 +++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/tools/pybind11Tools.cmake b/tools/pybind11Tools.cmake index 323135399..cc5ca21ca 100644 --- a/tools/pybind11Tools.cmake +++ b/tools/pybind11Tools.cmake @@ -45,31 +45,25 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}") find_package(PythonLibsNew ${PYBIND11_PYTHON_VERSION} MODULE REQUIRED ${_pybind11_quiet}) list(REMOVE_AT CMAKE_MODULE_PATH -1) +# Makes a normal variable a cached variable +macro(_PYBIND11_PROMOTE_TO_CACHE NAME) + set(_tmp_ptc "${${NAME}}") + # CMake 3.21 complains if a cached variable is shadowed by a normal one + unset(${NAME}) + set(${NAME} + "${_tmp_ptc}" + CACHE INTERNAL "") +endmacro() + # Cache variables so pybind11_add_module can be used in parent projects -set(PYTHON_INCLUDE_DIRS - ${PYTHON_INCLUDE_DIRS} - CACHE INTERNAL "") -set(PYTHON_LIBRARIES - ${PYTHON_LIBRARIES} - CACHE INTERNAL "") -set(PYTHON_MODULE_PREFIX - ${PYTHON_MODULE_PREFIX} - CACHE INTERNAL "") -set(PYTHON_MODULE_EXTENSION - ${PYTHON_MODULE_EXTENSION} - CACHE INTERNAL "") -set(PYTHON_VERSION_MAJOR - ${PYTHON_VERSION_MAJOR} - CACHE INTERNAL "") -set(PYTHON_VERSION_MINOR - ${PYTHON_VERSION_MINOR} - CACHE INTERNAL "") -set(PYTHON_VERSION - ${PYTHON_VERSION} - CACHE INTERNAL "") -set(PYTHON_IS_DEBUG - "${PYTHON_IS_DEBUG}" - CACHE INTERNAL "") +_pybind11_promote_to_cache(PYTHON_INCLUDE_DIRS) +_pybind11_promote_to_cache(PYTHON_LIBRARIES) +_pybind11_promote_to_cache(PYTHON_MODULE_PREFIX) +_pybind11_promote_to_cache(PYTHON_MODULE_EXTENSION) +_pybind11_promote_to_cache(PYTHON_VERSION_MAJOR) +_pybind11_promote_to_cache(PYTHON_VERSION_MINOR) +_pybind11_promote_to_cache(PYTHON_VERSION) +_pybind11_promote_to_cache(PYTHON_IS_DEBUG) if(PYBIND11_MASTER_PROJECT) if(PYTHON_MODULE_EXTENSION MATCHES "pypy") From 409be8336f4c597bcca9d9eaa09df0abc065afff Mon Sep 17 00:00:00 2001 From: Wenzel Jakob Date: Fri, 24 Sep 2021 13:03:57 +0200 Subject: [PATCH 4/5] CMake: react to python version changes The new FindPython-based variant of the CMake scripts caches information about the chosen Python version that can become stale. For example, suppose I configure a simple pybind11-based project as follows ``` cmake -S . -B build -GNinja -DPython_ROOT= ``` which will generate `my_extension.cpython-38-x86_64-linux-gnu.so`. A subsequent change to the python version like ``` cmake -S . -B build -GNinja -DPython_ROOT= ``` does not update all necessary build system information. In particular, the compiled file is still called `my_extension.cpython-38-x86_64-linux-gnu.so`. This commit fixes the problem by detecting changes in `Python_EXECUTABLE` and re-running Python as needed. Note that the previous way of detecting Python does not seem to be affected, it always specifies the right suffix. --- tools/pybind11NewTools.cmake | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tools/pybind11NewTools.cmake b/tools/pybind11NewTools.cmake index a20803bc7..b93f9145e 100644 --- a/tools/pybind11NewTools.cmake +++ b/tools/pybind11NewTools.cmake @@ -82,6 +82,15 @@ if(NOT DEFINED ${_Python}_EXECUTABLE) endif() +if(NOT ${_Python}_EXECUTABLE STREQUAL PYTHON_EXECUTABLE_LAST) + # Detect changes to the Python version/binary in subsequent CMake runs, and refresh config if needed + unset(PYTHON_IS_DEBUG CACHE) + unset(PYTHON_MODULE_EXTENSION CACHE) + set(PYTHON_EXECUTABLE_LAST + "${${_Python}_EXECUTABLE}" + CACHE INTERNAL "Python executable during the last CMake run") +endif() + if(NOT DEFINED PYTHON_IS_DEBUG) # Debug check - see https://stackoverflow.com/questions/646518/python-how-to-detect-debug-Interpreter execute_process( From 62c4909cce37829303329da77e427d9d3a907d3a Mon Sep 17 00:00:00 2001 From: Jeremy Maitin-Shepard Date: Fri, 24 Sep 2021 12:08:22 -0700 Subject: [PATCH 5/5] Add `custom_type_setup` attribute (#3287) * Fix `pybind11::object::operator=` to be safe if `*this` is accessible from Python * Add `custom_type_setup` attribute This allows for custom modifications to the PyHeapTypeObject prior to calling `PyType_Ready`. This may be used, for example, to define `tp_traverse` and `tp_clear` functions. --- docs/advanced/classes.rst | 34 ++++++++++++++++++++++ include/pybind11/attr.h | 29 ++++++++++++++++++ include/pybind11/detail/class.h | 6 ++-- include/pybind11/pytypes.h | 5 +++- tests/CMakeLists.txt | 1 + tests/test_custom_type_setup.cpp | 41 ++++++++++++++++++++++++++ tests/test_custom_type_setup.py | 50 ++++++++++++++++++++++++++++++++ 7 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 tests/test_custom_type_setup.cpp create mode 100644 tests/test_custom_type_setup.py diff --git a/docs/advanced/classes.rst b/docs/advanced/classes.rst index 7f8fcdf4b..5f01a2f11 100644 --- a/docs/advanced/classes.rst +++ b/docs/advanced/classes.rst @@ -1261,3 +1261,37 @@ object, just like ``type(ob)`` in Python. Other types, like ``py::type::of()``, do not work, see :ref:`type-conversions`. .. versionadded:: 2.6 + +Custom type setup +================= + +For advanced use cases, such as enabling garbage collection support, you may +wish to directly manipulate the `PyHeapTypeObject` corresponding to a +``py::class_`` definition. + +You can do that using ``py::custom_type_setup``: + +.. code-block:: cpp + + struct OwnsPythonObjects { + py::object value = py::none(); + }; + py::class_ cls( + m, "OwnsPythonObjects", py::custom_type_setup([](PyHeapTypeObject *heap_type) { + auto *type = &heap_type->ht_type; + type->tp_flags |= Py_TPFLAGS_HAVE_GC; + type->tp_traverse = [](PyObject *self_base, visitproc visit, void *arg) { + auto &self = py::cast(py::handle(self_base)); + Py_VISIT(self.value.ptr()); + return 0; + }; + type->tp_clear = [](PyObject *self_base) { + auto &self = py::cast(py::handle(self_base)); + self.value = py::none(); + return 0; + }; + })); + cls.def(py::init<>()); + cls.def_readwrite("value", &OwnsPythonObjects::value); + +.. versionadded:: 2.8 diff --git a/include/pybind11/attr.h b/include/pybind11/attr.h index 13f68bbe8..0dedbc08d 100644 --- a/include/pybind11/attr.h +++ b/include/pybind11/attr.h @@ -12,6 +12,8 @@ #include "cast.h" +#include + PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) /// \addtogroup annotations @@ -79,6 +81,23 @@ struct metaclass { explicit metaclass(handle value) : value(value) { } }; +/// Specifies a custom callback with signature `void (PyHeapTypeObject*)` that +/// may be used to customize the Python type. +/// +/// The callback is invoked immediately before `PyType_Ready`. +/// +/// Note: This is an advanced interface, and uses of it may require changes to +/// work with later versions of pybind11. You may wish to consult the +/// implementation of `make_new_python_type` in `detail/classes.h` to understand +/// the context in which the callback will be run. +struct custom_type_setup { + using callback = std::function; + + explicit custom_type_setup(callback value) : value(std::move(value)) {} + + callback value; +}; + /// Annotation that marks a class as local to the module: struct module_local { const bool value; constexpr explicit module_local(bool v = true) : value(v) {} @@ -272,6 +291,9 @@ struct type_record { /// Custom metaclass (optional) handle metaclass; + /// Custom type setup. + custom_type_setup::callback custom_type_setup_callback; + /// Multiple inheritance marker bool multiple_inheritance : 1; @@ -476,6 +498,13 @@ struct process_attribute : process_attribute_default static void init(const dynamic_attr &, type_record *r) { r->dynamic_attr = true; } }; +template <> +struct process_attribute { + static void init(const custom_type_setup &value, type_record *r) { + r->custom_type_setup_callback = value.value; + } +}; + template <> struct process_attribute : process_attribute_default { static void init(const is_final &, type_record *r) { r->is_final = true; } diff --git a/include/pybind11/detail/class.h b/include/pybind11/detail/class.h index f9822c7b3..b9376b4c0 100644 --- a/include/pybind11/detail/class.h +++ b/include/pybind11/detail/class.h @@ -683,11 +683,13 @@ inline PyObject* make_new_python_type(const type_record &rec) { if (rec.buffer_protocol) enable_buffer_protocol(heap_type); + if (rec.custom_type_setup_callback) + rec.custom_type_setup_callback(heap_type); + if (PyType_Ready(type) < 0) pybind11_fail(std::string(rec.name) + ": PyType_Ready failed (" + error_string() + ")!"); - assert(rec.dynamic_attr ? PyType_HasFeature(type, Py_TPFLAGS_HAVE_GC) - : !PyType_HasFeature(type, Py_TPFLAGS_HAVE_GC)); + assert(!rec.dynamic_attr || PyType_HasFeature(type, Py_TPFLAGS_HAVE_GC)); /* Register type with the parent scope */ if (rec.scope) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 383663b5e..f54d5fad6 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -259,8 +259,11 @@ public: object& operator=(const object &other) { other.inc_ref(); - dec_ref(); + // Use temporary variable to ensure `*this` remains valid while + // `Py_XDECREF` executes, in case `*this` is accessible from Python. + handle temp(m_ptr); m_ptr = other.m_ptr; + temp.dec_ref(); return *this; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f014771d5..70303879f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -104,6 +104,7 @@ set(PYBIND11_TEST_FILES test_constants_and_functions.cpp test_copy_move.cpp test_custom_type_casters.cpp + test_custom_type_setup.cpp test_docstring_options.cpp test_eigen.cpp test_enum.cpp diff --git a/tests/test_custom_type_setup.cpp b/tests/test_custom_type_setup.cpp new file mode 100644 index 000000000..42fae05d5 --- /dev/null +++ b/tests/test_custom_type_setup.cpp @@ -0,0 +1,41 @@ +/* + tests/test_custom_type_setup.cpp -- Tests `pybind11::custom_type_setup` + + Copyright (c) Google LLC + + 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" + +namespace py = pybind11; + +namespace { + +struct OwnsPythonObjects { + py::object value = py::none(); +}; +} // namespace + +TEST_SUBMODULE(custom_type_setup, m) { + py::class_ cls( + m, "OwnsPythonObjects", py::custom_type_setup([](PyHeapTypeObject *heap_type) { + auto *type = &heap_type->ht_type; + type->tp_flags |= Py_TPFLAGS_HAVE_GC; + type->tp_traverse = [](PyObject *self_base, visitproc visit, void *arg) { + auto &self = py::cast(py::handle(self_base)); + Py_VISIT(self.value.ptr()); + return 0; + }; + type->tp_clear = [](PyObject *self_base) { + auto &self = py::cast(py::handle(self_base)); + self.value = py::none(); + return 0; + }; + })); + cls.def(py::init<>()); + cls.def_readwrite("value", &OwnsPythonObjects::value); +} diff --git a/tests/test_custom_type_setup.py b/tests/test_custom_type_setup.py new file mode 100644 index 000000000..ef96f0814 --- /dev/null +++ b/tests/test_custom_type_setup.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +import gc +import weakref + +import pytest + +import env # noqa: F401 +from pybind11_tests import custom_type_setup as m + + +@pytest.fixture +def gc_tester(): + """Tests that an object is garbage collected. + + Assumes that any unreferenced objects are fully collected after calling + `gc.collect()`. That is true on CPython, but does not appear to reliably + hold on PyPy. + """ + + weak_refs = [] + + def add_ref(obj): + # PyPy does not support `gc.is_tracked`. + if hasattr(gc, "is_tracked"): + assert gc.is_tracked(obj) + weak_refs.append(weakref.ref(obj)) + + yield add_ref + + gc.collect() + for ref in weak_refs: + assert ref() is None + + +# PyPy does not seem to reliably garbage collect. +@pytest.mark.skipif("env.PYPY") +def test_self_cycle(gc_tester): + obj = m.OwnsPythonObjects() + obj.value = obj + gc_tester(obj) + + +# PyPy does not seem to reliably garbage collect. +@pytest.mark.skipif("env.PYPY") +def test_indirect_cycle(gc_tester): + obj = m.OwnsPythonObjects() + obj_list = [obj] + obj.value = obj_list + gc_tester(obj)