diff --git a/doc/python/corrade.containers.rst b/doc/python/corrade.containers.rst index ff098bd..32b4e91 100644 --- a/doc/python/corrade.containers.rst +++ b/doc/python/corrade.containers.rst @@ -193,3 +193,144 @@ .. py:function:: corrade.containers.MutableStridedArrayView4D.transposed :raise IndexError: if :p:`a` or :p:`b` is not :py:`0`, :py:`1` :py:`2` or :py:`3` or if they're the same + +.. py:class:: corrade.containers.BitArray + + An owning counterpart to :ref:`BitArrayView` / :ref:`MutableBitArrayView`. + Holds its own data buffer, thus doesn't have an equivalent to + :ref:`BitArrayView.owner`. Implicitly convertible to :ref:`BitArrayView`, + :ref:`MutableBitArrayView`, :ref:`StridedBitArrayView1D` and + :ref:`MutableStridedBitArrayView1D`, so all APIs consuming (strided) bit + array views work with this type as well. + +.. py:class:: corrade.containers.BitArrayView + + Comparex to an :ref:`ArrayView`, which operates with byte-sized types, + provides a view on individual bits. Convertible from a :ref:`BitArrayView`. + See :ref:`StridedBitArrayView1D` and others for more generic bit views. :ref:`BitArrayView` is immutable, see :ref:`MutableBitArrayView` for the + mutable alternative. All slicing operations are supported, specifying a + non-trivial stride will return a :ref:`StridedBitArrayView1D` instead of a :ref:`BitArrayView`. + + `Memory ownership and reference counting`_ + ========================================== + + Similarly to :ref:`ArrayView`, the view keeps a reference to the original + memory owner object in the :ref:`owner` field. Slicing a view creates a + new view referencing the same original object, without any dependency on + the previous view. The :py:`owner` is :py:`None` if the view is empty. + +.. py:class:: corrade.containers.MutableBitArrayView + + Equivalent to :ref:`BitArrayView`, but implementing :ref:`__setitem__()` as + well. + +.. py:class:: corrade.containers.StridedBitArrayView1D + + Provides one-dimensional read-only view on a memory range with custom + stride values. See :ref:`StridedBitArrayView2D`, + :ref:`StridedBitArrayView3D`, :ref:`StridedBitArrayView4D`, + :ref:`MutableStridedBitArrayView1D` and others for multi-dimensional and + mutable equivalents. + + `Memory ownership and reference counting`_ + ========================================== + + Similarly to :ref:`BitArrayView`, the view keeps a reference to the + original memory owner object in the :ref:`owner` field. Slicing a view + creates a new view referencing the same original object, without any + dependency on the previous view. The :py:`owner` is :py:`None` if the view + is empty. + +.. py:function:: corrade.containers.StridedBitArrayView1D.flipped + :raise IndexError: if :p:`dimension` is not :py:`0` +.. py:function:: corrade.containers.StridedBitArrayView1D.broadcasted + :raise IndexError: if :p:`dimension` is not :py:`0` + +.. py:class:: corrade.containers.MutableStridedBitArrayView1D + + Equivalent to :ref:`StridedBitArrayView1D`, but implementing + :ref:`__setitem__()` as well. + +.. py:function:: corrade.containers.MutableStridedBitArrayView1D.flipped + :raise IndexError: if :p:`dimension` is not :py:`0` +.. py:function:: corrade.containers.MutableStridedBitArrayView1D.broadcasted + :raise IndexError: if :p:`dimension` is not :py:`0` + +.. py:class:: corrade.containers.StridedBitArrayView2D + + See :ref:`StridedBitArrayView1D` for more information. + +.. py:function:: corrade.containers.StridedBitArrayView2D.flipped + :raise IndexError: if :p:`dimension` is not :py:`0` or :py:`1` +.. py:function:: corrade.containers.StridedBitArrayView2D.broadcasted + :raise IndexError: if :p:`dimension` is not :py:`0` or :py:`1` +.. py:function:: corrade.containers.StridedBitArrayView2D.transposed + :raise IndexError: if :p:`a` or :p:`b` is not :py:`0` or :py:`1` or if + they're the same + +.. py:class:: corrade.containers.MutableStridedBitArrayView2D + + See :ref:`StridedBitArrayView1D` and :ref:`MutableStridedBitArrayView1D` + for more information. + +.. py:function:: corrade.containers.MutableStridedBitArrayView2D.flipped + :raise IndexError: if :p:`dimension` is not :py:`0` or :py:`1` +.. py:function:: corrade.containers.MutableStridedBitArrayView2D.broadcasted + :raise IndexError: if :p:`dimension` is not :py:`0` or :py:`1` +.. py:function:: corrade.containers.MutableStridedBitArrayView2D.transposed + :raise IndexError: if :p:`a` or :p:`b` is not :py:`0` or :py:`1` or if + they're the same + +.. py:class:: corrade.containers.StridedBitArrayView3D + + See :ref:`StridedBitArrayView1D` for more information. + +.. py:function:: corrade.containers.StridedBitArrayView3D.flipped + :raise IndexError: if :p:`dimension` is not :py:`0`, :py:`1` or :py:`2` +.. py:function:: corrade.containers.StridedBitArrayView3D.broadcasted + :raise IndexError: if :p:`dimension` is not :py:`0`, :py:`1` or :py:`2` +.. py:function:: corrade.containers.StridedBitArrayView3D.transposed + :raise IndexError: if :p:`a` or :p:`b` is not :py:`0`, :py:`1` or :py:`2` + or if they're the same + +.. py:class:: corrade.containers.MutableStridedBitArrayView3D + + See :ref:`StridedBitArrayView1D` and :ref:`MutableStridedBitArrayView1D` + for more information. + +.. py:function:: corrade.containers.MutableStridedBitArrayView3D.flipped + :raise IndexError: if :p:`dimension` is not :py:`0`, :py:`1` or :py:`2` +.. py:function:: corrade.containers.MutableStridedBitArrayView3D.broadcasted + :raise IndexError: if :p:`dimension` is not :py:`0`, :py:`1` or :py:`2` +.. py:function:: corrade.containers.MutableStridedBitArrayView3D.transposed + :raise IndexError: if :p:`a` or :p:`b` is not :py:`0`, :py:`1` or :py:`2` + or if they're the same + +.. py:class:: corrade.containers.StridedBitArrayView4D + + See :ref:`StridedBitArrayView1D` for more information. + +.. py:function:: corrade.containers.StridedBitArrayView4D.flipped + :raise IndexError: if :p:`dimension` is not :py:`0`, :py:`1` :py:`2` or + :py:`3` +.. py:function:: corrade.containers.StridedBitArrayView4D.broadcasted + :raise IndexError: if :p:`dimension` is not :py:`0`, :py:`1` :py:`2` or + :py:`3` +.. py:function:: corrade.containers.StridedBitArrayView4D.transposed + :raise IndexError: if :p:`a` or :p:`b` is not :py:`0`, :py:`1` :py:`2` or + :py:`3` or if they're the same + +.. py:class:: corrade.containers.MutableStridedBitArrayView4D + + See :ref:`StridedBitArrayView1D` and :ref:`MutableStridedBitArrayView1D` + for more information. + +.. py:function:: corrade.containers.MutableStridedBitArrayView4D.flipped + :raise IndexError: if :p:`dimension` is not :py:`0`, :py:`1` :py:`2` or + :py:`3` +.. py:function:: corrade.containers.MutableStridedBitArrayView4D.broadcasted + :raise IndexError: if :p:`dimension` is not :py:`0`, :py:`1` :py:`2` or + :py:`3` +.. py:function:: corrade.containers.MutableStridedBitArrayView4D.transposed + :raise IndexError: if :p:`a` or :p:`b` is not :py:`0`, :py:`1` :py:`2` or + :py:`3` or if they're the same diff --git a/doc/python/pages/changelog.rst b/doc/python/pages/changelog.rst index 6f89411..7b34e2b 100644 --- a/doc/python/pages/changelog.rst +++ b/doc/python/pages/changelog.rst @@ -39,6 +39,11 @@ Changelog :ref:`magnum.BUILD_DEPRECATED` constants, as some features may work differently depending on these being enabled or not and it's useful to be able to query this +- Exposed the new :ref:`containers.BitArray`, :ref:`containers.BitArrayView`, + :ref:`containers.StridedBitArrayView1D` containers, their mutable and + multi-dimensional counterparts as well as the + :ref:`containers.StridedArrayView*D.slice_bit() ` + utility - Exposed missing :ref:`Vector4` constructor from a :ref:`Vector3` and a W component and :ref:`Vector3` from :ref:`Vector2` and a Z component - Renamed :py:`Matrix3.from()` / :py:`Matrix4.from()` to :ref:`Matrix3.from_()` diff --git a/src/python/corrade/containers.cpp b/src/python/corrade/containers.cpp index 5f24f6c..40228bc 100644 --- a/src/python/corrade/containers.cpp +++ b/src/python/corrade/containers.cpp @@ -26,6 +26,8 @@ #include #include /* so ArrayView is convertible from python array */ #include +#include +#include #include #include "Corrade/Containers/PythonBindings.h" @@ -192,6 +194,57 @@ template void mutableArrayView(py::class_, Con }, "Set a value at given position", py::arg("i"), py::arg("value")); } +template void bitArrayView(py::class_, Containers::PyArrayViewHolder>>& c) { + c + /* Constructor */ + .def(py::init(), "Default constructor") + .def(py::init([](Containers::BitArray& other) { + return pyArrayViewHolder(Containers::BasicBitArrayView{other}, other.size() ? py::cast(other) : py::none{}); + }), "Construct a view on a bit array", py::arg("array")) + + /* Offset, size and memory owning object */ + .def_property_readonly("offset", &Containers::BasicBitArrayView::offset, "Bit offset") + .def("__len__", &Containers::BasicBitArrayView::size, "Size in bits") + .def_property_readonly("owner", [](const Containers::BasicBitArrayView& self) { + return pyObjectHolderFor(self).owner; + }, "Memory owner object") + + /* Single item retrieval. Need to raise IndexError in order to allow + iteration: https://docs.python.org/3/reference/datamodel.html#object.__getitem__ */ + .def("__getitem__", [](const Containers::BasicBitArrayView& self, std::size_t i) { + if(i >= self.size()) { + PyErr_SetNone(PyExc_IndexError); + throw py::error_already_set{}; + } + return self[i]; + }, "Bit at given position", py::arg("i")) + + /* Slicing */ + .def("__getitem__", [](const Containers::BasicBitArrayView& self, py::slice slice) -> py::object { + const Slice calculated = calculateSlice(slice, self.size()); + + /* Non-trivial stride, return a different type */ + if(calculated.step != 1 || calculated.flip) { + return pyCastButNotShitty(arrayViewStridedSlice(Containers::BasicStridedBitArrayView<1, T>{self}, calculated, pyObjectHolderFor(self).owner)); + } + + /* Usual business */ + auto sliced = self.slice(calculated.start, calculated.stop); + return pyCastButNotShitty(Containers::pyArrayViewHolder(sliced, sliced.size() ? pyObjectHolderFor(self).owner : py::none{})); + }, "Slice the view", py::arg("slice")); +} + +template void mutableBitArrayView(py::class_, Containers::PyArrayViewHolder>>& c) { + c + .def("__setitem__", [](const Containers::BasicBitArrayView& self, std::size_t i, bool value) { + if(i >= self.size()) { + PyErr_SetNone(PyExc_IndexError); + throw py::error_already_set{}; + } + self.set(i, value); + }, "Set a bit at given position", py::arg("i"), py::arg("value")); +} + /* Tuple for given dimension */ template struct DimensionsTuple; template struct DimensionsTuple<1, T> { typedef std::tuple Type; }; @@ -528,6 +581,13 @@ template void stridedArrayView(py::class_& self, std::size_t index) { + if(index >= self.itemsize*8) { + PyErr_Format(PyExc_IndexError, "index %zu out of range for %zu bits", index, self.itemsize*8); + throw py::error_already_set{}; + } + return Containers::pyArrayViewHolder(self.sliceBit(index), pyObjectHolderFor(self).owner); + }, "Slice to a bit", py::arg("index")) .def("flipped", [](const Containers::PyStridedArrayView& self, const std::size_t dimension) { return Containers::pyArrayViewHolder(StridedOperation::flipped(self, dimension), pyObjectHolderFor(self).owner); }, "Flip a dimension", py::arg("dimension")) @@ -613,11 +673,180 @@ template void mutableStridedArrayViewND(py::class_ void stridedBitArrayView(py::class_, Containers::PyArrayViewHolder>>& c) { + c + /* Constructor */ + .def(py::init(), "Default constructor") + + /* Length, size/stride tuple, dimension count and memory owning object */ + .def_property_readonly("offset", &Containers::BasicStridedBitArrayView::offset, "Bit offset") + .def("__len__", [](const Containers::BasicStridedBitArrayView& self) { + return Containers::Size(self.size())[0]; + }, "View size in the top-level dimension") + .def_property_readonly("size", [](const Containers::BasicStridedBitArrayView& self) { + return size(self.size()); + }, "View size in each dimension") + .def_property_readonly("stride", [](const Containers::BasicStridedBitArrayView& self) { + return stride(self.stride()); + }, "View stride in each dimension") + .def_property_readonly("dimensions", [](const Containers::BasicStridedBitArrayView&) { return dimensions; }, "Dimension count") + .def_property_readonly("owner", [](const Containers::BasicStridedBitArrayView& self) { + return pyObjectHolderFor(self).owner; + }, "Memory owner object") + + /* Slicing of the top dimension */ + .def("__getitem__", [](const Containers::BasicStridedBitArrayView& self, py::slice slice) { + const Slice calculated = calculateSlice(slice, Containers::Size{self.size()}[0]); + return arrayViewStridedSlice(self, calculated, pyObjectHolderFor(self).owner); + }, "Slice the view", py::arg("slice")) + + /* Fancy operations */ + .def("flipped", [](const Containers::BasicStridedBitArrayView& self, const std::size_t dimension) { + return Containers::pyArrayViewHolder(StridedOperation::flipped(self, dimension), pyObjectHolderFor(self).owner); + }, "Flip a dimension", py::arg("dimension")) + .def("broadcasted", [](const Containers::BasicStridedBitArrayView& self, const std::size_t dimension, std::size_t size) { + return Containers::pyArrayViewHolder(StridedOperation::broadcasted(self, dimension, size), pyObjectHolderFor(self).owner); + }, "Broadcast a dimension", py::arg("dimension"), py::arg("size")); +} + +template void stridedBitArrayView1D(py::class_, Containers::PyArrayViewHolder>>& c) { + c + .def(py::init([](Containers::BitArray& other) { + return pyArrayViewHolder(Containers::BasicStridedBitArrayView<1, T>{Containers::BasicBitArrayView{other}}, other.size() ? py::cast(other) : py::none{}); + }), "Construct a view on a bit array", py::arg("array")) + .def(py::init([](Containers::BasicBitArrayView& other) { + return pyArrayViewHolder(Containers::BasicStridedBitArrayView<1, T>{other}, pyObjectHolderFor(other).owner); + }), "Construct from a bit array view", py::arg("view")) + /* Construction of a const view from a mutable is done directly in + containers() at the end */ + + /* Single item retrieval. Need to raise IndexError in order to allow + iteration: https://docs.python.org/3/reference/datamodel.html#object.__getitem__ */ + .def("__getitem__", [](const Containers::BasicStridedBitArrayView<1, T>& self, std::size_t i) { + if(i >= self.size()) { + PyErr_SetNone(PyExc_IndexError); + throw py::error_already_set{}; + } + return self[i]; + }, "Bit at given position", py::arg("i")); +} + +template void stridedBitArrayViewND(py::class_, Containers::PyArrayViewHolder>>& c) { + c + /* Sub-view and single bit retrieval. Need to raise IndexError in order + to allow iteration: https://docs.python.org/3/reference/datamodel.html#object.__getitem__ */ + .def("__getitem__", [](const Containers::BasicStridedBitArrayView& self, std::size_t i) { + if(i >= Containers::Size{self.size()}[0]) { + PyErr_SetNone(PyExc_IndexError); + throw py::error_already_set{}; + } + return Containers::pyArrayViewHolder(self[i], pyObjectHolderFor(self).owner); + }, "Sub-view at given position", py::arg("i")) + .def("__getitem__", [](const Containers::BasicStridedBitArrayView& self, const typename DimensionsTuple::Type& iTuple) { + Containers::Size iSize{NoInit}; + for(std::size_t j = 0; j != dimensions; ++j) { + const std::size_t i = dimensionsTupleGet(iTuple, j); + if(i >= self.size()[j]) { + PyErr_SetNone(PyExc_IndexError); + throw py::error_already_set{}; + } + iSize[j] = i; + } + return self[iSize]; + }, "Bit at given position", py::arg("i")) + + /* Multi-dimensional slicing */ + .def("__getitem__", [](const Containers::BasicStridedBitArrayView& self, const typename DimensionsTuple::Type& slice) { + return stridedArrayViewSlice(self, slice, pyObjectHolderFor(self).owner); + }, "Slice the view", py::arg("slice")) + + /* Fancy operations */ + .def("transposed", [](const Containers::BasicStridedBitArrayView& self, const std::size_t a, std::size_t b) { + return Containers::pyArrayViewHolder(StridedOperation::transposed(self, a, b), pyObjectHolderFor(self).owner); + }, "Transpose two dimensions", py::arg("a"), py::arg("b")); +} + +void mutableStridedBitArrayView1D(py::class_, Containers::PyArrayViewHolder>>& c) { + c + .def("__setitem__", [](const Containers::BasicStridedBitArrayView<1, char>& self, const std::size_t i, bool value) { + if(i >= self.size()) { + PyErr_SetNone(PyExc_IndexError); + throw py::error_already_set{}; + } + self.set(i, value); + }, "Set a bit at given position", py::arg("i"), py::arg("value")); +} + +template void mutableStridedBitArrayViewND(py::class_, Containers::PyArrayViewHolder>>& c) { + c + .def("__setitem__", [](const Containers::BasicStridedBitArrayView& self, const typename DimensionsTuple::Type& iTuple, bool value) { + Containers::Size iSize{NoInit}; + for(std::size_t j = 0; j != dimensions; ++j) { + const std::size_t i = dimensionsTupleGet(iTuple, j); + if(i >= self.size()[j]) { + PyErr_SetNone(PyExc_IndexError); + throw py::error_already_set{}; + } + iSize[j] = i; + } + self.set(iSize, value); + }, "Set a bit at given position", py::arg("i"), py::arg("value")); +} + } void containers(py::module_& m) { m.doc() = "Container implementations"; + py::class_{m, "BitArray", "Bit array"} + /* Constructors */ + .def_static("value_init", [](std::size_t size) { + return Containers::BitArray{ValueInit, size}; + }, "Construct a zero-initialized array", py::arg("size")) + .def_static("no_init", [](std::size_t size) { + return Containers::BitArray{NoInit, size}; + }, "Construct an array without initializing its contents", py::arg("size")) + .def_static("direct_init", [](std::size_t size, bool value) { + return Containers::BitArray{DirectInit, size, value}; + }, "Construct an array initialized to a particular bit value", py::arg("size"), py::arg("value")) + .def(py::init(), "Default constructor") + + /* Offset, size and memory owning object */ + .def_property_readonly("offset", &Containers::BitArray::offset, "Bit offset") + .def("__len__", &Containers::BitArray::size, "Size in bits") + + /* Single bit access. Need to raise IndexError in order to allow + iteration: https://docs.python.org/3/reference/datamodel.html#object.__getitem__ */ + .def("__getitem__", [](const Containers::BitArray& self, std::size_t i) { + if(i >= self.size()) { + PyErr_SetNone(PyExc_IndexError); + throw py::error_already_set{}; + } + return self[i]; + }, "Bit at given position", py::arg("i")) + .def("__setitem__", [](Containers::BitArray& self, std::size_t i, const bool value) { + if(i >= self.size()) { + PyErr_SetNone(PyExc_IndexError); + throw py::error_already_set{}; + } + self.set(i, value); + }, "Set a bit at given position", py::arg("i"), py::arg("value")) + + /* Slicing */ + .def("__getitem__", [](Containers::BitArray& self, py::slice slice) -> py::object { + const Slice calculated = calculateSlice(slice, self.size()); + + /* Non-trivial stride, return a different type */ + if(calculated.step != 1 || calculated.flip) { + /** @todo what's up with the cast?! */ + return pyCastButNotShitty(arrayViewStridedSlice(Containers::MutableStridedBitArrayView1D{Containers::MutableBitArrayView{self}}, calculated, py::cast(self))); + } + + /* Usual business */ + const auto sliced = self.slice(calculated.start, calculated.stop); + return pyCastButNotShitty(Containers::pyArrayViewHolder(sliced, sliced.size() ? py::cast(self) : py::none{})); + }, "Slice the view", py::arg("slice")); + py::class_, Containers::PyArrayViewHolder>> arrayView_{m, "ArrayView", "Array view", py::buffer_protocol{}}; arrayView(arrayView_); @@ -627,6 +856,59 @@ void containers(py::module_& m) { arrayView(mutableArrayView_); mutableArrayView(mutableArrayView_); + py::class_> bitArrayView_{m, + "BitArrayView", "Bit array view"}; + bitArrayView(bitArrayView_); + + py::class_> mutableBitArrayView_{m, + "MutableBitArrayView", "Mutable bit array view"}; + bitArrayView(mutableBitArrayView_); + mutableBitArrayView(mutableBitArrayView_); + + /* These have to be defined before StridedArrayView types in order to have + them ready for the return type of sliceBit() */ + py::class_> stridedBitArrayView1D_{m, + "StridedBitArrayView1D", "One-dimensional bit array view with stride information"}; + py::class_> stridedBitArrayView2D_{m, + "StridedBitArrayView2D", "Two-dimensional bit array view with stride information"}; + py::class_> stridedBitArrayView3D_{m, + "StridedBitArrayView3D", "Three-dimensional bit array view with stride information"}; + py::class_> stridedBitArrayView4D_{m, + "StridedBitArrayView4D", "Four-dimensional bit array view with stride information"}; + stridedBitArrayView(stridedBitArrayView1D_); + stridedBitArrayView1D(stridedBitArrayView1D_); + stridedBitArrayView1D_ + .def(py::init([](Containers::MutableBitArrayView& other) { + return pyArrayViewHolder(Containers::StridedBitArrayView1D{other}, pyObjectHolderFor(other).owner); + }), "Construct from a bit array view", py::arg("view")); + stridedBitArrayView(stridedBitArrayView2D_); + stridedBitArrayViewND(stridedBitArrayView2D_); + stridedBitArrayView(stridedBitArrayView3D_); + stridedBitArrayViewND(stridedBitArrayView3D_); + stridedBitArrayView(stridedBitArrayView4D_); + stridedBitArrayViewND(stridedBitArrayView4D_); + + py::class_> mutableStridedBitArrayView1D_{m, + "MutableStridedBitArrayView1D", "Mutable one-dimensional bit array view with stride information", py::buffer_protocol{}}; + py::class_> mutableStridedBitArrayView2D_{m, + "MutableStridedBitArrayView2D", "Mutable two-dimensional bit array view with stride information", py::buffer_protocol{}}; + py::class_> mutableStridedBitArrayView3D_{m, + "MutableStridedBitArrayView3D", "Mutable three-dimensional bit array view with stride information", py::buffer_protocol{}}; + py::class_> mutableStridedBitArrayView4D_{m, + "MutableStridedBitArrayView4D", "Mutable four-dimensional bit array view with stride information", py::buffer_protocol{}}; + stridedBitArrayView(mutableStridedBitArrayView1D_); + stridedBitArrayView1D(mutableStridedBitArrayView1D_); + stridedBitArrayView(mutableStridedBitArrayView2D_); + stridedBitArrayViewND(mutableStridedBitArrayView2D_); + stridedBitArrayView(mutableStridedBitArrayView3D_); + stridedBitArrayViewND(mutableStridedBitArrayView3D_); + stridedBitArrayView(mutableStridedBitArrayView4D_); + stridedBitArrayViewND(mutableStridedBitArrayView4D_); + mutableStridedBitArrayView1D(mutableStridedBitArrayView1D_); + mutableStridedBitArrayViewND(mutableStridedBitArrayView2D_); + mutableStridedBitArrayViewND(mutableStridedBitArrayView3D_); + mutableStridedBitArrayViewND(mutableStridedBitArrayView4D_); + py::class_, Containers::PyArrayViewHolder>> stridedArrayView1D_{m, "StridedArrayView1D", "One-dimensional array view with stride information", py::buffer_protocol{}}; py::class_, Containers::PyArrayViewHolder>> stridedArrayView2D_{m, diff --git a/src/python/corrade/test/test_containers.py b/src/python/corrade/test/test_containers.py index c8141fc..519c201 100644 --- a/src/python/corrade/test/test_containers.py +++ b/src/python/corrade/test/test_containers.py @@ -397,21 +397,24 @@ class StridedArrayView1D(unittest.TestCase): def test_ops(self): a = b'01234567' - v = memoryview(a).cast('b', shape=[8]) - b = containers.StridedArrayView1D(v).flipped(0) + # slice_bit() tested extensively in StridedBitArrayView tests + + b = containers.StridedArrayView1D(a).flipped(0) self.assertEqual(b.size, (8,)) self.assertEqual(b.stride, (-1,)) self.assertEqual(bytes(b), b'76543210') - d = containers.StridedArrayView1D(v)[3:4].broadcasted(0, 5) - self.assertEqual(d.size, (5,)) - self.assertEqual(d.stride, (0,)) - self.assertEqual(bytes(d), b'33333') + c = containers.StridedArrayView1D(a)[3:4].broadcasted(0, 5) + self.assertEqual(c.size, (5,)) + self.assertEqual(c.stride, (0,)) + self.assertEqual(bytes(c), b'33333') def test_ops_invalid(self): a = b'00' + with self.assertRaisesRegex(IndexError, "index 8 out of range for 8 bits"): + containers.StridedArrayView1D(a).slice_bit(8) with self.assertRaisesRegex(IndexError, "dimension 1 out of range for a 1D view"): containers.StridedArrayView1D().flipped(1) with self.assertRaisesRegex(IndexError, "dimension 1 out of range for a 1D view"): @@ -568,6 +571,19 @@ class StridedArrayView2D(unittest.TestCase): self.assertEqual(sys.getrefcount(b), b_refcount) self.assertEqual(sys.getrefcount(a), a_refcount + 1) + def test_slice_empty(self): + a = memoryview(b'01234567' + b'456789ab' + b'89abcdef').cast('b', shape=[3, 8]) + a_refcount = sys.getrefcount(a) + + b = containers.StridedArrayView2D(a)[1:1] + self.assertEqual(b.size, (0, 8)) + + # Empty view, original data not referenced at all + self.assertIs(b.owner, None) + self.assertEqual(sys.getrefcount(a), a_refcount) + def test_slice_multidimensional(self): a = memoryview(b'01234567' b'456789ab' @@ -684,6 +700,8 @@ class StridedArrayView2D(unittest.TestCase): b'89abcdef') v = memoryview(a).cast('b', shape=[3, 8]) + # slice_bit() tested extensively in StridedBitArrayView tests + b = containers.StridedArrayView2D(v).transposed(0, 1).flipped(0) self.assertEqual(b.size, (8, 3)) self.assertEqual(b.stride, (-1, 8)) @@ -708,6 +726,8 @@ class StridedArrayView2D(unittest.TestCase): a = b'00' v = memoryview(a).cast('b', shape=[1, 2]) + with self.assertRaisesRegex(IndexError, "index 8 out of range for 8 bits"): + containers.StridedArrayView2D(v).slice_bit(8) with self.assertRaisesRegex(IndexError, "dimension 2 out of range for a 2D view"): containers.StridedArrayView2D().flipped(2) with self.assertRaisesRegex(IndexError, "dimension 2 out of range for a 2D view"): @@ -738,6 +758,8 @@ class StridedArrayView2D(unittest.TestCase): self.assertEqual(sys.getrefcount(a), a_refcount + 1) self.assertEqual(sys.getrefcount(b), b_refcount + 1) +# Slicing is tested extensively for StridedArrayView2D, this checks just what +# differs, like constructors and fancy operations class StridedArrayView3D(unittest.TestCase): def test_init_buffer(self): a = (b'01234567' @@ -789,6 +811,8 @@ class StridedArrayView3D(unittest.TestCase): b'456789ab') v = memoryview(a).cast('b', shape=[2, 3, 8]) + # slice_bit() tested extensively in StridedBitArrayView tests + b = containers.StridedArrayView3D(v).transposed(0, 1).flipped(0) self.assertEqual(b.size, (3, 2, 8)) self.assertEqual(b.stride, (-8, 24, 1)) @@ -818,6 +842,8 @@ class StridedArrayView3D(unittest.TestCase): a = b'00' v = memoryview(a).cast('b', shape=[1, 1, 2]) + with self.assertRaisesRegex(IndexError, "index 8 out of range for 8 bits"): + containers.StridedArrayView3D(v).slice_bit(8) with self.assertRaisesRegex(IndexError, "dimension 3 out of range for a 3D view"): containers.StridedArrayView3D().flipped(3) with self.assertRaisesRegex(IndexError, "dimension 3 out of range for a 3D view"): @@ -880,6 +906,8 @@ class StridedArrayView4D(unittest.TestCase): b'456789ab') v = memoryview(a).cast('b', shape=[2, 1, 3, 8]) + # slice_bit() tested extensively in StridedBitArrayView tests + b = containers.StridedArrayView4D(v).transposed(0, 2).flipped(0) self.assertEqual(b.size, (3, 1, 2, 8)) self.assertEqual(b.stride, (-8, 24, 24, 1)) @@ -909,6 +937,8 @@ class StridedArrayView4D(unittest.TestCase): a = b'00' v = memoryview(a).cast('b', shape=[1, 1, 1, 2]) + with self.assertRaisesRegex(IndexError, "index 8 out of range for 8 bits"): + containers.StridedArrayView4D(v).slice_bit(8) with self.assertRaisesRegex(IndexError, "dimension 4 out of range for a 4D view"): containers.StridedArrayView4D().flipped(4) with self.assertRaisesRegex(IndexError, "dimension 4 out of range for a 4D view"): @@ -966,6 +996,9 @@ 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): a = test_stridedarrayview.MutableContainerDynamicType('f') self.assertEqual(a.view.size, (2, 3)) @@ -994,6 +1027,1145 @@ class StridedArrayViewCustomDynamicType(unittest.TestCase): self.assertEqual(a.view[1][1], -773) self.assertEqual(a.view[1][2], 0) +class BitArray(unittest.TestCase): + def test_init(self): + a = containers.BitArray() + self.assertEqual(len(a), 0) + self.assertEqual(a.offset, 0) + + def test_value_init(self): + a = containers.BitArray.value_init(3) + self.assertEqual(len(a), 3) + self.assertEqual(a.offset, 0) + self.assertEqual(a[0], False) + self.assertEqual(a[1], False) + self.assertEqual(a[2], False) + + def test_no_init(self): + a = containers.BitArray.no_init(3) + self.assertEqual(len(a), 3) + self.assertEqual(a.offset, 0) + # Values can be anything + + def test_direct_init(self): + a = containers.BitArray.direct_init(3, False) + b = containers.BitArray.direct_init(3, True) + self.assertEqual(len(a), 3) + self.assertEqual(len(b), 3) + self.assertEqual(a.offset, 0) + self.assertEqual(b.offset, 0) + self.assertEqual(a[0], False) + self.assertEqual(a[1], False) + self.assertEqual(a[2], False) + self.assertEqual(b[0], True) + self.assertEqual(b[1], True) + self.assertEqual(b[2], True) + + def test_mutable_access(self): + a = containers.BitArray.value_init(5) + a[2] = True + a[4] = True + self.assertEqual(a[0], False) + self.assertEqual(a[1], False) + self.assertEqual(a[2], True) + self.assertEqual(a[3], False) + self.assertEqual(a[4], True) + + def test_access_invalid(self): + a = containers.BitArray.value_init(3) + + with self.assertRaises(IndexError): + a[3] + with self.assertRaises(IndexError): + a[3] = True + + def test_slice(self): + a = containers.BitArray.value_init(10) + a[3] = True + a[5] = True + a[6] = True + a_refcount = sys.getrefcount(a) + + b = a[3:-2] + self.assertIsInstance(b, containers.MutableBitArrayView) + self.assertIs(b.owner, a) + self.assertEqual(len(b), 5) + self.assertEqual(b.offset, 3) + self.assertEqual(b[0], True) + self.assertEqual(b[1], False) + self.assertEqual(b[2], True) + self.assertEqual(b[3], True) + self.assertEqual(b[4], False) + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + + # Deleting a slice should reduce a's refcount again + del b + self.assertEqual(sys.getrefcount(a), a_refcount) + + def test_slice_empty(self): + a = containers.BitArray.value_init(5) + a_refcount = sys.getrefcount(a) + + # Because this is out of bounds, slice.start = slice.stop + b = a[7:8] + self.assertEqual(len(b), 0) + self.assertEqual(b.offset, 5) + + # Empty view, original data not referenced at all + self.assertIs(b.owner, None) + self.assertEqual(sys.getrefcount(a), a_refcount) + + def test_slice_invalid(self): + with self.assertRaisesRegex(ValueError, "slice step cannot be zero"): + containers.BitArray()[::0] + + def test_slice_stride(self): + a = containers.BitArray.value_init(15) + a[2] = True + a[5] = True + a[7] = False + a[11] = True + a_refcount = sys.getrefcount(a) + + b = a[2:-3:3] + self.assertIsInstance(b, containers.MutableStridedBitArrayView1D) + self.assertIs(b.owner, a) + self.assertEqual(len(b), 4) + self.assertEqual(b.offset, 2) + self.assertEqual(b.size, (4,)) + self.assertEqual(b.stride, (3,)) + self.assertEqual(b[0], True) + self.assertEqual(b[1], True) + self.assertEqual(b[2], False) + self.assertEqual(b[3], True) + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + + # Deleting a slice should reduce a's refcount again + del b + self.assertEqual(sys.getrefcount(a), a_refcount) + + def test_slice_stride_empty(self): + a = containers.BitArray.value_init(5) + a_refcount = sys.getrefcount(a) + + # Because this is out of bounds, slice.start = slice.stop + b = a[7:8:2] + self.assertEqual(len(b), 0) + self.assertEqual(b.offset, 5) + + # Empty view, original data not referenced at all + self.assertIs(b.owner, None) + self.assertEqual(sys.getrefcount(a), a_refcount) + + def test_slice_stride_negative(self): + a = containers.BitArray.value_init(15) + a[2] = True + a[5] = True + a[7] = False + a[11] = True + + # Check consistency with slices on lists + b1 = list(a)[-4:1:-3] # Like [2:-4:3] above, but reverted + self.assertEqual(len(b1), 4) + self.assertEqual(b1[0], True) + self.assertEqual(b1[1], False) + self.assertEqual(b1[2], True) + self.assertEqual(b1[3], True) + + b2 = a[-4:1:-3] + self.assertIsInstance(b2, containers.MutableStridedBitArrayView1D) + self.assertEqual(len(b2), 4) + self.assertEqual(b2.offset, 3) + self.assertEqual(b2.size, (4,)) + self.assertEqual(b2.stride, (-3,)) + self.assertEqual(b2[0], True) + self.assertEqual(b2[1], False) + self.assertEqual(b2[2], True) + self.assertEqual(b2[3], True) + + def test_slice_stride_reverse(self): + a = containers.BitArray.value_init(5) + a[0] = True + a[1] = True + a[2] = False + a[3] = True + a[4] = False + + # slice.stop = -1 + b = a[::-1] + self.assertEqual(len(b), 5) + self.assertEqual(b[0], False) + self.assertEqual(b[1], True) + self.assertEqual(b[2], False) + self.assertEqual(b[3], True) + self.assertEqual(b[4], True) + +class BitArrayView(unittest.TestCase): + def test_init(self): + a = containers.BitArrayView() + b = containers.MutableBitArrayView() + self.assertIs(a.owner, None) + self.assertIs(b.owner, None) + self.assertEqual(len(a), 0) + self.assertEqual(len(b), 0) + self.assertEqual(a.offset, 0) + self.assertEqual(b.offset, 0) + + def test_init_from_array(self): + data = containers.BitArray.value_init(6) + data[3] = True + data_refcount = sys.getrefcount(data) + + a = containers.BitArrayView(data) + b = containers.MutableBitArrayView(data) + self.assertIs(a.owner, data) + self.assertIs(b.owner, data) + self.assertEqual(len(a), 6) + self.assertEqual(len(b), 6) + self.assertEqual(a.offset, 0) + self.assertEqual(b.offset, 0) + self.assertEqual(a[3], True) + self.assertEqual(b[3], True) + self.assertEqual(sys.getrefcount(data), data_refcount + 2) + + self.assertEqual(data[4], False) + b[4] = True + self.assertEqual(data[3], True) + + # Deleting the views should reduce array's refcount again + del a + del b + self.assertEqual(sys.getrefcount(data), data_refcount) + + def test_init_from_empty_array(self): + data = containers.BitArray() + data_refcount = sys.getrefcount(data) + + # Empty view, original data not referenced at all + a = containers.BitArrayView(data) + self.assertIs(a.owner, None) + self.assertEqual(sys.getrefcount(data), data_refcount) + + def test_init_mutable_from_immutable(self): + # It's possible to construct only from a BitArray that's always + # mutable, nothing to test + pass + + def test_access_invalid(self): + data = containers.BitArray.value_init(3) + a = containers.BitArrayView(data) + b = containers.MutableBitArrayView(data) + + with self.assertRaises(IndexError): + a[3] + with self.assertRaises(IndexError): + b[3] = True + + def test_mutable_access_invalid(self): + data = containers.BitArray.value_init(3) + a = containers.BitArrayView(data) + + # This is Python's own exception + with self.assertRaises(TypeError): + a[0] = True + + # Mostly the same as BitArray.test_slice() except for the additional + # referencing logic that skips intermediate views + def test_slice(self): + data = containers.BitArray.value_init(10) + data[3] = True + data[5] = True + data[6] = True + data_refcount = sys.getrefcount(data) + + a = containers.BitArrayView(data) + a_refcount = sys.getrefcount(a) + self.assertEqual(sys.getrefcount(data), data_refcount + 1) + + # When slicing, a's refcount should not change but data's refcount + # should increase + b = a[3:-2] + self.assertIsInstance(b, containers.BitArrayView) + self.assertIs(b.owner, data) + self.assertEqual(len(b), 5) + self.assertEqual(b.offset, 3) + self.assertEqual(b[0], True) + self.assertEqual(b[1], False) + self.assertEqual(b[2], True) + self.assertEqual(b[3], True) + self.assertEqual(b[4], False) + self.assertEqual(sys.getrefcount(data), data_refcount + 2) + self.assertEqual(sys.getrefcount(a), a_refcount) + + # Deleting a slice should reduce data's refcount again, keep a's + # unchanged + del b + self.assertEqual(sys.getrefcount(data), data_refcount + 1) + self.assertEqual(sys.getrefcount(a), a_refcount) + + # Mostly the same as BitArray.test_slice_empty() + def test_slice_empty(self): + data = containers.BitArray.value_init(5) + data_refcount = sys.getrefcount(data) + + # Because this is out of bounds, slice.start = slice.stop + b = containers.BitArrayView(data)[7:8] + self.assertEqual(len(b), 0) + self.assertEqual(b.offset, 5) + + # Empty view, original data not referenced at all + self.assertIs(b.owner, None) + self.assertEqual(sys.getrefcount(data), data_refcount) + + def test_slice_invalid(self): + with self.assertRaisesRegex(ValueError, "slice step cannot be zero"): + containers.BitArrayView()[::0] + + # Mostly the same as BitArray.test_slice_stride() except for the additional + # referencing logic that skips intermediate views + def test_slice_stride(self): + data = containers.BitArray.value_init(15) + data[2] = True + data[5] = True + data[7] = False + data[11] = True + data_refcount = sys.getrefcount(data) + + a = containers.BitArrayView(data) + self.assertEqual(sys.getrefcount(data), data_refcount + 1) + + b = a[2:-3:3] + self.assertIsInstance(b, containers.StridedBitArrayView1D) + self.assertIs(b.owner, data) + self.assertEqual(len(b), 4) + self.assertEqual(b.offset, 2) + self.assertEqual(b.size, (4,)) + self.assertEqual(b.stride, (3,)) + self.assertEqual(b[0], True) + self.assertEqual(b[1], True) + self.assertEqual(b[2], False) + self.assertEqual(b[3], True) + self.assertEqual(sys.getrefcount(data), data_refcount + 2) + + # Deleting the original view should reduce data's refcount again -- b + # shouldn't depend on the view but on the original array + del a + self.assertEqual(sys.getrefcount(data), data_refcount + 1) + del b + self.assertEqual(sys.getrefcount(data), data_refcount) + + # Mostly the same as BitArray.test_slice_stride_empty() + def test_slice_stride_empty(self): + a = containers.BitArray.value_init(5) + a_refcount = sys.getrefcount(a) + + # Because this is out of bounds, slice.start = slice.stop + b = containers.BitArrayView(a)[7:8:2] + self.assertEqual(len(b), 0) + self.assertEqual(b.offset, 5) + + # Empty view, original data not referenced at all + self.assertIs(b.owner, None) + self.assertEqual(sys.getrefcount(a), a_refcount) + + # Mostly the same as BitArray.test_slice_stride_negative() + def test_slice_stride_negative(self): + data = containers.BitArray.value_init(15) + data[2] = True + data[5] = True + data[7] = False + data[11] = True + + # Check consistency with slices on lists + b1 = list(data)[-4:1:-3] # Like [2:-4:3] above, but reverted + self.assertEqual(len(b1), 4) + self.assertEqual(b1[0], True) + self.assertEqual(b1[1], False) + self.assertEqual(b1[2], True) + self.assertEqual(b1[3], True) + + b2 = containers.BitArrayView(data)[-4:1:-3] + self.assertIsInstance(b2, containers.StridedBitArrayView1D) + self.assertEqual(len(b2), 4) + self.assertEqual(b2.offset, 3) + self.assertEqual(b2.size, (4,)) + self.assertEqual(b2.stride, (-3,)) + self.assertEqual(b2[0], True) + self.assertEqual(b2[1], False) + self.assertEqual(b2[2], True) + self.assertEqual(b2[3], True) + + # Mostly the same as BitArray.test_slice_stride_reverse() + def test_slice_stride_reverse(self): + data = containers.BitArray.value_init(5) + data[0] = True + data[1] = True + data[2] = False + data[3] = True + data[4] = False + + # slice.stop = -1 + b = containers.BitArrayView(data)[::-1] + self.assertEqual(len(b), 5) + self.assertEqual(b[0], False) + self.assertEqual(b[1], True) + self.assertEqual(b[2], False) + self.assertEqual(b[3], True) + self.assertEqual(b[4], True) + +class StridedBitArrayView1D(unittest.TestCase): + def test_init(self): + a = containers.StridedBitArrayView1D() + b = containers.MutableStridedBitArrayView1D() + self.assertIs(a.owner, None) + self.assertIs(b.owner, None) + self.assertEqual(len(a), 0) + self.assertEqual(len(b), 0) + self.assertEqual(a.offset, 0) + self.assertEqual(b.offset, 0) + self.assertEqual(a.size, (0,)) + self.assertEqual(b.size, (0,)) + self.assertEqual(a.stride, (0,)) + self.assertEqual(b.stride, (0,)) + self.assertEqual(a.dimensions, 1) + self.assertEqual(b.dimensions, 1) + + def test_init_from_array(self): + data = containers.BitArray.value_init(6) + data[3] = True + data_refcount = sys.getrefcount(data) + + a = containers.StridedBitArrayView1D(data) + b = containers.MutableStridedBitArrayView1D(data) + self.assertIs(a.owner, data) + self.assertIs(b.owner, data) + self.assertEqual(len(a), 6) + self.assertEqual(len(b), 6) + self.assertEqual(a.offset, 0) + self.assertEqual(b.offset, 0) + self.assertEqual(a.size, (6,)) + self.assertEqual(b.size, (6,)) + self.assertEqual(a.stride, (1,)) + self.assertEqual(b.stride, (1,)) + self.assertEqual(a[3], True) + self.assertEqual(b[3], True) + self.assertEqual(sys.getrefcount(data), data_refcount + 2) + + self.assertEqual(data[4], False) + b[4] = True + self.assertEqual(data[3], True) + + # Deleting the views should reduce array's refcount again + del a + del b + self.assertEqual(sys.getrefcount(data), data_refcount) + + def test_init_from_empty_array(self): + data = containers.BitArray() + data_refcount = sys.getrefcount(data) + + # Empty view, original data not referenced at all + a = containers.StridedBitArrayView1D(data) + self.assertIs(a.owner, None) + self.assertEqual(sys.getrefcount(data), data_refcount) + + def test_init_from_arrayview(self): + data = containers.BitArray.value_init(6) + data[3] = True + data_refcount = sys.getrefcount(data) + + view = containers.BitArrayView(data) + mutable_view = containers.MutableBitArrayView(data) + + a = containers.StridedBitArrayView1D(view) + b = containers.StridedBitArrayView1D(mutable_view) + c = containers.MutableStridedBitArrayView1D(mutable_view) + self.assertIs(a.owner, data) + self.assertIs(b.owner, data) + self.assertIs(c.owner, data) + self.assertEqual(len(a), 6) + self.assertEqual(len(b), 6) + self.assertEqual(len(c), 6) + self.assertEqual(a.offset, 0) + self.assertEqual(b.offset, 0) + self.assertEqual(c.offset, 0) + self.assertEqual(a.size, (6,)) + self.assertEqual(b.size, (6,)) + self.assertEqual(c.size, (6,)) + self.assertEqual(a.stride, (1,)) + self.assertEqual(b.stride, (1,)) + self.assertEqual(c.stride, (1,)) + self.assertEqual(a[3], True) + self.assertEqual(b[3], True) + self.assertEqual(c[3], True) + self.assertEqual(sys.getrefcount(data), data_refcount + 5) + + self.assertEqual(data[4], False) + c[4] = True + self.assertEqual(data[3], True) + + # Deleting the original view should reduce data's refcount again -- a, + # b, c shouldn't depend on the view but on the original array + del view + del mutable_view + self.assertEqual(sys.getrefcount(data), data_refcount + 3) + del a + del b + del c + self.assertEqual(sys.getrefcount(data), data_refcount) + + def test_init_from_slice(self): + self.assertEqual(ord('0') % 2, 0) + self.assertEqual(ord('1') % 2, 1) + + data = b'11110010' + data_refcount = sys.getrefcount(data) + + view = containers.StridedArrayView1D(data) + self.assertEqual(sys.getrefcount(data), data_refcount + 1) + + a = view.slice_bit(0) + self.assertIs(a.owner, data) + self.assertEqual(len(a), 8) + self.assertEqual(a.offset, 0) + self.assertEqual(a.size, (8,)) + self.assertEqual(a.stride, (8,)) + self.assertEqual(a[0], True) + self.assertEqual(a[1], True) + self.assertEqual(a[2], True) + self.assertEqual(a[3], True) + self.assertEqual(a[4], False) + self.assertEqual(a[5], False) + self.assertEqual(a[6], True) + self.assertEqual(a[7], False) + self.assertEqual(sys.getrefcount(data), data_refcount + 2) + + # Deleting the original view should reduce data's refcount again -- a + # shouldn't depend on the view but on the original array + del a + self.assertEqual(sys.getrefcount(data), data_refcount + 1) + del view + self.assertEqual(sys.getrefcount(data), data_refcount) + + def test_init_mutable_from_immutable(self): + # It's possible to construct only via slice_bit() from a + # StridedArrayView and the mutability is then implicit, nothing to test + pass + + def test_access_invalid(self): + data = containers.BitArray.value_init(3) + a = containers.StridedBitArrayView1D(data) + b = containers.MutableStridedBitArrayView1D(data) + + with self.assertRaises(IndexError): + a[3] + with self.assertRaises(IndexError): + b[3] = True + + def test_mutable_access_invalid(self): + data = containers.BitArray.value_init(3) + a = containers.StridedBitArrayView1D(data) + + # This is Python's own exception + with self.assertRaises(TypeError): + a[0] = True + + # Mostly the same as BitArray.test_slice() except for the additional + # referencing logic that skips intermediate views + def test_slice(self): + data = containers.BitArray.value_init(10) + data[3] = True + data[5] = True + data[6] = True + data_refcount = sys.getrefcount(data) + + a = containers.StridedBitArrayView1D(data) + a_refcount = sys.getrefcount(a) + self.assertEqual(sys.getrefcount(data), data_refcount + 1) + + # When slicing, a's refcount should not change but data's refcount + # should increase + b = a[3:-2] + self.assertIsInstance(b, containers.StridedBitArrayView1D) + self.assertIs(b.owner, data) + self.assertEqual(len(b), 5) + self.assertEqual(b.offset, 3) + self.assertEqual(b.size, (5,)) + self.assertEqual(b.stride, (1,)) + self.assertEqual(b[0], True) + self.assertEqual(b[1], False) + self.assertEqual(b[2], True) + self.assertEqual(b[3], True) + self.assertEqual(b[4], False) + self.assertEqual(sys.getrefcount(data), data_refcount + 2) + self.assertEqual(sys.getrefcount(a), a_refcount) + + # Deleting a slice should reduce data's refcount again, keep a's + # unchanged + del b + self.assertEqual(sys.getrefcount(data), data_refcount + 1) + self.assertEqual(sys.getrefcount(a), a_refcount) + + # Mostly the same as BitArray.test_slice_empty() + def test_slice_empty(self): + data = containers.BitArray.value_init(5) + data_refcount = sys.getrefcount(data) + + # Because this is out of bounds, slice.start = slice.stop + b = containers.StridedBitArrayView1D(data)[7:8] + self.assertEqual(len(b), 0) + self.assertEqual(b.offset, 5) + self.assertEqual(b.size, (0,)) + self.assertEqual(b.stride, (1,)) + + # Empty view, original data not referenced at all + self.assertIs(b.owner, None) + self.assertEqual(sys.getrefcount(data), data_refcount) + + def test_slice_invalid(self): + with self.assertRaisesRegex(ValueError, "slice step cannot be zero"): + containers.StridedBitArrayView1D()[::0] + + # Mostly the same as BitArray.test_slice_stride() except for the additional + # referencing logic that skips intermediate views + def test_slice_stride(self): + data = containers.BitArray.value_init(15) + data[2] = True + data[5] = True + data[7] = False + data[11] = True + data_refcount = sys.getrefcount(data) + + a = containers.StridedBitArrayView1D(data) + self.assertEqual(sys.getrefcount(data), data_refcount + 1) + + b = a[2:-3:3] + self.assertIsInstance(b, containers.StridedBitArrayView1D) + self.assertIs(b.owner, data) + self.assertEqual(len(b), 4) + self.assertEqual(b.offset, 2) + self.assertEqual(b.size, (4,)) + self.assertEqual(b.stride, (3,)) + self.assertEqual(b[0], True) + self.assertEqual(b[1], True) + self.assertEqual(b[2], False) + self.assertEqual(b[3], True) + self.assertEqual(sys.getrefcount(data), data_refcount + 2) + + # Deleting the original view should reduce data's refcount again -- b + # shouldn't depend on the view but on the original array + del a + self.assertEqual(sys.getrefcount(data), data_refcount + 1) + del b + self.assertEqual(sys.getrefcount(data), data_refcount) + + # Mostly the same as BitArray.test_slice_stride_negative() + def test_slice_stride_negative(self): + data = containers.BitArray.value_init(15) + data[2] = True + data[5] = True + data[7] = False + data[11] = True + + # Check consistency with slices on lists + b1 = list(data)[-4:1:-3] # Like [2:-4:3] above, but reverted + self.assertEqual(len(b1), 4) + self.assertEqual(b1[0], True) + self.assertEqual(b1[1], False) + self.assertEqual(b1[2], True) + self.assertEqual(b1[3], True) + + b2 = containers.StridedBitArrayView1D(data)[-4:1:-3] + self.assertIsInstance(b2, containers.StridedBitArrayView1D) + self.assertEqual(len(b2), 4) + self.assertEqual(b2.offset, 3) + self.assertEqual(b2.size, (4,)) + self.assertEqual(b2.stride, (-3,)) + self.assertEqual(b2[0], True) + self.assertEqual(b2[1], False) + self.assertEqual(b2[2], True) + self.assertEqual(b2[3], True) + + def test_ops(self): + a = b'11110010' + v = containers.StridedArrayView1D(a).slice_bit(0) + + b = v.flipped(0) + self.assertEqual(b.offset, 0) + self.assertEqual(b.size, (8,)) + self.assertEqual(b.stride, (-8,)) + self.assertEqual(b[0], False) + self.assertEqual(b[1], True) + self.assertEqual(b[2], False) + self.assertEqual(b[3], False) + self.assertEqual(b[4], True) + self.assertEqual(b[5], True) + self.assertEqual(b[6], True) + self.assertEqual(b[7], True) + + c = v[3:4].broadcasted(0, 5) + self.assertEqual(c.offset, 0) + self.assertEqual(c.size, (5,)) + self.assertEqual(c.stride, (0,)) + self.assertEqual(c[0], True) + self.assertEqual(c[1], True) + self.assertEqual(c[2], True) + self.assertEqual(c[3], True) + self.assertEqual(c[4], True) + + def test_ops_invalid(self): + v = containers.StridedArrayView1D(b'00').slice_bit(0) + + with self.assertRaisesRegex(IndexError, "dimension 1 out of range for a 1D view"): + containers.StridedBitArrayView1D().flipped(1) + with self.assertRaisesRegex(IndexError, "dimension 1 out of range for a 1D view"): + v.broadcasted(1, 3) + with self.assertRaisesRegex(ValueError, "can't broadcast dimension 0 with 2 elements"): + v.broadcasted(0, 3) + +class StridedBitArrayView2D(unittest.TestCase): + def test_init(self): + a = containers.StridedBitArrayView2D() + b = containers.MutableStridedBitArrayView2D() + self.assertIs(a.owner, None) + self.assertIs(b.owner, None) + self.assertEqual(len(a), 0) + self.assertEqual(len(b), 0) + self.assertEqual(a.offset, 0) + self.assertEqual(b.offset, 0) + self.assertEqual(a.size, (0, 0)) + self.assertEqual(b.size, (0, 0)) + self.assertEqual(a.stride, (0, 0)) + self.assertEqual(b.stride, (0, 0)) + self.assertEqual(a.dimensions, 2) + self.assertEqual(b.dimensions, 2) + + def test_init_from_slice(self): + self.assertEqual(ord('0') % 2, 0) + self.assertEqual(ord('1') % 2, 1) + data = memoryview(bytearray( + b'1111' + b'0010')).cast('b', shape=[2, 4]) + data_refcount = sys.getrefcount(data) + + view = containers.MutableStridedArrayView2D(data) + self.assertEqual(sys.getrefcount(data), data_refcount + 1) + + a = view.slice_bit(0) + self.assertIsInstance(a, containers.MutableStridedBitArrayView2D) + self.assertIs(a.owner, data) + self.assertEqual(len(a), 2) + self.assertEqual(a.offset, 0) + self.assertEqual(a.size, (2, 4)) + self.assertEqual(a.stride, (32, 8)) + self.assertEqual(a[0, 0], True) + self.assertEqual(a[0, 1], True) + self.assertEqual(a[0, 2], True) + self.assertEqual(a[0, 3], True) + self.assertEqual(a[1, 0], False) + self.assertEqual(a[1, 1], False) + self.assertEqual(a[1, 2], True) + self.assertEqual(a[1, 3], False) + self.assertEqual(sys.getrefcount(data), data_refcount + 2) + + a[0, 2] = False + self.assertEqual(bytes(data), b'11010010') # haha! + + # Deleting the original view should reduce data's refcount again -- a + # shouldn't depend on the view but on the original array + del a + self.assertEqual(sys.getrefcount(data), data_refcount + 1) + del view + self.assertEqual(sys.getrefcount(data), data_refcount) + + def test_init_mutable_from_immutable(self): + # It's possible to construct only via slice_bit() from a + # StridedArrayView and the mutability is then implicit, nothing to test + pass + + def test_slice(self): + data = memoryview( + b'10' + b'10' + b'11' + b'10').cast('b', shape=[4, 2]) + data_refcount = sys.getrefcount(data) + + a = containers.StridedArrayView2D(data).slice_bit(0) + a_refcount = sys.getrefcount(a) + self.assertEqual(sys.getrefcount(data), data_refcount + 1) + + # When slicing, a's refcount should not change but data's refcount + # should increase + b = a[1:-1] + self.assertIsInstance(b, containers.StridedBitArrayView2D) + self.assertIs(a.owner, data) + self.assertEqual(len(b), 2) + self.assertEqual(b.offset, 0) + self.assertEqual(b.size, (2, 2)) + self.assertEqual(b.stride, (16, 8)) + self.assertEqual(b[0, 0], True) + self.assertEqual(b[0, 1], False) + self.assertEqual(b[1, 0], True) + self.assertEqual(b[1, 1], True) + self.assertEqual(sys.getrefcount(data), data_refcount + 2) + self.assertEqual(sys.getrefcount(a), a_refcount) + + # Deleting a slice should reduce data's refcount again, keep a's + # unchanged + del b + self.assertEqual(sys.getrefcount(data), data_refcount + 1) + self.assertEqual(sys.getrefcount(a), a_refcount) + + def test_slice_empty(self): + data = memoryview( + b'10' + b'10' + b'11' + b'10').cast('b', shape=[4, 2]) + data_refcount = sys.getrefcount(data) + + b = containers.StridedArrayView2D(data).slice_bit(0)[1:1] + + # Empty view, original data not referenced at all + self.assertIs(b.owner, None) + self.assertEqual(sys.getrefcount(data), data_refcount) + + def test_slice_multidimensional(self): + data = memoryview( + b'111000' + b'111000' + b'111100' + b'111000').cast('b', shape=[4, 6]) + data_refcount = sys.getrefcount(data) + + view = containers.StridedArrayView2D(data) + self.assertEqual(sys.getrefcount(data), data_refcount + 1) + + a = view.slice_bit(0) + a_refcount = sys.getrefcount(a) + self.assertEqual(sys.getrefcount(data), data_refcount + 2) + + # When slicing, a's refcount should not change but data's refcount + # should increase + b = a[1:3,2:4] + self.assertIsInstance(b, containers.StridedBitArrayView2D) + self.assertIs(a.owner, data) + self.assertEqual(len(b), 2) + self.assertEqual(b.offset, 0) + self.assertEqual(b.size, (2, 2)) + self.assertEqual(b.stride, (48, 8)) + self.assertEqual(b[0, 0], True) + self.assertEqual(b[0, 1], False) + self.assertEqual(b[1, 0], True) + self.assertEqual(b[1, 1], True) + self.assertEqual(sys.getrefcount(data), data_refcount + 3) + self.assertEqual(sys.getrefcount(a), a_refcount) + + # Deleting a slice should reduce data's refcount again, keep a's + # unchanged + del b + self.assertEqual(sys.getrefcount(data), data_refcount + 2) + self.assertEqual(sys.getrefcount(a), a_refcount) + + def test_slice_multidimensional_empty(self): + data = memoryview( + b'10' + b'10' + b'11' + b'10').cast('b', shape=[4, 2]) + data_refcount = sys.getrefcount(data) + + b = containers.StridedArrayView2D(data).slice_bit(0)[2:2,1:1] + + # Empty view, original data not referenced at all + self.assertIs(b.owner, None) + self.assertEqual(sys.getrefcount(data), data_refcount) + + def test_slice_invalid(self): + with self.assertRaisesRegex(ValueError, "slice step cannot be zero"): + containers.StridedBitArrayView2D()[-5:3:0] + + def test_slice_stride(self): + data = memoryview( + b'10' + b'00' + b'10' + b'00' + b'11' + b'00' + b'10').cast('b', shape=[7, 2]) + a = containers.StridedArrayView2D(data).slice_bit(0) + + b = a[2:5:2] + self.assertEqual(b.offset, 0) + self.assertEqual(b.size, (2, 2)) + self.assertEqual(b.stride, (32, 8)) + self.assertEqual(b[0, 0], True) + self.assertEqual(b[0, 1], False) + self.assertEqual(b[1, 0], True) + self.assertEqual(b[1, 1], True) + + def test_slice_stride_negative(self): + data = memoryview( + b'10' + b'00' + b'10' + b'00' + b'11' + b'00' + b'10').cast('b', shape=[7, 2]) + a = containers.StridedArrayView2D(data).slice_bit(0) + + b = a[4:1:-2] # like [2:5:2] above, but reverted + self.assertEqual(b.offset, 0) + self.assertEqual(b.size, (2, 2)) + self.assertEqual(b.stride, (-32, 8)) + self.assertEqual(b[0, 0], True) + self.assertEqual(b[0, 1], True) + self.assertEqual(b[1, 0], True) + self.assertEqual(b[1, 1], False) + + def test_slice_stride_negative_multidimensional(self): + data = memoryview( + b'110000' + b'100000' + b'110000' + b'100000' + b'110001' + b'100000' + b'110000').cast('b', shape=[7, 6]) + a = containers.StridedArrayView2D(data).slice_bit(0) + + # first dimension is like [2:5:2] above, but reverted, + # second like [1:6:4] but reverted + b = a[4:1:-2, 5:0:-4] + self.assertEqual(b.offset, 0) + self.assertEqual(b.size, (2, 2)) + self.assertEqual(b.stride, (-96, -32)) + self.assertEqual(b[0, 0], True) + self.assertEqual(b[0, 1], True) + self.assertEqual(b[1, 0], False) + self.assertEqual(b[1, 1], True) + + def test_ops(self): + data = memoryview( + b'1111' + b'0010').cast('b', shape=[2, 4]) + a = containers.StridedArrayView2D(data).slice_bit(0) + + b = a.transposed(0, 1) + self.assertEqual(b.offset, 0) + self.assertEqual(b.size, (4, 2)) + self.assertEqual(b.stride, (8, 32)) + self.assertEqual(b[0, 0], True) + self.assertEqual(b[1, 0], True) + self.assertEqual(b[2, 0], True) + self.assertEqual(b[3, 0], True) + self.assertEqual(b[0, 1], False) + self.assertEqual(b[1, 1], False) + self.assertEqual(b[2, 1], True) + self.assertEqual(b[3, 1], False) + + c = a.flipped(1) + self.assertEqual(c.offset, 0) + self.assertEqual(c.size, (2, 4)) + self.assertEqual(c.stride, (32, -8)) + self.assertEqual(c[0, 0], True) + self.assertEqual(c[0, 1], True) + self.assertEqual(c[0, 2], True) + self.assertEqual(c[0, 3], True) + self.assertEqual(c[1, 0], False) + self.assertEqual(c[1, 1], True) + self.assertEqual(c[1, 2], False) + self.assertEqual(c[1, 3], False) + + d = a.transposed(0, 1)[0:1].broadcasted(0, 4) + self.assertEqual(d.offset, 0) + self.assertEqual(d.size, (4, 2)) + self.assertEqual(d.stride, (0, 32)) + self.assertEqual(d[0, 0], True) + self.assertEqual(d[1, 0], True) + self.assertEqual(d[2, 0], True) + self.assertEqual(d[3, 0], True) + self.assertEqual(d[0, 1], False) + self.assertEqual(d[1, 1], False) + self.assertEqual(d[2, 1], False) + self.assertEqual(d[3, 1], False) + + def test_ops_invalid(self): + v = containers.StridedArrayView2D(memoryview(b'00').cast('b', shape=[1, 2])).slice_bit(0) + + with self.assertRaisesRegex(IndexError, "dimension 2 out of range for a 2D view"): + containers.StridedBitArrayView2D().flipped(2) + with self.assertRaisesRegex(IndexError, "dimensions 2, 1 can't be transposed in a 2D view"): + containers.StridedBitArrayView2D().transposed(2, 1) + with self.assertRaisesRegex(IndexError, "dimension 2 out of range for a 2D view"): + v.broadcasted(2, 3) + with self.assertRaisesRegex(ValueError, "can't broadcast dimension 1 with 2 elements"): + v.broadcasted(1, 3) + +# Multi-dimensional behavior is tested extensively for StridedBitArrayView2D, +# this checks just what differs, like constructors and fancy operations +class StridedBitArrayView3D(unittest.TestCase): + def test_init_from_slice(self): + data = containers.StridedArrayView3D(memoryview( + b'1111' + b'0010').cast('b', shape=[2, 1, 4])) + + a = data.slice_bit(0) + self.assertIsInstance(a, containers.StridedBitArrayView3D) + self.assertEqual(a.offset, 0) + self.assertEqual(a.size, (2, 1, 4)) + self.assertEqual(a.stride, (32, 32, 8)) + self.assertEqual(a[0, 0, 0], True) + self.assertEqual(a[0, 0, 1], True) + self.assertEqual(a[0, 0, 2], True) + self.assertEqual(a[0, 0, 3], True) + self.assertEqual(a[1, 0, 0], False) + self.assertEqual(a[1, 0, 1], False) + self.assertEqual(a[1, 0, 2], True) + self.assertEqual(a[1, 0, 3], False) + + def test_ops(self): + data = memoryview( + b'1111' + b'0010').cast('b', shape=[2, 1, 4]) + a = containers.StridedArrayView3D(data).slice_bit(0) + + b = a.transposed(2, 1) + self.assertEqual(b.offset, 0) + self.assertEqual(b.size, (2, 4, 1)) + self.assertEqual(b.stride, (32, 8, 32)) + self.assertEqual(b[0, 0, 0], True) + self.assertEqual(b[0, 1, 0], True) + self.assertEqual(b[0, 2, 0], True) + self.assertEqual(b[0, 3, 0], True) + self.assertEqual(b[1, 0, 0], False) + self.assertEqual(b[1, 1, 0], False) + self.assertEqual(b[1, 2, 0], True) + self.assertEqual(b[1, 3, 0], False) + + c = a.flipped(2) + self.assertEqual(c.offset, 0) + self.assertEqual(c.size, (2, 1, 4)) + self.assertEqual(c.stride, (32, 32, -8)) + self.assertEqual(c[0, 0, 0], True) + self.assertEqual(c[0, 0, 1], True) + self.assertEqual(c[0, 0, 2], True) + self.assertEqual(c[0, 0, 3], True) + self.assertEqual(c[1, 0, 0], False) + self.assertEqual(c[1, 0, 1], True) + self.assertEqual(c[1, 0, 2], False) + self.assertEqual(c[1, 0, 3], False) + + d = a.transposed(0, 2)[0:1].broadcasted(0, 4) + self.assertEqual(d.offset, 0) + self.assertEqual(d.size, (4, 1, 2)) + self.assertEqual(d.stride, (0, 32, 32)) + self.assertEqual(d[0, 0, 0], True) + self.assertEqual(d[1, 0, 0], True) + self.assertEqual(d[2, 0, 0], True) + self.assertEqual(d[3, 0, 0], True) + self.assertEqual(d[0, 0, 1], False) + self.assertEqual(d[1, 0, 1], False) + self.assertEqual(d[2, 0, 1], False) + self.assertEqual(d[3, 0, 1], False) + + def test_ops_invalid(self): + v = containers.StridedArrayView3D(memoryview(b'00').cast('b', shape=[1, 1, 2])).slice_bit(0) + + with self.assertRaisesRegex(IndexError, "dimension 3 out of range for a 3D view"): + containers.StridedBitArrayView3D().flipped(3) + with self.assertRaisesRegex(IndexError, "dimensions 2, 3 can't be transposed in a 3D view"): + containers.StridedBitArrayView3D().transposed(2, 3) + with self.assertRaisesRegex(IndexError, "dimension 3 out of range for a 3D view"): + v.broadcasted(3, 3) + with self.assertRaisesRegex(ValueError, "can't broadcast dimension 2 with 2 elements"): + v.broadcasted(2, 3) + +# This is just a dumb copy of the above with one dimension inserted at the +# second place. +class StridedBitArrayView4D(unittest.TestCase): + def test_init_from_slice(self): + data = containers.MutableStridedArrayView4D(memoryview(bytearray( + b'1111' + b'0010')).cast('b', shape=[1, 2, 1, 4])) + + a = data.slice_bit(0) + self.assertIsInstance(a, containers.MutableStridedBitArrayView4D) + self.assertEqual(a.offset, 0) + self.assertEqual(a.size, (1, 2, 1, 4)) + self.assertEqual(a.stride, (64, 32, 32, 8)) + self.assertEqual(a[0, 0, 0, 0], True) + self.assertEqual(a[0, 0, 0, 1], True) + self.assertEqual(a[0, 0, 0, 2], True) + self.assertEqual(a[0, 0, 0, 3], True) + self.assertEqual(a[0, 1, 0, 0], False) + self.assertEqual(a[0, 1, 0, 1], False) + self.assertEqual(a[0, 1, 0, 2], True) + self.assertEqual(a[0, 1, 0, 3], False) + + a[0, 0, 0, 2] = False + self.assertEqual(bytes(data), b'11010010') # haha! + + def test_ops(self): + data = memoryview( + b'1111' + b'0010').cast('b', shape=[1, 2, 1, 4]) + a = containers.StridedArrayView4D(data).slice_bit(0) + + b = a.transposed(3, 1) + self.assertEqual(b.offset, 0) + self.assertEqual(b.size, (1, 4, 1, 2)) + self.assertEqual(b.stride, (64, 8, 32, 32)) + self.assertEqual(b[0, 0, 0, 0], True) + self.assertEqual(b[0, 1, 0, 0], True) + self.assertEqual(b[0, 2, 0, 0], True) + self.assertEqual(b[0, 3, 0, 0], True) + self.assertEqual(b[0, 0, 0, 1], False) + self.assertEqual(b[0, 1, 0, 1], False) + self.assertEqual(b[0, 2, 0, 1], True) + self.assertEqual(b[0, 3, 0, 1], False) + + c = a.flipped(3) + self.assertEqual(c.offset, 0) + self.assertEqual(c.size, (1, 2, 1, 4)) + self.assertEqual(c.stride, (64, 32, 32, -8)) + self.assertEqual(c[0, 0, 0, 0], True) + self.assertEqual(c[0, 0, 0, 1], True) + self.assertEqual(c[0, 0, 0, 2], True) + self.assertEqual(c[0, 0, 0, 3], True) + self.assertEqual(c[0, 1, 0, 0], False) + self.assertEqual(c[0, 1, 0, 1], True) + self.assertEqual(c[0, 1, 0, 2], False) + self.assertEqual(c[0, 1, 0, 3], False) + + d = a.transposed(0, 3)[0:1].broadcasted(0, 4) + self.assertEqual(d.offset, 0) + self.assertEqual(d.size, (4, 2, 1, 1)) + self.assertEqual(d.stride, (0, 32, 32, 64)) + self.assertEqual(d[0, 0, 0, 0], True) + self.assertEqual(d[1, 0, 0, 0], True) + self.assertEqual(d[2, 0, 0, 0], True) + self.assertEqual(d[3, 0, 0, 0], True) + self.assertEqual(d[0, 1, 0, 0], False) + self.assertEqual(d[1, 1, 0, 0], False) + self.assertEqual(d[2, 1, 0, 0], False) + self.assertEqual(d[3, 1, 0, 0], False) + + def test_ops_invalid(self): + v = containers.StridedArrayView4D(memoryview(b'00').cast('b', shape=[1, 1, 1, 2])).slice_bit(0) + + with self.assertRaisesRegex(IndexError, "dimension 4 out of range for a 4D view"): + containers.StridedBitArrayView4D().flipped(4) + with self.assertRaisesRegex(IndexError, "dimensions 4, 3 can't be transposed in a 4D view"): + containers.StridedBitArrayView4D().transposed(4, 3) + with self.assertRaisesRegex(IndexError, "dimension 4 out of range for a 4D view"): + v.broadcasted(4, 3) + with self.assertRaisesRegex(ValueError, "can't broadcast dimension 3 with 2 elements"): + v.broadcasted(3, 3) + class Optional(unittest.TestCase): def test_simple(self): self.assertIsNone(test_optional.simple_type(False))