Skip to content

Commit 031a4ad

Browse files
committed
Reimplement static properties by extending PyProperty_Type
Instead of creating a new unique metaclass for each type, the builtin `property` type is subclassed to support static properties. The new setter/getters always pass types instead of instances in their `self` argument. A metaclass is still required to support this behavior, but it doesn't store any data anymore, so a new one doesn't need to be created for each class. There is now only one common metaclass which is shared by all pybind11 types.
1 parent a3f4a02 commit 031a4ad

File tree

9 files changed

+207
-51
lines changed

9 files changed

+207
-51
lines changed

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ set(PYBIND11_HEADERS
5656
include/pybind11/attr.h
5757
include/pybind11/cast.h
5858
include/pybind11/chrono.h
59+
include/pybind11/class_support.h
5960
include/pybind11/common.h
6061
include/pybind11/complex.h
6162
include/pybind11/descr.h

include/pybind11/cast.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
NAMESPACE_BEGIN(pybind11)
2020
NAMESPACE_BEGIN(detail)
21+
inline PyTypeObject *make_static_property_type();
22+
inline PyTypeObject *make_default_metaclass();
2123

2224
/// Additional type information which does not fit into the PyTypeObject
2325
struct type_info {
@@ -73,6 +75,8 @@ PYBIND11_NOINLINE inline internals &get_internals() {
7375
}
7476
}
7577
);
78+
internals_ptr->static_property_type = make_static_property_type();
79+
internals_ptr->default_metaclass = make_default_metaclass();
7680
}
7781
return *internals_ptr;
7882
}

