Skip to content

Commit 0a0758c

Browse files
TD22057jagerman
authored andcommitted
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.
1 parent 7117892 commit 0a0758c

File tree

5 files changed

+73
-18
lines changed

5 files changed

+73
-18
lines changed

docs/changelog.rst

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ v2.3.0 (Not yet released)
1515
for non-MSVC compilers).
1616
`#934 <https://github.com/pybind/pybind11/pull/934>`_.
1717

18+
* Added support for write only properties.
19+
`#1144 <https://github.com/pybind/pybind11/pull/1144>`_.
20+
1821
v2.2.1 (September 14, 2017)
1922
-----------------------------------------------------
2023

docs/classes.rst

+3
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,9 @@ the setter and getter functions:
155155
.def_property("name", &Pet::getName, &Pet::setName)
156156
// ... remainder ...
157157
158+
Write only properties can be defined by passing ``nullptr`` as the
159+
input for the read function.
160+
158161
.. seealso::
159162

160163
Similar functions :func:`class_::def_readwrite_static`,

include/pybind11/pybind11.h

+19-14
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
5151
class cpp_function : public function {
5252
public:
5353
cpp_function() { }
54+
cpp_function(std::nullptr_t) { }
5455

5556
/// Construct a cpp_function from a vanilla function pointer
5657
template <typename Return, typename... Args, typename... Extra>
@@ -951,18 +952,18 @@ class generic_type : public object {
951952
tinfo->get_buffer_data = get_buffer_data;
952953
}
953954

955+
// rec_func must be set for either fget or fset.
954956
void def_property_static_impl(const char *name,
955957
handle fget, handle fset,
956-
detail::function_record *rec_fget) {
957-
const auto is_static = !(rec_fget->is_method && rec_fget->scope);
958-
const auto has_doc = rec_fget->doc && pybind11::options::show_user_defined_docstrings();
959-
958+
detail::function_record *rec_func) {
959+
const auto is_static = rec_func && !(rec_func->is_method && rec_func->scope);
960+
const auto has_doc = rec_func && rec_func->doc && pybind11::options::show_user_defined_docstrings();
960961
auto property = handle((PyObject *) (is_static ? get_internals().static_property_type
961962
: &PyProperty_Type));
962963
attr(name) = property(fget.ptr() ? fget : none(),
963964
fset.ptr() ? fset : none(),
964965
/*deleter*/none(),
965-
pybind11::str(has_doc ? rec_fget->doc : ""));
966+
pybind11::str(has_doc ? rec_func->doc : ""));
966967
}
967968
};
968969

