diff --git a/doc/python/corrade.containers.rst b/doc/python/corrade.containers.rst index 0ccac6e..ff098bd 100644 --- a/doc/python/corrade.containers.rst +++ b/doc/python/corrade.containers.rst @@ -52,8 +52,8 @@ object in the :ref:`owner` field, meaning that calling :py:`del` on the original object will *not* invalidate the view. Slicing a view creates a new view referencing the same original object, without any dependency on - the previous view. That means a long chained slicing operation will not cause - increased memory usage. + the previous view. That means a long chained slicing operation will not + cause increased memory usage. .. code:: pycon @@ -81,8 +81,16 @@ Provides one-dimensional read-only view on a memory range with custom stride values. See :ref:`StridedArrayView2D`, :ref:`StridedArrayView3D`, - :ref:`MutableStridedArrayView1D` and others for multi-dimensional and - mutable equivalents. + :ref:`StridedArrayView4D`, :ref:`MutableStridedArrayView1D` and others for + multi-dimensional and mutable equivalents. + + `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. `Comparison to Python's memoryview`_ ==================================== @@ -92,25 +100,96 @@ multi-dimensional slicing as well (which raises :ref:`NotImplementedError` in Py3.7 :ref:`memoryview`). +.. py:function:: corrade.containers.StridedArrayView1D.flipped + :raise IndexError: if :p:`dimension` is not :py:`0` +.. py:function:: corrade.containers.StridedArrayView1D.broadcasted + :raise IndexError: if :p:`dimension` is not :py:`0` + .. py:class:: corrade.containers.MutableStridedArrayView1D Equivalent to :ref:`StridedArrayView1D`, but implementing :ref:`__setitem__()` as well. +.. py:function:: corrade.containers.MutableStridedArrayView1D.flipped + :raise IndexError: if :p:`dimension` is not :py:`0` +.. py:function:: corrade.containers.MutableStridedArrayView1D.broadcasted + :raise IndexError: if :p:`dimension` is not :py:`0` + .. py:class:: corrade.containers.StridedArrayView2D See :ref:`StridedArrayView1D` for more information. +.. py:function:: corrade.containers.StridedArrayView2D.flipped + :raise IndexError: if :p:`dimension` is not :py:`0` or :py:`1` +.. py:function:: corrade.containers.StridedArrayView2D.broadcasted + :raise IndexError: if :p:`dimension` is not :py:`0` or :py:`1` +.. py:function:: corrade.containers.StridedArrayView2D.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.MutableStridedArrayView2D See :ref:`StridedArrayView1D` and :ref:`MutableStridedArrayView1D` for more information. +.. py:function:: corrade.containers.MutableStridedArrayView2D.flipped + :raise IndexError: if :p:`dimension` is not :py:`0` or :py:`1` +.. py:function:: corrade.containers.MutableStridedArrayView2D.broadcasted + :raise IndexError: if :p:`dimension` is not :py:`0` or :py:`1` +.. py:function:: corrade.containers.MutableStridedArrayView2D.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.StridedArrayView3D See :ref:`StridedArrayView1D` for more information. +.. 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 + :raise IndexError: if :p:`dimension` is not :py:`0`, :py:`1` or :py:`2` +.. py:function:: corrade.containers.StridedArrayView3D.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.MutableStridedArrayView3D See :ref:`StridedArrayView1D` and :ref:`MutableStridedArrayView1D` for more information. + +.. 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 + :raise IndexError: if :p:`dimension` is not :py:`0`, :py:`1` or :py:`2` +.. py:function:: corrade.containers.MutableStridedArrayView3D.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.StridedArrayView4D + + See :ref:`StridedArrayView1D` for more information. + +.. py:function:: corrade.containers.StridedArrayView4D.flipped + :raise IndexError: if :p:`dimension` is not :py:`0`, :py:`1` :py:`2` or + :py:`3` +.. py:function:: corrade.containers.StridedArrayView4D.broadcasted + :raise IndexError: if :p:`dimension` is not :py:`0`, :py:`1` :py:`2` or + :py:`3` +.. py:function:: corrade.containers.StridedArrayView4D.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.MutableStridedArrayView4D + + See :ref:`StridedArrayView1D` and :ref:`MutableStridedArrayView1D` for more + information. + +.. py:function:: corrade.containers.MutableStridedArrayView4D.flipped + :raise IndexError: if :p:`dimension` is not :py:`0`, :py:`1` :py:`2` or + :py:`3` +.. py:function:: corrade.containers.MutableStridedArrayView4D.broadcasted + :raise IndexError: if :p:`dimension` is not :py:`0`, :py:`1` :py:`2` or + :py:`3` +.. 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 diff --git a/src/python/corrade/containers.cpp b/src/python/corrade/containers.cpp index 6707904..5f24f6c 100644 --- a/src/python/corrade/containers.cpp +++ b/src/python/corrade/containers.cpp @@ -326,13 +326,18 @@ template<> struct StridedOperation<1> { template class View, unsigned dimensions, class T> static View flipped(const View& view, unsigned dimension) { if(dimension == 0) return view.template flipped<0>(); - PyErr_Format(PyExc_ValueError, "dimension %u out of range for a %iD view", dimension, dimensions); + PyErr_Format(PyExc_IndexError, "dimension %u out of range for a %iD view", dimension, dimensions); throw py::error_already_set{}; } template class View, unsigned dimensions, class T> static View broadcasted(const View& view, unsigned dimension, std::size_t size) { - if(dimension == 0) + if(dimension == 0) { + if(Containers::Size{view.size()}[0] != 1) { + PyErr_Format(PyExc_ValueError, "can't broadcast dimension %u with %zu elements", dimension, Containers::Size{view.size()}[0]); + throw py::error_already_set{}; + } return view.template broadcasted<0>(size); - PyErr_Format(PyExc_ValueError, "dimension %u out of range for a %iD view", dimension, dimensions); + } + PyErr_Format(PyExc_IndexError, "dimension %u out of range for a %iD view", dimension, dimensions); throw py::error_already_set{}; } }; @@ -341,7 +346,7 @@ template<> struct StridedOperation<2> { if((a == 0 && b == 1) || (a == 1 && b == 0)) return view.template transposed<0, 1>(); - PyErr_Format(PyExc_ValueError, "dimensions %u, %u can't be transposed in a %iD view", a, b, dimensions); + PyErr_Format(PyExc_IndexError, "dimensions %u, %u can't be transposed in a %iD view", a, b, dimensions); throw py::error_already_set{}; } template class View, unsigned dimensions, class T> static View flipped(const View& view, unsigned dimension) { @@ -350,8 +355,13 @@ template<> struct StridedOperation<2> { return StridedOperation<1>::flipped(view, dimension); } template class View, unsigned dimensions, class T> static View broadcasted(const View& view, unsigned dimension, std::size_t size) { - if(dimension == 1) + if(dimension == 1) { + if(view.size()[1] != 1) { + PyErr_Format(PyExc_ValueError, "can't broadcast dimension %u with %zu elements", dimension, view.size()[1]); + throw py::error_already_set{}; + } return view.template broadcasted<1>(size); + } return StridedOperation<1>::broadcasted(view, dimension, size); } }; @@ -371,8 +381,13 @@ template<> struct StridedOperation<3> { return StridedOperation<2>::flipped(view, dimension); } template class View, unsigned dimensions, class T> static View broadcasted(const View& view, unsigned dimension, std::size_t size) { - if(dimension == 2) + if(dimension == 2) { + if(view.size()[2] != 1) { + PyErr_Format(PyExc_ValueError, "can't broadcast dimension %u with %zu elements", dimension, view.size()[2]); + throw py::error_already_set{}; + } return view.template broadcasted<2>(size); + } return StridedOperation<2>::broadcasted(view, dimension, size); } }; @@ -395,8 +410,13 @@ template<> struct StridedOperation<4> { return StridedOperation<3>::flipped(view, dimension); } template class View, unsigned dimensions, class T> static View broadcasted(const View& view, unsigned dimension, std::size_t size) { - if(dimension == 3) + if(dimension == 3) { + if(view.size()[3] != 1) { + PyErr_Format(PyExc_ValueError, "can't broadcast dimension %u with %zu elements", dimension, view.size()[3]); + throw py::error_already_set{}; + } return view.template broadcasted<3>(size); + } return StridedOperation<3>::broadcasted(view, dimension, size); } }; diff --git a/src/python/corrade/test/test_containers.py b/src/python/corrade/test/test_containers.py index ae20041..c7fa7da 100644 --- a/src/python/corrade/test/test_containers.py +++ b/src/python/corrade/test/test_containers.py @@ -395,6 +395,30 @@ class StridedArrayView1D(unittest.TestCase): self.assertEqual(c2.size, (6,)) self.assertEqual(c2.stride, (-2,)) + def test_ops(self): + a = b'01234567' + v = memoryview(a).cast('b', shape=[8]) + + b = containers.StridedArrayView1D(v).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') + + def test_ops_invalid(self): + a = b'00' + + 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"): + containers.StridedArrayView1D(a).broadcasted(1, 3) + with self.assertRaisesRegex(ValueError, "can't broadcast dimension 0 with 2 elements"): + containers.StridedArrayView1D(a).broadcasted(0, 3) + def test_convert_memoryview(self): a = b'World is hell!' a_refcount = sys.getrefcount(a) @@ -680,6 +704,19 @@ class StridedArrayView2D(unittest.TestCase): self.assertEqual(d.stride, (8, 0)) self.assertEqual(bytes(d), b'3377bb') + def test_ops_invalid(self): + a = b'00' + v = memoryview(a).cast('b', shape=[1, 2]) + + 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"): + containers.StridedArrayView2D(v).broadcasted(2, 3) + with self.assertRaisesRegex(ValueError, "can't broadcast dimension 1 with 2 elements"): + containers.StridedArrayView2D(v).broadcasted(1, 3) + with self.assertRaisesRegex(IndexError, "dimensions 0, 2 can't be transposed in a 2D view"): + containers.StridedArrayView2D().transposed(0, 2) + def test_convert_memoryview(self): a = memoryview(b'01234567' b'456789ab' @@ -777,6 +814,19 @@ class StridedArrayView3D(unittest.TestCase): self.assertEqual(f.stride, (24, 8, 0)) self.assertEqual(bytes(f), b'000004444488888ccccc0000044444') + def test_ops_invalid(self): + a = b'00' + v = memoryview(a).cast('b', shape=[1, 1, 2]) + + 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"): + containers.StridedArrayView3D(v).broadcasted(3, 3) + with self.assertRaisesRegex(ValueError, "can't broadcast dimension 2 with 2 elements"): + containers.StridedArrayView3D(v).broadcasted(2, 3) + with self.assertRaisesRegex(IndexError, "dimensions 1, 3 can't be transposed in a 3D view"): + containers.StridedArrayView3D().transposed(1, 3) + # This is just a dumb copy of the above with one dimension inserted at the # second place. class StridedArrayView4D(unittest.TestCase): @@ -855,6 +905,19 @@ class StridedArrayView4D(unittest.TestCase): self.assertEqual(f.stride, (24, 24, 8, 0)) self.assertEqual(bytes(f), b'000004444488888ccccc0000044444') + def test_ops_invalid(self): + a = b'00' + v = memoryview(a).cast('b', shape=[1, 1, 1, 2]) + + 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"): + containers.StridedArrayView4D(v).broadcasted(4, 3) + with self.assertRaisesRegex(ValueError, "can't broadcast dimension 3 with 2 elements"): + containers.StridedArrayView4D(v).broadcasted(3, 3) + with self.assertRaisesRegex(IndexError, "dimensions 4, 3 can't be transposed in a 4D view"): + containers.StridedArrayView4D().transposed(4, 3) + class StridedArrayViewCustomType(unittest.TestCase): def test_short(self): a = test_stridedarrayview.get_containers()