include/pybind11/class_support.h

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
pybind11/class_support.h: Python C API implementation details for py::class_
3+
4+
Copyright (c) 2017 Wenzel Jakob <[email protected]>
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+
#pragma once
11+
12+
#include "attr.h"
13+
14+
NAMESPACE_BEGIN(pybind11)
15+
NAMESPACE_BEGIN(detail)
16+
17+
#if !defined(PYPY_VERSION)
18+
19+
/// `pybind11_static_property.__get__()`: Always pass the class instead of the instance.
20+
extern "C" inline PyObject *pybind11_static_get(PyObject *self, PyObject * /*ob*/, PyObject *cls) {
21+
return PyProperty_Type.tp_descr_get(self, cls, cls);
22+
}
23+
24+
/// `pybind11_static_property.__set__()`: Just like the above `__get__()`.
25+
extern "C" inline int pybind11_static_set(PyObject *self, PyObject *obj, PyObject *value) {
26+
PyObject *cls = PyType_Check(obj) ? obj : (PyObject *) Py_TYPE(obj);
27+
return PyProperty_Type.tp_descr_set(self, cls, value);
28+
}
29+
30+
/** A `static_property` is the same as a `property` but the `__get__()` and `__set__()`
31+
methods are modified to always use the object type instead of a concrete instance.
32+
Return value: New reference. */
33+
inline PyTypeObject *make_static_property_type() {
34+
constexpr auto *name = "pybind11_static_property";
35+
auto name_obj = reinterpret_steal<object>(PYBIND11_FROM_STRING(name));
36+
37+
/* Danger zone: from now (and until PyType_Ready), make sure to
38+
issue no Python C API calls which could potentially invoke the
39+
garbage collector (the GC will call type_traverse(), which will in
40+
turn find the newly constructed type in an invalid state) */
41+
auto heap_type = (PyHeapTypeObject *) PyType_Type.tp_alloc(&PyType_Type, 0);
42+
if (!heap_type)
43+
pybind11_fail("make_static_property_type(): error allocating type!");
44+
45+
heap_type->ht_name = name_obj.inc_ref().ptr();
46+
#if PY_MAJOR_VERSION >= 3 && PY_MINOR_VERSION >= 3
47+
heap_type->ht_qualname = name_obj.inc_ref().ptr();
48+
#endif
49+
50+
auto type = &heap_type->ht_type;
51+
type->tp_name = name;
52+
type->tp_base = &PyProperty_Type;
53+
type->tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HEAPTYPE;
54+
type->tp_descr_get = pybind11_static_get;
55+
type->tp_descr_set = pybind11_static_set;
56+
57+
if (PyType_Ready(type) < 0)
58+
pybind11_fail("make_static_property_type(): failure in PyType_Ready()!");
59+
60+
return type;
61+
}
62+
63+
#else // PYPY
64+
65+
/** PyPy has some issues with the above C API, so we evaluate Python code instead.
66+
This function will only be called once so performance isn't really a concern.
67+
Return value: New reference. */
68+
inline PyTypeObject *make_static_property_type() {
69+
auto d = dict();
70+
PyObject *result = PyRun_String(R"(\
71+
class pybind11_static_property(property):
72+
def __get__(self, obj, cls):
73+
return property.__get__(self, cls, cls)
74+
75+
def __set__(self, obj, value):
76+
cls = obj if isinstance(obj, type) else type(obj)
77+
property.__set__(self, cls, value)
78+
)", Py_file_input, d.ptr(), d.ptr()
79+
);
80+
if (result == nullptr)
81+
throw error_already_set();
82+
Py_DECREF(result);
83+
return (PyTypeObject *) d["pybind11_static_property"].cast<object>().release().ptr();
84+
}
85+
86+
#endif // PYPY
87+
88+
/** Types with static properties need to handle `Type.static_prop = x` in a specific way.
89+
By default, Python replaces the `static_property` itself, but for wrapped C++ types
90+
we need to call `static_property.__set__()` in order to propagate the new value to
91+
the underlying C++ data structure. */
92+
extern "C" inline int pybind11_meta_setattro(PyObject* obj, PyObject* name, PyObject* value) {
93+
// Use `_PyType_Lookup()` instead of `PyObject_GetAttr()` in order to get the raw
94+
// descriptor (`property`) instead of calling `tp_descr_get` (`property.__get__()`).
95+
PyObject *descr = _PyType_Lookup((PyTypeObject *) obj, name);
96+
97+
// Call `static_property.__set__()` instead of replacing the `static_property`.
98+
if (descr && PyObject_IsInstance(descr, (PyObject *) get_internals().static_property_type)) {
99+
#if !defined(PYPY_VERSION)
100+
return Py_TYPE(descr)->tp_descr_set(descr, obj, value);
101+
#else
102+
if (PyObject *result = PyObject_CallMethod(descr, "__set__", "OO", obj, value)) {
103+
Py_DECREF(result);
104+
return 0;
105+
} else {
106+
return -1;
107+
}
108+
#endif
109+
} else {
110+
return PyType_Type.tp_setattro(obj, name, value);
111+
}
112+
}
113+
114+
/** This metaclass is assigned by default to all pybind11 types and is required in order
115+
for static properties to function correctly. Users may override this using `py::metaclass`.
116+
Return value: New reference. */
117+
inline PyTypeObject* make_default_metaclass() {
118+
constexpr auto *name = "pybind11_type";
119+
auto name_obj = reinterpret_steal<object>(PYBIND11_FROM_STRING(name));
120+
121+
/* Danger zone: from now (and until PyType_Ready), make sure to
122+
issue no Python C API calls which could potentially invoke the
123+
garbage collector (the GC will call type_traverse(), which will in
124+
turn find the newly constructed type in an invalid state) */
125+
auto heap_type = (PyHeapTypeObject *) PyType_Type.tp_alloc(&PyType_Type, 0);
126+
if (!heap_type)
127+
pybind11_fail("make_default_metaclass(): error allocating metaclass!");
128+
129+
heap_type->ht_name = name_obj.inc_ref().ptr();
130+
#if PY_MAJOR_VERSION >= 3 && PY_MINOR_VERSION >= 3
131+
heap_type->ht_qualname = name_obj.inc_ref().ptr();
132+
#endif
133+
134+
auto type = &heap_type->ht_type;
135+
type->tp_name = name;
136+
type->tp_base = &PyType_Type;
137+
type->tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HEAPTYPE;
138+
type->tp_setattro = pybind11_meta_setattro;
139+
140+
if (PyType_Ready(type) < 0)
141+
pybind11_fail("make_default_metaclass(): failure in PyType_Ready()!");
142+
143+
return type;
144+
}
145+
146+
NAMESPACE_END(detail)
147+
NAMESPACE_END(pybind11)

include/pybind11/common.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ struct overload_hash {
352352
}
353353
};
354354

