mirror of
https://github.com/pybind/pybind11.git
synced 2024-11-22 05:05:11 +00:00
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.
This commit is contained in:
parent
409be8336f
commit
62c4909cce
@ -1261,3 +1261,37 @@ object, just like ``type(ob)`` in Python.
|
|||||||
Other types, like ``py::type::of<int>()``, do not work, see :ref:`type-conversions`.
|
Other types, like ``py::type::of<int>()``, do not work, see :ref:`type-conversions`.
|
||||||
|
|
||||||
.. versionadded:: 2.6
|
.. 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_<OwnsPythonObjects> 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<OwnsPythonObjects&>(py::handle(self_base));
|
||||||
|
Py_VISIT(self.value.ptr());
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
type->tp_clear = [](PyObject *self_base) {
|
||||||
|
auto &self = py::cast<OwnsPythonObjects&>(py::handle(self_base));
|
||||||
|
self.value = py::none();
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
cls.def(py::init<>());
|
||||||
|
cls.def_readwrite("value", &OwnsPythonObjects::value);
|
||||||
|
|
||||||
|
.. versionadded:: 2.8
|
||||||
|
@ -12,6 +12,8 @@
|
|||||||
|
|
||||||
#include "cast.h"
|
#include "cast.h"
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
|
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
|
||||||
|
|
||||||
/// \addtogroup annotations
|
/// \addtogroup annotations
|
||||||
@ -79,6 +81,23 @@ struct metaclass {
|
|||||||
explicit metaclass(handle value) : value(value) { }
|
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<void(PyHeapTypeObject *heap_type)>;
|
||||||
|
|
||||||
|
explicit custom_type_setup(callback value) : value(std::move(value)) {}
|
||||||
|
|
||||||
|
callback value;
|
||||||
|
};
|
||||||
|
|
||||||
/// Annotation that marks a class as local to the module:
|
/// Annotation that marks a class as local to the module:
|
||||||
struct module_local { const bool value;
|
struct module_local { const bool value;
|
||||||
constexpr explicit module_local(bool v = true) : value(v) {}
|
constexpr explicit module_local(bool v = true) : value(v) {}
|
||||||
@ -272,6 +291,9 @@ struct type_record {
|
|||||||
/// Custom metaclass (optional)
|
/// Custom metaclass (optional)
|
||||||
handle metaclass;
|
handle metaclass;
|
||||||
|
|
||||||
|
/// Custom type setup.
|
||||||
|
custom_type_setup::callback custom_type_setup_callback;
|
||||||
|
|
||||||
/// Multiple inheritance marker
|
/// Multiple inheritance marker
|
||||||
bool multiple_inheritance : 1;
|
bool multiple_inheritance : 1;
|
||||||
|
|
||||||
@ -476,6 +498,13 @@ struct process_attribute<dynamic_attr> : process_attribute_default<dynamic_attr>
|
|||||||
static void init(const dynamic_attr &, type_record *r) { r->dynamic_attr = true; }
|
static void init(const dynamic_attr &, type_record *r) { r->dynamic_attr = true; }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
template <>
|
||||||
|
struct process_attribute<custom_type_setup> {
|
||||||
|
static void init(const custom_type_setup &value, type_record *r) {
|
||||||
|
r->custom_type_setup_callback = value.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
template <>
|
template <>
|
||||||
struct process_attribute<is_final> : process_attribute_default<is_final> {
|
struct process_attribute<is_final> : process_attribute_default<is_final> {
|
||||||
static void init(const is_final &, type_record *r) { r->is_final = true; }
|
static void init(const is_final &, type_record *r) { r->is_final = true; }
|
||||||
|
@ -683,11 +683,13 @@ inline PyObject* make_new_python_type(const type_record &rec) {
|
|||||||
if (rec.buffer_protocol)
|
if (rec.buffer_protocol)
|
||||||
enable_buffer_protocol(heap_type);
|
enable_buffer_protocol(heap_type);
|
||||||
|
|
||||||
|
if (rec.custom_type_setup_callback)
|
||||||
|
rec.custom_type_setup_callback(heap_type);
|
||||||
|
|
||||||
if (PyType_Ready(type) < 0)
|
if (PyType_Ready(type) < 0)
|
||||||
pybind11_fail(std::string(rec.name) + ": PyType_Ready failed (" + error_string() + ")!");
|
pybind11_fail(std::string(rec.name) + ": PyType_Ready failed (" + error_string() + ")!");
|
||||||
|
|
||||||
assert(rec.dynamic_attr ? PyType_HasFeature(type, Py_TPFLAGS_HAVE_GC)
|
assert(!rec.dynamic_attr || PyType_HasFeature(type, Py_TPFLAGS_HAVE_GC));
|
||||||
: !PyType_HasFeature(type, Py_TPFLAGS_HAVE_GC));
|
|
||||||
|
|
||||||
/* Register type with the parent scope */
|
/* Register type with the parent scope */
|
||||||
if (rec.scope)
|
if (rec.scope)
|
||||||
|
@ -259,8 +259,11 @@ public:
|
|||||||
|
|
||||||
object& operator=(const object &other) {
|
object& operator=(const object &other) {
|
||||||
other.inc_ref();
|
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;
|
m_ptr = other.m_ptr;
|
||||||
|
temp.dec_ref();
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,6 +104,7 @@ set(PYBIND11_TEST_FILES
|
|||||||
test_constants_and_functions.cpp
|
test_constants_and_functions.cpp
|
||||||
test_copy_move.cpp
|
test_copy_move.cpp
|
||||||
test_custom_type_casters.cpp
|
test_custom_type_casters.cpp
|
||||||
|
test_custom_type_setup.cpp
|
||||||
test_docstring_options.cpp
|
test_docstring_options.cpp
|
||||||
test_eigen.cpp
|
test_eigen.cpp
|
||||||
test_enum.cpp
|
test_enum.cpp
|
||||||
|
41
tests/test_custom_type_setup.cpp
Normal file
41
tests/test_custom_type_setup.cpp
Normal file
@ -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 <pybind11/pybind11.h>
|
||||||
|
|
||||||
|
#include "pybind11_tests.h"
|
||||||
|
|
||||||
|
namespace py = pybind11;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct OwnsPythonObjects {
|
||||||
|
py::object value = py::none();
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_SUBMODULE(custom_type_setup, m) {
|
||||||
|
py::class_<OwnsPythonObjects> 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<OwnsPythonObjects &>(py::handle(self_base));
|
||||||
|
Py_VISIT(self.value.ptr());
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
type->tp_clear = [](PyObject *self_base) {
|
||||||
|
auto &self = py::cast<OwnsPythonObjects &>(py::handle(self_base));
|
||||||
|
self.value = py::none();
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
cls.def(py::init<>());
|
||||||
|
cls.def_readwrite("value", &OwnsPythonObjects::value);
|
||||||
|
}
|
50
tests/test_custom_type_setup.py
Normal file
50
tests/test_custom_type_setup.py
Normal file
@ -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)
|
Loading…
Reference in New Issue
Block a user