diff --git a/Changes b/Changes index 9604b01697..7ac9a2018f 100644 --- a/Changes +++ b/Changes @@ -1,7 +1,10 @@ 10.6.x.x (relative to 10.6.2.1) ======== +API +--- +- IECorePython : Added `Buffer` object and associated `asReadOnlyBuffer()` and `asReadWriteBuffer()` methods for numeric-based `*VectorData` types. This adds support for Python's buffer protocol which allows direct access to the vector. 10.6.2.1 (relative to 10.6.2.0) ======== diff --git a/SConstruct b/SConstruct index 92b2770fac..c2a5e76196 100644 --- a/SConstruct +++ b/SConstruct @@ -93,10 +93,13 @@ o.Add( # https://developercommunity.visualstudio.com/content/problem/756694/including-windowsh-and-boostinterprocess-headers-l.html # /DBOOST_ALL_NO_LIB is needed to find Boost when it is built without # verbose system information added to file and directory names. +# /DZc:strictStrings- fixes a compilation error when setting the `Py_buffer.format` member in `VectorTypedDataBinding.inl` +# `getBuffer()` method. Python declares that member as `char *` but MSVC requires `const char *` for string literals. +# Disabling strict strings relaxes that requirement. o.Add( "CXXFLAGS", "The extra flags to pass to the C++ compiler during compilation.", - [ "-pipe", "-Wall", "-Wextra", "-Wsuggest-override" ] if Environment()["PLATFORM"] != "win32" else [ "/permissive-", "/D_USE_MATH_DEFINES", "/Zc:externC-", "/DBOOST_ALL_NO_LIB" ], + [ "-pipe", "-Wall", "-Wextra", "-Wsuggest-override" ] if Environment()["PLATFORM"] != "win32" else [ "/permissive-", "/D_USE_MATH_DEFINES", "/Zc:externC-", "/DBOOST_ALL_NO_LIB", "/Zc:strictStrings-" ], ) o.Add( diff --git a/include/IECore/DataAlgo.inl b/include/IECore/DataAlgo.inl index ad64a51e2c..f6d47e08de 100644 --- a/include/IECore/DataAlgo.inl +++ b/include/IECore/DataAlgo.inl @@ -174,6 +174,14 @@ typename std::invoke_result_t dispatch( Data *data, F &&fu return functor( static_cast( data ), std::forward( args )... ); case V3dVectorDataTypeId : return functor( static_cast( data ), std::forward( args )... ); + case Box2iVectorDataTypeId : + return functor( static_cast( data ), std::forward( args )... ); + case Box2fVectorDataTypeId : + return functor( static_cast( data ), std::forward( args )... ); + case Box2dVectorDataTypeId : + return functor( static_cast( data ), std::forward( args )... ); + case Box3iVectorDataTypeId : + return functor( static_cast( data ), std::forward( args )... ); case Box3fVectorDataTypeId : return functor( static_cast( data ), std::forward( args )... ); case Box3dVectorDataTypeId : @@ -328,6 +336,14 @@ typename std::invoke_result_t dispatch( const Data * return functor( static_cast( data ), std::forward( args )... ); case V3dVectorDataTypeId : return functor( static_cast( data ), std::forward( args )... ); + case Box2iVectorDataTypeId : + return functor( static_cast( data ), std::forward( args )... ); + case Box2fVectorDataTypeId : + return functor( static_cast( data ), std::forward( args )... ); + case Box2dVectorDataTypeId : + return functor( static_cast( data ), std::forward( args )... ); + case Box3iVectorDataTypeId : + return functor( static_cast( data ), std::forward( args )... ); case Box3fVectorDataTypeId : return functor( static_cast( data ), std::forward( args )... ); case Box3dVectorDataTypeId : diff --git a/include/IECorePython/GeometricTypedDataBinding.inl b/include/IECorePython/GeometricTypedDataBinding.inl index f1fa448800..17d8cd6ac1 100644 --- a/include/IECorePython/GeometricTypedDataBinding.inl +++ b/include/IECorePython/GeometricTypedDataBinding.inl @@ -262,6 +262,7 @@ class GeometricVectorTypedDataFunctions : ThisBinder "\nor any other python built-in type that is convertible to it. Alternatively accepts the size of the new vector.") \ .def("getInterpretation", &ThisClass::getInterpretation, "Returns the geometric interpretation of this data.") \ .def("setInterpretation", &ThisClass::setInterpretation, "Sets the geometric interpretation of this data.") \ + BIND_BUFFER_PROTOCOL_METHODS \ ; \ } \ diff --git a/include/IECorePython/VectorTypedDataBinding.h b/include/IECorePython/VectorTypedDataBinding.h index 735dd10cd8..dc5d461788 100644 --- a/include/IECorePython/VectorTypedDataBinding.h +++ b/include/IECorePython/VectorTypedDataBinding.h @@ -37,8 +37,35 @@ #include "IECorePython/Export.h" +#include "IECore/Data.h" +#include "IECore/RefCounted.h" + + namespace IECorePython { + +class Buffer : public IECore::RefCounted +{ + public : + IE_CORE_DECLAREMEMBERPTR( Buffer ); + + Buffer( IECore::Data *data, const bool writable ); + ~Buffer() override; + + IECore::DataPtr asData() const; + + bool isWritable() const; + + static int getBuffer( PyObject *object, Py_buffer *view, int flags ); + static void releaseBuffer( PyObject *object, Py_buffer *view ); + + private : + IECore::DataPtr m_data; + const bool m_writable; +}; + +IE_CORE_DECLAREPTR( Buffer ) + extern IECOREPYTHON_API void bindAllVectorTypedData(); } diff --git a/include/IECorePython/VectorTypedDataBinding.inl b/include/IECorePython/VectorTypedDataBinding.inl index b8134652ec..85eadb1fd5 100644 --- a/include/IECorePython/VectorTypedDataBinding.inl +++ b/include/IECorePython/VectorTypedDataBinding.inl @@ -39,6 +39,7 @@ #include "IECorePython/IECoreBinding.h" #include "IECorePython/RunTimeTypedBinding.h" +#include "IECorePython/VectorTypedDataBinding.h" #include "boost/numeric/conversion/cast.hpp" #include "boost/python/suite/indexing/container_utils.hpp" @@ -541,6 +542,21 @@ class VectorTypedDataFunctions ); } + static bool dataSourceEqual( ThisClass &x, ThisClass &y ) + { + return &x.readable() == &y.readable(); + } + + static IECorePython::BufferPtr asReadOnlyBuffer( ThisClass &x ) + { + return new Buffer( &x, /* writable = */ false ); + } + + static IECorePython::BufferPtr asReadWriteBuffer( ThisClass &x ) + { + return new Buffer( &x, /* writable = */ true ); + } + protected: /* @@ -695,7 +711,7 @@ std::string str > >( IECore::TypedData ThisBinder; \ \ - RunTimeTypedClass( \ + RunTimeTypedClass( \ Tname "-type vector class derived from Data class.\n" \ "This class behaves like the native python lists, except that it only accepts " Tname " values.\n" \ "The copy constructor accepts another instance of this class or a python list containing " Tname \ @@ -725,6 +741,7 @@ std::string str > >( IECore::TypedData ) \ .def("__repr__", &repr ) \ @@ -736,6 +753,10 @@ std::string str > >( IECore::TypedData > >( IECore::TypedData > >( IECore::TypedData > >( IECore::TypedData >, Tname) \ + .def("__cmp__", &ThisBinder::invalidOperator, "Raises an exception. This vector type does not support comparison operators.") \ + BIND_BUFFER_PROTOCOL_METHODS \ + ; \ + } + void bindImathBoxVectorTypedData() { - BIND_VECTOR_TYPEDDATA ( Box< V2i >, "Box2i") - BIND_VECTOR_TYPEDDATA ( Box< V2f >, "Box2f") - BIND_VECTOR_TYPEDDATA ( Box< V2d >, "Box2d") - BIND_VECTOR_TYPEDDATA ( Box< V3i >, "Box3i") - BIND_VECTOR_TYPEDDATA ( Box< V3f >, "Box3f") - BIND_VECTOR_TYPEDDATA ( Box< V3d >, "Box3d") + BIND_BOX_VECTOR_TYPEDDATA ( Box< V2i >, "Box2i") + BIND_BOX_VECTOR_TYPEDDATA ( Box< V2f >, "Box2f") + BIND_BOX_VECTOR_TYPEDDATA ( Box< V2d >, "Box2d") + BIND_BOX_VECTOR_TYPEDDATA ( Box< V3i >, "Box3i") + BIND_BOX_VECTOR_TYPEDDATA ( Box< V3f >, "Box3f") + BIND_BOX_VECTOR_TYPEDDATA ( Box< V3d >, "Box3d") } } // namespace IECorePython diff --git a/src/IECorePython/VectorTypedDataBinding.cpp b/src/IECorePython/VectorTypedDataBinding.cpp index 848e9b1fe8..cea431a1f4 100644 --- a/src/IECorePython/VectorTypedDataBinding.cpp +++ b/src/IECorePython/VectorTypedDataBinding.cpp @@ -47,7 +47,9 @@ #include "IECorePython/RunTimeTypedBinding.h" #include "IECorePython/VectorTypedDataBinding.inl" +#include "IECore/DataAlgo.h" #include "IECore/Export.h" +#include "IECore/TypeTraits.h" #include "IECore/VectorTypedData.h" IECORE_PUSH_DEFAULT_VISIBILITY @@ -68,6 +70,52 @@ using namespace boost::python; using namespace Imath; using namespace IECore; +namespace +{ + +template +struct PythonType +{ + static const char *value(); +}; + +template<> struct PythonType{ static const char *value(){ return "e"; } }; +template<> struct PythonType{ static const char *value(){ return "f"; } }; +template<> struct PythonType{ static const char *value(){ return "d"; } }; +template<> struct PythonType{ static const char *value(){ return "i"; } }; +template<> struct PythonType{ static const char *value(){ return "I"; } }; +template<> struct PythonType{ static const char *value(){ return "b"; } }; +template<> struct PythonType{ static const char *value(){ return "B"; } }; +template<> struct PythonType{ static const char *value(){ return "h"; } }; +template<> struct PythonType{ static const char *value(){ return "H"; } }; +template<> struct PythonType{ static const char *value(){ return "q"; } }; +template<> struct PythonType{ static const char *value(){ return "Q"; } }; + +template +constexpr std::pair typeInfo() +{ + if constexpr( + IECore::TypeTraits::IsMatrix::value || + IECore::TypeTraits::IsColor::value || + IECore::TypeTraits::IsQuat::value || + IECore::TypeTraits::IsVec::value + ) + { + return { PythonType::value(), sizeof( typename T::BaseType ) }; + } + else if constexpr( IECore::TypeTraits::IsBox::value ) + { + using ElementType = decltype( T::min ); + return { PythonType::value(), sizeof( typename ElementType::BaseType ) }; + } + else + { + return { PythonType::value(), sizeof( T ) }; + } +} + +} // namespace + namespace IECorePython { @@ -124,6 +172,201 @@ std::string str( BoolVectorData &x ) return s.str(); } +Buffer::Buffer( Data *data, const bool writable ) : m_data( data->copy() ), m_writable( writable ) +{ + +} + +Buffer::~Buffer() +{ + +} + +DataPtr Buffer::asData() const +{ + return m_data->copy(); +} + +bool Buffer::isWritable() const +{ + return m_writable; +} + +int Buffer::getBuffer( PyObject *object, Py_buffer *view, int flags ) +{ + // This method is a customized variation on Python's `PyBuffer_FillInfo` to suit our needs. + if( view == NULL ) { + PyErr_SetString( PyExc_ValueError, "getBuffer(): view==NULL argument is obsolete" ); + return -1; + } + + if( flags != PyBUF_SIMPLE ) + { + if( flags == PyBUF_READ || flags == PyBUF_WRITE ) + { + PyErr_BadInternalCall(); + return -1; + } + } + + IECorePython::BufferPtr self = boost::python::extract( object ); + if( !self ) + { + /// \todo reword this + PyErr_SetString( PyExc_ValueError, "getBuffer(): Buffer type does not match expected type." ); + return -1; + } + + if( ( flags | PyBUF_WRITABLE ) == PyBUF_WRITABLE && !self->isWritable() ) + { + return -1; + } + + try + { + dispatch( + self->m_data.get(), + [&flags, &view, &object, &self]( auto *bufferData ) -> void + { + using DataType = typename std::remove_const_t< std::remove_pointer_t< decltype( bufferData ) > >; + + if constexpr( TypeTraits::HasBaseType::value && TypeTraits::IsNumericBasedVectorTypedData::value ) + { + using ElementType = typename DataType::ValueType::value_type; + const auto [format, itemSize] = typeInfo(); + + int ndim = 1; + Py_ssize_t *shape = NULL; + Py_ssize_t *strides = NULL; + if constexpr( IECore::TypeTraits::IsMatrix44::value ) + { + ndim = 3; + if( ( flags & PyBUF_ND ) == PyBUF_ND ) + { + shape = new Py_ssize_t[3]{ (Py_ssize_t)bufferData->readable().size(), 4, 4 }; + } + if( ( flags & PyBUF_STRIDES ) == PyBUF_STRIDES ) + { + strides = new Py_ssize_t[3]{ 16 * itemSize, 4 * itemSize, itemSize }; + } + } + else if constexpr( IECore::TypeTraits::IsMatrix33::value ) + { + ndim = 3; + if( ( flags & PyBUF_ND ) == PyBUF_ND ) + { + shape = new Py_ssize_t[3]{ (Py_ssize_t)bufferData->readable().size(), 3, 3 }; + } + if( ( flags & PyBUF_STRIDES ) == PyBUF_STRIDES ) + { + strides = new Py_ssize_t[3]{ 9 * itemSize, 3 * itemSize, itemSize }; + } + } + else if constexpr( IECore::TypeTraits::IsQuat::value ) + { + ndim = 2; + if( ( flags & PyBUF_ND ) == PyBUF_ND ) + { + shape = new Py_ssize_t[2]{ (Py_ssize_t)bufferData->readable().size(), 4 }; + } + if( ( flags & PyBUF_STRIDES ) == PyBUF_STRIDES ) + { + strides = new Py_ssize_t[2]{ 4 * itemSize, itemSize }; + } + } + else if constexpr( IECore::TypeTraits::IsColor3::value ) + { + // `Color3` doesn't have `dimensions()` + ndim = 2; + if( ( flags & PyBUF_ND ) == PyBUF_ND ) + { + shape = new Py_ssize_t[2]{ (Py_ssize_t)bufferData->readable().size(), 3 }; + } + if( ( flags & PyBUF_STRIDES ) == PyBUF_STRIDES ) + { + strides = new Py_ssize_t[2]{ 3 * itemSize, itemSize }; + } + } + else if constexpr( IECore::TypeTraits::IsColor4::value || IECore::TypeTraits::IsVec::value ) + { + ndim = 2; + if( ( flags & PyBUF_ND ) == PyBUF_ND ) + { + shape = new Py_ssize_t[2]{ (Py_ssize_t)bufferData->readable().size(), ElementType::dimensions() }; + } + if( ( flags & PyBUF_STRIDES ) == PyBUF_STRIDES ) + { + strides = new Py_ssize_t[2]{ ElementType::dimensions() * itemSize, itemSize }; + } + } + else if constexpr( IECore::TypeTraits::IsBox::value ) + { + ndim = 3; + using ElementType = decltype( ElementType::min ); + if( ( flags & PyBUF_ND ) == PyBUF_ND ) + { + shape = new Py_ssize_t[3]{ (Py_ssize_t)bufferData->readable().size(), 2, ElementType::dimensions() }; + } + if( ( flags & PyBUF_STRIDES ) == PyBUF_STRIDES ) + { + strides = new Py_ssize_t[3]{ 2 * ElementType::dimensions() * itemSize, ElementType::dimensions() * itemSize, itemSize }; + } + } + else + { + shape = new Py_ssize_t{ (Py_ssize_t)bufferData->readable().size() }; + } + + Py_ssize_t shapeProduct = 1; + Py_ssize_t *idx = shape; + for( int i = 0; i < ndim; ++i, ++idx ) + { + shapeProduct *= *idx; + } + + view->obj = Py_XNewRef( object ); + view->readonly = !self->isWritable(); + view->buf = view->readonly ? (void*)bufferData->baseReadable() : (void*)bufferData->baseWritable(); + view->len = view->itemsize * shapeProduct; + view->itemsize = itemSize; + view->format = ( ( flags & PyBUF_FORMAT ) == PyBUF_FORMAT ) ? const_cast( format ) : NULL; + view->ndim = ndim; + view->internal = NULL; + view->shape = shape; + view->strides = strides; + view->suboffsets = NULL; + } + } + ); + } + catch( Exception &e ) + { + view->obj = NULL; + PyErr_SetString( PyExc_BufferError, e.what() ); + return -1; + } + + return 0; +} + +void Buffer::releaseBuffer( PyObject *object, Py_buffer *view ) +{ + if( view->shape != NULL ) + { + delete view->shape; + } + if( view->strides != NULL ) + { + delete view->strides; + } + // Python takes care of decrementing `object` +} + +static PyBufferProcs BufferProtocol = { + (getbufferproc)Buffer::getBuffer, + (releasebufferproc)Buffer::releaseBuffer, +}; + void bindAllVectorTypedData() { // basic types @@ -189,6 +432,16 @@ void bindAllVectorTypedData() bindImathColorVectorTypedData(); bindImathBoxVectorTypedData(); bindImathQuatVectorTypedData(); + + auto c = RefCountedClass( "Buffer" ) + .def( init() ) + .def( "asData", &Buffer::asData ) + .def( "isWritable", &Buffer::isWritable ) + ; + PyTypeObject *o = (PyTypeObject *)c.ptr(); + o->tp_as_buffer = &BufferProtocol; + PyType_Modified( o ); + } diff --git a/test/IECore/VectorData.py b/test/IECore/VectorData.py index 459b37b107..d6d82a1647 100644 --- a/test/IECore/VectorData.py +++ b/test/IECore/VectorData.py @@ -37,6 +37,7 @@ import math import os import unittest +import struct import imath import IECore @@ -1341,6 +1342,252 @@ def test( self ) : self.assertEqual( d2, d ) +class TestSourceDataEquality( unittest.TestCase ) : + + def test( self ) : + + a = IECore.FloatVectorData( [ 1, 2, 3 ] ) + b = IECore.FloatVectorData( [ 4, 5, 6 ] ) + + self.assertTrue( a._dataSourceEqual( a ) ) + self.assertFalse( a._dataSourceEqual( b ) ) + + ca = a.copy() + self.assertTrue( a._dataSourceEqual( ca ) ) + + ca[0] = 42 + self.assertFalse( a._dataSourceEqual( ca ) ) + + ca2 = a.copy() + ca3 = ca2.copy() + self.assertTrue( a._dataSourceEqual( ca2 ) ) + self.assertTrue( a._dataSourceEqual( ca3 ) ) + + a[0] = 99 + self.assertFalse( a._dataSourceEqual( ca2 ) ) + self.assertTrue(ca2._dataSourceEqual( ca3 ) ) + +class TestBufferProtocol( unittest.TestCase ) : + + def __assertMemoryBufferProperties( self, pythonBuffer, cortexBuffer, elementFormat, ndim, shape, strides ) : + + self.assertIs( pythonBuffer.obj, cortexBuffer ) + self.assertTrue( pythonBuffer.readonly ) + self.assertEqual( pythonBuffer.format, elementFormat ) + self.assertEqual( pythonBuffer.ndim, ndim ) + self.assertEqual( pythonBuffer.shape, shape ) + self.assertEqual( pythonBuffer.itemsize, struct.calcsize( elementFormat ) ) + self.assertEqual( pythonBuffer.strides, strides ) + self.assertTrue( pythonBuffer.c_contiguous ) + self.assertTrue( pythonBuffer.contiguous ) + + def testSimpleTypes( self ) : + + for elementType, vectorType, elementFormat in [ + # It would be nice to test for `IECore.HalfVectorData: "e"` + # but that is not supported in our Python. + ( float, IECore.FloatVectorData, "f" ), + ( float, IECore.DoubleVectorData, "d" ), + ( int, IECore.IntVectorData, "i" ), + ( int, IECore.UIntVectorData, "I" ), + ( chr, IECore.CharVectorData, "b" ), + ( int, IECore.UCharVectorData, "B" ), + ( int, IECore.ShortVectorData, "h" ), + ( int, IECore.UShortVectorData, "H" ), + ( int, IECore.Int64VectorData, "q" ), + ( int, IECore.UInt64VectorData, "Q" ), + ] : + with self.subTest( elementType = elementType, vectorType = vectorType ) : + v = vectorType( [ elementType( 1 ), elementType( 2 ), elementType( 3 ) ] ) + + b = v.asReadOnlyBuffer() + m = memoryview( b ) + + # `memoryview` returns `int` for C `char` / Python `chr` types. Cast to `chr` + self.assertEqual( list( m ) if elementType != chr else [ chr(i) for i in m ], list( v ) ) + self.__assertMemoryBufferProperties( m, b, elementFormat, 1, ( len( v ), ), ( m.itemsize, ) ) + + def testMatrixTypes( self ) : + + for elementType, vectorType, matrixSize, elementFormat in [ + ( imath.M44f, IECore.M44fVectorData, 4, "f" ), + ( imath.M44d, IECore.M44dVectorData, 4, "d" ), + ( imath.M33f, IECore.M33fVectorData, 3, "f" ), + ( imath.M33d, IECore.M33dVectorData, 3, "d" ), + ] : + with self.subTest( elementType = elementType, vectorType = vectorType ) : + v = vectorType( + [ + elementType( *list( range( 0, matrixSize * matrixSize ) ) ), + elementType( *list( range( 1, 1 + matrixSize * matrixSize ) ) ), + elementType( *list( range( 2, 2 + matrixSize * matrixSize ) ) ), + ] + ) + + b = v.asReadOnlyBuffer() + m = memoryview( b ) + + self.assertIsNotNone( m ) + for i in range( 0, len( v ) ) : + for j in range( 0, matrixSize ) : + for k in range( 0, matrixSize ) : + self.assertEqual( m.tolist()[i][j][k], v[i][j][k] ) + + self.__assertMemoryBufferProperties( + m, + b, + elementFormat, + 3, + ( len( v ), matrixSize, matrixSize ), + ( matrixSize * matrixSize * m.itemsize, matrixSize * m.itemsize, m.itemsize ) + ) + + def testQuat( self ) : + + for elementType, vectorType, dimensions, elementFormat in [ + ( imath.Quatf, IECore.QuatfVectorData, 4, "f" ), + ( imath.Quatd, IECore.QuatdVectorData, 4, "d" ), + ] : + with self.subTest( elementType = elementType, vectorType = vectorType ) : + v = vectorType( + [ + elementType( *list( range( 0, dimensions ) ) ), + elementType( *list( range( 1, 1 + dimensions ) ) ), + elementType( *list( range( 2, 2 + dimensions ) ) ), + ] + ) + + b = v.asReadOnlyBuffer() + m = memoryview( b ) + + self.assertIsNotNone( m ) + for i in range( 0, len( v ) ) : + mList = m.tolist() + self.assertEqual( elementType( mList[i][0], mList[i][1], mList[i][2], mList[i][3] ), v[i] ) + + self.__assertMemoryBufferProperties( m, b, elementFormat, 2, ( len( v ), dimensions ), ( dimensions * m.itemsize, m.itemsize ) ) + + def testTwoDimensionalBuffers( self ) : + + for elementType, vectorType, dimensions, elementFormat in [ + ( imath.Color3f, IECore.Color3fVectorData, 3, "f" ), + ( imath.Color4f, IECore.Color4fVectorData, 4, "f" ), + ( imath.V2f, IECore.V2fVectorData, 2, "f" ), + ( imath.V2d, IECore.V2dVectorData, 2, "d" ), + ( imath.V2i, IECore.V2iVectorData, 2, "i" ), + ( imath.V3f, IECore.V3fVectorData, 3, "f" ), + ( imath.V3d, IECore.V3dVectorData, 3, "d" ), + ( imath.V3i, IECore.V3iVectorData, 3, "i" ), + ] : + with self.subTest( elementType = elementType, vectorType = vectorType ) : + v = vectorType( + [ + elementType( *list( range( 0, dimensions ) ) ), + elementType( *list( range( 1, 1 + dimensions ) ) ), + elementType( *list( range( 2, 2 + dimensions ) ) ), + ] + ) + + b = v.asReadOnlyBuffer() + m = memoryview( b ) + + self.assertIsNotNone( m ) + for i in range( 0, len( v ) ) : + for j in range( 0, dimensions ) : + self.assertEqual( m.tolist()[i][j], v[i][j] ) + + self.__assertMemoryBufferProperties( m, b, elementFormat, 2, ( len( v ), dimensions ), ( dimensions * m.itemsize, m.itemsize ) ) + + def testBoxTypes( self ) : + + for elementType, vectorType, elementFormat, componentType in [ + ( imath.Box2i, IECore.Box2iVectorData, "i", imath.V2i ), + ( imath.Box2f, IECore.Box2fVectorData, "f", imath.V2f ), + ( imath.Box2d, IECore.Box2dVectorData, "d", imath.V2d ), + ( imath.Box3i, IECore.Box3iVectorData, "i", imath.V3i ), + ( imath.Box3f, IECore.Box3fVectorData, "f", imath.V3f ), + ( imath.Box3d, IECore.Box3dVectorData, "d", imath.V3d ) + ] : + with self.subTest( elementType = elementType, vectorType = vectorType ) : + v = vectorType( + [ + elementType( + componentType( *list( range( i, i + componentType.dimensions() ) ) ), + componentType( *list( range( i + 10, i + 10 + componentType.dimensions() ) ) ) + ) for i in range( 0, 3 ) + ] + ) + + b = v.asReadOnlyBuffer() + m = memoryview( b ) + + self.assertIsNotNone( m ) + for i in range( 0, len( v ) ) : + for j in range( 0, 2 ) : + for k in range( 0, componentType.dimensions() ) : + if j == 0 : + self.assertEqual( m.tolist()[i][j][k], v[i].min()[k] ) + else : + self.assertEqual( m.tolist()[i][j][k], v[i].max()[k] ) + + self.__assertMemoryBufferProperties( + m, + b, + elementFormat, + 3, + ( len( v ), 2, componentType.dimensions() ), + ( 2 * componentType.dimensions() * m.itemsize, componentType.dimensions() * m.itemsize, m.itemsize ) + ) + + def testReadOnlyBuffer( self ) : + + v = IECore.FloatVectorData( [ 1, 2, 3 ] ) + buffer = v.asReadOnlyBuffer() + bufferData = buffer.asData() + self.assertFalse( buffer.isWritable() ) + self.assertTrue( v._dataSourceEqual( bufferData ) ) + self.assertTrue( bufferData._dataSourceEqual( buffer.asData() ) ) + + m = memoryview( buffer ) + + self.assertTrue( m.readonly ) + self.assertTrue( v._dataSourceEqual( bufferData ) ) + self.assertTrue( bufferData._dataSourceEqual( buffer.asData() ) ) + + # We can modify `v` without affecting our buffer. + v.append( 99 ) + self.assertEqual( list( m ), [ 1, 2, 3 ] ) + self.assertFalse( v._dataSourceEqual( bufferData ) ) + self.assertTrue( bufferData._dataSourceEqual( buffer.asData() ) ) + + # We can modify `bufferData`. It's a copy so writing to it results + # in a deep copy of the source data. + bufferData[0] = 42 + self.assertEqual( list( bufferData ), [ 42, 2, 3 ] ) + self.assertFalse( bufferData._dataSourceEqual( buffer.asData() ) ) + self.assertEqual( list( buffer.asData() ), [ 1, 2, 3 ] ) + + def testReadWriteBuffer( self ) : + + v = IECore.FloatVectorData( [ 1, 2, 3 ] ) + buffer = v.asReadWriteBuffer() + bufferData = buffer.asData() + self.assertTrue( buffer.isWritable() ) + self.assertTrue( v._dataSourceEqual( bufferData ) ) + self.assertTrue( bufferData._dataSourceEqual( buffer.asData() ) ) + + # Creating a writable `memoryview` makes `bufferData` unique. + m = memoryview( buffer ) + self.assertFalse( m.readonly ) + self.assertFalse( v._dataSourceEqual( buffer.asData() ) ) + self.assertFalse( bufferData._dataSourceEqual( buffer.asData() ) ) + + # Modify the memoryview, which is reflected in the buffer. + m[0] = 42 + self.assertEqual( list( m ), [ 42, 2, 3 ] ) + self.assertEqual( list( buffer.asData() ), list( m ) ) + + if __name__ == "__main__": unittest.main()