355-
/// Internal data struture used to track registered instances and types
355+
/// Internal data structure used to track registered instances and types
356356
struct internals {
357357
std::unordered_map<std::type_index, void*> registered_types_cpp; // std::type_index -> type_info
358358
std::unordered_map<const void *, void*> registered_types_py; // PyTypeObject* -> type_info
@@ -361,6 +361,8 @@ struct internals {
361361
std::unordered_map<std::type_index, std::vector<bool (*)(PyObject *, void *&)>> direct_conversions;
362362
std::forward_list<void (*) (std::exception_ptr)> registered_exception_translators;
363363
std::unordered_map<std::string, void *> shared_data; // Custom data to be shared across extensions
364+
PyTypeObject *static_property_type;
365+
PyTypeObject *default_metaclass;
364366
#if defined(WITH_THREAD)
365367
decltype(PyThread_create_key()) tstate = 0; // Usually an int but a long on Cygwin64 with Python 3.x
366368
PyInterpreterState *istate = nullptr;

include/pybind11/pybind11.h

Lines changed: 16 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535

3636
#include "attr.h"
3737
#include "options.h"
38+
#include "class_support.h"
3839

3940
NAMESPACE_BEGIN(pybind11)
4041

@@ -818,15 +819,12 @@ class generic_type : public object {
818819
object scope_qualname;
819820
if (rec->scope && hasattr(rec->scope, "__qualname__"))
820821
scope_qualname = rec->scope.attr("__qualname__");
821-
object ht_qualname, ht_qualname_meta;
822+
object ht_qualname;
822823
if (scope_qualname)
823824
ht_qualname = reinterpret_steal<object>(PyUnicode_FromFormat(
824825
"%U.%U", scope_qualname.ptr(), name.ptr()));
825826
else
826827
ht_qualname = name;
827-
if (rec->metaclass)
828-
ht_qualname_meta = reinterpret_steal<object>(
829-
PyUnicode_FromFormat("%U__Meta", ht_qualname.ptr()));
830828
#endif
831829

832830
#if !defined(PYPY_VERSION)
@@ -836,36 +834,6 @@ class generic_type : public object {
836834
std::string full_name = std::string(rec->name);
837835
#endif
838836

839-
/* Create a custom metaclass if requested (used for static properties) */
840-
object metaclass;
841-
if (rec->metaclass) {
842-
std::string meta_name_ = full_name + "__Meta";
843-
object meta_name = reinterpret_steal<object>(PYBIND11_FROM_STRING(meta_name_.c_str()));
844-
metaclass = reinterpret_steal<object>(PyType_Type.tp_alloc(&PyType_Type, 0));
845-
if (!metaclass || !name)
846-
pybind11_fail("generic_type::generic_type(): unable to create metaclass!");
847-
848-
/* Danger zone: from now (and until PyType_Ready), make sure to
849-
issue no Python C API calls which could potentially invoke the
850-
garbage collector (the GC will call type_traverse(), which will in
851-
turn find the newly constructed type in an invalid state) */
852-
853-
auto type = (PyHeapTypeObject*) metaclass.ptr();
854-
type->ht_name = meta_name.release().ptr();
855-
856-
#if PY_MAJOR_VERSION >= 3 && PY_MINOR_VERSION >= 3
857-
/* Qualified names for Python >= 3.3 */
858-
type->ht_qualname = ht_qualname_meta.release().ptr();
859-
#endif
860-
type->ht_type.tp_name = strdup(meta_name_.c_str());
861-
type->ht_type.tp_base = &PyType_Type;
862-
type->ht_type.tp_flags |= (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HEAPTYPE) &
863-
~Py_TPFLAGS_HAVE_GC;
864-
865-
if (PyType_Ready(&type->ht_type) < 0)
866-
pybind11_fail("generic_type::generic_type(): failure in PyType_Ready() for metaclass!");
867-
}
868-
869837
size_t num_bases = rec->bases.size();
870838
auto bases = tuple(rec->bases);
871839

@@ -915,8 +883,9 @@ class generic_type : public object {
915883
type->ht_qualname = ht_qualname.release().ptr();
916884
#endif
917885

918-
/* Metaclass */
919-
PYBIND11_OB_TYPE(type->ht_type) = (PyTypeObject *) metaclass.release().ptr();
886+
/* Custom metaclass if requested (used for static properties) */
887+
if (rec->metaclass)
888+
PYBIND11_OB_TYPE(type->ht_type) = internals.default_metaclass;
920889

921890
/* Supported protocols */
922891
type->ht_type.tp_as_number = &type->as_number;
@@ -1105,15 +1074,10 @@ class generic_type : public object {
11051074
void def_property_static_impl(const char *name,
11061075
handle fget, handle fset,
11071076
detail::function_record *rec_fget) {
1108-
pybind11::str doc_obj = pybind11::str(
1109-
(rec_fget->doc && pybind11::options::show_user_defined_docstrings())
1110-
? rec_fget->doc : "");
1111-
const auto property = reinterpret_steal<object>(
1112-
PyObject_CallFunctionObjArgs((PyObject *) &PyProperty_Type, fget.ptr() ? fget.ptr() : Py_None,
1113-
fset.ptr() ? fset.ptr() : Py_None, Py_None, doc_obj.ptr(), nullptr));
1114-
if (rec_fget->is_method && rec_fget->scope) {
1115-
attr(name) = property;
1116-
} else {
1077+
const auto is_static = !(rec_fget->is_method && rec_fget->scope);
1078+
const auto has_doc = rec_fget->doc && pybind11::options::show_user_defined_docstrings();
1079+
1080+
if (is_static) {
11171081
auto mclass = handle((PyObject *) PYBIND11_OB_TYPE(*((PyTypeObject *) m_ptr)));
11181082

11191083
if ((PyTypeObject *) mclass.ptr() == &PyType_Type)
@@ -1123,8 +1087,14 @@ class generic_type : public object {
11231087
"' requires the type to have a custom metaclass. Please "
11241088
"ensure that one is created by supplying the pybind11::metaclass() "
11251089
"annotation to the associated class_<>(..) invocation.");
1126-
mclass.attr(name) = property;
11271090
}
1091+
1092+
auto property = handle((PyObject *) (is_static ? get_internals().static_property_type
1093+
: &PyProperty_Type));
1094+
attr(name) = property(fget.ptr() ? fget : none(),
1095+
fset.ptr() ? fset : none(),
1096+
/*deleter*/none(),
1097+
pybind11::str(has_doc ? rec_fget->doc : ""));
11281098
}
11291099
};
11301100

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
'include/pybind11/attr.h',
1616
'include/pybind11/cast.h',
1717
'include/pybind11/chrono.h',
18+
'include/pybind11/class_support.h',
1819
'include/pybind11/common.h',
1920
'include/pybind11/complex.h',
2021
'include/pybind11/descr.h',

tests/test_methods_and_attributes.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,10 @@ test_initializer methods_and_attributes([](py::module &m) {
214214
[](py::object) { return TestProperties::static_get(); })
215215
.def_property_static("def_property_static",
216216
[](py::object) { return TestProperties::static_get(); },
217-
[](py::object, int v) { return TestProperties::static_set(v); });
217+
[](py::object, int v) { TestProperties::static_set(v); })
218+
.def_property_static("static_cls",
219+
[](py::object cls) { return cls; },
220+
[](py::object cls, py::function f) { f(cls); });
218221

219222
py::class_<SimpleValue>(m, "SimpleValue")
220223
.def_readwrite("value", &SimpleValue::value);

tests/test_methods_and_attributes.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,19 +84,47 @@ def test_static_properties():
8484
from pybind11_tests import TestProperties as Type
8585

8686
assert Type.def_readonly_static == 1
87-
with pytest.raises(AttributeError):
87+
with pytest.raises(AttributeError) as excinfo:
8888
Type.def_readonly_static = 2
89+
assert "can't set attribute" in str(excinfo)
8990

9091
Type.def_readwrite_static = 2
9192
assert Type.def_readwrite_static == 2
9293

9394
assert Type.def_property_readonly_static == 2
94-
with pytest.raises(AttributeError):
95+
with pytest.raises(AttributeError) as excinfo:
9596
Type.def_property_readonly_static = 3
97+
assert "can't set attribute" in str(excinfo)
9698

9799
Type.def_property_static = 3
98100
assert Type.def_property_static == 3
99101

102+
# Static property read and write via instance
103+
instance = Type()
104+
105+
Type.def_readwrite_static = 0
106+
assert Type.def_readwrite_static == 0
107+
assert instance.def_readwrite_static == 0
108+
109+
instance.def_readwrite_static = 2
110+
assert Type.def_readwrite_static == 2
111+
assert instance.def_readwrite_static == 2
112+
113+
114+
def test_static_cls():
115+
"""Static property getter and setters expect the type object as the their only argument"""
116+
from pybind11_tests import TestProperties as Type
117+
118+
instance = Type()
119+
assert Type.static_cls is Type
120+
assert instance.static_cls is Type
121+
122+
def check_self(self):
123+
assert self is Type
124+
125+
Type.static_cls = check_self
126+
instance.static_cls = check_self
127+
100128

101129
@pytest.mark.parametrize("access", ["ro", "rw", "static_ro", "static_rw"])
102130
def test_property_return_value_policies(access):

tests/test_python_types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
def test_repr():
88
# In Python 3.3+, repr() accesses __qualname__
9-
assert "ExamplePythonTypes__Meta" in repr(type(ExamplePythonTypes))
9+
assert "pybind11_type" in repr(type(ExamplePythonTypes))
1010
assert "ExamplePythonTypes" in repr(ExamplePythonTypes)
1111

1212

0 commit comments

Comments
 (0)