Added write only property functions for issue #1142 (#1144)

py::class_<T>'s `def_property` and `def_property_static` can now take a
`nullptr` as the getter to allow a write-only property to be established
(mirroring Python's `property()` built-in when `None` is given for the
getter).

This also updates properties to use the new nullptr constructor internally.
This commit is contained in:
Ted Drain 2017-11-07 08:35:27 -08:00 committed by Jason Rhinelander
parent 71178922fd
commit 0a0758ce3a
5 changed files with 73 additions and 18 deletions

View File

@ -15,6 +15,9 @@ v2.3.0 (Not yet released)
for non-MSVC compilers).
`#934 <https://github.com/pybind/pybind11/pull/934>`_.
* Added support for write only properties.
`#1144 <https://github.com/pybind/pybind11/pull/1144>`_.
v2.2.1 (September 14, 2017)
-----------------------------------------------------

View File

@ -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`,

View File

@ -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 <typename Return, typename... Args, typename... Extra>
@ -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 <typename... Extra>
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 <typename... Extra>
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 <typename... Extra>
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<Extra...>::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<Extra...>::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<Extra...>::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;
}

View File

@ -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); })

View File

@ -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