Skip to content

Commit 0e8b028

Browse files
TD22057jagerman
authored andcommitted
Added write only property functions
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).
1 parent 5c7a290 commit 0e8b028

File tree

4 files changed

+57
-16
lines changed

4 files changed

+57
-16
lines changed

docs/classes.rst

Lines changed: 3 additions & 0 deletions
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

Lines changed: 17 additions & 12 deletions
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>
@@ -954,18 +955,18 @@ class generic_type : public object {
954955
tinfo->get_buffer_data = get_buffer_data;
955956
}
956957

958+
// rec_func must be set for either fget or fset.
957959
void def_property_static_impl(const char *name,
958960
handle fget, handle fset,
959-
detail::function_record *rec_fget) {
960-
const auto is_static = !(rec_fget->is_method && rec_fget->scope);
961-
const auto has_doc = rec_fget->doc && pybind11::options::show_user_defined_docstrings();
962-
961+
detail::function_record *rec_func) {
962+
const auto is_static = !(rec_func->is_method && rec_func->scope);
963+
const auto has_doc = rec_func->doc && pybind11::options::show_user_defined_docstrings();
963964
auto property = handle((PyObject *) (is_static ? get_internals().static_property_type
964965
: &PyProperty_Type));
965966
attr(name) = property(fget.ptr() ? fget : none(),
966967
fset.ptr() ? fset : none(),
967968
/*deleter*/none(),
968-
pybind11::str(has_doc ? rec_fget->doc : ""));
969+
pybind11::str(has_doc ? rec_func->doc : ""));
969970
}
970971
};
971972

@@ -1241,21 +1242,25 @@ class class_ : public detail::generic_type {
12411242
template <typename... Extra>
12421243
class_ &def_property_static(const char *name, const cpp_function &fget, const cpp_function &fset, const Extra& ...extra) {
12431244
auto rec_fget = get_function_record(fget), rec_fset = get_function_record(fset);
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);
1245+
auto rec_active = rec_fget;
1246+
if (rec_fget) {
1247+
char *doc_prev = rec_fget->doc; /* 'extra' field may include a property-specific documentation string */
1248+
detail::process_attributes<Extra...>::init(extra..., rec_fget);
1249+
if (rec_fget->doc && rec_fget->doc != doc_prev) {
1250+
free(doc_prev);
1251+
rec_fget->doc = strdup(rec_fget->doc);
1252+
}
12491253
}
12501254
if (rec_fset) {
1251-
doc_prev = rec_fset->doc;
1255+
char *doc_prev = rec_fset->doc;
12521256
detail::process_attributes<Extra...>::init(extra..., rec_fset);
12531257
if (rec_fset->doc && rec_fset->doc != doc_prev) {
12541258
free(doc_prev);
12551259
rec_fset->doc = strdup(rec_fset->doc);
12561260
}
1261+
if (! rec_active) rec_active = rec_fset;
12571262
}
1258-
def_property_static_impl(name, fget, fset, rec_fget);
1263+
def_property_static_impl(name, fget, fset, rec_active);
12591264
return *this;
12601265
}
12611266

tests/test_methods_and_attributes.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,12 +279,19 @@ 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)
284287
.def_readonly_static("def_readonly_static", &TestProperties::static_value)
285288
.def_readwrite_static("def_readwrite_static", &TestProperties::static_value)
289+
.def_property_static("def_writeonly_static", nullptr,
290+
[](py::object, int v) { TestProperties::static_value = v; })
286291
.def_property_readonly_static("def_property_readonly_static",
287292
[](py::object) { return TestProperties::static_get(); })
293+
.def_property_static("def_property_writeonly_static", nullptr,
294+
[](py::object, int v) { return TestProperties::static_set(v); })
288295
.def_property_static("def_property_static",
289296
[](py::object) { return TestProperties::static_get(); },
290297
[](py::object, int v) { TestProperties::static_set(v); })

tests/test_methods_and_attributes.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ def test_properties():
9898
instance.def_property = 3
9999
assert instance.def_property == 3
100100

101+
with pytest.raises(AttributeError):
102+
dummy = instance.def_property_writeonly # noqa: F841 unused var
103+
instance.def_property_writeonly = 4
104+
assert instance.def_property_readonly == 4
105+
101106

102107
def test_static_properties():
103108
assert m.TestProperties.def_readonly_static == 1
@@ -108,13 +113,27 @@ def test_static_properties():
108113
m.TestProperties.def_readwrite_static = 2
109114
assert m.TestProperties.def_readwrite_static == 2
110115

111-
assert m.TestProperties.def_property_readonly_static == 2
112116
with pytest.raises(AttributeError) as excinfo:
113-
m.TestProperties.def_property_readonly_static = 3
117+
dummy = m.TestProperties.def_writeonly_static # noqa: F841 unused var
118+
assert "unreadable attribute" in str(excinfo)
119+
120+
m.TestProperties.def_writeonly_static = 3
121+
assert m.TestProperties.def_readonly_static == 3
122+
123+
assert m.TestProperties.def_property_readonly_static == 3
124+
with pytest.raises(AttributeError) as excinfo:
125+
m.TestProperties.def_property_readonly_static = 99
114126
assert "can't set attribute" in str(excinfo)
115127

116-
m.TestProperties.def_property_static = 3
117-
assert m.TestProperties.def_property_static == 3
128+
m.TestProperties.def_property_static = 4
129+
assert m.TestProperties.def_property_static == 4
130+
131+
with pytest.raises(AttributeError) as excinfo:
132+
dummy = m.TestProperties.def_property_writeonly_static
133+
assert "unreadable attribute" in str(excinfo)
134+
135+
m.TestProperties.def_property_writeonly_static = 5
136+
assert m.TestProperties.def_property_static == 5
118137

119138
# Static property read and write via instance
120139
instance = m.TestProperties()
@@ -127,6 +146,13 @@ def test_static_properties():
127146
assert m.TestProperties.def_readwrite_static == 2
128147
assert instance.def_readwrite_static == 2
129148

149+
with pytest.raises(AttributeError) as excinfo:
150+
dummy = instance.def_property_writeonly_static # noqa: F841 unused var
151+
assert "unreadable attribute" in str(excinfo)
152+
153+
instance.def_property_writeonly_static = 4
154+
assert instance.def_property_static == 4
155+
130156
# It should be possible to override properties in derived classes
131157
assert m.TestPropertiesOverride().def_readonly == 99
132158
assert m.TestPropertiesOverride.def_readonly_static == 99

0 commit comments

Comments
 (0)