From 78897dddf7dc3b797c5b8c82ec09ef57900c7d93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sat, 27 Jan 2024 19:05:37 +0100 Subject: [PATCH] python: preserve type in StridedArrayViews created from buffers. This finally makes it possible to expose APIs that take StridedArrayView instances as an input, until now the type information was always lost, making all views plain bytes and thus impossible to check whether the types passed were a large enough size at least, if nothing else. Preserving the type means there has to be type-dependent implementation for __getitem__() and __setitem__(). So far this is only done for the very basic builtin types, similarly to what Python's own array supports. --- doc/python/corrade.containers.rst | 91 ++++++++++++++++++- .../StridedArrayViewPythonBindings.h | 18 ++-- src/python/corrade/containers.cpp | 70 ++++++++++++-- src/python/corrade/test/test_containers.py | 86 +++++++++++++++--- .../corrade/test/test_containers_numpy.py | 34 ++++++- 5 files changed, 266 insertions(+), 33 deletions(-) diff --git a/doc/python/corrade.containers.rst b/doc/python/corrade.containers.rst index 42defa7..cfb40ea 100644 --- a/doc/python/corrade.containers.rst +++ b/doc/python/corrade.containers.rst @@ -25,6 +25,7 @@ .. doctest setup >>> from corrade import containers + >>> import array .. py:class:: corrade.containers.ArrayView @@ -82,9 +83,21 @@ .. py:class:: corrade.containers.StridedArrayView1D - Provides an one-dimensional read-only view on a memory range with custom - stride values. Convertible both to and from Python objects supporting the - Buffer Protocol. See :ref:`StridedArrayView2D`, :ref:`StridedArrayView3D`, + Provides a typed one-dimensional read-only view on a memory range with + custom stride values. Convertible both to and from Python objects + supporting the Buffer Protocol, preserving the dimensionality, type and + stride information: + + .. code:: pycon + + >>> a = array.array('f', [2.5, 3.14, -1.75, 53.2]) + >>> b = containers.StridedArrayView1D(memoryview(a)[::2]) + >>> b[0] + 2.5 + >>> b[1] + -1.75 + + See :ref:`StridedArrayView2D`, :ref:`StridedArrayView3D`, :ref:`StridedArrayView4D`, :ref:`MutableStridedArrayView1D` and others for multi-dimensional and mutable equivalents. @@ -104,6 +117,12 @@ multi-dimensional slicing as well (which raises :ref:`NotImplementedError` in Py3.7 :ref:`memoryview`). +.. py:function:: corrade.containers.StridedArrayView1D.__getitem__(self, i: int) + :raise IndexError: If :p:`i` is out of range + :raise NotImplementedError: If the view was created from a buffer and + :ref:`format ` is not one of :py:`'b'`, + :py:`'B'`, :py:`'h'`, :py:`'H'`, :py:`'i'`, :py:`'I'`, :py:`'q'`, + :py:`'Q'`, :py:`'f'` or :py:`'d'` .. py:function:: corrade.containers.StridedArrayView1D.flipped :raise IndexError: if :p:`dimension` is not :py:`0` .. py:function:: corrade.containers.StridedArrayView1D.broadcasted @@ -118,6 +137,18 @@ Equivalent to :ref:`StridedArrayView1D`, but implementing :ref:`__setitem__()` as well. +.. py:function:: corrade.containers.MutableStridedArrayView1D.__getitem__(self, i: int) + :raise IndexError: If :p:`i` is out of range + :raise NotImplementedError: If the view was created from a buffer and + :ref:`format ` is not one of + :py:`'b'`, :py:`'B'`, :py:`'h'`, :py:`'H'`, :py:`'i'`, :py:`'I'`, + :py:`'q'`, :py:`'Q'`, :py:`'f'` or :py:`'d'` +.. py:function:: corrade.containers.MutableStridedArrayView1D.__setitem__(self, i: int, value: handle) + :raise IndexError: If :p:`i` is out of range + :raise NotImplementedError: If the view was created from a buffer and + :ref:`format ` is not one of + :py:`'b'`, :py:`'B'`, :py:`'h'`, :py:`'H'`, :py:`'i'`, :py:`'I'`, + :py:`'q'`, :py:`'Q'`, :py:`'f'` or :py:`'d'` .. py:function:: corrade.containers.MutableStridedArrayView1D.flipped :raise IndexError: if :p:`dimension` is not :py:`0` .. py:function:: corrade.containers.MutableStridedArrayView1D.broadcasted @@ -131,6 +162,12 @@ See :ref:`StridedArrayView1D` for more information. +.. py:function:: corrade.containers.StridedArrayView2D.__getitem__(self, i: typing.Tuple[int, int]) + :raise IndexError: If :p:`i` is out of range + :raise NotImplementedError: If the view was created from a buffer and + :ref:`format ` is not one of :py:`'b'`, + :py:`'B'`, :py:`'h'`, :py:`'H'`, :py:`'i'`, :py:`'I'`, :py:`'q'`, + :py:`'Q'`, :py:`'f'` or :py:`'d'` .. py:function:: corrade.containers.StridedArrayView2D.flipped :raise IndexError: if :p:`dimension` is not :py:`0` or :py:`1` .. py:function:: corrade.containers.StridedArrayView2D.broadcasted @@ -148,6 +185,18 @@ See :ref:`StridedArrayView1D` and :ref:`MutableStridedArrayView1D` for more information. +.. py:function:: corrade.containers.MutableStridedArrayView2D.__getitem__(self, i: typing.Tuple[int, int]) + :raise IndexError: If :p:`i` is out of range + :raise NotImplementedError: If the view was created from a buffer and + :ref:`format ` is not one of + :py:`'b'`, :py:`'B'`, :py:`'h'`, :py:`'H'`, :py:`'i'`, :py:`'I'`, + :py:`'q'`, :py:`'Q'`, :py:`'f'` or :py:`'d'` +.. py:function:: corrade.containers.MutableStridedArrayView2D.__setitem__(self, i: typing.Tuple[int, int], value: handle) + :raise IndexError: If :p:`i` is out of range + :raise NotImplementedError: If the view was created from a buffer and + :ref:`format ` is not one of + :py:`'b'`, :py:`'B'`, :py:`'h'`, :py:`'H'`, :py:`'i'`, :py:`'I'`, + :py:`'q'`, :py:`'Q'`, :py:`'f'` or :py:`'d'` .. py:function:: corrade.containers.MutableStridedArrayView2D.flipped :raise IndexError: if :p:`dimension` is not :py:`0` or :py:`1` .. py:function:: corrade.containers.MutableStridedArrayView2D.broadcasted @@ -164,6 +213,12 @@ See :ref:`StridedArrayView1D` for more information. +.. py:function:: corrade.containers.StridedArrayView3D.__getitem__(self, i: typing.Tuple[int, int, int]) + :raise IndexError: If :p:`i` is out of range + :raise NotImplementedError: If the view was created from a buffer and + :ref:`format ` is not one of :py:`'b'`, + :py:`'B'`, :py:`'h'`, :py:`'H'`, :py:`'i'`, :py:`'I'`, :py:`'q'`, + :py:`'Q'`, :py:`'f'` or :py:`'d'` .. py:function:: corrade.containers.StridedArrayView3D.flipped :raise IndexError: if :p:`dimension` is not :py:`0`, :py:`1` or :py:`2` .. py:function:: corrade.containers.StridedArrayView3D.broadcasted @@ -181,6 +236,18 @@ See :ref:`StridedArrayView1D` and :ref:`MutableStridedArrayView1D` for more information. +.. py:function:: corrade.containers.MutableStridedArrayView3D.__getitem__(self, i: typing.Tuple[int, int, int]) + :raise IndexError: If :p:`i` is out of range + :raise NotImplementedError: If the view was created from a buffer and + :ref:`format ` is not one of + :py:`'b'`, :py:`'B'`, :py:`'h'`, :py:`'H'`, :py:`'i'`, :py:`'I'`, + :py:`'q'`, :py:`'Q'`, :py:`'f'` or :py:`'d'` +.. py:function:: corrade.containers.MutableStridedArrayView3D.__setitem__(self, i: typing.Tuple[int, int, int], value: handle) + :raise IndexError: If :p:`i` is out of range + :raise NotImplementedError: If the view was created from a buffer and + :ref:`format ` is not one of + :py:`'b'`, :py:`'B'`, :py:`'h'`, :py:`'H'`, :py:`'i'`, :py:`'I'`, + :py:`'q'`, :py:`'Q'`, :py:`'f'` or :py:`'d'` .. py:function:: corrade.containers.MutableStridedArrayView3D.flipped :raise IndexError: if :p:`dimension` is not :py:`0`, :py:`1` or :py:`2` .. py:function:: corrade.containers.MutableStridedArrayView3D.broadcasted @@ -197,6 +264,12 @@ See :ref:`StridedArrayView1D` for more information. +.. py:function:: corrade.containers.StridedArrayView4D.__getitem__(self, i: typing.Tuple[int, int, int, int]) + :raise IndexError: If :p:`i` is out of range + :raise NotImplementedError: If the view was created from a buffer and + :ref:`format ` is not one of :py:`'b'`, + :py:`'B'`, :py:`'h'`, :py:`'H'`, :py:`'i'`, :py:`'I'`, :py:`'q'`, + :py:`'Q'`, :py:`'f'` or :py:`'d'` .. py:function:: corrade.containers.StridedArrayView4D.flipped :raise IndexError: if :p:`dimension` is not :py:`0`, :py:`1` :py:`2` or :py:`3` @@ -212,6 +285,18 @@ See :ref:`StridedArrayView1D` and :ref:`MutableStridedArrayView1D` for more information. +.. py:function:: corrade.containers.MutableStridedArrayView4D.__getitem__(self, i: typing.Tuple[int, int, int, int]) + :raise IndexError: If :p:`i` is out of range + :raise NotImplementedError: If the view was created from a buffer and + :ref:`format ` is not one of + :py:`'b'`, :py:`'B'`, :py:`'h'`, :py:`'H'`, :py:`'i'`, :py:`'I'`, + :py:`'q'`, :py:`'Q'`, :py:`'f'` or :py:`'d'` +.. py:function:: corrade.containers.MutableStridedArrayView4D.__setitem__(self, i: typing.Tuple[int, int, int, int], value: handle) + :raise IndexError: If :p:`i` is out of range + :raise NotImplementedError: If the view was created from a buffer and + :ref:`format ` is not one of + :py:`'b'`, :py:`'B'`, :py:`'h'`, :py:`'H'`, :py:`'i'`, :py:`'I'`, + :py:`'q'`, :py:`'Q'`, :py:`'f'` or :py:`'d'` .. py:function:: corrade.containers.MutableStridedArrayView4D.flipped :raise IndexError: if :p:`dimension` is not :py:`0`, :py:`1` :py:`2` or :py:`3` diff --git a/src/Corrade/Containers/StridedArrayViewPythonBindings.h b/src/Corrade/Containers/StridedArrayViewPythonBindings.h index 10d3d6d..0fe49c6 100644 --- a/src/Corrade/Containers/StridedArrayViewPythonBindings.h +++ b/src/Corrade/Containers/StridedArrayViewPythonBindings.h @@ -27,13 +27,16 @@ #include #include +#include namespace Corrade { namespace Containers { namespace Implementation { /* For maintainability please keep in the same order as - https://docs.python.org/3/library/struct.html#format-characters */ + https://docs.python.org/3/library/struct.html#format-characters. Each of + these has also a corresponding entry in accessorsForFormat() in + containers.cpp in the same order. */ template constexpr const char* pythonFormatString() { static_assert(sizeof(T) == 0, "format string unknown for this type, supply it explicitly"); return {}; @@ -111,7 +114,7 @@ template class PyStridedArrayView: public StridedA template explicit PyStridedArrayView(const StridedArrayView& view): PyStridedArrayView{view, Implementation::pythonFormatString::type>(), sizeof(U)} {} - template explicit PyStridedArrayView(const StridedArrayView& view, const char* format, std::size_t itemsize): PyStridedArrayView{ + template explicit PyStridedArrayView(const StridedArrayView& view, Containers::StringView format, std::size_t itemsize): PyStridedArrayView{ arrayCast(view), format, itemsize, @@ -119,7 +122,7 @@ template class PyStridedArrayView: public StridedA Implementation::PyStridedArrayViewSetItem::set } {} - explicit PyStridedArrayView(const StridedArrayView& view, const char* format, std::size_t itemsize, pybind11::object(*getitem)(const char*), void(*setitem)(char*, pybind11::handle)): StridedArrayView{view}, format{format}, itemsize{itemsize}, getitem{getitem}, setitem{setitem} {} + explicit PyStridedArrayView(const StridedArrayView& view, Containers::StringView format, std::size_t itemsize, pybind11::object(*getitem)(const char*), void(*setitem)(char*, pybind11::handle)): StridedArrayView{view}, format{format}, itemsize{itemsize}, getitem{getitem}, setitem{setitem} {} /* All APIs that are exposed by bindings and return a StridedArrayView have to return the wrapper now */ @@ -169,7 +172,10 @@ template class PyStridedArrayView: public StridedA } /* has to be public as it's accessed by the bindings directly */ - const char* format; + /* The assumption is that >99% of format strings should be just a few + characters, stored with a SSO. I.e., not even bothering with + String::nullTerminatedGlobalView() anywhere. */ + Containers::String format; std::size_t itemsize; pybind11::object(*getitem)(const char*); void(*setitem)(char*, pybind11::handle); @@ -178,13 +184,13 @@ template class PyStridedArrayView: public StridedA namespace Implementation { template struct PyStridedElement { - static PyStridedArrayView wrap(const StridedArrayView& element, const char* format, std::size_t itemsize, pybind11::object(*getitem)(const char*), void(*setitem)(char*, pybind11::handle)) { + static PyStridedArrayView wrap(const StridedArrayView& element, Containers::StringView format, std::size_t itemsize, pybind11::object(*getitem)(const char*), void(*setitem)(char*, pybind11::handle)) { return PyStridedArrayView{element, format, itemsize, getitem, setitem}; } }; template struct PyStridedElement<1, T> { - static T& wrap(T& element, const char*, std::size_t, pybind11::object(*)(const char*), void(*)(char*, pybind11::handle)) { + static T& wrap(T& element, Containers::StringView, std::size_t, pybind11::object(*)(const char*), void(*)(char*, pybind11::handle)) { return element; } }; diff --git a/src/python/corrade/containers.cpp b/src/python/corrade/containers.cpp index e6f3bc2..520dadc 100644 --- a/src/python/corrade/containers.cpp +++ b/src/python/corrade/containers.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include "Corrade/Containers/PythonBindings.h" @@ -38,6 +39,8 @@ namespace corrade { +using namespace Containers::Literals; + namespace { struct Slice { @@ -350,7 +353,9 @@ template bool stridedArrayViewBufferProtocol(T& self, Py_buffer& buffer buffer.buf = const_cast::type*>(self.data()); buffer.readonly = std::is_const::value; if((flags & PyBUF_FORMAT) == PyBUF_FORMAT) - buffer.format = const_cast(self.format); + /* A valid format shouldn't be an empty string. If it is, it's because + it was null originally. Pass null in that case again. */ + buffer.format = self.format ? self.format.data() : nullptr; /* The view is immutable (can't change its size after it has been constructed), so referencing the size/stride directly is okay */ buffer.shape = const_cast(reinterpret_cast(Containers::Implementation::sizeRef(self).begin())); @@ -557,6 +562,44 @@ template class Steps, class T> Container return Containers::pyArrayViewHolder(sliced, empty ? py::none{} : std::move(owner)); } +Containers::Pair accessorsForFormat(const char* const format) { + /* The format string can be null, in which case B should be assumed: + https://docs.python.org/3/c-api/buffer.html#c.Py_buffer.format */ + const Containers::StringView formatString = format ? format : "B"_s; + + /* Matching the entries (and order) in StridedArrayViewPythonBindings.h */ + #define _c(string, type) \ + if(formatString == #string ## _s) return { \ + [](const char* item) { \ + return py::cast(*reinterpret_cast(item)); \ + }, \ + [](char* item, py::handle object) { \ + *reinterpret_cast(item) = py::cast(object); \ + }}; + _c(b, std::int8_t) + _c(B, std::uint8_t) + _c(h, std::int16_t) + _c(H, std::uint16_t) + _c(i, std::int32_t) + _c(I, std::uint32_t) + /** @todo numpy's np.int64 is a `l` even though struct says it's 4 bytes, + what to do?! */ + _c(q, std::int64_t) + _c(Q, std::uint64_t) + _c(f, float) + _c(d, double) + + return { + [](const char*) -> py::object { + PyErr_SetString(PyExc_NotImplementedError, "access to this data format is not implemented, sorry"); + throw py::error_already_set{}; + }, + [](char*, py::handle) { + PyErr_SetString(PyExc_NotImplementedError, "access to this data format is not implemented, sorry"); + throw py::error_already_set{}; + }}; +} + template void stridedArrayView(py::class_, Containers::PyArrayViewHolder>>& c) { /* Implicitly convertible from a buffer */ py::implicitly_convertible>(); @@ -571,7 +614,7 @@ template void stridedArrayView(py::class_::value ? 0 : PyBUF_WRITABLE)) != 0) + if(PyObject_GetBuffer(other.ptr(), &buffer, PyBUF_STRIDES|PyBUF_FORMAT|(std::is_const::value ? 0 : PyBUF_WRITABLE)) != 0) throw py::error_already_set{}; Containers::ScopeGuard e{&buffer, PyBuffer_Release}; @@ -581,6 +624,8 @@ template void stridedArrayView(py::class_ accessors = accessorsForFormat(buffer.format); + /* Calculate total memory size that spans the whole view. Mainly to make the constructor assert happy, not used otherwise */ std::size_t size = 0; @@ -592,12 +637,15 @@ template void stridedArrayView(py::class_{Containers::StridedArrayView{ - {static_cast(buffer.buf), size}, - Containers::StaticArrayView{reinterpret_cast(buffer.shape)}, - Containers::StaticArrayView{reinterpret_cast(buffer.strides)}}}, + return Containers::pyArrayViewHolder(Containers::PyStridedArrayView{ + Containers::StridedArrayView{ + {static_cast(buffer.buf), size}, + Containers::StaticArrayView{reinterpret_cast(buffer.shape)}, + Containers::StaticArrayView{reinterpret_cast(buffer.strides)}}, + buffer.format, + std::size_t(buffer.itemsize), + accessors.first(), + accessors.second()}, buffer.len ? py::reinterpret_borrow(buffer.obj) : py::none{}); }), "Construct from a buffer") @@ -613,7 +661,11 @@ template void stridedArrayView(py::class_&) { return dimensions; }, "Dimension count") .def_property_readonly("format", [](const Containers::PyStridedArrayView& self) { - return self.format; + /* A valid format shouldn't be an empty string. If it is, it's + because it was null originally. Return null in that case to + turn this into a None, consistently with how Python's + memoryview etc expects the format strings to be. */ + return self.format ? self.format.data() : nullptr; }, "Format of each item") .def_property_readonly("owner", [](const Containers::PyStridedArrayView& self) { return pyObjectHolderFor(self).owner; diff --git a/src/python/corrade/test/test_containers.py b/src/python/corrade/test/test_containers.py index cfe876e..ed858e3 100644 --- a/src/python/corrade/test/test_containers.py +++ b/src/python/corrade/test/test_containers.py @@ -275,9 +275,10 @@ class StridedArrayView1D(unittest.TestCase): self.assertEqual(bytes(b), b'hello') self.assertEqual(b.size, (5, )) self.assertEqual(b.stride, (1, )) - # We don't provide typed access for views created from buffers, so the - # format is unspecified to convey "generic data" - self.assertEqual(b.format, None) + # We get B as "general data", consistently with what memoryview() does + # for bytes/bytearray + self.assertEqual(memoryview(a).format, 'B') + self.assertEqual(b.format, 'B') self.assertEqual(b[2], ord('l')) self.assertEqual(sys.getrefcount(a), a_refcount + 1) @@ -296,6 +297,19 @@ class StridedArrayView1D(unittest.TestCase): del b self.assertTrue(sys.getrefcount(a), a_refcount) + def test_init_buffer_no_format(self): + a = b'hello' + + # ArrayView doesn't preserve the format, so this should then get None + # for the format, instead of B + b = containers.StridedArrayView1D(containers.ArrayView(a)) + self.assertEqual(len(b), 5) + self.assertEqual(bytes(b), b'hello') + self.assertEqual(b.size, (5, )) + self.assertEqual(b.stride, (1, )) + self.assertEqual(b.format, None) + self.assertEqual(b[2], ord('l')) + def test_init_buffer_empty(self): a = b'' a_refcount = sys.getrefcount(a) @@ -305,7 +319,7 @@ class StridedArrayView1D(unittest.TestCase): self.assertEqual(len(b), 0) self.assertEqual(b.size, (0, )) self.assertEqual(b.stride, (1, )) - self.assertEqual(b.format, None) + self.assertEqual(b.format, 'B') self.assertEqual(sys.getrefcount(a), a_refcount) def test_init_buffer_memoryview_obj(self): @@ -326,9 +340,10 @@ class StridedArrayView1D(unittest.TestCase): b = containers.MutableStridedArrayView1D(a) self.assertEqual(b.size, (5, )) self.assertEqual(b.stride, (1, )) - # We don't provide typed access for views created from buffers, so the - # format is unspecified to convey "generic data" - self.assertEqual(b.format, None) + # We get B as "general data", consistently with what memoryview() does + # for bytes/bytearray + self.assertEqual(memoryview(a).format, 'B') + self.assertEqual(b.format, 'B') self.assertEqual(b[4], ord('?')) b[4] = ord('!') self.assertEqual(b[4], ord('!')) @@ -1033,6 +1048,10 @@ class StridedArrayView4D(unittest.TestCase): containers.StridedArrayView4D().transposed(4, 3) class StridedArrayViewCustomType(unittest.TestCase): + # This tests exposing statically typed StridedArrayView instances from C++, + # see StridedArrayViewCustomDynamicType below for types specified + # dynamically and types inherited from the buffer protocol + def test_short(self): a = test_stridedarrayview.get_containers() self.assertEqual(type(a.view), containers.StridedArrayView2D) @@ -1080,10 +1099,7 @@ class StridedArrayViewCustomType(unittest.TestCase): # as memoryview can't handle their types class StridedArrayViewCustomDynamicType(unittest.TestCase): - # TODO test construction from a (typed) array or memory view, should work - # and now it doesn't - - def test_float(self): + def test_binding_float(self): a = test_stridedarrayview.MutableContainerDynamicType('f') self.assertEqual(a.view.size, (2, 3)) self.assertEqual(a.view.stride, (12, 4)) @@ -1097,7 +1113,7 @@ class StridedArrayViewCustomDynamicType(unittest.TestCase): self.assertEqual(a.view[1][1], 0.0) self.assertEqual(a.view[1][2], 0.0) - def test_int(self): + def test_binding_int(self): a = test_stridedarrayview.MutableContainerDynamicType('i') self.assertEqual(a.view.size, (2, 3)) self.assertEqual(a.view.stride, (12, 4)) @@ -1111,6 +1127,52 @@ class StridedArrayViewCustomDynamicType(unittest.TestCase): self.assertEqual(a.view[1][1], -773) self.assertEqual(a.view[1][2], 0) + def test_init_float(self): + a = array.array('f', [1.5, 0.75, 103.125]) + b = containers.MutableStridedArrayView1D(a) + self.assertEqual(b.size, (3,)) + self.assertEqual(b.stride, (4,)) + self.assertEqual(b.format, 'f') + b[1] *= 3.0 + self.assertEqual(b[0], 1.5) + self.assertEqual(b[1], 2.25) + self.assertEqual(b[2], 103.125) + + def test_init_short(self): + a = array.array('H', [12, 247, 65535, 2206]) + b = containers.MutableStridedArrayView1D(a) + self.assertEqual(b.size, (4,)) + self.assertEqual(b.stride, (2,)) + self.assertEqual(b.format, 'H') + b[2] -= 12765 + self.assertEqual(b[0], 12) + self.assertEqual(b[1], 247) + self.assertEqual(b[2], 52770) + self.assertEqual(b[3], 2206) + + def test_init_strided(self): + a = array.array('f', [2.5, 3.14, -1.75, 53.2]) + b = memoryview(a)[::2] + c = containers.StridedArrayView1D(b) + self.assertIs(c.owner, b) + self.assertEqual(c.size, (2,)) + self.assertEqual(c.stride, (8,)) + self.assertEqual(c.format, 'f') + self.assertEqual(c[0], 2.5) + self.assertEqual(c[1], -1.75) + + def test_init_access_not_implemented(self): + # TODO numpy np.int64 results in l even though python's struct + # classifies that as a 4-byte type, what the fuck?! + a = array.array('L', [1, 2, 3]) + b = containers.MutableStridedArrayView1D(a) + self.assertEqual(b.format, 'L') + + with self.assertRaisesRegex(NotImplementedError, "access to this data format is not implemented, sorry"): + b[2] + with self.assertRaisesRegex(NotImplementedError, "access to this data format is not implemented, sorry"): + b[1] = 5 + class BitArray(unittest.TestCase): def test_init(self): a = containers.BitArray() diff --git a/src/python/corrade/test/test_containers_numpy.py b/src/python/corrade/test/test_containers_numpy.py index f5ecaab..555fe08 100644 --- a/src/python/corrade/test/test_containers_numpy.py +++ b/src/python/corrade/test/test_containers_numpy.py @@ -34,8 +34,11 @@ except ModuleNotFoundError: raise unittest.SkipTest("numpy not installed") class StridedArrayViewCustomType(unittest.TestCase): - # short and mutable_int tested in test_containers, as for those memoryview - # works well... well, for one dimension it does + # This tests exposing statically typed StridedArrayView instances from C++, + # see StridedArrayViewCustomDynamicType below for types specified + # dynamically and types inherited from the buffer protocol. The short and + # mutable_int variants tested in test_containers, as for those memoryview + # works well... well, for one dimension it does. def test_mutable_vector3d(self): a = test_stridedarrayview.MutableContainer3d() @@ -132,7 +135,7 @@ class StridedArrayViewCustomType(unittest.TestCase): ]) class StridedArrayViewCustomDynamicType(unittest.TestCase): - def test_short_short(self): + def test_binding_short_short(self): a = test_stridedarrayview.MutableContainerDynamicType('hh') self.assertEqual(a.view.size, (2, 3)) self.assertEqual(a.view.stride, (12, 4)) @@ -155,3 +158,28 @@ class StridedArrayViewCustomDynamicType(unittest.TestCase): self.assertEqual(a.view[1][0], (22563, -17665)) self.assertEqual(a.view[1][1], (-22, 18)) self.assertEqual(a.view[1][2], (0, 0)) + + def test_init_long(self): + a = np.array([[1, 2, 3], [-4, 5000000000, 6]], np.dtype('q')) + self.assertEqual(a.dtype, 'int64') + + b = containers.MutableStridedArrayView2D(a) + self.assertEqual(b.size, (2, 3)) + self.assertEqual(b.stride, (24, 8)) + self.assertEqual(b.format, 'q') + b[1, 1] *= 2 + self.assertEqual(b[0, 2], 3) + self.assertEqual(b[1, 0], -4) + self.assertEqual(b[1, 1], 10000000000) + + def test_init_double(self): + a = np.array([[[1.0], [2.0]], [[-4.0], [5.0]]], np.dtype('d')) + self.assertEqual(a.dtype, 'float64') + + b = containers.MutableStridedArrayView3D(a) + self.assertEqual(b.size, (2, 2, 1)) + self.assertEqual(b.stride, (16, 8, 8)) + self.assertEqual(b.format, 'd') + b[1, 1, 0] *= -2.0 + self.assertEqual(b[0, 1, 0], 2.0) + self.assertEqual(b[1, 1, 0], -10.0)