diff --git a/CMakeLists.txt b/CMakeLists.txt index 63f34830..4305cee5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,6 +56,7 @@ set(PYBIND11_HEADERS include/pybind11/attr.h include/pybind11/cast.h include/pybind11/chrono.h + include/pybind11/class_support.h include/pybind11/common.h include/pybind11/complex.h include/pybind11/descr.h diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index cfc6f8b7..a2e7f02a 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -18,6 +18,8 @@ NAMESPACE_BEGIN(pybind11) NAMESPACE_BEGIN(detail) +inline PyTypeObject *make_static_property_type(); +inline PyTypeObject *make_default_metaclass(); /// Additional type information which does not fit into the PyTypeObject struct type_info { @@ -73,6 +75,8 @@ PYBIND11_NOINLINE inline internals &get_internals() { } } ); + internals_ptr->static_property_type = make_static_property_type(); + internals_ptr->default_metaclass = make_default_metaclass(); } return *internals_ptr; } diff --git a/include/pybind11/class_support.h b/include/pybind11/class_support.h new file mode 100644 index 00000000..ed2eade3 --- /dev/null +++ b/include/pybind11/class_support.h @@ -0,0 +1,147 @@ +/* + pybind11/class_support.h: Python C API implementation details for py::class_ + + Copyright (c) 2017 Wenzel Jakob + + All rights reserved. Use of this source code is governed by a + BSD-style license that can be found in the LICENSE file. +*/ + +#pragma once + +#include "attr.h" + +NAMESPACE_BEGIN(pybind11) +NAMESPACE_BEGIN(detail) + +#if !defined(PYPY_VERSION) + +/// `pybind11_static_property.__get__()`: Always pass the class instead of the instance. +extern "C" inline PyObject *pybind11_static_get(PyObject *self, PyObject * /*ob*/, PyObject *cls) { + return PyProperty_Type.tp_descr_get(self, cls, cls); +} + +/// `pybind11_static_property.__set__()`: Just like the above `__get__()`. +extern "C" inline int pybind11_static_set(PyObject *self, PyObject *obj, PyObject *value) { + PyObject *cls = PyType_Check(obj) ? obj : (PyObject *) Py_TYPE(obj); + return PyProperty_Type.tp_descr_set(self, cls, value); +} + +/** A `static_property` is the same as a `property` but the `__get__()` and `__set__()` + methods are modified to always use the object type instead of a concrete instance. + Return value: New reference. */ +inline PyTypeObject *make_static_property_type() { + constexpr auto *name = "pybind11_static_property"; + auto name_obj = reinterpret_steal(PYBIND11_FROM_STRING(name)); + + /* Danger zone: from now (and until PyType_Ready), make sure to + issue no Python C API calls which could potentially invoke the + garbage collector (the GC will call type_traverse(), which will in + turn find the newly constructed type in an invalid state) */ + auto heap_type = (PyHeapTypeObject *) PyType_Type.tp_alloc(&PyType_Type, 0); + if (!heap_type) + pybind11_fail("make_static_property_type(): error allocating type!"); + + heap_type->ht_name = name_obj.inc_ref().ptr(); +#if PY_MAJOR_VERSION >= 3 && PY_MINOR_VERSION >= 3 + heap_type->ht_qualname = name_obj.inc_ref().ptr(); +#endif + + auto type = &heap_type->ht_type; + type->tp_name = name; + type->tp_base = &PyProperty_Type; + type->tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HEAPTYPE; + type->tp_descr_get = pybind11_static_get; + type->tp_descr_set = pybind11_static_set; + + if (PyType_Ready(type) < 0) + pybind11_fail("make_static_property_type(): failure in PyType_Ready()!"); + + return type; +} + +#else // PYPY + +/** PyPy has some issues with the above C API, so we evaluate Python code instead. + This function will only be called once so performance isn't really a concern. + Return value: New reference. */ +inline PyTypeObject *make_static_property_type() { + auto d = dict(); + PyObject *result = PyRun_String(R"(\ + class pybind11_static_property(property): + def __get__(self, obj, cls): + return property.__get__(self, cls, cls) + + def __set__(self, obj, value): + cls = obj if isinstance(obj, type) else type(obj) + property.__set__(self, cls, value) + )", Py_file_input, d.ptr(), d.ptr() + ); + if (result == nullptr) + throw error_already_set(); + Py_DECREF(result); + return (PyTypeObject *) d["pybind11_static_property"].cast().release().ptr(); +} + +#endif // PYPY + +/** Types with static properties need to handle `Type.static_prop = x` in a specific way. + By default, Python replaces the `static_property` itself, but for wrapped C++ types + we need to call `static_property.__set__()` in order to propagate the new value to + the underlying C++ data structure. */ +extern "C" inline int pybind11_meta_setattro(PyObject* obj, PyObject* name, PyObject* value) { + // Use `_PyType_Lookup()` instead of `PyObject_GetAttr()` in order to get the raw + // descriptor (`property`) instead of calling `tp_descr_get` (`property.__get__()`). + PyObject *descr = _PyType_Lookup((PyTypeObject *) obj, name); + + // Call `static_property.__set__()` instead of replacing the `static_property`. + if (descr && PyObject_IsInstance(descr, (PyObject *) get_internals().static_property_type)) { +#if !defined(PYPY_VERSION) + return Py_TYPE(descr)->tp_descr_set(descr, obj, value); +#else + if (PyObject *result = PyObject_CallMethod(descr, "__set__", "OO", obj, value)) { + Py_DECREF(result); + return 0; + } else { + return -1; + } +#endif + } else { + return PyType_Type.tp_setattro(obj, name, value); + } +} + +/** This metaclass is assigned by default to all pybind11 types and is required in order + for static properties to function correctly. Users may override this using `py::metaclass`. + Return value: New reference. */ +inline PyTypeObject* make_default_metaclass() { + constexpr auto *name = "pybind11_type"; + auto name_obj = reinterpret_steal(PYBIND11_FROM_STRING(name)); + + /* Danger zone: from now (and until PyType_Ready), make sure to + issue no Python C API calls which could potentially invoke the + garbage collector (the GC will call type_traverse(), which will in + turn find the newly constructed type in an invalid state) */ + auto heap_type = (PyHeapTypeObject *) PyType_Type.tp_alloc(&PyType_Type, 0); + if (!heap_type) + pybind11_fail("make_default_metaclass(): error allocating metaclass!"); + + heap_type->ht_name = name_obj.inc_ref().ptr(); +#if PY_MAJOR_VERSION >= 3 && PY_MINOR_VERSION >= 3 + heap_type->ht_qualname = name_obj.inc_ref().ptr(); +#endif + + auto type = &heap_type->ht_type; + type->tp_name = name; + type->tp_base = &PyType_Type; + type->tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HEAPTYPE; + type->tp_setattro = pybind11_meta_setattro; + + if (PyType_Ready(type) < 0) + pybind11_fail("make_default_metaclass(): failure in PyType_Ready()!"); + + return type; +} + +NAMESPACE_END(detail) +NAMESPACE_END(pybind11) diff --git a/include/pybind11/common.h b/include/pybind11/common.h index c3f1e304..a367e758 100644 --- a/include/pybind11/common.h +++ b/include/pybind11/common.h @@ -352,7 +352,7 @@ struct overload_hash { } }; -/// Internal data struture used to track registered instances and types +/// Internal data structure used to track registered instances and types struct internals { std::unordered_map registered_types_cpp; // std::type_index -> type_info std::unordered_map registered_types_py; // PyTypeObject* -> type_info @@ -361,6 +361,8 @@ struct internals { std::unordered_map> direct_conversions; std::forward_list registered_exception_translators; std::unordered_map shared_data; // Custom data to be shared across extensions + PyTypeObject *static_property_type; + PyTypeObject *default_metaclass; #if defined(WITH_THREAD) decltype(PyThread_create_key()) tstate = 0; // Usually an int but a long on Cygwin64 with Python 3.x PyInterpreterState *istate = nullptr; diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 5a604cd2..6cc6d5e8 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -35,6 +35,7 @@ #include "attr.h" #include "options.h" +#include "class_support.h" NAMESPACE_BEGIN(pybind11) @@ -818,15 +819,12 @@ protected: object scope_qualname; if (rec->scope && hasattr(rec->scope, "__qualname__")) scope_qualname = rec->scope.attr("__qualname__"); - object ht_qualname, ht_qualname_meta; + object ht_qualname; if (scope_qualname) ht_qualname = reinterpret_steal(PyUnicode_FromFormat( "%U.%U", scope_qualname.ptr(), name.ptr())); else ht_qualname = name; - if (rec->metaclass) - ht_qualname_meta = reinterpret_steal( - PyUnicode_FromFormat("%U__Meta", ht_qualname.ptr())); #endif #if !defined(PYPY_VERSION) @@ -836,36 +834,6 @@ protected: std::string full_name = std::string(rec->name); #endif - /* Create a custom metaclass if requested (used for static properties) */ - object metaclass; - if (rec->metaclass) { - std::string meta_name_ = full_name + "__Meta"; - object meta_name = reinterpret_steal(PYBIND11_FROM_STRING(meta_name_.c_str())); - metaclass = reinterpret_steal(PyType_Type.tp_alloc(&PyType_Type, 0)); - if (!metaclass || !name) - pybind11_fail("generic_type::generic_type(): unable to create metaclass!"); - - /* Danger zone: from now (and until PyType_Ready), make sure to - issue no Python C API calls which could potentially invoke the - garbage collector (the GC will call type_traverse(), which will in - turn find the newly constructed type in an invalid state) */ - - auto type = (PyHeapTypeObject*) metaclass.ptr(); - type->ht_name = meta_name.release().ptr(); - -#if PY_MAJOR_VERSION >= 3 && PY_MINOR_VERSION >= 3 - /* Qualified names for Python >= 3.3 */ - type->ht_qualname = ht_qualname_meta.release().ptr(); -#endif - type->ht_type.tp_name = strdup(meta_name_.c_str()); - type->ht_type.tp_base = &PyType_Type; - type->ht_type.tp_flags |= (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HEAPTYPE) & - ~Py_TPFLAGS_HAVE_GC; - - if (PyType_Ready(&type->ht_type) < 0) - pybind11_fail("generic_type::generic_type(): failure in PyType_Ready() for metaclass!"); - } - size_t num_bases = rec->bases.size(); auto bases = tuple(rec->bases); @@ -915,8 +883,9 @@ protected: type->ht_qualname = ht_qualname.release().ptr(); #endif - /* Metaclass */ - PYBIND11_OB_TYPE(type->ht_type) = (PyTypeObject *) metaclass.release().ptr(); + /* Custom metaclass if requested (used for static properties) */ + if (rec->metaclass) + PYBIND11_OB_TYPE(type->ht_type) = internals.default_metaclass; /* Supported protocols */ type->ht_type.tp_as_number = &type->as_number; @@ -1105,15 +1074,10 @@ protected: void def_property_static_impl(const char *name, handle fget, handle fset, detail::function_record *rec_fget) { - pybind11::str doc_obj = pybind11::str( - (rec_fget->doc && pybind11::options::show_user_defined_docstrings()) - ? rec_fget->doc : ""); - const auto property = reinterpret_steal( - PyObject_CallFunctionObjArgs((PyObject *) &PyProperty_Type, fget.ptr() ? fget.ptr() : Py_None, - fset.ptr() ? fset.ptr() : Py_None, Py_None, doc_obj.ptr(), nullptr)); - if (rec_fget->is_method && rec_fget->scope) { - attr(name) = property; - } else { + const auto is_static = !(rec_fget->is_method && rec_fget->scope); + const auto has_doc = rec_fget->doc && pybind11::options::show_user_defined_docstrings(); + + if (is_static) { auto mclass = handle((PyObject *) PYBIND11_OB_TYPE(*((PyTypeObject *) m_ptr))); if ((PyTypeObject *) mclass.ptr() == &PyType_Type) @@ -1123,8 +1087,14 @@ protected: "' requires the type to have a custom metaclass. Please " "ensure that one is created by supplying the pybind11::metaclass() " "annotation to the associated class_<>(..) invocation."); - mclass.attr(name) = property; } + + auto property = handle((PyObject *) (is_static ? get_internals().static_property_type + : &PyProperty_Type)); + attr(name) = property(fget.ptr() ? fget : none(), + fset.ptr() ? fset : none(), + /*deleter*/none(), + pybind11::str(has_doc ? rec_fget->doc : "")); } }; diff --git a/setup.py b/setup.py index f3011b0d..0cf4e47c 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,7 @@ else: 'include/pybind11/attr.h', 'include/pybind11/cast.h', 'include/pybind11/chrono.h', + 'include/pybind11/class_support.h', 'include/pybind11/common.h', 'include/pybind11/complex.h', 'include/pybind11/descr.h', diff --git a/tests/test_methods_and_attributes.cpp b/tests/test_methods_and_attributes.cpp index 5bccf49c..11c36403 100644 --- a/tests/test_methods_and_attributes.cpp +++ b/tests/test_methods_and_attributes.cpp @@ -214,7 +214,10 @@ test_initializer methods_and_attributes([](py::module &m) { [](py::object) { return TestProperties::static_get(); }) .def_property_static("def_property_static", [](py::object) { return TestProperties::static_get(); }, - [](py::object, int v) { return TestProperties::static_set(v); }); + [](py::object, int v) { TestProperties::static_set(v); }) + .def_property_static("static_cls", + [](py::object cls) { return cls; }, + [](py::object cls, py::function f) { f(cls); }); py::class_(m, "SimpleValue") .def_readwrite("value", &SimpleValue::value); diff --git a/tests/test_methods_and_attributes.py b/tests/test_methods_and_attributes.py index 1ea669a4..b5d5afdc 100644 --- a/tests/test_methods_and_attributes.py +++ b/tests/test_methods_and_attributes.py @@ -84,19 +84,47 @@ def test_static_properties(): from pybind11_tests import TestProperties as Type assert Type.def_readonly_static == 1 - with pytest.raises(AttributeError): + with pytest.raises(AttributeError) as excinfo: Type.def_readonly_static = 2 + assert "can't set attribute" in str(excinfo) Type.def_readwrite_static = 2 assert Type.def_readwrite_static == 2 assert Type.def_property_readonly_static == 2 - with pytest.raises(AttributeError): + with pytest.raises(AttributeError) as excinfo: Type.def_property_readonly_static = 3 + assert "can't set attribute" in str(excinfo) Type.def_property_static = 3 assert Type.def_property_static == 3 + # Static property read and write via instance + instance = Type() + + Type.def_readwrite_static = 0 + assert Type.def_readwrite_static == 0 + assert instance.def_readwrite_static == 0 + + instance.def_readwrite_static = 2 + assert Type.def_readwrite_static == 2 + assert instance.def_readwrite_static == 2 + + +def test_static_cls(): + """Static property getter and setters expect the type object as the their only argument""" + from pybind11_tests import TestProperties as Type + + instance = Type() + assert Type.static_cls is Type + assert instance.static_cls is Type + + def check_self(self): + assert self is Type + + Type.static_cls = check_self + instance.static_cls = check_self + @pytest.mark.parametrize("access", ["ro", "rw", "static_ro", "static_rw"]) def test_property_return_value_policies(access): diff --git a/tests/test_python_types.py b/tests/test_python_types.py index c5ade90b..cf5412df 100644 --- a/tests/test_python_types.py +++ b/tests/test_python_types.py @@ -6,7 +6,7 @@ from pybind11_tests import ExamplePythonTypes, ConstructorStats, has_optional, h def test_repr(): # In Python 3.3+, repr() accesses __qualname__ - assert "ExamplePythonTypes__Meta" in repr(type(ExamplePythonTypes)) + assert "pybind11_type" in repr(type(ExamplePythonTypes)) assert "ExamplePythonTypes" in repr(ExamplePythonTypes)