Skip to content

Commit dc65d66

Browse files
skoslowskiwjakob
authored andcommitted
support for readonly buffers (pybind#863) (pybind#1466)
1 parent bd24155 commit dc65d66

File tree

6 files changed

+83
-13
lines changed

6 files changed

+83
-13
lines changed

include/pybind11/buffer_info.h

+17-11
Original file line numberDiff line numberDiff line change
@@ -22,33 +22,38 @@ struct buffer_info {
2222
ssize_t ndim = 0; // Number of dimensions
2323
std::vector<ssize_t> shape; // Shape of the tensor (1 entry per dimension)
2424
std::vector<ssize_t> strides; // Number of bytes between adjacent entries (for each per dimension)
25+
bool readonly = false; // flag to indicate if the underlying storage may be written to
2526

2627
buffer_info() { }
2728

2829
buffer_info(void *ptr, ssize_t itemsize, const std::string &format, ssize_t ndim,
29-
detail::any_container<ssize_t> shape_in, detail::any_container<ssize_t> strides_in)
30+
detail::any_container<ssize_t> shape_in, detail::any_container<ssize_t> strides_in, bool readonly=false)
3031
: ptr(ptr), itemsize(itemsize), size(1), format(format), ndim(ndim),
31-
shape(std::move(shape_in)), strides(std::move(strides_in)) {
32+
shape(std::move(shape_in)), strides(std::move(strides_in)), readonly(readonly) {
3233
if (ndim != (ssize_t) shape.size() || ndim != (ssize_t) strides.size())
3334
pybind11_fail("buffer_info: ndim doesn't match shape and/or strides length");
3435
for (size_t i = 0; i < (size_t) ndim; ++i)
3536
size *= shape[i];
3637
}
3738

3839
template <typename T>
39-
buffer_info(T *ptr, detail::any_container<ssize_t> shape_in, detail::any_container<ssize_t> strides_in)
40-
: buffer_info(private_ctr_tag(), ptr, sizeof(T), format_descriptor<T>::format(), static_cast<ssize_t>(shape_in->size()), std::move(shape_in), std::move(strides_in)) { }
40+
buffer_info(T *ptr, detail::any_container<ssize_t> shape_in, detail::any_container<ssize_t> strides_in, bool readonly=false)
41+
: buffer_info(private_ctr_tag(), ptr, sizeof(T), format_descriptor<T>::format(), static_cast<ssize_t>(shape_in->size()), std::move(shape_in), std::move(strides_in), readonly) { }
4142

42-
buffer_info(void *ptr, ssize_t itemsize, const std::string &format, ssize_t size)
43-
: buffer_info(ptr, itemsize, format, 1, {size}, {itemsize}) { }
43+
buffer_info(void *ptr, ssize_t itemsize, const std::string &format, ssize_t size, bool readonly=false)
44+
: buffer_info(ptr, itemsize, format, 1, {size}, {itemsize}, readonly) { }
4445

4546
template <typename T>
46-
buffer_info(T *ptr, ssize_t size)
47-
: buffer_info(ptr, sizeof(T), format_descriptor<T>::format(), size) { }
47+
buffer_info(T *ptr, ssize_t size, bool readonly=false)
48+
: buffer_info(ptr, sizeof(T), format_descriptor<T>::format(), size, readonly) { }
49+
50+
template <typename T>
51+
buffer_info(const T *ptr, ssize_t size, bool readonly=true)
52+
: buffer_info(const_cast<T*>(ptr), sizeof(T), format_descriptor<T>::format(), size, readonly) { }
4853

4954
explicit buffer_info(Py_buffer *view, bool ownview = true)
5055
: buffer_info(view->buf, view->itemsize, view->format, view->ndim,
51-
{view->shape, view->shape + view->ndim}, {view->strides, view->strides + view->ndim}) {
56+
{view->shape, view->shape + view->ndim}, {view->strides, view->strides + view->ndim}, view->readonly) {
5257
this->view = view;
5358
this->ownview = ownview;
5459
}
@@ -70,6 +75,7 @@ struct buffer_info {
7075
strides = std::move(rhs.strides);
7176
std::swap(view, rhs.view);
7277
std::swap(ownview, rhs.ownview);
78+
readonly = rhs.readonly;
7379
return *this;
7480
}
7581

@@ -81,8 +87,8 @@ struct buffer_info {
8187
struct private_ctr_tag { };
8288

8389
buffer_info(private_ctr_tag, void *ptr, ssize_t itemsize, const std::string &format, ssize_t ndim,
84-
detail::any_container<ssize_t> &&shape_in, detail::any_container<ssize_t> &&strides_in)
85-
: buffer_info(ptr, itemsize, format, ndim, std::move(shape_in), std::move(strides_in)) { }
90+
detail::any_container<ssize_t> &&shape_in, detail::any_container<ssize_t> &&strides_in, bool readonly)
91+
: buffer_info(ptr, itemsize, format, ndim, std::move(shape_in), std::move(strides_in), readonly) { }
8692

8793
Py_buffer *view = nullptr;
8894
bool ownview = false;

include/pybind11/detail/class.h

+7
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,13 @@ extern "C" inline int pybind11_getbuffer(PyObject *obj, Py_buffer *view, int fla
491491
view->len = view->itemsize;
492492
for (auto s : info->shape)
493493
view->len *= s;
494+
view->readonly = info->readonly;
495+
if ((flags & PyBUF_WRITABLE) == PyBUF_WRITABLE && info->readonly) {
496+
if (view)
497+
view->obj = nullptr;
498+
PyErr_SetString(PyExc_BufferError, "Writable buffer requested for readonly storage");
499+
return -1;
500+
}
494501
if ((flags & PyBUF_FORMAT) == PyBUF_FORMAT)
495502
view->format = const_cast<char *>(info->format.c_str());
496503
if ((flags & PyBUF_STRIDES) == PyBUF_STRIDES) {

include/pybind11/pytypes.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -1345,7 +1345,7 @@ class memoryview : public object {
13451345
buf.strides = py_strides.data();
13461346
buf.shape = py_shape.data();
13471347
buf.suboffsets = nullptr;
1348-
buf.readonly = false;
1348+
buf.readonly = info.readonly;
13491349
buf.internal = nullptr;
13501350

13511351
m_ptr = PyMemoryView_FromBuffer(&buf);

tests/constructor_stats.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ class ConstructorStats {
180180
}
181181
}
182182
}
183-
catch (const std::out_of_range &) {}
183+
catch (const std::out_of_range&) {}
184184
if (!t1) throw std::runtime_error("Unknown class passed to ConstructorStats::get()");
185185
auto &cs1 = get(*t1);
186186
// If we have both a t1 and t2 match, one is probably the trampoline class; return whichever

tests/test_buffers.cpp

+26
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,30 @@ TEST_SUBMODULE(buffers, m) {
166166
.def_readwrite("value", (int32_t DerivedBuffer::*) &DerivedBuffer::value)
167167
.def_buffer(&DerivedBuffer::get_buffer_info);
168168

169+
struct BufferReadOnly {
170+
const uint8_t value = 0;
171+
BufferReadOnly(uint8_t value): value(value) {}
172+
173+
py::buffer_info get_buffer_info() {
174+
return py::buffer_info(&value, 1);
175+
}
176+
};
177+
py::class_<BufferReadOnly>(m, "BufferReadOnly", py::buffer_protocol())
178+
.def(py::init<uint8_t>())
179+
.def_buffer(&BufferReadOnly::get_buffer_info);
180+
181+
struct BufferReadOnlySelect {
182+
uint8_t value = 0;
183+
bool readonly = false;
184+
185+
py::buffer_info get_buffer_info() {
186+
return py::buffer_info(&value, 1, readonly);
187+
}
188+
};
189+
py::class_<BufferReadOnlySelect>(m, "BufferReadOnlySelect", py::buffer_protocol())
190+
.def(py::init<>())
191+
.def_readwrite("value", &BufferReadOnlySelect::value)
192+
.def_readwrite("readonly", &BufferReadOnlySelect::readonly)
193+
.def_buffer(&BufferReadOnlySelect::get_buffer_info);
194+
169195
}

tests/test_buffers.py

+31
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1+
import io
12
import struct
3+
import sys
4+
25
import pytest
6+
37
from pybind11_tests import buffers as m
48
from pybind11_tests import ConstructorStats
59

10+
PY3 = sys.version_info[0] >= 3
11+
612
pytestmark = pytest.requires_numpy
713

814
with pytest.suppress(ImportError):
@@ -85,3 +91,28 @@ def test_pointer_to_member_fn():
8591
buf.value = 0x12345678
8692
value = struct.unpack('i', bytearray(buf))[0]
8793
assert value == 0x12345678
94+
95+
96+
@pytest.unsupported_on_pypy
97+
def test_readonly_buffer():
98+
buf = m.BufferReadOnly(0x64)
99+
view = memoryview(buf)
100+
assert view[0] == 0x64 if PY3 else b'd'
101+
assert view.readonly
102+
103+
104+
@pytest.unsupported_on_pypy
105+
def test_selective_readonly_buffer():
106+
buf = m.BufferReadOnlySelect()
107+
108+
memoryview(buf)[0] = 0x64 if PY3 else b'd'
109+
assert buf.value == 0x64
110+
111+
io.BytesIO(b'A').readinto(buf)
112+
assert buf.value == ord(b'A')
113+
114+
buf.readonly = True
115+
with pytest.raises(TypeError):
116+
memoryview(buf)[0] = 0 if PY3 else b'\0'
117+
with pytest.raises(TypeError):
118+
io.BytesIO(b'1').readinto(buf)

0 commit comments

Comments
 (0)