diff --git a/docs/changelog.rst b/docs/changelog.rst index 8b7047df4..b0d958d49 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,6 +15,9 @@ v2.3.0 (Not yet released) for non-MSVC compilers). `#934 `_. +* Added support for write only properties. + `#1144 `_. + v2.2.1 (September 14, 2017) ----------------------------------------------------- diff --git a/docs/classes.rst b/docs/classes.rst index ca2477e83..890257d5d 100644 --- a/docs/classes.rst +++ b/docs/classes.rst @@ -155,6 +155,9 @@ the setter and getter functions: .def_property("name", &Pet::getName, &Pet::setName) // ... remainder ... +Write only properties can be defined by passing ``nullptr`` as the +input for the read function. + .. seealso:: Similar functions :func:`class_::def_readwrite_static`, diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 30c691702..b489bb249 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -51,6 +51,7 @@ NAMESPACE_BEGIN(PYBIND11_NAMESPACE) class cpp_function : public function { public: cpp_function() { } + cpp_function(std::nullptr_t) { } /// Construct a cpp_function from a vanilla function pointer template @@ -951,18 +952,18 @@ protected: tinfo->get_buffer_data = get_buffer_data; } + // rec_func must be set for either fget or fset. void def_property_static_impl(const char *name, handle fget, handle fset, - detail::function_record *rec_fget) { - const auto is_static = !(rec_fget->is_method && rec_fget->scope); - const auto has_doc = rec_fget->doc && pybind11::options::show_user_defined_docstrings(); - + detail::function_record *rec_func) { + const auto is_static = rec_func && !(rec_func->is_method && rec_func->scope); + const auto has_doc = rec_func && rec_func->doc && pybind11::options::show_user_defined_docstrings(); 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 : "")); + pybind11::str(has_doc ? rec_func->doc : "")); } }; @@ -1196,7 +1197,7 @@ public: /// Uses cpp_function's return_value_policy by default template class_ &def_property_readonly(const char *name, const cpp_function &fget, const Extra& ...extra) { - return def_property(name, fget, cpp_function(), extra...); + return def_property(name, fget, nullptr, extra...); } /// Uses return_value_policy::reference by default @@ -1208,7 +1209,7 @@ public: /// Uses cpp_function's return_value_policy by default template class_ &def_property_readonly_static(const char *name, const cpp_function &fget, const Extra& ...extra) { - return def_property_static(name, fget, cpp_function(), extra...); + return def_property_static(name, fget, nullptr, extra...); } /// Uses return_value_policy::reference_internal by default @@ -1238,21 +1239,25 @@ public: template class_ &def_property_static(const char *name, const cpp_function &fget, const cpp_function &fset, const Extra& ...extra) { auto rec_fget = get_function_record(fget), rec_fset = get_function_record(fset); - char *doc_prev = rec_fget->doc; /* 'extra' field may include a property-specific documentation string */ - detail::process_attributes::init(extra..., rec_fget); - if (rec_fget->doc && rec_fget->doc != doc_prev) { - free(doc_prev); - rec_fget->doc = strdup(rec_fget->doc); + auto *rec_active = rec_fget; + if (rec_fget) { + char *doc_prev = rec_fget->doc; /* 'extra' field may include a property-specific documentation string */ + detail::process_attributes::init(extra..., rec_fget); + if (rec_fget->doc && rec_fget->doc != doc_prev) { + free(doc_prev); + rec_fget->doc = strdup(rec_fget->doc); + } } if (rec_fset) { - doc_prev = rec_fset->doc; + char *doc_prev = rec_fset->doc; detail::process_attributes::init(extra..., rec_fset); if (rec_fset->doc && rec_fset->doc != doc_prev) { free(doc_prev); rec_fset->doc = strdup(rec_fset->doc); } + if (! rec_active) rec_active = rec_fset; } - def_property_static_impl(name, fget, fset, rec_fget); + def_property_static_impl(name, fget, fset, rec_active); return *this; } diff --git a/tests/test_methods_and_attributes.cpp b/tests/test_methods_and_attributes.cpp index cd15869f4..fde152b9f 100644 --- a/tests/test_methods_and_attributes.cpp +++ b/tests/test_methods_and_attributes.cpp @@ -279,12 +279,20 @@ TEST_SUBMODULE(methods_and_attributes, m) { .def(py::init<>()) .def_readonly("def_readonly", &TestProperties::value) .def_readwrite("def_readwrite", &TestProperties::value) + .def_property("def_writeonly", nullptr, + [](TestProperties& s,int v) { s.value = v; } ) + .def_property("def_property_writeonly", nullptr, &TestProperties::set) .def_property_readonly("def_property_readonly", &TestProperties::get) .def_property("def_property", &TestProperties::get, &TestProperties::set) + .def_property("def_property_impossible", nullptr, nullptr) .def_readonly_static("def_readonly_static", &TestProperties::static_value) .def_readwrite_static("def_readwrite_static", &TestProperties::static_value) + .def_property_static("def_writeonly_static", nullptr, + [](py::object, int v) { TestProperties::static_value = v; }) .def_property_readonly_static("def_property_readonly_static", [](py::object) { return TestProperties::static_get(); }) + .def_property_static("def_property_writeonly_static", nullptr, + [](py::object, int v) { return TestProperties::static_set(v); }) .def_property_static("def_property_static", [](py::object) { return TestProperties::static_get(); }, [](py::object, int v) { TestProperties::static_set(v); }) diff --git a/tests/test_methods_and_attributes.py b/tests/test_methods_and_attributes.py index 9fd9cb75c..86b2c3b4b 100644 --- a/tests/test_methods_and_attributes.py +++ b/tests/test_methods_and_attributes.py @@ -98,6 +98,21 @@ def test_properties(): instance.def_property = 3 assert instance.def_property == 3 + with pytest.raises(AttributeError) as excinfo: + dummy = instance.def_property_writeonly # noqa: F841 unused var + assert "unreadable attribute" in str(excinfo) + + instance.def_property_writeonly = 4 + assert instance.def_property_readonly == 4 + + with pytest.raises(AttributeError) as excinfo: + dummy = instance.def_property_impossible # noqa: F841 unused var + assert "unreadable attribute" in str(excinfo) + + with pytest.raises(AttributeError) as excinfo: + instance.def_property_impossible = 5 + assert "can't set attribute" in str(excinfo) + def test_static_properties(): assert m.TestProperties.def_readonly_static == 1 @@ -108,13 +123,27 @@ def test_static_properties(): m.TestProperties.def_readwrite_static = 2 assert m.TestProperties.def_readwrite_static == 2 - assert m.TestProperties.def_property_readonly_static == 2 with pytest.raises(AttributeError) as excinfo: - m.TestProperties.def_property_readonly_static = 3 + dummy = m.TestProperties.def_writeonly_static # noqa: F841 unused var + assert "unreadable attribute" in str(excinfo) + + m.TestProperties.def_writeonly_static = 3 + assert m.TestProperties.def_readonly_static == 3 + + assert m.TestProperties.def_property_readonly_static == 3 + with pytest.raises(AttributeError) as excinfo: + m.TestProperties.def_property_readonly_static = 99 assert "can't set attribute" in str(excinfo) - m.TestProperties.def_property_static = 3 - assert m.TestProperties.def_property_static == 3 + m.TestProperties.def_property_static = 4 + assert m.TestProperties.def_property_static == 4 + + with pytest.raises(AttributeError) as excinfo: + dummy = m.TestProperties.def_property_writeonly_static + assert "unreadable attribute" in str(excinfo) + + m.TestProperties.def_property_writeonly_static = 5 + assert m.TestProperties.def_property_static == 5 # Static property read and write via instance instance = m.TestProperties() @@ -127,6 +156,13 @@ def test_static_properties(): assert m.TestProperties.def_readwrite_static == 2 assert instance.def_readwrite_static == 2 + with pytest.raises(AttributeError) as excinfo: + dummy = instance.def_property_writeonly_static # noqa: F841 unused var + assert "unreadable attribute" in str(excinfo) + + instance.def_property_writeonly_static = 4 + assert instance.def_property_static == 4 + # It should be possible to override properties in derived classes assert m.TestPropertiesOverride().def_readonly == 99 assert m.TestPropertiesOverride.def_readonly_static == 99