@@ -1196,7 +1197,7 @@ class class_ : public detail::generic_type {
11961197
/// Uses cpp_function's return_value_policy by default
11971198
template <typename... Extra>
11981199
class_ &def_property_readonly(const char *name, const cpp_function &fget, const Extra& ...extra) {
1199-
return def_property(name, fget, cpp_function(), extra...);
1200+
return def_property(name, fget, nullptr, extra...);
12001201
}
12011202

12021203
/// Uses return_value_policy::reference by default
@@ -1208,7 +1209,7 @@ class class_ : public detail::generic_type {
12081209
/// Uses cpp_function's return_value_policy by default
12091210
template <typename... Extra>
12101211
class_ &def_property_readonly_static(const char *name, const cpp_function &fget, const Extra& ...extra) {
1211-
return def_property_static(name, fget, cpp_function(), extra...);
1212+
return def_property_static(name, fget, nullptr, extra...);
12121213
}
12131214

12141215
/// Uses return_value_policy::reference_internal by default
@@ -1238,21 +1239,25 @@ class class_ : public detail::generic_type {
12381239
template <typename... Extra>
12391240
class_ &def_property_static(const char *name, const cpp_function &fget, const cpp_function &fset, const Extra& ...extra) {
12401241
auto rec_fget = get_function_record(fget), rec_fset = get_function_record(fset);
1241-
char *doc_prev = rec_fget->doc; /* 'extra' field may include a property-specific documentation string */
1242-
detail::process_attributes<Extra...>::init(extra..., rec_fget);
1243-
if (rec_fget->doc && rec_fget->doc != doc_prev) {
1244-
free(doc_prev);
1245-
rec_fget->doc = strdup(rec_fget->doc);
1242+
auto *rec_active = rec_fget;
1243+
if (rec_fget) {
1244+
char *doc_prev = rec_fget->doc; /* 'extra' field may include a property-specific documentation string */
1245+
detail::process_attributes<Extra...>::init(extra..., rec_fget);
1246+
if (rec_fget->doc && rec_fget->doc != doc_prev) {
1247+
free(doc_prev);
1248+
rec_fget->doc = strdup(rec_fget->doc);
1249+
}
12461250
}
12471251
if (rec_fset) {
1248-
doc_prev = rec_fset->doc;
1252+
char *doc_prev = rec_fset->doc;
12491253
detail::process_attributes<Extra...>::init(extra..., rec_fset);
12501254
if (rec_fset->doc && rec_fset->doc != doc_prev) {
12511255
free(doc_prev);
12521256
rec_fset->doc = strdup(rec_fset->doc);
12531257
}
1258+
if (! rec_active) rec_active = rec_fset;
12541259
}
1255-
def_property_static_impl(name, fget, fset, rec_fget);
1260+
def_property_static_impl(name, fget, fset, rec_active);
12561261
return *this;
12571262
}
12581263

tests/test_methods_and_attributes.cpp

+8
Original file line numberDiff line numberDiff line change
@@ -279,12 +279,20 @@ TEST_SUBMODULE(methods_and_attributes, m) {
279279
.def(py::init<>())
280280
.def_readonly("def_readonly", &TestProperties::value)
281281
.def_readwrite("def_readwrite", &TestProperties::value)
282+
.def_property("def_writeonly", nullptr,
283+
[](TestProperties& s,int v) { s.value = v; } )
284+
.def_property("def_property_writeonly", nullptr, &TestProperties::set)
282285
.def_property_readonly("def_property_readonly", &TestProperties::get)
283286
.def_property("def_property", &TestProperties::get, &TestProperties::set)
287+
.def_property("def_property_impossible", nullptr, nullptr)
284288
.def_readonly_static("def_readonly_static", &TestProperties::static_value)
285289
.def_readwrite_static("def_readwrite_static", &TestProperties::static_value)
290+
.def_property_static("def_writeonly_static", nullptr,
291+
[](py::object, int v) { TestProperties::static_value = v; })
286292
.def_property_readonly_static("def_property_readonly_static",
287293
[](py::object) { return TestProperties::static_get(); })
294+
.def_property_static("def_property_writeonly_static", nullptr,
295+
[](py::object, int v) { return TestProperties::static_set(v); })
288296
.def_property_static("def_property_static",
289297
[](py::object) { return TestProperties::static_get(); },
290298
[](py::object, int v) { TestProperties::static_set(v); })

tests/test_methods_and_attributes.py

+40-4
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,21 @@ def test_properties():
9898
instance.def_property = 3
9999
assert instance.def_property == 3
100100

101+
with pytest.raises(AttributeError) as excinfo:
102+
dummy = instance.def_property_writeonly # noqa: F841 unused var
103+
assert "unreadable attribute" in str(excinfo)
104+
105+
instance.def_property_writeonly = 4
106+
assert instance.def_property_readonly == 4
107+
108+
with pytest.raises(AttributeError) as excinfo:
109+
dummy = instance.def_property_impossible # noqa: F841 unused var
110+
assert "unreadable attribute" in str(excinfo)
111+
112+
with pytest.raises(AttributeError) as excinfo:
113+
instance.def_property_impossible = 5
114+
assert "can't set attribute" in str(excinfo)
115+
101116

102117
def test_static_properties():
103118
assert m.TestProperties.def_readonly_static == 1
@@ -108,13 +123,27 @@ def test_static_properties():
108123
m.TestProperties.def_readwrite_static = 2
109124
assert m.TestProperties.def_readwrite_static == 2
110125

111-
assert m.TestProperties.def_property_readonly_static == 2
112126
with pytest.raises(AttributeError) as excinfo:
113-
m.TestProperties.def_property_readonly_static = 3
127+
dummy = m.TestProperties.def_writeonly_static # noqa: F841 unused var
128+
assert "unreadable attribute" in str(excinfo)
129+
130+
m.TestProperties.def_writeonly_static = 3
131+
assert m.TestProperties.def_readonly_static == 3
132+
133+
assert m.TestProperties.def_property_readonly_static == 3
134+
with pytest.raises(AttributeError) as excinfo:
135+
m.TestProperties.def_property_readonly_static = 99
114136
assert "can't set attribute" in str(excinfo)
115137

116-
m.TestProperties.def_property_static = 3
117-
assert m.TestProperties.def_property_static == 3
138+
m.TestProperties.def_property_static = 4
139+
assert m.TestProperties.def_property_static == 4
140+
141+
with pytest.raises(AttributeError) as excinfo:
142+
dummy = m.TestProperties.def_property_writeonly_static
143+
assert "unreadable attribute" in str(excinfo)
144+
145+
m.TestProperties.def_property_writeonly_static = 5
146+
assert m.TestProperties.def_property_static == 5
118147

119148
# Static property read and write via instance
120149
instance = m.TestProperties()
@@ -127,6 +156,13 @@ def test_static_properties():
127156
assert m.TestProperties.def_readwrite_static == 2
128157
assert instance.def_readwrite_static == 2
129158

159+
with pytest.raises(AttributeError) as excinfo:
160+
dummy = instance.def_property_writeonly_static # noqa: F841 unused var
161+
assert "unreadable attribute" in str(excinfo)
162+
163+
instance.def_property_writeonly_static = 4
164+
assert instance.def_property_static == 4
165+
130166
# It should be possible to override properties in derived classes
131167
assert m.TestPropertiesOverride().def_readonly == 99
132168
assert m.TestPropertiesOverride.def_readonly_static == 99

0 commit comments

Comments
 (0)