diff --git a/package/archlinux/PKGBUILD b/package/archlinux/PKGBUILD index ebf4cba..2d954d8 100644 --- a/package/archlinux/PKGBUILD +++ b/package/archlinux/PKGBUILD @@ -29,6 +29,9 @@ build() { } check() { + cd "$_rootdir/src/python/corrade" + python -m unittest -v + cd "$_rootdir/src/python/magnum" python -m unittest -v } diff --git a/package/ci/travis-desktop.sh b/package/ci/travis-desktop.sh index d01893d..f5b6d34 100755 --- a/package/ci/travis-desktop.sh +++ b/package/ci/travis-desktop.sh @@ -57,5 +57,8 @@ cd src/python python3 setup.py install --root="$TRAVIS_BUILD_DIR/install" --prefix=/usr # Run tests -cd ../../../src/python/magnum +cd ../../../src/python/corrade +coverage run -m unittest -v + +cd ../magnum coverage run -m unittest -v diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index ffe1b3e..d6ac2fc 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -33,6 +33,7 @@ else() set(output_dir ${CMAKE_CURRENT_BINARY_DIR}) endif() +add_subdirectory(corrade) add_subdirectory(magnum) file(GENERATE OUTPUT ${output_dir}/setup.py diff --git a/src/python/corrade/.coveragerc b/src/python/corrade/.coveragerc new file mode 100644 index 0000000..35543e2 --- /dev/null +++ b/src/python/corrade/.coveragerc @@ -0,0 +1,3 @@ +[run] +omit = + */test/* diff --git a/src/python/corrade/CMakeLists.txt b/src/python/corrade/CMakeLists.txt new file mode 100644 index 0000000..6893a9f --- /dev/null +++ b/src/python/corrade/CMakeLists.txt @@ -0,0 +1,40 @@ +# +# This file is part of Magnum. +# +# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 +# Vladimír Vondruš +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# + +set(corrade_containers_SRCS + containers.cpp) + +pybind11_add_module(corrade_containers ${corrade_containers_SRCS}) +target_include_directories(corrade_containers PRIVATE ${PROJECT_SOURCE_DIR}/src/python) +target_link_libraries(corrade_containers PRIVATE + Corrade::Containers + Corrade::Utility) +set_target_properties(corrade_containers PROPERTIES + FOLDER "python/corrade" + OUTPUT_NAME "containers" + LIBRARY_OUTPUT_DIRECTORY ${output_dir}/corrade) + +file(GENERATE OUTPUT ${output_dir}/corrade/__init__.py + INPUT ${CMAKE_CURRENT_SOURCE_DIR}/__init__.py) diff --git a/src/python/corrade/PyArrayView.h b/src/python/corrade/PyArrayView.h new file mode 100644 index 0000000..3b2731c --- /dev/null +++ b/src/python/corrade/PyArrayView.h @@ -0,0 +1,54 @@ +#ifndef corrade_PyArrayView_h +#define corrade_PyArrayView_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include +#include +#include + +#include "corrade/bootstrap.h" + +namespace corrade { + +/* Wrapper for Containers::ArrayView holding a reference to the memory owner */ +template struct PyArrayView: Containers::ArrayView { + /*implicit*/PyArrayView() noexcept: obj{py::none{}} {} + explicit PyArrayView(Containers::ArrayView view, py::object obj) noexcept: Containers::ArrayView{view}, obj{std::move(obj)} {} + + py::object obj; +}; + +/* Wrapper for Containers::StridedArrayView holding a reference to the memory owner */ +template struct PyStridedArrayView: Containers::StridedArrayView { + /*implicit*/ PyStridedArrayView() noexcept: obj{py::none{}} {} + explicit PyStridedArrayView(Containers::StridedArrayView view, py::object obj) noexcept: Containers::StridedArrayView{view}, obj{std::move(obj)} {} + + py::object obj; +}; + +} + +#endif diff --git a/src/python/corrade/PybindExtras.h b/src/python/corrade/PybindExtras.h new file mode 100644 index 0000000..8e8e06d --- /dev/null +++ b/src/python/corrade/PybindExtras.h @@ -0,0 +1,37 @@ +#ifndef corrade_PybindExtras_h +#define corrade_PybindExtras_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include + +namespace pybind11 { + +// TODO: add this to pybind itself +PYBIND11_RUNTIME_EXCEPTION(buffer_error, PyExc_BufferError) + +} + +#endif diff --git a/src/python/corrade/__init__.py b/src/python/corrade/__init__.py new file mode 100644 index 0000000..917f2c4 --- /dev/null +++ b/src/python/corrade/__init__.py @@ -0,0 +1,26 @@ +# +# This file is part of Magnum. +# +# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 +# Vladimír Vondruš +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# + +"""Root Corrade module""" diff --git a/src/python/corrade/bootstrap.h b/src/python/corrade/bootstrap.h new file mode 100644 index 0000000..b87659e --- /dev/null +++ b/src/python/corrade/bootstrap.h @@ -0,0 +1,38 @@ +#ifndef corrade_h +#define corrade_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +namespace pybind11 { class module; } +namespace Corrade {} + +namespace corrade { + +using namespace Corrade; +namespace py = pybind11; + +} + +#endif diff --git a/src/python/corrade/containers.cpp b/src/python/corrade/containers.cpp new file mode 100644 index 0000000..ffc6ce4 --- /dev/null +++ b/src/python/corrade/containers.cpp @@ -0,0 +1,452 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include +#include +#include + +#include "corrade/bootstrap.h" +#include "corrade/PyArrayView.h" +#include "corrade/PybindExtras.h" + +namespace corrade { namespace { + +struct Slice { + std::size_t start; + std::size_t stop; + std::ptrdiff_t step; +}; + +Slice calculateSlice(py::slice slice, std::size_t containerSize) { + std::size_t size; + std::ptrdiff_t start, stop, step; + + /* Happens for example when passing a tuple as a slice or a zero step */ + if(!slice.compute(containerSize, reinterpret_cast(&start), reinterpret_cast(&stop), reinterpret_cast(&step), &size)) + throw py::error_already_set{}; + + /* If step is negative, start > stop and we have to recalculate */ + CORRADE_INTERNAL_ASSERT((start <= stop) == (step > 0)); + if(step < 0) { + std::swap(start, stop); + start += 1; + stop += 1; + } + + return Slice{std::size_t(start), std::size_t(stop), step}; +} + +template void arrayView(py::class_>& c) { + /* Implicitly convertible from a buffer */ + py::implicitly_convertible>(); + + c + /* Constructor */ + .def(py::init(), "Default constructor") + + /* Buffer protocol */ + .def(py::init([](py::buffer buffer) { + py::buffer_info info = buffer.request(!std::is_const::value); + + // TODO: test for items that are not 1 byte size + + if(info.ndim != 1) + throw py::buffer_error{Utility::formatString("expected one dimension but got {}", info.ndim)}; + if(info.strides[0] != 1) + throw py::buffer_error{Utility::formatString("expected stride of 1 but got {}", info.strides[0])}; + + // TODO: need to take buffer.obj, not buffer! + return PyArrayView{{static_cast(info.ptr), std::size_t(info.shape[0])}, buffer}; + }), "Construct from a buffer") + .def_buffer([](const PyArrayView& self) -> py::buffer_info { + return py::buffer_info{ + const_cast(self.data()), + sizeof(T), + py::format_descriptor::format(), + 1, + {self.size()}, + {1} // TODO: need to pass self.obj to the buffer, not self! + }; + }) + + /* Length and memory owning object */ + .def("__len__", &PyArrayView::size, "View size") + .def_readonly("obj", &PyArrayView::obj, "Memory owner object") + + /* Conversion to bytes */ + .def("__bytes__", [](const PyArrayView& self) { + return py::bytes(self.data(), self.size()); + }, "Convert to bytes") + + /* Single item retrieval. Need to throw IndexError in order to allow + iteration: https://docs.python.org/3/reference/datamodel.html#object.__getitem__ */ + .def("__getitem__", [](const PyArrayView& self, std::size_t i) { + if(i >= self.size()) throw pybind11::index_error{}; + return self[i]; + }, "Value at given position") + + /* Slicing */ + .def("__getitem__", [](const PyArrayView& self, py::slice slice) -> py::object { + const Slice calculated = calculateSlice(slice, self.size()); + + /* Non-trivial stride, return a different type */ + if(calculated.step != 1) { + return py::cast(PyStridedArrayView<1, T>( + Containers::stridedArrayView(self).slice(calculated.start, calculated.stop).every(calculated.step), self.obj)); + } + + /* Usual business */ + return py::cast(PyArrayView{self.slice(calculated.start, calculated.stop), self.obj}); + }, "Slice the view"); +} + +template void mutableArrayView(py::class_>& c) { + c + .def("__setitem__", [](const PyArrayView& self, std::size_t i, const T& value) { + if(i >= self.size()) throw pybind11::index_error{}; + self[i] = value; + }, "Set a value at given position"); +} + +/* Tuple for given dimension */ +template struct DimensionsTuple; +template struct DimensionsTuple<1, T> { typedef std::tuple Type; }; +template struct DimensionsTuple<2, T> { typedef std::tuple Type; }; +template struct DimensionsTuple<3, T> { typedef std::tuple Type; }; + +/* Size tuple for given dimension */ +template typename DimensionsTuple::Type size(Containers::StridedDimensions); +template<> std::tuple size(Containers::StridedDimensions<1, std::size_t> size) { + return std::make_tuple(size[0]); +} +template<> std::tuple size(Containers::StridedDimensions<2, std::size_t> size) { + return std::make_tuple(size[0], size[1]); +} +template<> std::tuple size(Containers::StridedDimensions<3, std::size_t> size) { + return std::make_tuple(size[0], size[1], size[2]); +} + +/* Stride tuple for given dimension */ +template typename DimensionsTuple::Type stride(Containers::StridedDimensions); +template<> std::tuple stride(Containers::StridedDimensions<1, std::ptrdiff_t> stride) { + return std::make_tuple(stride[0]); +} +template<> std::tuple stride(Containers::StridedDimensions<2, std::ptrdiff_t> stride) { + return std::make_tuple(stride[0], stride[1]); +} +template<> std::tuple stride(Containers::StridedDimensions<3, std::ptrdiff_t> stride) { + return std::make_tuple(stride[0], stride[1], stride[2]); +} + +/* Byte conversion for given dimension */ +template Containers::Array bytes(Containers::StridedArrayView); +template<> Containers::Array bytes(Containers::StridedArrayView1D view) { + Containers::Array out{view.size()}; + std::size_t pos = 0; + for(const char i: view) out[pos++] = i; + return out; +} +template<> Containers::Array bytes(Containers::StridedArrayView2D view) { + Containers::Array out{view.size()[0]*view.size()[1]}; + std::size_t pos = 0; + for(Containers::StridedArrayView1D i: view) + for(const char j: i) out[pos++] = j; + return out; +} +template<> Containers::Array bytes(Containers::StridedArrayView3D view) { + Containers::Array out{view.size()[0]*view.size()[1]*view.size()[2]}; + std::size_t pos = 0; + for(Containers::StridedArrayView2D i: view) + for(Containers::StridedArrayView1D j: i) + for(const char k: j) out[pos++] = k; + return out; +} + +/* Getting a runtime tuple index. Ugh. */ +template const T& dimensionsTupleGet(const typename DimensionsTuple<1, T>::Type& tuple, std::size_t i) { + if(i == 0) return std::get<0>(tuple); + CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ +} +template const T& dimensionsTupleGet(const typename DimensionsTuple<2, T>::Type& tuple, std::size_t i) { + if(i == 0) return std::get<0>(tuple); + if(i == 1) return std::get<1>(tuple); + CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ +} +template const T& dimensionsTupleGet(const typename DimensionsTuple<3, T>::Type& tuple, std::size_t i) { + if(i == 0) return std::get<0>(tuple); + if(i == 1) return std::get<1>(tuple); + if(i == 2) return std::get<2>(tuple); + CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ +} + +template void stridedArrayView(py::class_>& c) { + c + /* Constructor */ + .def(py::init(), "Default constructor") + + /* Buffer protocol */ + .def(py::init([](py::buffer buffer) { + py::buffer_info info = buffer.request(!std::is_const::value); + + if(info.ndim != dimensions) + throw py::buffer_error{Utility::formatString("expected {} dimensions but got {}", dimensions, info.ndim)}; + + return PyStridedArrayView{{ + {static_cast(info.ptr), std::size_t(info.size*info.strides[0])}, + Containers::StaticArrayView{reinterpret_cast(&info.shape[0])}, + Containers::StaticArrayView{reinterpret_cast(&info.strides[0])}}, + // TODO: need to take buffer.obj, not buffer! + buffer}; + }), "Construct from a buffer") + .def_buffer([](const PyStridedArrayView& self) -> py::buffer_info { + std::vector shape(dimensions); + Containers::StridedDimensions selfSize{self.size()}; + for(std::size_t i = 0; i != dimensions; ++i) shape[i] = selfSize[i]; + + std::vector strides(dimensions); + Containers::StridedDimensions selfStride{self.stride()}; + for(std::size_t i = 0; i != dimensions; ++i) strides[i] = selfStride[i]; + return py::buffer_info{ + const_cast(self.data()), + sizeof(T), + py::format_descriptor::format(), + dimensions, + shape, strides + // TODO: need to pass self.obj to the buffer, not self! + }; + }) + + // TODO: construct from a buffer + size/stride + + /* Length, size/stride tuple, dimension count and memory owning object */ + .def("__len__", [](const PyStridedArrayView& self) { + return Containers::StridedDimensions(self.size())[0]; + }, "View size in the top-level dimension") + .def_property_readonly("size", [](const PyStridedArrayView& self) { + return size(self.size()); + }, "View size in each dimension") + .def_property_readonly("stride", [](const PyStridedArrayView& self) { + return stride(self.stride()); + }, "View stride in each dimension") + .def_property_readonly("dimensions", [](const PyStridedArrayView&) { return dimensions; }, "Dimension count") + .def_readonly("obj", &PyStridedArrayView::obj, "Memory owner object") + + /* Conversion to bytes */ + .def("__bytes__", [](const PyStridedArrayView& self) { + /* TODO: use _PyBytes_Resize() to avoid the double copy */ + const Containers::Array out = bytes(Containers::arrayCast(self)); + return py::bytes(out.data(), out.size()); + }, "Convert to bytes") + + /* Slicing of the top dimension */ + .def("__getitem__", [](const PyStridedArrayView& self, py::slice slice) -> py::object { + const Slice calculated = calculateSlice(slice, Containers::StridedDimensions{self.size()}[0]); + return py::cast(PyStridedArrayView(self.slice(calculated.start, calculated.stop).every(calculated.step), self.obj)); + }, "Slice the view"); +} + +template void stridedArrayView1D(py::class_>& c) { + c + /* Single item retrieval. Need to throw IndexError in order to allow + iteration: https://docs.python.org/3/reference/datamodel.html#object.__getitem__ */ + .def("__getitem__", [](const PyStridedArrayView<1, T>& self, std::size_t i) { + if(i >= self.size()) throw pybind11::index_error{}; + return self[i]; + }, "Value at given position"); +} + +template void stridedArrayViewND(py::class_>& c) { + c + /* Sub-view retrieval. Need to throw IndexError in order to allow + iteration: https://docs.python.org/3/reference/datamodel.html#object.__getitem__ */ + .def("__getitem__", [](const PyStridedArrayView& self, std::size_t i) { + if(i >= Containers::StridedDimensions{self.size()}[0]) throw pybind11::index_error{}; + return PyStridedArrayView{self[i], self.obj}; + }, "Sub-view at given position") + + /* Single-item retrieval. Need to throw IndexError in order to allow + iteration: https://docs.python.org/3/reference/datamodel.html#object.__getitem__ */ + + /* Multi-dimensional slicing */ + .def("__getitem__", [](const PyStridedArrayView& self, const typename DimensionsTuple::Type& slice) -> py::object { + Containers::StridedDimensions starts; + Containers::StridedDimensions stops; + Containers::StridedDimensions steps; + + for(std::size_t i = 0; i != dimensions; ++i) { + const Slice calculated = calculateSlice(dimensionsTupleGet(slice, i), self.size()[i]); + starts[i] = calculated.start; + stops[i] = calculated.stop; + steps[i] = calculated.step; + } + + return py::cast(PyStridedArrayView(self.slice(starts, stops).every(steps), self.obj)); + }, "Slice the view"); +} + +template void stridedArrayView2D(py::class_>& c) { + c + .def("__getitem__", [](const PyStridedArrayView<2, T>& self, const std::tuple& i) { + if(std::get<0>(i) >= self.size()[0] || + std::get<1>(i) >= self.size()[1]) throw py::index_error{}; + return self[std::get<0>(i)][std::get<1>(i)]; + }, "Value at given position") + .def("transposed", [](const PyStridedArrayView<2, T>& self, const std::size_t a, std::size_t b) { + if((a == 0 && b == 1) || + (a == 1 && b == 0)) + return PyStridedArrayView<2, T>{self.template transposed<0, 1>(), self.obj}; + throw py::value_error{Utility::formatString("dimensions {}, {} can't be transposed in a {}D view", a, b, 2)}; + }, "Transpose two dimensions") + .def("flipped", [](const PyStridedArrayView<2, T>& self, const std::size_t dimension) { + if(dimension == 0) + return PyStridedArrayView<2, T>{self.template flipped<0>(), self.obj}; + if(dimension == 1) + return PyStridedArrayView<2, T>{self.template flipped<1>(), self.obj}; + throw py::value_error{Utility::formatString("dimension {} out of range for a {}D view", dimension, 2)}; + }, "Flip a dimension") + .def("broadcasted", [](const PyStridedArrayView<2, T>& self, const std::size_t dimension, std::size_t size) { + if(dimension == 0) + return PyStridedArrayView<2, T>{self.template broadcasted<0>(size), self.obj}; + if(dimension == 1) + return PyStridedArrayView<2, T>{self.template broadcasted<1>(size), self.obj}; + throw py::value_error{Utility::formatString("dimension {} out of range for a {}D view", dimension, 2)}; + }, "Broadcast a dimension"); +} + +template void stridedArrayView3D(py::class_>& c) { + c + .def("__getitem__", [](const PyStridedArrayView<3, T>& self, const std::tuple& i) { + if(std::get<0>(i) >= self.size()[0] || + std::get<1>(i) >= self.size()[1] || + std::get<2>(i) >= self.size()[2]) throw pybind11::index_error{}; + return self[std::get<0>(i)][std::get<1>(i)][std::get<2>(i)]; + }, "Value at given position") + .def("transposed", [](const PyStridedArrayView<3, T>& self, const std::size_t a, std::size_t b) { + if((a == 0 && b == 1) || + (a == 1 && b == 0)) + return PyStridedArrayView<3, T>{self.template transposed<0, 1>(), self.obj}; + if((a == 0 && b == 2) || + (a == 2 && b == 0)) + return PyStridedArrayView<3, T>{self.template transposed<0, 2>(), self.obj}; + if((a == 1 && b == 2) || + (a == 2 && b == 1)) + return PyStridedArrayView<3, T>{self.template transposed<1, 2>(), self.obj}; + throw py::value_error{Utility::formatString("dimensions {}, {} can't be transposed in a {}D view", a, b, 3)}; + }, "Transpose two dimensions") + .def("flipped", [](const PyStridedArrayView<3, T>& self, const std::size_t dimension) { + if(dimension == 0) + return PyStridedArrayView<3, T>{self.template flipped<0>(), self.obj}; + if(dimension == 1) + return PyStridedArrayView<3, T>{self.template flipped<1>(), self.obj}; + if(dimension == 2) + return PyStridedArrayView<3, T>{self.template flipped<2>(), self.obj}; + throw py::value_error{Utility::formatString("dimension {} out of range for a {}D view", dimension, 3)}; + }, "Flip a dimension") + .def("broadcasted", [](const PyStridedArrayView<3, T>& self, const std::size_t dimension, std::size_t size) { + if(dimension == 0) + return PyStridedArrayView<3, T>{self.template broadcasted<0>(size), self.obj}; + if(dimension == 1) + return PyStridedArrayView<3, T>{self.template broadcasted<1>(size), self.obj}; + if(dimension == 2) + return PyStridedArrayView<3, T>{self.template broadcasted<2>(size), self.obj}; + throw py::value_error{Utility::formatString("dimension {} out of range for a {}D view", dimension, 3)}; + }, "Broadcast a dimension"); +} + +template void mutableStridedArrayView1D(py::class_>& c) { + c + .def("__setitem__", [](const PyStridedArrayView<1, T>& self, const std::size_t i, const T& value) { + if(i >= self.size()) throw pybind11::index_error{}; + self[i] = value; + }, "Set a value at given position"); +} + +template void mutableStridedArrayView2D(py::class_>& c) { + c + .def("__setitem__", [](const PyStridedArrayView<2, T>& self, const std::tuple& i, const T& value) { + if(std::get<0>(i) >= self.size()[0] || + std::get<1>(i) >= self.size()[1]) throw pybind11::index_error{}; + self[std::get<0>(i)][std::get<1>(i)] = value; + }, "Set a value at given position"); +} + +template void mutableStridedArrayView3D(py::class_>& c) { + c + .def("__setitem__", [](const PyStridedArrayView<3, T>& self, const std::tuple& i, const T& value) { + if(std::get<0>(i) >= self.size()[0] || + std::get<1>(i) >= self.size()[1] || + std::get<2>(i) >= self.size()[2]) throw pybind11::index_error{}; + self[std::get<0>(i)][std::get<1>(i)][std::get<2>(i)] = value; + }, "Set a value at given position"); +} + +void containers(py::module& m) { + py::class_> arrayView_{m, + "ArrayView", "Array view", py::buffer_protocol{}}; + arrayView(arrayView_); + + py::class_> mutableArrayView_{m, + "MutableArrayView", "Mutable array view", py::buffer_protocol{}}; + arrayView(mutableArrayView_); + mutableArrayView(mutableArrayView_); + + py::class_> stridedArrayView1D_{m, + "StridedArrayView1D", "One-dimensional array view with stride information", py::buffer_protocol{}}; + py::class_> stridedArrayView2D_{m, + "StridedArrayView2D", "Two-dimensional array view with stride information", py::buffer_protocol{}}; + py::class_> stridedArrayView3D_{m, + "StridedArrayView3D", "Three-dimensional array view with stride information", py::buffer_protocol{}}; + stridedArrayView(stridedArrayView1D_); + stridedArrayView1D(stridedArrayView1D_); + stridedArrayView(stridedArrayView2D_); + stridedArrayViewND(stridedArrayView2D_); + stridedArrayView2D(stridedArrayView2D_); + stridedArrayView(stridedArrayView3D_); + stridedArrayViewND(stridedArrayView3D_); + stridedArrayView3D(stridedArrayView3D_); + + py::class_> mutableStridedArrayView1D_{m, + "MutableStridedArrayView1D", "Mutable one-dimensional array view with stride information", py::buffer_protocol{}}; + py::class_> mutableStridedArrayView2D_{m, + "MutableStridedArrayView2D", "Mutable two-dimensional array view with stride information", py::buffer_protocol{}}; + py::class_> mutableStridedArrayView3D_{m, + "MutableStridedArrayView3D", "Mutable three-dimensional array view with stride information", py::buffer_protocol{}}; + stridedArrayView(mutableStridedArrayView1D_); + stridedArrayView1D(mutableStridedArrayView1D_); + stridedArrayView(mutableStridedArrayView2D_); + stridedArrayViewND(mutableStridedArrayView2D_); + stridedArrayView(mutableStridedArrayView3D_); + stridedArrayViewND(mutableStridedArrayView3D_); + mutableStridedArrayView1D(mutableStridedArrayView1D_); + mutableStridedArrayView2D(mutableStridedArrayView2D_); + mutableStridedArrayView3D(mutableStridedArrayView3D_); +} + +}} + +PYBIND11_MODULE(containers, m) { + m.doc() = "Corrade containers module"; + corrade::containers(m); +} diff --git a/src/python/corrade/test/__init__.py b/src/python/corrade/test/__init__.py new file mode 100644 index 0000000..af32f8e --- /dev/null +++ b/src/python/corrade/test/__init__.py @@ -0,0 +1,31 @@ +# +# This file is part of Magnum. +# +# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 +# Vladimír Vondruš +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# + +import os +import sys + +# TODO: do this differently / more robustly +sys.path = [os.path.join(os.path.dirname(__file__), '../../../../build/src/python')] + sys.path + diff --git a/src/python/corrade/test/test_containers.py b/src/python/corrade/test/test_containers.py new file mode 100644 index 0000000..e902f23 --- /dev/null +++ b/src/python/corrade/test/test_containers.py @@ -0,0 +1,694 @@ +# +# This file is part of Magnum. +# +# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 +# Vladimír Vondruš +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# + +import sys +import unittest + +from corrade import containers + +class ArrayView(unittest.TestCase): + def test_init(self): + a = containers.ArrayView() + b = containers.MutableArrayView() + self.assertIs(a.obj, None) + self.assertIs(b.obj, None) + self.assertEqual(len(a), 0) + self.assertEqual(len(b), 0) + self.assertEqual(bytes(a), b'') + self.assertEqual(bytes(b), b'') + + def test_init_buffer(self): + a = b'hello' + a_refcount = sys.getrefcount(a) + + b = containers.ArrayView(a) + self.assertIs(b.obj, a) + self.assertEqual(len(b), 5) + self.assertEqual(bytes(b), b'hello') + self.assertEqual(b[2], 'l') + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + + # Not mutable + with self.assertRaisesRegex(TypeError, "object does not support item assignment"): + b[4] = '!' + + # b should keep a reference to a, so deleting the local reference + # shouldn't affect it + del a + self.assertTrue(sys.getrefcount(b.obj), a_refcount) + self.assertEqual(b[2], 'l') + + # Now, if we delete b, a should not be referenced by anything anymore + a = b.obj + del b + self.assertTrue(sys.getrefcount(a), a_refcount) + + def test_init_buffer_mutable(self): + a = bytearray(b'hello') + b = containers.MutableArrayView(a) + b[4] = '!' + self.assertEqual(b[4], '!') + self.assertEqual(bytes(b), b'hell!') + + def test_init_buffer_unexpected_dimensions(self): + a = memoryview(b'123456').cast('b', shape=[2, 3]) + self.assertEqual(bytes(a), b'123456') + with self.assertRaisesRegex(BufferError, "expected one dimension but got 2"): + b = containers.ArrayView(a) + + def test_init_buffer_unexpected_stride(self): + a = memoryview(b'hello')[::2] + self.assertEqual(bytes(a), b'hlo') + with self.assertRaisesRegex(BufferError, "expected stride of 1 but got 2"): + b = containers.ArrayView(a) + + def test_init_buffer_mutable_from_immutable(self): + a = b'hello' + with self.assertRaisesRegex(BufferError, "Object is not writable."): + b = containers.MutableArrayView(a) + + def test_slice(self): + a = b'World is hell!' + a_refcount = sys.getrefcount(a) + + b = containers.ArrayView(a) + b_refcount = sys.getrefcount(b) + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + + # When slicing, b's refcount should not change but a's refcount should + # increase + c = b[4:-4] + self.assertIsInstance(c, containers.ArrayView) + self.assertEqual(bytes(c), b'd is h') + self.assertEqual(sys.getrefcount(b), b_refcount) + self.assertEqual(sys.getrefcount(a), a_refcount + 2) + + # Deleting a slice should reduce a's refcount again, keep b's unchanged + del c + self.assertEqual(sys.getrefcount(b), b_refcount) + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + + def test_slice_empty(self): + # slice.start = slice.stop + a = containers.ArrayView(b'hello')[7:8] + self.assertEqual(len(a), 0) + + def test_slice_invalid(self): + with self.assertRaisesRegex(ValueError, "slice step cannot be zero"): + containers.ArrayView()[::0] + + def test_slice_stride(self): + a = b'World_ _i_s_ _hell!' + a_refcount = sys.getrefcount(a) + + b = containers.ArrayView(a) + b_refcount = sys.getrefcount(b) + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + + # When slicing to a strided array view, b's refcount should not change + # but a's refcount should increase. Check consistency with slices on + # bytes, slicing bytes will make a copy so it doesn't affect the + # refcount. + c1 = a[4:-4:2] + c2 = b[4:-4:2] + self.assertIsInstance(c2, containers.StridedArrayView1D) + self.assertEqual(len(c1), 6) + self.assertEqual(len(c2), 6) + self.assertEqual(bytes(c1), b'd is h') + self.assertEqual(bytes(c2), b'd is h') + self.assertEqual(c2.size, (6,)) + self.assertEqual(c2.stride, (2,)) + self.assertEqual(sys.getrefcount(b), b_refcount) + self.assertEqual(sys.getrefcount(a), a_refcount + 2) + + # Deleting a slice should reduce a's refcount again, keep b's unchanged + del c2 + self.assertEqual(sys.getrefcount(b), b_refcount) + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + + def test_slice_stride_negative(self): + a = b'World_ _i_s_ _hell!' + b = containers.ArrayView(a) + + # Check consistency with slices on bytes + c1 = a[-5:3:-2] # like [4:-4:2] above, but reverted + c2 = b[-5:3:-2] + self.assertEqual(len(c1), 6) + self.assertEqual(len(c2), 6) + self.assertEqual(bytes(c1), b'h si d') # like b'd is h' but reverted + self.assertEqual(bytes(c2), b'h si d') + self.assertEqual(c2.size, (6,)) + self.assertEqual(c2.stride, (-2,)) + + def test_slice_stride_reverse(self): + # slice.stop = -1 + a = containers.ArrayView(b'hello')[::-1] + self.assertEqual(len(a), 5) + self.assertEqual(bytes(a), b'olleh') + + def test_convert_memoryview(self): + a = b'World is hell!' + a_refcount = sys.getrefcount(a) + + b = containers.ArrayView(a) + b_refcount = sys.getrefcount(b) + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + + # TODO: fix when pybind is replaced + + c = memoryview(b) + self.assertEqual(c.obj, b) # TODO: should be a + self.assertEqual(sys.getrefcount(b), b_refcount + 1) # TODO: should not hcange + self.assertEqual(sys.getrefcount(a), a_refcount + 1) # TODO: should be +2 + +class StridedArrayView1D(unittest.TestCase): + def test_init(self): + a = containers.StridedArrayView1D() + b = containers.MutableStridedArrayView1D() + self.assertIs(a.obj, None) + self.assertIs(b.obj, None) + self.assertEqual(len(a), 0) + self.assertEqual(len(b), 0) + self.assertEqual(bytes(a), b'') + self.assertEqual(bytes(b), b'') + 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_buffer(self): + a = b'hello' + a_refcount = sys.getrefcount(a) + + b = containers.StridedArrayView1D(a) + self.assertIs(b.obj, 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[2], 'l') + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + + # Not mutable + with self.assertRaisesRegex(TypeError, "object does not support item assignment"): + b[4] = '!' + + # b should keep a reference to a, so deleting the local reference + # shouldn't affect it + del a + self.assertTrue(sys.getrefcount(b.obj), a_refcount) + self.assertEqual(b[2], 'l') + + # Now, if we delete b, a should not be referenced by anything anymore + a = b.obj + del b + self.assertTrue(sys.getrefcount(a), a_refcount) + + @unittest.expectedFailure + def test_init_buffer_memoryview_obj(self): + a = b'hello' + v = memoryview(a) + b = containers.StridedArrayView1D(v) + self.assertIs(b.obj, a) # TODO: it's b because pybind is stupid + + def test_init_buffer_mutable(self): + a = bytearray(b'hello') + b = containers.MutableStridedArrayView1D(a) + b[4] = '!' + self.assertEqual(b[4], '!') + self.assertEqual(bytes(b), b'hell!') + + def test_init_buffer_unexpected_dimensions(self): + a = memoryview(b'123456').cast('b', shape=[2, 3]) + self.assertEqual(bytes(a), b'123456') + with self.assertRaisesRegex(BufferError, "expected 1 dimensions but got 2"): + b = containers.StridedArrayView1D(a) + + def test_init_buffer_stride(self): + a = memoryview(b'hello')[::2] + self.assertEqual(bytes(a), b'hlo') + b = containers.StridedArrayView1D(a) + self.assertEqual(len(b), 3) + self.assertEqual(bytes(b), b'hlo') + self.assertEqual(b.size, (3, )) + self.assertEqual(b.stride, (2, )) + self.assertEqual(b[2], 'o') + + def test_init_buffer_mutable_from_immutable(self): + a = b'hello' + with self.assertRaisesRegex(BufferError, "Object is not writable."): + b = containers.MutableStridedArrayView1D(a) + + def test_slice(self): + a = b'World is hell!' + a_refcount = sys.getrefcount(a) + + b = containers.StridedArrayView1D(a) + b_refcount = sys.getrefcount(b) + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + + # When slicing, b's refcount should not change but a's refcount should + # increase + c = b[4:-4] + self.assertEqual(c.size, (6,)) + self.assertEqual(c.stride, (1,)) + self.assertIsInstance(c, containers.StridedArrayView1D) + self.assertEqual(bytes(c), b'd is h') + self.assertEqual(sys.getrefcount(b), b_refcount) + self.assertEqual(sys.getrefcount(a), a_refcount + 2) + + # Deleting a slice should reduce a's refcount again, keep b's unchanged + del c + self.assertEqual(sys.getrefcount(b), b_refcount) + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + + def test_slice_invalid(self): + with self.assertRaisesRegex(TypeError, "indices must be integers"): + containers.StridedArrayView1D()[-5:3:"boo"] + + def test_slice_stride(self): + a = b'World_ _i_s_ _hell!' + b = containers.StridedArrayView1D(a) + + # Check consistency with slices on bytes + c1 = a[4:-4:2] + c2 = b[4:-4:2] + self.assertIsInstance(c2, containers.StridedArrayView1D) + self.assertEqual(len(c1), 6) + self.assertEqual(len(c2), 6) + self.assertEqual(bytes(c1), b'd is h') + self.assertEqual(bytes(c2), b'd is h') + self.assertEqual(c2.size, (6,)) + self.assertEqual(c2.stride, (2,)) + + def test_slice_stride_negative(self): + a = b'World_ _i_s_ _hell!' + b = containers.StridedArrayView1D(a) + + # Check consistency with slices on bytes + c1 = a[-5:3:-2] # like [4:-4:2] above, but reverted + c2 = b[-5:3:-2] + self.assertEqual(len(c1), 6) + self.assertEqual(len(c2), 6) + self.assertEqual(bytes(c1), b'h si d') # like b'd is h' but reverted + self.assertEqual(bytes(c2), b'h si d') + self.assertEqual(c2.size, (6,)) + self.assertEqual(c2.stride, (-2,)) + + def test_convert_memoryview(self): + a = b'World is hell!' + a_refcount = sys.getrefcount(a) + + b = containers.StridedArrayView1D(a) + b_refcount = sys.getrefcount(b) + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + + # TODO: fix when pybind is replaced + + c = memoryview(b) + self.assertEqual(c.ndim, 1) + self.assertEqual(len(c), len(a)) + self.assertEqual(bytes(c), a) + self.assertEqual(c.obj, b) # TODO: should be a + self.assertEqual(sys.getrefcount(b), b_refcount + 1) # TODO: should not change + self.assertEqual(sys.getrefcount(a), a_refcount + 1) # TODO: should be +2 + + c[-1] = ord('?') # TODO: wrong, should fail + self.assertEqual(a, b'World is hell?') # TODO: wrong + +class StridedArrayView2D(unittest.TestCase): + def test_init(self): + a = containers.StridedArrayView2D() + b = containers.MutableStridedArrayView2D() + self.assertIs(a.obj, None) + self.assertIs(b.obj, None) + self.assertEqual(len(a), 0) + self.assertEqual(len(b), 0) + self.assertEqual(bytes(a), b'') + self.assertEqual(bytes(b), b'') + 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_buffer(self): + a = (b'01234567' + b'456789ab' + b'89abcdef') + a_refcount = sys.getrefcount(a) + + b = containers.StridedArrayView2D(memoryview(a).cast('b', shape=[3, 8])) + # TODO: construct as containers.StridedArrayView2D(a, (3, 8), (8, 1)) + #self.assertIs(b.obj, a) # TODO + self.assertEqual(len(b), 3) + self.assertEqual(bytes(b), b'01234567' + b'456789ab' + b'89abcdef') + self.assertEqual(b.size, (3, 8)) + self.assertEqual(b.stride, (8, 1)) + self.assertIsInstance(b[1], containers.StridedArrayView1D) + self.assertEqual(bytes(b[1]), b'456789ab') + self.assertEqual(b[1, 2], '6') + self.assertEqual(b[1][2], '6') + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + + # Not mutable + with self.assertRaisesRegex(TypeError, "object does not support item assignment"): + b[1, 2] = '!' + + # b should keep a reference to a, so deleting the local reference + # shouldn't affect it + del a + self.assertTrue(sys.getrefcount(b.obj), a_refcount) + self.assertEqual(b[1][2], '6') + + # Now, if we delete b, a should not be referenced by anything anymore + a = b.obj + del b + self.assertTrue(sys.getrefcount(a), a_refcount) + + def test_init_buffer_mutable(self): + a = bytearray(b'01234567' + b'456789ab' + b'89abcdef') + b = containers.MutableStridedArrayView2D(memoryview(a).cast('b', shape=[3, 8])) + b[0, 7] = '!' + b[1, 7] = '!' + b[2, 7] = '!' + self.assertEqual(b[0][7], '!') + self.assertEqual(bytes(b), b'0123456!' + b'456789a!' + b'89abcde!') + + def test_init_buffer_unexpected_dimensions(self): + a = b'123456' + with self.assertRaisesRegex(BufferError, "expected 2 dimensions but got 1"): + b = containers.StridedArrayView2D(a) + + def test_init_buffer_stride(self): + a = memoryview(b'01234567' + b'456789ab' + b'89abcdef').cast('b', shape=[3, 8])[::2] + self.assertEqual(bytes(a), b'0123456789abcdef') + b = containers.StridedArrayView2D(a) + self.assertEqual(len(b), 2) + self.assertEqual(bytes(b), b'0123456789abcdef') + self.assertEqual(b.size, (2, 8)) + self.assertEqual(b.stride, (16, 1)) + self.assertEqual(bytes(b[1]), b'89abcdef') + self.assertEqual(b[1][3], 'b') + + def test_init_buffer_mutable_from_immutable(self): + a = memoryview(b'01234567' + b'456789ab' + b'89abcdef').cast('b', shape=[3, 8]) + with self.assertRaisesRegex(BufferError, "underlying buffer is not writable"): + b = containers.MutableStridedArrayView2D(a) + + def test_slice(self): + a = (b'01234567' + b'456789ab' + b'89abcdef') + a_refcount = sys.getrefcount(a) + + v = memoryview(a).cast('b', shape=[3, 8]) + v_refcount = sys.getrefcount(v) + + # TODO: Pybind refcounts against v, not a + + b = containers.StridedArrayView2D(v) + b_refcount = sys.getrefcount(b) + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + self.assertEqual(sys.getrefcount(v), v_refcount + 1) # TODO: should not change + + # When slicing, b's refcount should not change but a's refcount should + # increase + c = b[0:-1] + self.assertIsInstance(c, containers.StridedArrayView2D) + self.assertEqual(c.size, (2, 8)) + self.assertEqual(c.stride, (8, 1)) + self.assertEqual(bytes(c), b'01234567456789ab') + self.assertEqual(sys.getrefcount(b), b_refcount) + self.assertEqual(sys.getrefcount(a), a_refcount + 1) # TODO: should be +2 + self.assertEqual(sys.getrefcount(v), v_refcount + 2) # TODO: should not change + + # Deleting a slice should reduce a's refcount again, keep b's unchanged + del c + self.assertEqual(sys.getrefcount(b), b_refcount) + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + self.assertEqual(sys.getrefcount(v), v_refcount + 1) # TODO: should not change + + def test_slice_multidimensional(self): + a = (b'01234567' + b'456789ab' + b'89abcdef') + a_refcount = sys.getrefcount(a) + + v = memoryview(a).cast('b', shape=[3, 8]) + v_refcount = sys.getrefcount(v) + + # TODO: Pybind refcounts against v, not a + + b = containers.StridedArrayView2D(v) + b_refcount = sys.getrefcount(b) + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + self.assertEqual(sys.getrefcount(v), v_refcount + 1) # TODO: should not change + + # When slicing, b's refcount should not change but a's refcount should + # increase + c = b[1:3,4:7] + self.assertIsInstance(c, containers.StridedArrayView2D) + self.assertEqual(c.size, (2, 3)) + self.assertEqual(c.stride, (8, 1)) + self.assertEqual(bytes(c[0]), b'89a') + self.assertEqual(bytes(c[1]), b'cde') + self.assertEqual(sys.getrefcount(b), b_refcount) + self.assertEqual(sys.getrefcount(a), a_refcount + 1) # TODO: should be +2 + self.assertEqual(sys.getrefcount(v), v_refcount + 2) # TODO: should not change + + # Deleting a slice should reduce a's refcount again, keep b's unchanged + del c + self.assertEqual(sys.getrefcount(b), b_refcount) + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + self.assertEqual(sys.getrefcount(v), v_refcount + 1) # TODO: should not change + + def test_slice_invalid(self): + with self.assertRaisesRegex(ValueError, "slice step cannot be zero"): + containers.StridedArrayView1D()[-5:3:0] + + def test_slice_stride(self): + a = (b'01234567' + b'456789ab' + b'89abcdef') + v = memoryview(a).cast('b', shape=[3, 8]) + b = containers.StridedArrayView2D(v) + + # Check consistency with slices on memoryview + c1 = v[0:3:2] + c2 = b[0:3:2] + self.assertEqual(len(c1), 2) + self.assertEqual(len(c2), 2) + self.assertIsInstance(c2, containers.StridedArrayView2D) + self.assertEqual(bytes(c1), b'0123456789abcdef') + self.assertEqual(bytes(c2), b'0123456789abcdef') + self.assertEqual(c2.size, (2, 8)) + self.assertEqual(c2.stride, (16, 1)) + self.assertEqual(bytes(c2[1]), b'89abcdef') + + def test_slice_stride_negative(self): + a = (b'01234567' + b'456789ab' + b'89abcdef') + v = memoryview(a).cast('b', shape=[3, 8]) + b = containers.StridedArrayView2D(v) + + # Check consistency with slices on memoryview + self.assertEqual(v.shape, (3, 8)) + self.assertEqual(b.size, (3, 8)) + self.assertEqual(v.strides, (8, 1)) + self.assertEqual(b.stride, (8, 1)) + c1 = v[-1:None:-2] # like [0:3:2] above, but reverted + c2 = b[-1:None:-2] + self.assertEqual(len(c1), 2) + self.assertEqual(len(c2), 2) + self.assertEqual(bytes(c1), b'89abcdef01234567') # like above but reverted + self.assertEqual(bytes(c2), b'89abcdef01234567') + self.assertEqual(c1.shape, (2, 8)) + self.assertEqual(c2.size, (2, 8)) + self.assertEqual(c1.strides, (-16, 1)) + self.assertEqual(c2.stride, (-16, 1)) + + def test_slice_stride_negative_multidimensional(self): + a = (b'01234567' + b'456789ab' + b'89abcdef') + v = memoryview(a).cast('b', shape=[3, 8]) + b = containers.StridedArrayView2D(v) + + # Check consistency with slices on memoryview + self.assertEqual(v.shape, (3, 8)) + self.assertEqual(b.size, (3, 8)) + self.assertEqual(v.strides, (8, 1)) + self.assertEqual(b.stride, (8, 1)) + + with self.assertRaises(NotImplementedError): + c1 = v[-1:None:-2, -2:2:-3] # HAH! + + c2 = b[-1:None:-2, -2:2:-3] + self.assertEqual(len(c2), 2) + self.assertEqual(bytes(c2), b'eb63') + self.assertEqual(c2.size, (2, 2)) + self.assertEqual(c2.stride, (-16, -3)) + + def test_ops(self): + a = (b'01234567' + b'456789ab' + b'89abcdef') + v = memoryview(a).cast('b', shape=[3, 8]) + + b = containers.StridedArrayView2D(v).transposed(0, 1).flipped(0) + self.assertEqual(b.size, (8, 3)) + self.assertEqual(b.stride, (-1, 8)) + self.assertEqual(bytes(b), b'7bf6ae59d48c37b26a159048') + + c = containers.StridedArrayView2D(v).transposed(1, 0).flipped(1) + self.assertEqual(c.size, (8, 3)) + self.assertEqual(c.stride, (1, -8)) + self.assertEqual(bytes(c), b'840951a62b73c84d95ea6fb7') + + d = containers.StridedArrayView2D(v).transposed(0, 1)[3:4].broadcasted(0, 5) + self.assertEqual(d.size, (5, 3)) + self.assertEqual(d.stride, (0, 8)) + self.assertEqual(bytes(d), b'37b37b37b37b37b') + + d = containers.StridedArrayView2D(v)[:, 3:4].broadcasted(1, 2) + self.assertEqual(d.size, (3, 2)) + self.assertEqual(d.stride, (8, 0)) + self.assertEqual(bytes(d), b'3377bb') + + def test_convert_memoryview(self): + a = (b'01234567' + b'456789ab' + b'89abcdef') + a_refcount = sys.getrefcount(a) + v = memoryview(a).cast('b', shape=[3, 8]) + v_refcount = sys.getrefcount(v) + + b = containers.StridedArrayView2D(v) + b_refcount = sys.getrefcount(b) + self.assertEqual(sys.getrefcount(a), a_refcount + 1) # TODO: should be + 2 + self.assertEqual(sys.getrefcount(v), v_refcount + 1) # TODO: should not change + + c = memoryview(b) + self.assertEqual(c.ndim, 2) + self.assertEqual(c.shape, (3, 8)) + self.assertEqual(c.strides, (8, 1)) + self.assertEqual(c.obj, v) # TODO: should be a + self.assertEqual(sys.getrefcount(b), b_refcount + 1) # TODO: should not change + self.assertEqual(sys.getrefcount(a), a_refcount + 1) # TODO: should be + 2 + self.assertEqual(sys.getrefcount(v), v_refcount + 1) # TODO: should not change + + c[2, 1] = ord('!') # TODO: wrong, should fail + self.assertEqual(chr(c[2, 1]), '!') # TODO: should be 9 + +class StridedArrayView3D(unittest.TestCase): + def test_init_buffer(self): + a = (b'01234567' + b'456789ab' + b'89abcdef' + + b'cdef0123' + b'01234567' + b'456789ab') + b = containers.StridedArrayView3D(memoryview(a).cast('b', shape=[2, 3, 8])) + self.assertEqual(len(b), 2) + self.assertEqual(bytes(b), b'01234567456789ab89abcdefcdef012301234567456789ab') + self.assertEqual(b.size, (2, 3, 8)) + self.assertEqual(b.stride, (24, 8, 1)) + self.assertEqual(b[1, 2, 3], '7') + self.assertEqual(b[1][2][3], '7') + + def test_init_buffer_mutable(self): + a = bytearray(b'01234567' + b'456789ab' + b'89abcdef' + + b'cdef0123' + b'01234567' + b'456789ab') + b = containers.MutableStridedArrayView3D(memoryview(a).cast('b', shape=[2, 3, 8])) + b[0, 0, 7] = '!' + b[0, 1, 7] = '!' + b[0, 2, 7] = '!' + b[1, 0, 7] = '!' + b[1, 1, 7] = '!' + b[1, 2, 7] = '!' + self.assertEqual(b[1][1][7], '!') + self.assertEqual(bytes(b), b'0123456!' + b'456789a!' + b'89abcde!' + + b'cdef012!' + b'0123456!' + b'456789a!') + + def test_ops(self): + a = (b'01234567' + b'456789ab' + b'89abcdef' + + b'cdef0123' + b'01234567' + b'456789ab') + v = memoryview(a).cast('b', shape=[2, 3, 8]) + + b = containers.StridedArrayView3D(v).transposed(0, 1).flipped(0) + self.assertEqual(b.size, (3, 2, 8)) + self.assertEqual(b.stride, (-8, 24, 1)) + self.assertEqual(bytes(b), b'89abcdef456789ab456789ab0123456701234567cdef0123') + + c = containers.StridedArrayView3D(v).transposed(2, 0).flipped(1) + self.assertEqual(c.size, (8, 3, 2)) + self.assertEqual(c.stride, (1, -8, 24)) + self.assertEqual(bytes(c), b'84400c95511da6622eb7733fc88440d99551eaa662fbb773') + + d = containers.StridedArrayView3D(v).transposed(1, 2)[0:1, 3:5, :].broadcasted(0, 5) + self.assertEqual(d.size, (5, 2, 3)) + self.assertEqual(d.stride, (0, 1, 8)) + self.assertEqual(bytes(d), b'37b48c37b48c37b48c37b48c37b48c') + + e = containers.StridedArrayView3D(v)[:, 1:2, 3:4].flipped(2).broadcasted(1, 2) + self.assertEqual(e.size, (2, 2, 1)) + self.assertEqual(e.stride, (24, 0, -1)) + self.assertEqual(bytes(e), b'7733') + + f = containers.StridedArrayView3D(v)[:, :, 0:1].broadcasted(2, 5) + self.assertEqual(f.size, (2, 3, 5)) + self.assertEqual(f.stride, (24, 8, 0)) + self.assertEqual(bytes(f), b'000004444488888ccccc0000044444') diff --git a/src/python/setup.py.cmake b/src/python/setup.py.cmake index d4f82f9..ddbb1f6 100644 --- a/src/python/setup.py.cmake +++ b/src/python/setup.py.cmake @@ -31,6 +31,7 @@ from setuptools.command.build_ext import build_ext extension_paths = { # Filled in by cmake + 'corrade.containers': '$', 'magnum._magnum': '$', } @@ -41,7 +42,7 @@ class TheExtensionIsAlreadyBuiltWhyThisHasToBeSoDamnComplicated(build_ext): setup( name='magnum', - packages=['magnum'], + packages=['corrade', 'magnum'], ext_modules=[Extension(name, sources=[]) for name, path in extension_paths.items() if path], cmdclass = { 'build_ext': TheExtensionIsAlreadyBuiltWhyThisHasToBeSoDamnComplicated