Skip to content

Commit de5147b

Browse files
committed
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.
1 parent 5025e0e commit de5147b

File tree

6 files changed

+160
-2
lines changed

6 files changed

+160
-2
lines changed

docs/advanced/classes.rst

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1261,3 +1261,37 @@ object, just like ``type(ob)`` in Python.
12611261
Other types, like ``py::type::of<int>()``, do not work, see :ref:`type-conversions`.
12621262

12631263
.. versionadded:: 2.6
1264+
1265+
Custom type setup
1266+
=================
1267+
1268+
For advanced use cases, such as enabling garbage collection support, you may
1269+
wish to directly manipulate the `PyHeapTypeObject` corresponding to a
1270+
``py::class_`` definition.
1271+
1272+
You can do that using ``py::custom_type_setup``:
1273+
1274+
.. code-block:: cpp
1275+
1276+
struct OwnsPythonObjects {
1277+
py::object value = py::none();
1278+
};
1279+
py::class_<OwnsPythonObjects> cls(
1280+
m, "OwnsPythonObjects", py::custom_type_setup([](PyHeapTypeObject *heap_type) {
1281+
auto *type = &heap_type->ht_type;
1282+
type->tp_flags |= Py_TPFLAGS_HAVE_GC;
1283+
type->tp_traverse = +[](PyObject *self_base, visitproc visit, void *arg) {
1284+
auto &self = py::cast<OwnsPythonObjects&>(py::handle(self_base));
1285+
Py_VISIT(self.value.ptr());
1286+
return 0;
1287+
};
1288+
type->tp_clear = +[](PyObject *self_base) {
1289+
auto &self = py::cast<OwnsPythonObjects&>(py::handle(self_base));
1290+
self.value = py::none();
1291+
return 0;
1292+
};
1293+
}));
1294+
cls.def(py::init([] { return OwnsPythonObjects{}; }));
1295+
cls.def_readwrite("value", &OwnsPythonObjects::value);
1296+
1297+
.. versionadded:: 2.8

include/pybind11/attr.h

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212

1313
#include "cast.h"
1414

15+
// Include after pybind11 headers.
16+
#include <functional>
17+
1518
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
1619

1720
/// \addtogroup annotations
@@ -79,6 +82,23 @@ struct metaclass {
7982
explicit metaclass(handle value) : value(value) { }
8083
};
8184

