diff --git a/src/python/magnum/math.cpp b/src/python/magnum/math.cpp index 598083b..cb6442e 100644 --- a/src/python/magnum/math.cpp +++ b/src/python/magnum/math.cpp @@ -37,6 +37,56 @@ namespace magnum { +/* Keep in sync with math.h */ + +const char* const FormatStrings[]{ + /* 0. Representing bytes as unsigned. Not using 'c' because then it behaves + differently from bytes/bytearray, where you can do `a[0] = ord('A')`. */ + "B", + + "b", /* 1 -- std::int8_t */ + "B", /* 2 -- std::uint8_t */ + "i", /* 3 -- std::int32_t */ + "I", /* 4 -- std::uint32_t */ + "f", /* 5 -- float */ + "d" /* 6 -- double */ +}; + +/* Flipped as numpy expects row-major */ +const Py_ssize_t MatrixShapes[][2]{ + {2, 2}, /* 0 -- 2 cols, 2 rows */ + {3, 2}, /* 1 -- 2 cols, 3 rows */ + {4, 2}, /* 2 -- 2 cols, 4 rows */ + {2, 3}, /* 3 -- 3 cols, 2 rows */ + {3, 3}, /* 4 -- 3 cols, 3 rows */ + {4, 3}, /* 5 -- 3 cols, 4 rows */ + {2, 4}, /* 6 -- 4 cols, 2 rows */ + {3, 4}, /* 7 -- 4 cols, 3 rows */ + {4, 4} /* 8 -- 4 cols, 4 rows */ +}; +const Py_ssize_t MatrixStridesFloat[][2]{ + {4, 4*2}, /* 0 -- 2 cols, 2 rows */ + {4, 4*3}, /* 1 -- 2 cols, 3 rows */ + {4, 4*4}, /* 2 -- 2 cols, 4 rows */ + {4, 4*2}, /* 3 -- 3 cols, 2 rows */ + {4, 4*3}, /* 4 -- 3 cols, 3 rows */ + {4, 4*4}, /* 5 -- 3 cols, 4 rows */ + {4, 4*2}, /* 6 -- 4 cols, 2 rows */ + {4, 4*3}, /* 7 -- 4 cols, 3 rows */ + {4, 4*4} /* 8 -- 4 cols, 4 rows */ +}; +const Py_ssize_t MatrixStridesDouble[][2]{ + {8, 8*2}, /* 0 -- 2 cols, 2 rows */ + {8, 8*3}, /* 1 -- 2 cols, 3 rows */ + {8, 8*4}, /* 2 -- 2 cols, 4 rows */ + {8, 8*2}, /* 3 -- 3 cols, 2 rows */ + {8, 8*3}, /* 4 -- 3 cols, 3 rows */ + {8, 8*4}, /* 5 -- 3 cols, 4 rows */ + {8, 8*2}, /* 6 -- 4 cols, 2 rows */ + {8, 8*3}, /* 7 -- 4 cols, 3 rows */ + {8, 8*4} /* 8 -- 4 cols, 4 rows */ +}; + namespace { template void angle(py::class_& c) { diff --git a/src/python/magnum/math.h b/src/python/magnum/math.h index 3ee74f8..a28a482 100644 --- a/src/python/magnum/math.h +++ b/src/python/magnum/math.h @@ -26,6 +26,7 @@ */ #include +#include #include #include @@ -33,6 +34,40 @@ namespace magnum { +/* Keep in sync with math.cpp */ + +extern const char* const FormatStrings[]; +template constexpr std::size_t formatIndex(); +template<> constexpr std::size_t formatIndex() { return 0; } +template<> constexpr std::size_t formatIndex() { return 1; } +template<> constexpr std::size_t formatIndex() { return 2; } +template<> constexpr std::size_t formatIndex() { return 3; } +template<> constexpr std::size_t formatIndex() { return 4; } +template<> constexpr std::size_t formatIndex() { return 5; } +template<> constexpr std::size_t formatIndex() { return 6; } + +extern const Py_ssize_t MatrixShapes[][2]; +template constexpr std::size_t matrixShapeStrideIndex(); +template<> constexpr std::size_t matrixShapeStrideIndex<2, 2>() { return 0; } +template<> constexpr std::size_t matrixShapeStrideIndex<2, 3>() { return 1; } +template<> constexpr std::size_t matrixShapeStrideIndex<2, 4>() { return 2; } +template<> constexpr std::size_t matrixShapeStrideIndex<3, 2>() { return 3; } +template<> constexpr std::size_t matrixShapeStrideIndex<3, 3>() { return 4; } +template<> constexpr std::size_t matrixShapeStrideIndex<3, 4>() { return 5; } +template<> constexpr std::size_t matrixShapeStrideIndex<4, 2>() { return 6; } +template<> constexpr std::size_t matrixShapeStrideIndex<4, 3>() { return 7; } +template<> constexpr std::size_t matrixShapeStrideIndex<4, 4>() { return 8; } + +extern const Py_ssize_t MatrixStridesFloat[][2]; +extern const Py_ssize_t MatrixStridesDouble[][2]; +template constexpr const Py_ssize_t* matrixStridesFor(std::size_t i); +template<> constexpr const Py_ssize_t* matrixStridesFor(std::size_t i) { + return MatrixStridesFloat[i]; +} +template<> constexpr const Py_ssize_t* matrixStridesFor(std::size_t i) { + return MatrixStridesDouble[i]; +} + template std::string repr(const T& value) { std::ostringstream out; Debug{&out, Debug::Flag::NoNewlineAtTheEnd} << value; diff --git a/src/python/magnum/math.matrix.h b/src/python/magnum/math.matrix.h index 1c1cfc9..2330699 100644 --- a/src/python/magnum/math.matrix.h +++ b/src/python/magnum/math.matrix.h @@ -27,11 +27,13 @@ #include #include +#include #include #include #include #include "corrade/PybindExtras.h" +#include "corrade/PyBuffer.h" #include "magnum/math.h" @@ -44,10 +46,10 @@ template struct VectorTraits<2, T> { typedef Math::Vector2 Type; }; template struct VectorTraits<3, T> { typedef Math::Vector3 Type; }; template struct VectorTraits<4, T> { typedef Math::Vector4 Type; }; -template void initFromBuffer(T& out, const py::buffer_info& info) { +template void initFromBuffer(T& out, const Py_buffer& buffer) { for(std::size_t i = 0; i != T::Cols; ++i) for(std::size_t j = 0; j != T::Rows; ++j) - out[i][j] = static_cast(*reinterpret_cast(static_cast(info.ptr) + i*info.strides[1] + j*info.strides[0])); + out[i][j] = static_cast(*reinterpret_cast(static_cast(buffer.buf) + i*buffer.strides[1] + j*buffer.strides[0])); } /* Called for both Matrix3x3 and Matrix3 in order to return a proper type / @@ -67,24 +69,30 @@ template void everyRectangularMatrix(py::class_(), "Construct a matrix with one value for all components") - /* Buffer protocol, needed in order to make numpy treat the matric - correctly as column-major. Has to be defined *before* the from-tuple - constructor so it gets precedence for types that implement the - buffer protocol. */ - .def(py::init([](py::buffer buffer) { - py::buffer_info info = buffer.request(); + /* Buffer protocol, needed in order to properly detect row-major + layouts. Has to be defined *before* the from-tuple constructor so it + gets precedence for types that implement the buffer protocol. */ + .def(py::init([](py::buffer other) { + Py_buffer buffer{}; + if(PyObject_GetBuffer(other.ptr(), &buffer, PyBUF_FORMAT|PyBUF_STRIDES) != 0) + throw py::error_already_set{}; - if(info.ndim != 2) - throw py::buffer_error{Utility::formatString("expected 2 dimensions but got {}", info.ndim)}; + Containers::ScopeGuard e{&buffer, PyBuffer_Release}; - if(info.shape[0] != T::Rows ||info.shape[1] != T::Cols) - throw py::buffer_error{Utility::formatString("expected {}x{} elements but got {}x{}", T::Cols, T::Rows, info.shape[1], info.shape[0])}; + if(buffer.ndim != 2) + throw py::buffer_error{Utility::formatString("expected 2 dimensions but got {}", buffer.ndim)}; + + if(buffer.shape[0] != T::Rows || buffer.shape[1] != T::Cols) + throw py::buffer_error{Utility::formatString("expected {}x{} elements but got {}x{}", T::Cols, T::Rows, buffer.shape[1], buffer.shape[0])}; T out{Math::NoInit}; - if(info.format == "f") initFromBuffer(out, info); - else if(info.format == "d") initFromBuffer(out, info); - else throw py::buffer_error{Utility::formatString("expected format f or d but got {}", info.format)}; + /* Expecting just an one-letter format */ + if(buffer.format[0] == 'f' && !buffer.format[1]) + initFromBuffer(out, buffer); + else if(buffer.format[0] == 'd' && !buffer.format[1]) + initFromBuffer(out, buffer); + else throw py::buffer_error{Utility::formatString("expected format f or d but got {}", buffer.format)}; return out; }), "Construct from a buffer") @@ -113,6 +121,31 @@ template void everyRectangularMatrix(py::class_ bool rectangularMatrixBufferProtocol(T& self, Py_buffer& buffer, int flags) { + /* I hate the const_casts but I assume this is to make editing easier, NOT + to make it possible for users to stomp on these values. */ + buffer.ndim = 2; + buffer.itemsize = sizeof(typename T::Type); + buffer.len = sizeof(T); + buffer.buf = self.data(); + buffer.readonly = false; + if((flags & PyBUF_FORMAT) == PyBUF_FORMAT) + buffer.format = const_cast(FormatStrings[formatIndex()]); + if(flags != PyBUF_SIMPLE) { + /* Reusing shape definitions from matrices because I don't want to + create another useless array for that and reinterpret_cast on the + buffer.internal is UGLY. It's flipped from column-major to + row-major, so adjusting the row instead. */ + buffer.shape = const_cast(MatrixShapes[matrixShapeStrideIndex()]); + CORRADE_INTERNAL_ASSERT(buffer.shape[0] == T::Rows); + CORRADE_INTERNAL_ASSERT(buffer.shape[1] == T::Cols); + if((flags & PyBUF_STRIDES) == PyBUF_STRIDES) + buffer.strides = const_cast(matrixStridesFor(matrixShapeStrideIndex())); + } + + return true; +} + template void rectangularMatrix(py::class_& c) { /* Missing APIs: @@ -128,21 +161,6 @@ template void rectangularMatrix(py::class_& c) { */ c - /* Buffer protocol, needed in order to make numpy treat the matrix - correctly as column-major. The constructor is defined in - everyRectangularMatrix(). */ - .def_buffer([](const T& self) -> py::buffer_info { - // TODO: ownership? - return py::buffer_info{ - const_cast(self.data()), - sizeof(typename T::Type), - py::format_descriptor::format(), - 2, - {T::Rows, T::Cols}, - {sizeof(typename T::Type), sizeof(typename T::Type)*T::Rows} - }; - }) - /* Comparison */ .def(py::self == py::self, "Equality comparison") .def(py::self != py::self, "Non-equality comparison") @@ -170,6 +188,11 @@ template void rectangularMatrix(py::class_& c) { .def("__repr__", repr, "Object representation"); + /* Buffer protocol, needed in order to make numpy treat the matrix + correctly as column-major. The constructor is defined in + everyRectangularMatrix(). */ + corrade::enableBetterBufferProtocol(c); + /* Matrix column count */ char lenDocstring[] = "Matrix column count. Returns _."; lenDocstring[sizeof(lenDocstring) - 3] = '0' + T::Cols; diff --git a/src/python/magnum/math.matrixdouble.cpp b/src/python/magnum/math.matrixdouble.cpp index 173ce98..27a99a0 100644 --- a/src/python/magnum/math.matrixdouble.cpp +++ b/src/python/magnum/math.matrixdouble.cpp @@ -40,8 +40,13 @@ void mathMatrixDouble(py::module& root) { py::class_ matrix4x3d{root, "Matrix4x3d", "4x3 double matrix", py::buffer_protocol{}}; py::class_ matrix4x4d{root, "Matrix4x4d", "4x4 double matrix", py::buffer_protocol{}}; - py::class_ matrix3d{root, "Matrix3d", "2D double transformation matrix", py::buffer_protocol{}}; - py::class_ matrix4d{root, "Matrix4d", "3D double transformation matrix", py::buffer_protocol{}}; + /* The subclasses don't have buffer protocol enabled, as that's already + done by the base classes. Moreover, just adding py::buffer_protocol{} + would cause it to not find the buffer functions as we don't add them + anywhere, thus failing with `pybind11_getbuffer(): Internal error`. */ + + py::class_ matrix3d{root, "Matrix3d", "2D double transformation matrix"}; + py::class_ matrix4d{root, "Matrix4d", "3D double transformation matrix"}; matrices( matrix2x2d, matrix2x3d, matrix2x4d, diff --git a/src/python/magnum/math.matrixfloat.cpp b/src/python/magnum/math.matrixfloat.cpp index a453fbe..75d8a41 100644 --- a/src/python/magnum/math.matrixfloat.cpp +++ b/src/python/magnum/math.matrixfloat.cpp @@ -40,8 +40,13 @@ void mathMatrixFloat(py::module& root) { py::class_ matrix4x3{root, "Matrix4x3", "4x3 float matrix", py::buffer_protocol{}}; py::class_ matrix4x4{root, "Matrix4x4", "4x4 float matrix", py::buffer_protocol{}}; - py::class_ matrix3{root, "Matrix3", "2D float transformation matrix", py::buffer_protocol{}}; - py::class_ matrix4{root, "Matrix4", "3D float transformation matrix", py::buffer_protocol{}}; + /* The subclasses don't have buffer protocol enabled, as that's already + done by the base classes. Moreover, just adding py::buffer_protocol{} + would cause it to not find the buffer functions as we don't add them + anywhere, thus failing with `pybind11_getbuffer(): Internal error`. */ + + py::class_ matrix3{root, "Matrix3", "2D float transformation matrix"}; + py::class_ matrix4{root, "Matrix4", "3D float transformation matrix"}; matrices( matrix2x2, matrix2x3, matrix2x4, diff --git a/src/python/magnum/math.vector.h b/src/python/magnum/math.vector.h index 00dbf11..2a3f4a8 100644 --- a/src/python/magnum/math.vector.h +++ b/src/python/magnum/math.vector.h @@ -26,53 +26,55 @@ */ #include +#include #include #include #include #include "corrade/PybindExtras.h" +#include "corrade/PyBuffer.h" #include "magnum/math.h" namespace magnum { -template bool isTypeCompatible(const std::string&); -template<> inline bool isTypeCompatible(const std::string& format) { - return format == "f" || format == "d"; +template constexpr bool isTypeCompatible(char); +template<> constexpr bool isTypeCompatible(char format) { + return format == 'f' || format == 'd'; } -template<> inline bool isTypeCompatible(const std::string& format) { - return format == "f" || format == "d"; +template<> constexpr bool isTypeCompatible(char format) { + return format == 'f' || format == 'd'; } -template<> inline bool isTypeCompatible(const std::string& format) { - return format == "i" || format == "l"; +template<> constexpr bool isTypeCompatible(char format) { + return format == 'i' || format == 'l'; } -template<> inline bool isTypeCompatible(const std::string& format) { - return format == "I" || format == "L"; +template<> constexpr bool isTypeCompatible(char format) { + return format == 'I' || format == 'L'; } -template void initFromBuffer(T& out, const py::buffer_info& info) { +template void initFromBuffer(T& out, const Py_buffer& buffer) { for(std::size_t i = 0; i != T::Size; ++i) - out[i] = static_cast(*reinterpret_cast(static_cast(info.ptr) + i*info.strides[0])); + out[i] = static_cast(*reinterpret_cast(static_cast(buffer.buf) + i*buffer.strides[0])); } /* Floating-point init */ -template void initFromBuffer(T& out, const py::buffer_info& info, std::true_type, std::true_type) { - if(info.format == "f") initFromBuffer(out, info); - else if(info.format == "d") initFromBuffer(out, info); +template void initFromBuffer(typename std::enable_if::value, T>::type& out, const Py_buffer& buffer) { + if(buffer.format[0] == 'f') initFromBuffer(out, buffer); + else if(buffer.format[0] == 'd') initFromBuffer(out, buffer); else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ } -/* Signed integeral init */ -template void initFromBuffer(T& out, const py::buffer_info& info, std::false_type, std::true_type) { - if(info.format == "i") initFromBuffer(out, info); - else if(info.format == "l") initFromBuffer(out, info); +/* Signed integral init */ +template void initFromBuffer(typename std::enable_if::value && std::is_signed::value, T>::type& out, const Py_buffer& buffer) { + if(buffer.format[0] == 'i') initFromBuffer(out, buffer); + else if(buffer.format[0] == 'l') initFromBuffer(out, buffer); else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ } -/* Unsigned integeral init */ -template void initFromBuffer(T& out, const py::buffer_info& info, std::false_type, std::false_type) { - if(info.format == "I") initFromBuffer(out, info); - else if(info.format == "L") initFromBuffer(out, info); +/* Unsigned integral init */ +template void initFromBuffer(typename std::enable_if::value && std::is_unsigned::value, T>::type& out, const Py_buffer& buffer) { + if(buffer.format[0] == 'I') initFromBuffer(out, buffer); + else if(buffer.format[0] == 'L') initFromBuffer(out, buffer); else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ } @@ -91,25 +93,6 @@ template void everyVector(py::class_& c) { }, "Construct a zero vector") .def(py::init(), "Default constructor") - /* Ideally, only the constructor (in vectorBuffer()) would be needed - (and thus also no py::buffer_protocol() specified for the class), - but conversion of vectors to lists is extremely slow due to pybind - exceptions being somehow extra heavy compared to native python ones, - so in order to have acceptable performance we need the buffer - protocol on the other side as well. See test/benchmark_math.py for - more information. */ - .def_buffer([](const T& self) -> py::buffer_info { - // TODO: ownership? - return py::buffer_info{ - const_cast(self.data()), - sizeof(typename T::Type), - py::format_descriptor::format(), - 1, - {T::Size}, - {sizeof(typename T::Type)} - }; - }) - /* Operators */ .def(-py::self, "Negated vector") .def(py::self += py::self, "Add and assign a vector") @@ -135,24 +118,53 @@ template void vectorBuffer(py::class_& c) { /* Buffer protocol. If not present, implicit conversion from numpy arrays of non-default types somehow doesn't work. There's also the other part in vectorBuffer(). */ - .def(py::init([](py::buffer buffer) { - py::buffer_info info = buffer.request(); + .def(py::init([](py::buffer other) { + Py_buffer buffer{}; + if(PyObject_GetBuffer(other.ptr(), &buffer, PyBUF_FORMAT|PyBUF_STRIDES) != 0) + throw py::error_already_set{}; + + Containers::ScopeGuard e{&buffer, PyBuffer_Release}; - if(info.ndim != 1) - throw py::buffer_error{Utility::formatString("expected 1 dimension but got {}", info.ndim)}; + if(buffer.ndim != 1) + throw py::buffer_error{Utility::formatString("expected 1 dimension but got {}", buffer.ndim)}; - if(info.shape[0] != T::Size) - throw py::buffer_error{Utility::formatString("expected {} elements but got {}", T::Size, info.shape[0])}; + if(buffer.shape[0] != T::Size) + throw py::buffer_error{Utility::formatString("expected {} elements but got {}", T::Size, buffer.shape[0])}; - if(!isTypeCompatible(info.format)) - throw py::buffer_error{Utility::formatString("unexpected format {} for a {} vector", info.format, py::format_descriptor::format())}; + /* Expecting just an one-letter format */ + if(!buffer.format[0] || buffer.format[1] || !isTypeCompatible(buffer.format[0])) + throw py::buffer_error{Utility::formatString("unexpected format {} for a {} vector", buffer.format, FormatStrings[formatIndex()])}; T out{Math::NoInit}; - initFromBuffer(out, info, std::is_floating_point{}, std::is_signed{}); + initFromBuffer(out, buffer); return out; }), "Construct from a buffer"); } +template bool vectorBufferProtocol(T& self, Py_buffer& buffer, int flags) { + /* I hate the const_casts but I assume this is to make editing easier, NOT + to make it possible for users to stomp on these values. */ + buffer.ndim = 1; + buffer.itemsize = sizeof(typename T::Type); + buffer.len = sizeof(T); + buffer.buf = self.data(); + buffer.readonly = false; + if((flags & PyBUF_FORMAT) == PyBUF_FORMAT) + buffer.format = const_cast(FormatStrings[formatIndex()]); + if(flags != PyBUF_SIMPLE) { + /* Reusing shape definitions from matrices because I don't want to + create another useless array for that and reinterpret_cast on the + buffer.internal is UGLY. It's flipped from column-major to + row-major, so adjusting the row instead. */ + buffer.shape = const_cast(MatrixShapes[matrixShapeStrideIndex<2, T::Size>()]); + CORRADE_INTERNAL_ASSERT(buffer.shape[0] == T::Size); + if((flags & PyBUF_STRIDES) == PyBUF_STRIDES) + buffer.strides = &buffer.itemsize; + } + + return true; +} + /* Things common for vectors of all sizes and types */ template void vector(py::module& m, py::class_& c) { /* @@ -203,6 +215,15 @@ template void vector(py::module& m, py::class_& c) { .def("__repr__", repr, "Object representation"); + /* Ideally, only the constructor (in vectorBuffer()) would be needed + (and thus also no py::buffer_protocol() specified for the class), + but conversion of vectors to lists is extremely slow due to pybind + exceptions being somehow extra heavy compared to native python ones, + so in order to have acceptable performance we need the buffer + protocol on the other side as well. See test/benchmark_math.py for more + information. */ + corrade::enableBetterBufferProtocol(c); + /* Vector length */ char lenDocstring[] = "Vector size. Returns _."; lenDocstring[sizeof(lenDocstring) - 3] = '0' + T::Size; diff --git a/src/python/magnum/math.vectorfloat.cpp b/src/python/magnum/math.vectorfloat.cpp index de9c6db..e31a94d 100644 --- a/src/python/magnum/math.vectorfloat.cpp +++ b/src/python/magnum/math.vectorfloat.cpp @@ -89,12 +89,17 @@ void mathVectorFloat(py::module& root, py::module& m) { vectorsFloat(m, vector2, vector3, vector4); vectorsFloat(m, vector2d, vector3d, vector4d); - py::class_ color3_{root, "Color3", "Color in linear RGB color space", py::buffer_protocol{}}; + /* The subclasses don't have buffer protocol enabled, as that's already + done by the base classes. Moreover, just adding py::buffer_protocol{} + would cause it to not find the buffer functions as we don't add them + anywhere, thus failing with `pybind11_getbuffer(): Internal error`. */ + + py::class_ color3_{root, "Color3", "Color in linear RGB color space"}; everyVector(color3_); color(color3_); color3(color3_); - py::class_ color4_{root, "Color4", "Color in linear RGBA color space", py::buffer_protocol{}}; + py::class_ color4_{root, "Color4", "Color in linear RGBA color space"}; everyVector(color4_); color(color4_); color4(color4_); diff --git a/src/python/magnum/test/test_math.py b/src/python/magnum/test/test_math.py index aaf2515..df97c13 100644 --- a/src/python/magnum/test/test_math.py +++ b/src/python/magnum/test/test_math.py @@ -23,6 +23,7 @@ # DEALINGS IN THE SOFTWARE. # +import array import unittest from magnum import * @@ -263,6 +264,14 @@ class Vector(unittest.TestCase): def test_repr(self): self.assertEqual(repr(Vector3(1.0, 3.14, -13.37)), 'Vector(1, 3.14, -13.37)') + def test_from_buffer(self): + a = Vector3i(array.array('i', [2, 3, 5])) + self.assertEqual(a, Vector3i(2, 3, 5)) + + def test_to_buffer(self): + a = memoryview(Vector4(1.0, 2.0, 3.0, 4.0)) + self.assertEqual(a.tolist(), [1.0, 2.0, 3.0, 4.0]) + class Color3_(unittest.TestCase): def test_init(self): a1 = Color3() @@ -350,6 +359,16 @@ class Color4_(unittest.TestCase): self.assertIsInstance(Color4().rgb, Color3) self.assertIsInstance(Color4().xyz, Color3) + def test_from_buffer(self): + a = Color3(array.array('f', [2.0, 3.0, 5.0])) + self.assertEqual(a, Color3(2.0, 3.0, 5.0)) + + def test_to_buffer(self): + # Color4 doesn't define py::buffer_protocol(), the one from base should + # "just work" + a = memoryview(Color4(1.0, 2.0, 3.0, 4.0)) + self.assertEqual(a.tolist(), [1.0, 2.0, 3.0, 4.0]) + class Matrix(unittest.TestCase): def test_init(self): a = Matrix3x2() @@ -582,6 +601,13 @@ class Matrix(unittest.TestCase): ' 2, 5,\n' ' 3, 6)') + # conversion from buffer is tested in test_math_numpy, array.array is + # one-dimensional and I don't want to drag numpy here just for one test + + def test_to_buffer(self): + a = memoryview(Matrix2x2((1.0, 2.0), (3.0, 4.0))) + self.assertEqual(a.tolist(), [[1.0, 3.0], [2.0, 4.0]]) + class Matrix3_(unittest.TestCase): def test_init(self): a = Matrix3() @@ -673,6 +699,20 @@ class Matrix3_(unittest.TestCase): self.assertIsInstance(Matrix3().transposed(), Matrix3) self.assertIsInstance(Matrix3().inverted(), Matrix3) + # conversion from buffer is tested in test_math_numpy, array.array is + # one-dimensional and I don't want to drag numpy here just for one test + + def test_to_buffer(self): + # Matrix3 doesn't define py::buffer_protocol(), the one from base + # should "just work" + a = memoryview(Matrix3((1.0, 2.0, 3.0), + (4.0, 5.0, 6.0), + (7.0, 8.0, 9.0))) + self.assertEqual(a.tolist(), [ + [1.0, 4.0, 7.0], + [2.0, 5.0, 8.0], + [3.0, 6.0, 9.0]]) + class Matrix4_(unittest.TestCase): def test_init(self): a = Matrix4() @@ -778,6 +818,22 @@ class Matrix4_(unittest.TestCase): self.assertIsInstance(Matrix4().transposed(), Matrix4) self.assertIsInstance(Matrix4().inverted(), Matrix4) + # conversion from buffer is tested in test_math_numpy, array.array is + # one-dimensional and I don't want to drag numpy here just for one test + + def test_to_buffer(self): + # Matrix3 doesn't define py::buffer_protocol(), the one from base + # should "just work" + a = memoryview(Matrix4((1.0, 2.0, 3.0, 4.0), + (5.0, 6.0, 7.0, 8.0), + (9.0, 10.0, 11.0, 12.0), + (13.0, 14.0, 15.0, 16.0))) + self.assertEqual(a.tolist(), [ + [1.0, 5.0, 9.0, 13.0], + [2.0, 6.0, 10.0, 14.0], + [3.0, 7.0, 11.0, 15.0], + [4.0, 8.0, 12.0, 16.0]]) + class Quaternion_(unittest.TestCase): def test_init(self): a = Quaternion()