Browse Source

python: expose ArrayView and StridedArrayView.

Wow. This took a while and still is far from ideal.
pull/1/head
Vladimír Vondruš 7 years ago
parent
commit
d34366ac46
  1. 3
      package/archlinux/PKGBUILD
  2. 5
      package/ci/travis-desktop.sh
  3. 1
      src/python/CMakeLists.txt
  4. 3
      src/python/corrade/.coveragerc
  5. 40
      src/python/corrade/CMakeLists.txt
  6. 54
      src/python/corrade/PyArrayView.h
  7. 37
      src/python/corrade/PybindExtras.h
  8. 26
      src/python/corrade/__init__.py
  9. 38
      src/python/corrade/bootstrap.h
  10. 452
      src/python/corrade/containers.cpp
  11. 31
      src/python/corrade/test/__init__.py
  12. 694
      src/python/corrade/test/test_containers.py
  13. 3
      src/python/setup.py.cmake

3
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
}

5
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

1
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

3
src/python/corrade/.coveragerc

@ -0,0 +1,3 @@
[run]
omit =
*/test/*

40
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š <mosra@centrum.cz>
#
# 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)

54
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š <mosra@centrum.cz>
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 <pybind11/pybind11.h>
#include <Corrade/Containers/ArrayView.h>
#include <Corrade/Containers/StridedArrayView.h>
#include "corrade/bootstrap.h"
namespace corrade {
/* Wrapper for Containers::ArrayView holding a reference to the memory owner */
template<class T> struct PyArrayView: Containers::ArrayView<T> {
/*implicit*/PyArrayView() noexcept: obj{py::none{}} {}
explicit PyArrayView(Containers::ArrayView<T> view, py::object obj) noexcept: Containers::ArrayView<T>{view}, obj{std::move(obj)} {}
py::object obj;
};
/* Wrapper for Containers::StridedArrayView holding a reference to the memory owner */
template<unsigned dimensions, class T> struct PyStridedArrayView: Containers::StridedArrayView<dimensions, T> {
/*implicit*/ PyStridedArrayView() noexcept: obj{py::none{}} {}
explicit PyStridedArrayView(Containers::StridedArrayView<dimensions, T> view, py::object obj) noexcept: Containers::StridedArrayView<dimensions, T>{view}, obj{std::move(obj)} {}
py::object obj;
};
}
#endif

37
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š <mosra@centrum.cz>
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 <pybind11/pybind11.h>
namespace pybind11 {
// TODO: add this to pybind itself
PYBIND11_RUNTIME_EXCEPTION(buffer_error, PyExc_BufferError)
}
#endif

26
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š <mosra@centrum.cz>
#
# 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"""

38
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š <mosra@centrum.cz>
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