85+
/// Specifies a custom callback with signature `void (PyHeapTypeObject*)` that
86+
/// may be used to customize the Python type.
87+
///
88+
/// The callback is invoked immediately before `PyType_Ready`.
89+
///
90+
/// Note: This is an advanced interface, and uses of it may require changes to
91+
/// work with later versions of pybind11. You may wish to consult the
92+
/// implementation of `make_new_python_type` in `detail/classes.h` to understand
93+
/// the context in which the callback will be run.
94+
struct custom_type_setup {
95+
using callback = std::function<void(PyHeapTypeObject *heap_type)>;
96+
97+
explicit custom_type_setup(callback value) : value(std::move(value)) {}
98+
99+
callback value;
100+
};
101+
82102
/// Annotation that marks a class as local to the module:
83103
struct module_local { const bool value;
84104
constexpr explicit module_local(bool v = true) : value(v) {}
@@ -272,6 +292,9 @@ struct type_record {
272292
/// Custom metaclass (optional)
273293
handle metaclass;
274294

295+
/// Custom type setup.
296+
custom_type_setup::callback custom_type_setup_callback;
297+
275298
/// Multiple inheritance marker
276299
bool multiple_inheritance : 1;
277300

@@ -476,6 +499,13 @@ struct process_attribute<dynamic_attr> : process_attribute_default<dynamic_attr>
476499
static void init(const dynamic_attr &, type_record *r) { r->dynamic_attr = true; }
477500
};
478501

502+
template <>
503+
struct process_attribute<custom_type_setup> {
504+
static void init(const custom_type_setup &value, type_record *r) {
505+
r->custom_type_setup_callback = value.value;
506+
}
507+
};
508+
479509
template <>
480510
struct process_attribute<is_final> : process_attribute_default<is_final> {
481511
static void init(const is_final &, type_record *r) { r->is_final = true; }

include/pybind11/detail/class.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -683,11 +683,13 @@ inline PyObject* make_new_python_type(const type_record &rec) {
683683
if (rec.buffer_protocol)
684684
enable_buffer_protocol(heap_type);
685685

686+
if (rec.custom_type_setup_callback)
687+
rec.custom_type_setup_callback(heap_type);
688+
686689
if (PyType_Ready(type) < 0)
687690
pybind11_fail(std::string(rec.name) + ": PyType_Ready failed (" + error_string() + ")!");
688691

689-
assert(rec.dynamic_attr ? PyType_HasFeature(type, Py_TPFLAGS_HAVE_GC)
690-
: !PyType_HasFeature(type, Py_TPFLAGS_HAVE_GC));
692+
assert(!rec.dynamic_attr || PyType_HasFeature(type, Py_TPFLAGS_HAVE_GC));
691693

692694
/* Register type with the parent scope */
693695
if (rec.scope)

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ set(PYBIND11_TEST_FILES
104104
test_constants_and_functions.cpp
105105
test_copy_move.cpp
106106
test_custom_type_casters.cpp
107+
test_custom_type_setup.cpp
107108
test_docstring_options.cpp
108109
test_eigen.cpp
109110
test_enum.cpp

tests/test_custom_type_setup.cpp

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
tests/test_custom_type_setup.cpp -- Tests `pybind11::custom_type_setup`
3+
4+
Copyright (c) Google LLC
5+
6+
All rights reserved. Use of this source code is governed by a
7+
BSD-style license that can be found in the LICENSE file.
8+
*/
9+
10+
#include <pybind11/pybind11.h>
11+
12+
#include "pybind11_tests.h"
13+
14+
namespace py = pybind11;
15+
16+
namespace {
17+
18+
struct OwnsPythonObjects {
19+
py::object value = py::none();
20+
};
21+
} // namespace
22+
23+
TEST_SUBMODULE(custom_type_setup, m) {
24+
py::class_<OwnsPythonObjects> cls(
25+
m, "OwnsPythonObjects", py::custom_type_setup([](PyHeapTypeObject *heap_type) {
26+
auto *type = &heap_type->ht_type;
27+
type->tp_flags |= Py_TPFLAGS_HAVE_GC;
28+
type->tp_traverse = [](PyObject *self_base, visitproc visit, void *arg) {
29+
auto &self = py::cast<OwnsPythonObjects &>(py::handle(self_base));
30+
Py_VISIT(self.value.ptr());
31+
return 0;
32+
};
33+
type->tp_clear = [](PyObject *self_base) {
34+
auto &self = py::cast<OwnsPythonObjects &>(py::handle(self_base));
35+
self.value = py::none();
36+
return 0;
37+
};
38+
}));
39+
cls.def(py::init([] { return OwnsPythonObjects{}; }));
40+
cls.def_readwrite("value", &OwnsPythonObjects::value);
41+
}

tests/test_custom_type_setup.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import gc
4+
import weakref
5+
6+
import pytest
7+
8+
import env # noqa: F401
9+
from pybind11_tests import custom_type_setup as m
10+
11+
12+
@pytest.fixture
13+
def gc_tester():
14+
"""Tests that an object is garbage collected.
15+
16+
Assumes that any unreferenced objects are fully collected after calling
17+
`gc.collect()`. That is true on CPython, but does not appear to reliably
18+
hold on PyPy.
19+
"""
20+
21+
weak_refs = []
22+
23+
def add_ref(obj):
24+
# PyPy does not support `gc.is_tracked`.
25+
if hasattr(gc, "is_tracked"):
26+
assert gc.is_tracked(obj)
27+
weak_refs.append(weakref.ref(obj))
28+
29+
yield add_ref
30+
31+
gc.collect()
32+
for ref in weak_refs:
33+
assert ref() is None
34+
35+
36+
# PyPy does not seem to reliably garbage collect.
37+
@pytest.mark.skipif("env.PYPY")
38+
def test_self_cycle(gc_tester):
39+
obj = m.OwnsPythonObjects()
40+
obj.value = obj
41+
gc_tester(obj)
42+
43+
44+
# PyPy does not seem to reliably garbage collect.
45+
@pytest.mark.skipif("env.PYPY")
46+
def test_indirect_cycle(gc_tester):
47+
obj = m.OwnsPythonObjects()
48+
obj_list = [obj]
49+
obj.value = obj_list
50+
gc_tester(obj)

0 commit comments

Comments
 (0)