452
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š <mosra@centrum.cz>
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 <pybind11/pybind11.h>
#include <Corrade/Containers/Array.h>
#include <Corrade/Utility/FormatStl.h>
#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<std::size_t*>(&start), reinterpret_cast<std::size_t*>(&stop), reinterpret_cast<std::size_t*>(&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<class T> void arrayView(py::class_<PyArrayView<T>>& c) {
/* Implicitly convertible from a buffer */
py::implicitly_convertible<py::buffer, PyArrayView<T>>();
c
/* Constructor */
.def(py::init(), "Default constructor")
/* Buffer protocol */
.def(py::init([](py::buffer buffer) {
py::buffer_info info = buffer.request(!std::is_const<T>::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<T>{{static_cast<T*>(info.ptr), std::size_t(info.shape[0])}, buffer};
}), "Construct from a buffer")
.def_buffer([](const PyArrayView<T>& self) -> py::buffer_info {
return py::buffer_info{
const_cast<char*>(self.data()),
sizeof(T),
py::format_descriptor<T>::format(),
1,
{self.size()},
{1} // TODO: need to pass self.obj to the buffer, not self!
};
})
/* Length and memory owning object */
.def("__len__", &PyArrayView<T>::size, "View size")
.def_readonly("obj", &PyArrayView<T>::obj, "Memory owner object")
/* Conversion to bytes */
.def("__bytes__", [](const PyArrayView<T>& 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<T>& 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<T>& 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<T>{self.slice(calculated.start, calculated.stop), self.obj});
}, "Slice the view");
}
template<class T> void mutableArrayView(py::class_<PyArrayView<T>>& c) {
c
.def("__setitem__", [](const PyArrayView<T>& 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<unsigned dimensions, class T> struct DimensionsTuple;
template<class T> struct DimensionsTuple<1, T> { typedef std::tuple<T> Type; };
template<class T> struct DimensionsTuple<2, T> { typedef std::tuple<T, T> Type; };
template<class T> struct DimensionsTuple<3, T> { typedef std::tuple<T, T, T> Type; };
/* Size tuple for given dimension */
template<unsigned dimensions> typename DimensionsTuple<dimensions, std::size_t>::Type size(Containers::StridedDimensions<dimensions, std::size_t>);
template<> std::tuple<std::size_t> size(Containers::StridedDimensions<1, std::size_t> size) {
return std::make_tuple(size[0]);
}
template<> std::tuple<std::size_t, std::size_t> size(Containers::StridedDimensions<2, std::size_t> size) {
return std::make_tuple(size[0], size[1]);
}
template<> std::tuple<std::size_t, std::size_t, std::size_t> size(Containers::StridedDimensions<3, std::size_t> size) {
return std::make_tuple(size[0], size[1], size[2]);
}
/* Stride tuple for given dimension */
template<unsigned dimensions> typename DimensionsTuple<dimensions, std::ptrdiff_t>::Type stride(Containers::StridedDimensions<dimensions, std::ptrdiff_t>);
template<> std::tuple<std::ptrdiff_t> stride(Containers::StridedDimensions<1, std::ptrdiff_t> stride) {
return std::make_tuple(stride[0]);
}
template<> std::tuple<std::ptrdiff_t, std::ptrdiff_t> stride(Containers::StridedDimensions<2, std::ptrdiff_t> stride) {
return std::make_tuple(stride[0], stride[1]);
}
template<> std::tuple<std::ptrdiff_t, std::ptrdiff_t, std::ptrdiff_t> stride(Containers::StridedDimensions<3, std::ptrdiff_t> stride) {
return std::make_tuple(stride[0], stride[1], stride[2]);
}
/* Byte conversion for given dimension */
template<unsigned dimensions> Containers::Array<char> bytes(Containers::StridedArrayView<dimensions, const char>);
template<> Containers::Array<char> bytes(Containers::StridedArrayView1D<const char> view) {
Containers::Array<char> out{view.size()};
std::size_t pos = 0;
for(const char i: view) out[pos++] = i;
return out;
}
template<> Containers::Array<char> bytes(Containers::StridedArrayView2D<const char> view) {
Containers::Array<char> out{view.size()[0]*view.size()[1]};
std::size_t pos = 0;
for(Containers::StridedArrayView1D<const char> i: view)
for(const char j: i) out[pos++] = j;
return out;
}
template<> Containers::Array<char> bytes(Containers::StridedArrayView3D<const char> view) {
Containers::Array<char> out{view.size()[0]*view.size()[1]*view.size()[2]};
std::size_t pos = 0;
for(Containers::StridedArrayView2D<const char> i: view)
for(Containers::StridedArrayView1D<const char> j: i)
for(const char k: j) out[pos++] = k;
return out;
}
/* Getting a runtime tuple index. Ugh. */
template<class T> 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<class T> 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<class T> 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<unsigned dimensions, class T> void stridedArrayView(py::class_<PyStridedArrayView<dimensions, T>>& 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<T>::value);
if(info.ndim != dimensions)
throw py::buffer_error{Utility::formatString("expected {} dimensions but got {}", dimensions, info.ndim)};
return PyStridedArrayView<dimensions, T>{{
{static_cast<T*>(info.ptr), std::size_t(info.size*info.strides[0])},
Containers::StaticArrayView<dimensions, const std::size_t>{reinterpret_cast<std::size_t*>(&info.shape[0])},
Containers::StaticArrayView<dimensions, const std::ptrdiff_t>{reinterpret_cast<std::ptrdiff_t*>(&info.strides[0])}},
// TODO: need to take buffer.obj, not buffer!
buffer};
}), "Construct from a buffer")
.def_buffer([](const PyStridedArrayView<dimensions, T>& self) -> py::buffer_info {
std::vector<py::ssize_t> shape(dimensions);
Containers::StridedDimensions<dimensions, std::size_t> selfSize{self.size()};
for(std::size_t i = 0; i != dimensions; ++i) shape[i] = selfSize[i];
std::vector<py::ssize_t> strides(dimensions);
Containers::StridedDimensions<dimensions, std::ptrdiff_t> selfStride{self.stride()};
for(std::size_t i = 0; i != dimensions; ++i) strides[i] = selfStride[i];
return py::buffer_info{
const_cast<void*>(self.data()),
sizeof(T),
py::format_descriptor<T>::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<dimensions, T>& self) {
return Containers::StridedDimensions<dimensions, std::size_t>(self.size())[0];
}, "View size in the top-level dimension")
.def_property_readonly("size", [](const PyStridedArrayView<dimensions, T>& self) {
return size<dimensions>(self.size());
}, "View size in each dimension")
.def_property_readonly("stride", [](const PyStridedArrayView<dimensions, T>& self) {
return stride<dimensions>(self.stride());
}, "View stride in each dimension")
.def_property_readonly("dimensions", [](const PyStridedArrayView<dimensions, T>&) { return dimensions; }, "Dimension count")
.def_readonly("obj", &PyStridedArrayView<dimensions, T>::obj, "Memory owner object")
/* Conversion to bytes */
.def("__bytes__", [](const PyStridedArrayView<dimensions, T>& self) {
/* TODO: use _PyBytes_Resize() to avoid the double copy */
const Containers::Array<char> out = bytes(Containers::arrayCast<const char>(self));
return py::bytes(out.data(), out.size());
}, "Convert to bytes")
/* Slicing of the top dimension */
.def("__getitem__", [](const PyStridedArrayView<dimensions, T>& self, py::slice slice) -> py::object {
const Slice calculated = calculateSlice(slice, Containers::StridedDimensions<dimensions, const std::size_t>{self.size()}[0]);
return py::cast(PyStridedArrayView<dimensions, T>(self.slice(calculated.start, calculated.stop).every(calculated.step), self.obj));
}, "Slice the view");
}
template<class T> void stridedArrayView1D(py::class_<PyStridedArrayView<1, T>>& 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<unsigned dimensions, class T> void stridedArrayViewND(py::class_<PyStridedArrayView<dimensions, T>>& 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<dimensions, T>& self, std::size_t i) {
if(i >= Containers::StridedDimensions<dimensions, const std::size_t>{self.size()}[0]) throw pybind11::index_error{};
return PyStridedArrayView<dimensions - 1, T>{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<dimensions, T>& self, const typename DimensionsTuple<dimensions, py::slice>::Type& slice) -> py::object {
Containers::StridedDimensions<dimensions, std::size_t> starts;
Containers::StridedDimensions<dimensions, std::size_t> stops;
Containers::StridedDimensions<dimensions, std::ptrdiff_t> steps;
for(std::size_t i = 0; i != dimensions; ++i) {
const Slice calculated = calculateSlice(dimensionsTupleGet<py::slice>(slice, i), self.size()[i]);
starts[i] = calculated.start;
stops[i] = calculated.stop;
steps[i] = calculated.step;
}
return py::cast(PyStridedArrayView<dimensions, T>(self.slice(starts, stops).every(steps), self.obj));
}, "Slice the view");
}
template<class T> void stridedArrayView2D(py::class_<PyStridedArrayView<2, T>>& c) {
c
.def("__getitem__", [](const PyStridedArrayView<2, T>& self, const std::tuple<std::size_t, std::size_t>& 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<class T> void stridedArrayView3D(py::class_<PyStridedArrayView<3, T>>& c) {
c
.def("__getitem__", [](const PyStridedArrayView<3, T>& self, const std::tuple<std::size_t, std::size_t, std::size_t>& 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<class T> void mutableStridedArrayView1D(py::class_<PyStridedArrayView<1, T>>& 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<class T> void mutableStridedArrayView2D(py::class_<PyStridedArrayView<2, T>>& c) {
c
.def("__setitem__", [](const PyStridedArrayView<2, T>& self, const std::tuple<std::size_t, std::size_t>& 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<class T> void mutableStridedArrayView3D(py::class_<PyStridedArrayView<3, T>>& c) {
c
.def("__setitem__", [](const PyStridedArrayView<3, T>& self, const std::tuple<std::size_t, std::size_t, std::size_t>& 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_<PyArrayView<const char>> arrayView_{m,
"ArrayView", "Array view", py::buffer_protocol{}};
arrayView(arrayView_);
py::class_<PyArrayView<char>> mutableArrayView_{m,
"MutableArrayView", "Mutable array view", py::buffer_protocol{}};
arrayView(mutableArrayView_);
mutableArrayView(mutableArrayView_);
py::class_<PyStridedArrayView<1, const char>> stridedArrayView1D_{m,
"StridedArrayView1D", "One-dimensional array view with stride information", py::buffer_protocol{}};
py::class_<PyStridedArrayView<2, const char>> stridedArrayView2D_{m,
"StridedArrayView2D", "Two-dimensional array view with stride information", py::buffer_protocol{}};
py::class_<PyStridedArrayView<3, const char>> 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_<PyStridedArrayView<1, char>> mutableStridedArrayView1D_{m,
"MutableStridedArrayView1D", "Mutable one-dimensional array view with stride information", py::buffer_protocol{}};
py::class_<PyStridedArrayView<2, char>> mutableStridedArrayView2D_{m,
"MutableStridedArrayView2D", "Mutable two-dimensional array view with stride information", py::buffer_protocol{}};
py::class_<PyStridedArrayView<3, char>> 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);
}

31
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š <mosra@centrum.cz>
#
# 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

694
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š <mosra@centrum.cz>
#
# 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')

3
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': '$<TARGET_FILE:corrade_containers>',
'magnum._magnum': '$<TARGET_FILE: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

Loading…
Cancel
Save